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 +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
|