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 +5 -5
- data/CHANGELOG.md +8 -0
- data/Gemfile +1 -1
- data/README.md +102 -4
- data/ahoy_matey.gemspec +1 -0
- data/lib/ahoy.rb +22 -0
- data/lib/ahoy/base_store.rb +15 -1
- data/lib/ahoy/controller.rb +7 -2
- data/lib/ahoy/model.rb +3 -1
- data/lib/ahoy/tracker.rb +22 -5
- data/lib/ahoy/version.rb +1 -1
- data/lib/ahoy/visit_properties.rb +64 -27
- data/lib/generators/ahoy/templates/active_record_migration.rb +1 -2
- data/lib/generators/ahoy/templates/database_store_initializer.rb +3 -0
- data/vendor/assets/javascripts/ahoy.js +458 -561
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f0b1c52401a07fcd017af2e52aca841090ee47a1330c9e2c941d19a82fd2b6d7
|
4
|
+
data.tar.gz: 0d9ddec6c4bbe552672dc41cc444cffc2fa0ffda7b4a6a63200bd2de8d8e65e6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a8c9083093116b8931861b9f180902038871d76e6a363833bd10d55bbd9e8ea89346f62a609ec3990da3232b24e151e075fa4fd3641a247850e18c40db30d912
|
7
|
+
data.tar.gz: a5fe8df81cf9510e8472cda07ff31e2c85d7b2af2102655303623a6677937fb922706db9f2cabac78e49aaf132936c19834be09c3e8acda4ebea3eec7ef82063
|
data/CHANGELOG.md
CHANGED
@@ -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
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
|
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
|
-
|
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](
|
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](
|
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)
|
data/ahoy_matey.gemspec
CHANGED
@@ -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"
|
data/lib/ahoy.rb
CHANGED
@@ -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
|
data/lib/ahoy/base_store.rb
CHANGED
@@ -46,7 +46,21 @@ module Ahoy
|
|
46
46
|
protected
|
47
47
|
|
48
48
|
def bot?
|
49
|
-
|
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?
|
data/lib/ahoy/controller.rb
CHANGED
@@ -21,8 +21,13 @@ module Ahoy
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def set_ahoy_cookies
|
24
|
-
|
25
|
-
|
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
|
data/lib/ahoy/model.rb
CHANGED
@@ -2,7 +2,9 @@ module Ahoy
|
|
2
2
|
module Model
|
3
3
|
def visitable(name = :visit, **options)
|
4
4
|
class_eval do
|
5
|
-
|
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 %{
|
data/lib/ahoy/tracker.rb
CHANGED
@@ -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
|
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
|
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
|
data/lib/ahoy/version.rb
CHANGED
@@ -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]
|
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
|
-
|
45
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
67
|
-
|
68
|
-
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:
|
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
|