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(); | |
| } | |