ahoy_matey 2.0.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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