ahoy_matey 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0c48c8c5793b7e8f7c53ee1fcfed647340749cee
4
- data.tar.gz: ad2e9686bebde7233be8804ac899a1672f352240
2
+ SHA256:
3
+ metadata.gz: f0b1c52401a07fcd017af2e52aca841090ee47a1330c9e2c941d19a82fd2b6d7
4
+ data.tar.gz: 0d9ddec6c4bbe552672dc41cc444cffc2fa0ffda7b4a6a63200bd2de8d8e65e6
5
5
  SHA512:
6
- metadata.gz: 5b3a16dd65efc501a56fa2a6e2574dc228dbc236d97d352153bd6012b85243450753a62f4155737a2d43610ac619f225004492ed8bcfc8a5e088ef93b67fb56a
7
- data.tar.gz: 23e2ee9cc5ec7d772cce7a4af348b95b93090c5d3a7113fd1d47fcd9fe3bbca4cbbb145cbc21d3826ca159d984566e3e1c8f3b45d89d9a7319bba2ae2d8026eb
6
+ metadata.gz: a8c9083093116b8931861b9f180902038871d76e6a363833bd10d55bbd9e8ea89346f62a609ec3990da3232b24e151e075fa4fd3641a247850e18c40db30d912
7
+ data.tar.gz: a5fe8df81cf9510e8472cda07ff31e2c85d7b2af2102655303623a6677937fb922706db9f2cabac78e49aaf132936c19834be09c3e8acda4ebea3eec7ef82063
@@ -1,3 +1,11 @@
1
+ ## 2.1.0
2
+
3
+ - Added option for IP masking
4
+ - Added option to use anonymity sets instead of cookies
5
+ - Added `user_agent_parser` option
6
+ - Fixed `visitable` for Rails 4.2
7
+ - Removed `search_keyword` from new installs
8
+
1
9
  ## 2.0.2
2
10
 
3
11
  - Fixed error on duplicate records
data/Gemfile CHANGED
@@ -3,4 +3,4 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in ahoy.gemspec
4
4
  gemspec
5
5
 
6
- gem "rails", "~> 5.1.0"
6
+ gem "rails", "~> 5.2.0"
data/README.md CHANGED
@@ -56,7 +56,11 @@ And track an event with:
56
56
  ahoy.track("My second event", {language: "JavaScript"});
57
57
  ```
58
58
 
59
- For native apps, see the [API spec](#api-spec).
59
+ For Android, check out [Ahoy Android](https://github.com/instacart/ahoy-android). For other platforms, see the [API spec](#api-spec).
60
+
61
+ ### GDPR Compliance
62
+
63
+ Ahoy provides a number of options to help with GDPR compliance. See the [GDPR section](#gdpr-compliance-1) for more info.
60
64
 
61
65
  ## How It Works
62
66
 
@@ -127,7 +131,7 @@ end
127
131
 
128
132
  #### Native Apps
129
133
 
130
- See the [API spec](#api-spec).
134
+ For Android, check out [Ahoy Android](https://github.com/instacart/ahoy-android). For other platforms, see the [API spec](#api-spec).
131
135
 
132
136
  ### Associated Models
133
137
 
@@ -314,6 +318,64 @@ Exceptions are rescued so analytics do not break your app. Ahoy uses [Safely](ht
314
318
  Safely.report_exception_method = ->(e) { Rollbar.error(e) }
315
319
  ```
316
320
 
321
+ ## GDPR Compliance
322
+
323
+ Ahoy provides a number of options to help with [GDPR compliance](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation).
324
+
325
+ Update `config/initializers/ahoy.rb` with:
326
+
327
+ ```ruby
328
+ class Ahoy::Store < Ahoy::DatabaseStore
329
+ def authenticate(data)
330
+ # disables automatic linking of visits and users
331
+ end
332
+ end
333
+
334
+ Ahoy.mask_ips = true
335
+ Ahoy.cookies = false
336
+ ```
337
+
338
+ This:
339
+
340
+ - Masks IP addresses
341
+ - Switches from cookies to anonymity sets
342
+ - Disables automatic linking of visits and users
343
+
344
+ If you use JavaScript tracking, also set:
345
+
346
+ ```javascript
347
+ ahoy.configure({cookies: false});
348
+ ```
349
+
350
+ ### IP Masking
351
+
352
+ Ahoy can mask IPs with the same approach [Google Analytics uses for IP anonymization](https://support.google.com/analytics/answer/2763052). This means:
353
+
354
+ - For IPv4, the last octet is set to 0 (`8.8.4.4` becomes `8.8.4.0`)
355
+ - For IPv6, the last 80 bits are set to zeros (`2001:4860:4860:0:0:0:0:8844` becomes `2001:4860:4860::`)
356
+
357
+ ```ruby
358
+ Ahoy.mask_ips = true
359
+ ```
360
+
361
+ To mask previously collected IPs, use:
362
+
363
+ ```ruby
364
+ Ahoy::Visit.find_each do |visit|
365
+ visit.update_column :ip, Ahoy.mask_ip(visit.ip)
366
+ end
367
+ ```
368
+
369
+ ### Anonymity Sets & Cookies
370
+
371
+ 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.
372
+
373
+ ```ruby
374
+ Ahoy.cookies = false
375
+ ```
376
+
377
+ Previously set cookies are automatically deleted.
378
+
317
379
  ## Development
318
380
 
319
381
  Ahoy is built with developers in mind. You can run the following code in your browser’s console.
@@ -414,7 +476,7 @@ Ahoy::Visit.group(:country).count
414
476
  Ahoy::Visit.group(:referring_domain).count
415
477
  ```
416
478
 
417
- [Chartkick](http://chartkick.com/) and [Groupdate](https://github.com/ankane/groupdate) make it easy to visualize the data.
479
+ [Chartkick](https://www.chartkick.com/) and [Groupdate](https://github.com/ankane/groupdate) make it easy to visualize the data.
418
480
 
419
481
  ```erb
420
482
  <%= line_chart Ahoy::Visit.group_by_day(:started_at).count %>
@@ -456,7 +518,7 @@ The same approach also works with visitor tokens.
456
518
 
457
519
  ### Visits
458
520
 
459
- Generate visit and visitor tokens as [UUIDs](http://en.wikipedia.org/wiki/Universally_unique_identifier), and include these values in the `Ahoy-Visit` and `Ahoy-Visitor` headers with all requests.
521
+ Generate visit and visitor tokens as [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier), and include these values in the `Ahoy-Visit` and `Ahoy-Visitor` headers with all requests.
460
522
 
461
523
  Send a `POST` request to `/ahoy/visits` with `Content-Type: application/json` and a body like:
462
524
 
@@ -507,6 +569,42 @@ Then include it in your pack.
507
569
  import ahoy from "ahoy.js";
508
570
  ```
509
571
 
572
+ ## Upgrading
573
+
574
+ ### 2.1
575
+
576
+ Ahoy recommends [Device Detector](https://github.com/podigee/device_detector) for user agent parsing and makes it the default for new installations. To switch, add to `config/initializers/ahoy.rb`:
577
+
578
+ ```ruby
579
+ Ahoy.user_agent_parser = :device_detector
580
+ ```
581
+
582
+ Backfill existing records with:
583
+
584
+ ```ruby
585
+ Ahoy::Visit.find_each do |visit|
586
+ client = DeviceDetector.new(visit.user_agent)
587
+ device_type =
588
+ case client.device_type
589
+ when "smartphone"
590
+ "Mobile"
591
+ when "tv"
592
+ "TV"
593
+ else
594
+ client.device_type.try(:titleize)
595
+ end
596
+
597
+ visit.browser = client.name
598
+ visit.os = client.os_name
599
+ visit.device_type = device_type
600
+ visit.save(validate: false) if visit.changed?
601
+ end
602
+ ```
603
+
604
+ ### 2.0
605
+
606
+ See the [upgrade guide](docs/Ahoy-2-Upgrade.md)
607
+
510
608
  ## History
511
609
 
512
610
  View the [changelog](https://github.com/ankane/ahoy/blob/master/CHANGELOG.md)
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.add_dependency "user_agent_parser"
26
26
  spec.add_dependency "request_store"
27
27
  spec.add_dependency "safely_block", ">= 0.2.1"
28
+ spec.add_dependency "device_detector"
28
29
 
29
30
  spec.add_development_dependency "bundler"
30
31
  spec.add_development_dependency "rake"
@@ -1,3 +1,5 @@
1
+ require "ipaddr"
2
+
1
3
  require "active_support"
2
4
  require "active_support/core_ext"
3
5
  require "addressable/uri"
@@ -22,6 +24,9 @@ module Ahoy
22
24
  mattr_accessor :visitor_duration
23
25
  self.visitor_duration = 2.years
24
26
 
27
+ mattr_accessor :cookies
28
+ self.cookies = true
29
+
25
30
  mattr_accessor :cookie_domain
26
31
 
27
32
  mattr_accessor :server_side_visits
@@ -67,9 +72,26 @@ module Ahoy
67
72
  mattr_accessor :token_generator
68
73
  self.token_generator = -> { SecureRandom.uuid }
69
74
 
75
+ mattr_accessor :mask_ips
76
+ self.mask_ips = false
77
+
78
+ mattr_accessor :user_agent_parser
79
+ self.user_agent_parser = :legacy
80
+
70
81
  def self.log(message)
71
82
  Rails.logger.info { "[ahoy] #{message}" }
72
83
  end
84
+
85
+ def self.mask_ip(ip)
86
+ addr = IPAddr.new(ip)
87
+ if addr.ipv4?
88
+ # set last octet to 0
89
+ addr.mask(24).to_s
90
+ else
91
+ # set last 80 bits to zeros
92
+ addr.mask(48).to_s
93
+ end
94
+ end
73
95
  end
74
96
 
75
97
  ActiveSupport.on_load(:action_controller) do
@@ -46,7 +46,21 @@ module Ahoy
46
46
  protected
47
47
 
48
48
  def bot?
49
- @bot ||= request ? Browser.new(request.user_agent).bot? : false
49
+ unless defined?(@bot)
50
+ @bot = begin
51
+ if request
52
+ if Ahoy.user_agent_parser == :device_detector
53
+ DeviceDetector.new(request.user_agent).bot?
54
+ else
55
+ Browser.new(request.user_agent).bot?
56
+ end
57
+ else
58
+ false
59
+ end
60
+ end
61
+ end
62
+
63
+ @bot
50
64
  end
51
65
 
52
66
  def exclude_by_method?
@@ -21,8 +21,13 @@ module Ahoy
21
21
  end
22
22
 
23
23
  def set_ahoy_cookies
24
- ahoy.set_visitor_cookie
25
- ahoy.set_visit_cookie
24
+ if Ahoy.cookies
25
+ ahoy.set_visitor_cookie
26
+ ahoy.set_visit_cookie
27
+ else
28
+ # delete cookies if exist
29
+ ahoy.reset
30
+ end
26
31
  end
27
32
 
28
33
  def track_ahoy_visit
@@ -2,7 +2,9 @@ module Ahoy
2
2
  module Model
3
3
  def visitable(name = :visit, **options)
4
4
  class_eval do
5
- belongs_to(name, optional: true, class_name: "Ahoy::Visit", **options)
5
+ safe_options = options.dup
6
+ safe_options[:optional] = true if Rails::VERSION::MAJOR >= 5
7
+ belongs_to(name, class_name: "Ahoy::Visit", **safe_options)
6
8
  before_create :set_ahoy_visit
7
9
  end
8
10
  class_eval %{
@@ -1,5 +1,9 @@
1
+ require "active_support/core_ext/digest/uuid"
2
+
1
3
  module Ahoy
2
4
  class Tracker
5
+ UUID_NAMESPACE = "a82ae811-5011-45ab-a728-569df7499c5f"
6
+
3
7
  attr_reader :request, :controller
4
8
 
5
9
  def initialize(**options)
@@ -102,7 +106,7 @@ module Ahoy
102
106
  end
103
107
 
104
108
  def new_visit?
105
- !existing_visit_token
109
+ Ahoy.cookies ? !existing_visit_token : visit.nil?
106
110
  end
107
111
 
108
112
  def new_visitor?
@@ -156,7 +160,7 @@ module Ahoy
156
160
  end
157
161
 
158
162
  def missing_params?
159
- if api? && Ahoy.protect_from_forgery
163
+ if Ahoy.cookies && api? && Ahoy.protect_from_forgery
160
164
  !(existing_visit_token && existing_visitor_token)
161
165
  else
162
166
  false
@@ -164,6 +168,9 @@ module Ahoy
164
168
  end
165
169
 
166
170
  def set_cookie(name, value, duration = nil, use_domain = true)
171
+ # safety net
172
+ return unless Ahoy.cookies
173
+
167
174
  cookie = {
168
175
  value: value
169
176
  }
@@ -174,7 +181,7 @@ module Ahoy
174
181
  end
175
182
 
176
183
  def delete_cookie(name)
177
- request.cookie_jar.delete(name)
184
+ request.cookie_jar.delete(name) if request.cookie_jar[name]
178
185
  end
179
186
 
180
187
  def trusted_time(time = nil)
@@ -200,6 +207,7 @@ module Ahoy
200
207
  def visit_token_helper
201
208
  @visit_token_helper ||= begin
202
209
  token = existing_visit_token
210
+ token ||= visit_anonymity_set unless Ahoy.cookies
203
211
  token ||= generate_id unless Ahoy.api_only
204
212
  token
205
213
  end
@@ -208,6 +216,7 @@ module Ahoy
208
216
  def visitor_token_helper
209
217
  @visitor_token_helper ||= begin
210
218
  token = existing_visitor_token
219
+ token ||= visitor_anonymity_set unless Ahoy.cookies
211
220
  token ||= generate_id unless Ahoy.api_only
212
221
  token
213
222
  end
@@ -216,7 +225,7 @@ module Ahoy
216
225
  def existing_visit_token
217
226
  @existing_visit_token ||= begin
218
227
  token = visit_header
219
- token ||= visit_cookie unless api? && Ahoy.protect_from_forgery
228
+ token ||= visit_cookie if Ahoy.cookies && !(api? && Ahoy.protect_from_forgery)
220
229
  token ||= visit_param if api?
221
230
  token
222
231
  end
@@ -225,12 +234,20 @@ module Ahoy
225
234
  def existing_visitor_token
226
235
  @existing_visitor_token ||= begin
227
236
  token = visitor_header
228
- token ||= visitor_cookie unless api? && Ahoy.protect_from_forgery
237
+ token ||= visitor_cookie if Ahoy.cookies && !(api? && Ahoy.protect_from_forgery)
229
238
  token ||= visitor_param if api?
230
239
  token
231
240
  end
232
241
  end
233
242
 
243
+ def visit_anonymity_set
244
+ @visit_anonymity_set ||= Digest::UUID.uuid_v5(UUID_NAMESPACE, ["visit", Ahoy.mask_ip(request.remote_ip), request.user_agent].join("/"))
245
+ end
246
+
247
+ def visitor_anonymity_set
248
+ @visitor_anonymity_set ||= Digest::UUID.uuid_v5(UUID_NAMESPACE, ["visitor", Ahoy.mask_ip(request.remote_ip), request.user_agent].join("/"))
249
+ end
250
+
234
251
  def visit_cookie
235
252
  @visit_cookie ||= request && request.cookies["ahoy_visit"]
236
253
  end
@@ -1,3 +1,3 @@
1
1
  module Ahoy
2
- VERSION = "2.0.2"
2
+ VERSION = "2.1.0"
3
3
  end
@@ -1,4 +1,5 @@
1
1
  require "browser"
2
+ require "device_detector"
2
3
  require "referer-parser"
3
4
  require "user_agent_parser"
4
5
 
@@ -36,43 +37,73 @@ module Ahoy
36
37
 
37
38
  {
38
39
  referring_domain: (Addressable::URI.parse(referrer).host.first(255) rescue nil),
39
- search_keyword: (@@referrer_parser.parse(@referrer)[:term][0..255] rescue nil).presence
40
+ search_keyword: (@@referrer_parser.parse(@referrer)[:term].first(255) rescue nil).presence
40
41
  }
41
42
  end
42
43
 
43
44
  def tech_properties
44
- # cache for performance
45
- @@user_agent_parser ||= UserAgentParser::Parser.new
45
+ if Ahoy.user_agent_parser == :device_detector
46
+ client = DeviceDetector.new(request.user_agent)
47
+ device_type =
48
+ case client.device_type
49
+ when "smartphone"
50
+ "Mobile"
51
+ when "tv"
52
+ "TV"
53
+ else
54
+ client.device_type.try(:titleize)
55
+ end
46
56
 
47
- user_agent = request.user_agent
48
- agent = @@user_agent_parser.parse(user_agent)
49
- browser = Browser.new(user_agent)
50
- device_type =
51
- if browser.bot?
52
- "Bot"
53
- elsif browser.device.tv?
54
- "TV"
55
- elsif browser.device.console?
56
- "Console"
57
- elsif browser.device.tablet?
58
- "Tablet"
59
- elsif browser.device.mobile?
60
- "Mobile"
61
- else
62
- "Desktop"
63
- end
57
+ {
58
+ browser: client.name,
59
+ os: client.os_name,
60
+ device_type: device_type
61
+ }
62
+ else
63
+ # cache for performance
64
+ @@user_agent_parser ||= UserAgentParser::Parser.new
64
65
 
65
- {
66
- browser: agent.name,
67
- os: agent.os.name,
68
- device_type: device_type,
69
- }
66
+ user_agent = request.user_agent
67
+ agent = @@user_agent_parser.parse(user_agent)
68
+ browser = Browser.new(user_agent)
69
+ device_type =
70
+ if browser.bot?
71
+ "Bot"
72
+ elsif browser.device.tv?
73
+ "TV"
74
+ elsif browser.device.console?
75
+ "Console"
76
+ elsif browser.device.tablet?
77
+ "Tablet"
78
+ elsif browser.device.mobile?
79
+ "Mobile"
80
+ else
81
+ "Desktop"
82
+ end
83
+
84
+ {
85
+ browser: agent.name,
86
+ os: agent.os.name,
87
+ device_type: device_type
88
+ }
89
+ end
90
+ end
91
+
92
+ # masking based on Google Analytics anonymization
93
+ # https://support.google.com/analytics/answer/2763052
94
+ def ip
95
+ ip = request.remote_ip
96
+ if ip && Ahoy.mask_ips
97
+ Ahoy.mask_ip(ip)
98
+ else
99
+ ip
100
+ end
70
101
  end
71
102
 
72
103
  def request_properties
73
104
  {
74
- ip: request.remote_ip,
75
- user_agent: request.user_agent,
105
+ ip: ip,
106
+ user_agent: ensure_utf8(request.user_agent),
76
107
  referrer: referrer,
77
108
  landing_page: landing_page,
78
109
  platform: params["platform"],
@@ -82,5 +113,11 @@ module Ahoy
82
113
  screen_width: params["screen_width"]
83
114
  }
84
115
  end
116
+
117
+ def ensure_utf8(str)
118
+ if str
119
+ str.encode("UTF-8", "binary", invalid: :replace, undef: :replace, replace: "")
120
+ end
121
+ end
85
122
  end
86
123
  end