Added option to use anonymity sets instead of cookies

Andrew committed May 02, 2018
commit 44f7956bad8662f1d28e3c97c7902d4201755548
Showing 6 changed files with 97 additions and 17 deletions
CHANGELOG.md +3 -2
@@ @@ -1,6 +1,7 @@
- ## 2.0.3 [unreleased]
+ ## 2.1.0 [unreleased]
- - Added IP masking
+ - Added option for IP masking
+ - Added option to use anonymity sets instead of cookies
- Fixed `visitable` for Rails 4.2
## 2.0.2
README.md +52 -0
@@ @@ -58,6 +58,37 @@ ahoy.track("My second event", {language: "JavaScript"});
For Android, check out [Ahoy Android](https://github.com/instacart/ahoy-android). For other platforms, see the [API spec](#api-spec).
+ ### GDPR Compliance [master]
+
+ Ahoy provides a number of options to help with [GDPR compliance](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation).
+
+ Update `config/initializers/ahoy.rb` with:
+
+ ```ruby
+ class Ahoy::Store < Ahoy::DatabaseStore
+ def authenticate(data)
+ # disables automatic linking of visits and users
+ end
+ end
+
+ Ahoy.mask_ips = true
+ Ahoy.cookies = false
+ ```
+
+ This:
+
+ - Masks IP addresses
+ - Switches from cookies to anonymity sets
+ - Disables linking visits and users
+
+ If you use JavaScript tracking, also set:
+
+ ```javascript
+ ahoy.configure({cookies: false});
+ ```
+
+ Set [extended GDPR section](#gdpr-compliance-master-1) for more info.
+
## How It Works
### Visits
@@ @@ -314,6 +345,27 @@ Exceptions are rescued so analytics do not break your app. Ahoy uses [Safely](ht
Safely.report_exception_method = ->(e) { Rollbar.error(e) }
```
+ ## GDPR Compliance [master]
+
+ ### IP Masking
+
+ Ahoy can mask IPs with the same approach [Google Analytics uses for IP anonymization](https://support.google.com/analytics/answer/2763052). This means:
+
+ - For IPv4, the last octet is set to 0 (`8.8.4.4` becomes `8.8.4.0`)
+ - For IPv6, the last 80 bits are set to zeros (`2a03:2880:2110:df07:face:b00c::1` becomes `2a03:2880:2110::`)
+
+ ```ruby
+ Ahoy.mask_ips = true
+ ```
+
+ ### Anonymity Sets & Cookies
+
+ Ahoy can switch from cookies to [anonymity sets](https://privacypatterns.org/patterns/Anonymity-set). Instead of cookies, visitors with the same IP mask and user agent are grouped together in an anonymity set.
+
+ ```ruby
+ Ahoy.cookies = false
+ ```
+
## Development
Ahoy is built with developers in mind. You can run the following code in your browser’s console.
ahoy.rb b/lib/ahoy.rb +3 -0
@@ @@ -24,6 +24,9 @@ module Ahoy
mattr_accessor :visitor_duration
self.visitor_duration = 2.years
+ mattr_accessor :cookies
+ self.cookies = true
+
mattr_accessor :cookie_domain
mattr_accessor :server_side_visits
ahoy/controller.rb b/lib/ahoy/controller.rb +7 -2
@@ @@ -21,8 +21,13 @@ module Ahoy
end
def set_ahoy_cookies
- ahoy.set_visitor_cookie
- ahoy.set_visit_cookie
+ if Ahoy.cookies
+ ahoy.set_visitor_cookie
+ ahoy.set_visit_cookie
+ else
+ # delete cookies if exist
+ ahoy.reset
+ end
end
def track_ahoy_visit
ahoy/tracker.rb b/lib/ahoy/tracker.rb +22 -5
@@ @@ -1,5 +1,9 @@
+ require "active_support/core_ext/digest/uuid"
+
module Ahoy
class Tracker
+ UUID_NAMESPACE = "a82ae811-5011-45ab-a728-569df7499c5f"
+
attr_reader :request, :controller
def initialize(**options)
@@ @@ -102,7 +106,7 @@ module Ahoy
end
def new_visit?
- !existing_visit_token
+ Ahoy.cookies ? !existing_visit_token : visit.nil?
end
def new_visitor?
@@ @@ -156,7 +160,7 @@ module Ahoy
end
def missing_params?
- if api? && Ahoy.protect_from_forgery
+ if Ahoy.cookies && api? && Ahoy.protect_from_forgery
!(existing_visit_token && existing_visitor_token)
else
false
@@ @@ -164,6 +168,9 @@ module Ahoy
end
def set_cookie(name, value, duration = nil, use_domain = true)
+ # safety net
+ return unless Ahoy.cookies
+
cookie = {
value: value
}
@@ @@ -174,7 +181,7 @@ module Ahoy
end
def delete_cookie(name)
- request.cookie_jar.delete(name)
+ request.cookie_jar.delete(name) if request.cookie_jar[name]
end
def trusted_time(time = nil)
@@ @@ -200,6 +207,7 @@ module Ahoy
def visit_token_helper
@visit_token_helper ||= begin
token = existing_visit_token
+ token ||= visit_hash unless Ahoy.cookies
token ||= generate_id unless Ahoy.api_only
token
end
@@ @@ -208,6 +216,7 @@ module Ahoy
def visitor_token_helper
@visitor_token_helper ||= begin
token = existing_visitor_token
+ token ||= visitor_hash unless Ahoy.cookies
token ||= generate_id unless Ahoy.api_only
token
end
@@ @@ -216,7 +225,7 @@ module Ahoy
def existing_visit_token
@existing_visit_token ||= begin
token = visit_header
- token ||= visit_cookie unless api? && Ahoy.protect_from_forgery
+ token ||= visit_cookie if Ahoy.cookies && !(api? && Ahoy.protect_from_forgery)
token ||= visit_param if api?
token
end
@@ @@ -225,12 +234,20 @@ module Ahoy
def existing_visitor_token
@existing_visitor_token ||= begin
token = visitor_header
- token ||= visitor_cookie unless api? && Ahoy.protect_from_forgery
+ token ||= visitor_cookie if Ahoy.cookies && !(api? && Ahoy.protect_from_forgery)
token ||= visitor_param if api?
token
end
end
+ def visit_hash
+ @visit_hash ||= Digest::UUID.uuid_v5(UUID_NAMESPACE, [Ahoy.mask_ip(request.remote_ip), request.user_agent].join("/"))
+ end
+
+ def visitor_hash
+ visit_hash
+ end
+
def visit_cookie
@visit_cookie ||= request && request.cookies["ahoy_visit"]
end
vendor/assets/javascripts/ahoy.js +10 -8
@@ @@ -117,7 +117,8 @@
platform: "Web",
useBeacon: true,
startOnReady: true,
- trackVisits: true
+ trackVisits: true,
+ cookies: true
};
var ahoy = window.ahoy || window.Ahoy || {};
@@ @@ -228,7 +229,7 @@
}
function saveEventQueue() {
- if (canStringify) {
+ if (config.cookies && canStringify) {
setCookie("ahoy_events", JSON.stringify(eventQueue), 1);
}
}
@@ @@ -279,11 +280,12 @@
function eventData(event) {
var data = {
- events: [event],
- visit_token: event.visit_token,
- visitor_token: event.visitor_token
+ events: [event]
};
- delete event.visit_token;
+ if (config.cookies) {
+ data.visit_token = event.visit_token;
+ data.visitor_token = event.visitor_token;
+ } delete event.visit_token;
delete event.visitor_token;
return data;
}
@@ @@ -363,7 +365,7 @@
visitorId = ahoy.getVisitorId();
track = getCookie("ahoy_track");
- if (config.trackVisits == false) {
+ if (config.cookies === false || config.trackVisits === false) {
log("Visit tracking disabled");
setReady();
} else if (visitId && visitorId && !track) {
@@ @@ -450,7 +452,7 @@
};
ready( function () {
- if (!ahoy.getVisitId()) {
+ if (config.cookies && !ahoy.getVisitId()) {
createVisit();
}