ahoy_matey 2.2.0 → 2.2.1
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 +4 -4
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +1 -1
- data/README.md +72 -27
- data/lib/ahoy.rb +1 -1
- data/lib/ahoy/base_store.rb +3 -2
- data/lib/ahoy/version.rb +1 -1
- data/lib/generators/ahoy/templates/active_record_migration.rb.tt +2 -0
- data/lib/generators/ahoy/templates/mongoid_visit_model.rb.tt +3 -6
- data/vendor/assets/javascripts/ahoy.js +93 -35
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7b0851dfc7538c6768d6f694d7e8356f0aef3e3ff66ba361447e000a2db5bac
|
4
|
+
data.tar.gz: c28eb30ab780037d83550d359a36d6790e3a16f60b314b5d0be170d11b3c427b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85fb407054564104efc83ae3d425c6253db7495d6f6d4156c741156f64fcd7fe048035f78e4563bf9ec92c69c7e6dfef41e29f7d1031174705b17c595ef96572
|
7
|
+
data.tar.gz: ff03757afbb0deca1a11dc4ece02310bbd8a03f644f675c2eee32c1bf7711013f0cd38eb0fd009541b97b3a5b914a756279e88dbe0cbc8a8bd14ed064d648089
|
data/CHANGELOG.md
CHANGED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -8,6 +8,8 @@ Track visits and events in Ruby, JavaScript, and native apps. Data is stored in
|
|
8
8
|
|
9
9
|
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
|
10
10
|
|
11
|
+
[](https://travis-ci.org/ankane/ahoy)
|
12
|
+
|
11
13
|
## Installation
|
12
14
|
|
13
15
|
Add this line to your application’s Gemfile:
|
@@ -42,13 +44,25 @@ Ahoy.api = true
|
|
42
44
|
|
43
45
|
And restart your web server.
|
44
46
|
|
45
|
-
For JavaScript
|
47
|
+
For JavaScript and Rails 6 / Webpacker, run:
|
48
|
+
|
49
|
+
```sh
|
50
|
+
yarn add ahoy.js
|
51
|
+
```
|
52
|
+
|
53
|
+
And add to `app/javascript/packs/application.js`:
|
54
|
+
|
55
|
+
```javascript
|
56
|
+
import ahoy from "ahoy.js";
|
57
|
+
```
|
58
|
+
|
59
|
+
For JavaScript and Rails 5 / Sprockets, add to `app/assets/javascripts/application.js`:
|
46
60
|
|
47
61
|
```javascript
|
48
62
|
//= require ahoy
|
49
63
|
```
|
50
64
|
|
51
|
-
|
65
|
+
Track an event with:
|
52
66
|
|
53
67
|
```javascript
|
54
68
|
ahoy.track("My second event", {language: "JavaScript"});
|
@@ -85,7 +99,11 @@ Prevent certain Rails actions from creating visits with:
|
|
85
99
|
skip_before_action :track_ahoy_visit
|
86
100
|
```
|
87
101
|
|
88
|
-
This is typically useful for APIs.
|
102
|
+
This is typically useful for APIs. If your entire Rails app is an API, you can use:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
Ahoy.api_only = true
|
106
|
+
```
|
89
107
|
|
90
108
|
You can also defer visit tracking to JavaScript. This is useful for preventing bots (that aren’t detected by their user agent) and users with cookies disabled from creating a new visit on each request. `:when_needed` will create visits server-side only when needed by events, and `false` will disable server-side creation completely, discarding events without a visit.
|
91
109
|
|
@@ -154,34 +172,34 @@ Say we want to associate orders with visits. Just add `visitable` to the model.
|
|
154
172
|
|
155
173
|
```ruby
|
156
174
|
class Order < ApplicationRecord
|
157
|
-
visitable
|
175
|
+
visitable :ahoy_visit
|
158
176
|
end
|
159
177
|
```
|
160
178
|
|
161
|
-
When a visitor places an order, the `
|
179
|
+
When a visitor places an order, the `ahoy_visit_id` column is automatically set :tada:
|
162
180
|
|
163
181
|
See where orders are coming from with simple joins:
|
164
182
|
|
165
183
|
```ruby
|
166
|
-
Order.joins(:
|
167
|
-
Order.joins(:
|
168
|
-
Order.joins(:
|
184
|
+
Order.joins(:ahoy_visit).group("referring_domain").count
|
185
|
+
Order.joins(:ahoy_visit).group("city").count
|
186
|
+
Order.joins(:ahoy_visit).group("device_type").count
|
169
187
|
```
|
170
188
|
|
171
|
-
Here’s what the migration to add the `
|
189
|
+
Here’s what the migration to add the `ahoy_visit_id` column should look like:
|
172
190
|
|
173
191
|
```ruby
|
174
|
-
class AddVisitIdToOrders < ActiveRecord::Migration[5.
|
192
|
+
class AddVisitIdToOrders < ActiveRecord::Migration[5.2]
|
175
193
|
def change
|
176
|
-
add_column :orders, :
|
194
|
+
add_column :orders, :ahoy_visit_id, :bigint
|
177
195
|
end
|
178
196
|
end
|
179
197
|
```
|
180
198
|
|
181
|
-
Customize the column
|
199
|
+
Customize the column with:
|
182
200
|
|
183
201
|
```ruby
|
184
|
-
visitable :sign_up_visit
|
202
|
+
visitable :sign_up_visit
|
185
203
|
```
|
186
204
|
|
187
205
|
### Users
|
@@ -236,6 +254,28 @@ class ApplicationController < ActionController::Base
|
|
236
254
|
end
|
237
255
|
```
|
238
256
|
|
257
|
+
#### Knock
|
258
|
+
|
259
|
+
To attach the user with [Knock](https://github.com/nsarno/knock), either include `Knock::Authenticable`in `ApplicationController`:
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
class ApplicationController < ActionController::API
|
263
|
+
include Knock::Authenticable
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
Or include it in Ahoy:
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
Ahoy::BaseController.include Knock::Authenticable
|
271
|
+
```
|
272
|
+
|
273
|
+
And use:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
Ahoy.user_method = ->(controller) { controller.send(:authenticate_entity, "user") }
|
277
|
+
```
|
278
|
+
|
239
279
|
### Exclusions
|
240
280
|
|
241
281
|
Bots are excluded from tracking by default. To include them, use:
|
@@ -303,6 +343,8 @@ Geocoder.configure(
|
|
303
343
|
)
|
304
344
|
```
|
305
345
|
|
346
|
+
If you use Heroku, you can use an unofficial buildpack like [this one](https://github.com/temedica/heroku-buildpack-maxmind-geolite2) to avoid including the database in your repo.
|
347
|
+
|
306
348
|
### Token Generation
|
307
349
|
|
308
350
|
Ahoy uses random UUIDs for visit and visitor tokens by default, but you can use your own generator like [Druuid](https://github.com/recurly/druuid).
|
@@ -465,6 +507,23 @@ end
|
|
465
507
|
|
466
508
|
Two useful methods you can use are `request` and `controller`.
|
467
509
|
|
510
|
+
You can pass additional visit data from JavaScript with: [master]
|
511
|
+
|
512
|
+
```javascript
|
513
|
+
ahoy.configure({visitParams: {referral_code: 123}});
|
514
|
+
```
|
515
|
+
|
516
|
+
And use:
|
517
|
+
|
518
|
+
```ruby
|
519
|
+
class Ahoy::Store < Ahoy::DatabaseStore
|
520
|
+
def track_visit(data)
|
521
|
+
data[:referral_code] = request.parameters[:referral_code]
|
522
|
+
super(data)
|
523
|
+
end
|
524
|
+
end
|
525
|
+
```
|
526
|
+
|
468
527
|
### Use Different Models
|
469
528
|
|
470
529
|
```ruby
|
@@ -570,20 +629,6 @@ Send a `POST` request to `/ahoy/events` with `Content-Type: application/json` an
|
|
570
629
|
}
|
571
630
|
```
|
572
631
|
|
573
|
-
## Webpacker
|
574
|
-
|
575
|
-
For Webpacker, use Yarn to install the JavaScript library:
|
576
|
-
|
577
|
-
```sh
|
578
|
-
yarn add ahoy.js
|
579
|
-
```
|
580
|
-
|
581
|
-
Then include it in your pack.
|
582
|
-
|
583
|
-
```es6
|
584
|
-
import ahoy from "ahoy.js";
|
585
|
-
```
|
586
|
-
|
587
632
|
## Upgrading
|
588
633
|
|
589
634
|
### 2.2
|
data/lib/ahoy.rb
CHANGED
@@ -63,7 +63,7 @@ module Ahoy
|
|
63
63
|
|
64
64
|
mattr_accessor :user_method
|
65
65
|
self.user_method = lambda do |controller|
|
66
|
-
(controller.respond_to?(:current_user) && controller.current_user) || (controller.respond_to?(:current_resource_owner, true) && controller.send(:current_resource_owner)) || nil
|
66
|
+
(controller.respond_to?(:current_user, true) && controller.send(:current_user)) || (controller.respond_to?(:current_resource_owner, true) && controller.send(:current_resource_owner)) || nil
|
67
67
|
end
|
68
68
|
|
69
69
|
mattr_accessor :exclude_method
|
data/lib/ahoy/base_store.rb
CHANGED
@@ -50,10 +50,11 @@ module Ahoy
|
|
50
50
|
@bot = begin
|
51
51
|
if request
|
52
52
|
if Ahoy.user_agent_parser == :device_detector
|
53
|
+
detector = DeviceDetector.new(request.user_agent)
|
53
54
|
if Ahoy.bot_detection_version == 2
|
54
|
-
|
55
|
+
detector.bot? || detector.device_type.nil?
|
55
56
|
else
|
56
|
-
|
57
|
+
detector.bot?
|
57
58
|
end
|
58
59
|
else
|
59
60
|
Browser.new(request.user_agent).bot?
|
data/lib/ahoy/version.rb
CHANGED
@@ -26,6 +26,8 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
|
|
26
26
|
t.string :country
|
27
27
|
t.string :region
|
28
28
|
t.string :city
|
29
|
+
t.decimal :latitude, precision: 10, scale: 8
|
30
|
+
t.decimal :longitude, precision: 11, scale: 8
|
29
31
|
|
30
32
|
# utm parameters
|
31
33
|
t.string :utm_source
|
@@ -16,23 +16,20 @@ class Ahoy::Visit
|
|
16
16
|
field :ip, type: String
|
17
17
|
field :user_agent, type: String
|
18
18
|
field :referrer, type: String
|
19
|
-
field :landing_page, type: String
|
20
|
-
|
21
|
-
# traffic source
|
22
19
|
field :referring_domain, type: String
|
23
|
-
field :
|
20
|
+
field :landing_page, type: String
|
24
21
|
|
25
22
|
# technology
|
26
23
|
field :browser, type: String
|
27
24
|
field :os, type: String
|
28
25
|
field :device_type, type: String
|
29
|
-
field :screen_height, type: Integer
|
30
|
-
field :screen_width, type: Integer
|
31
26
|
|
32
27
|
# location
|
33
28
|
field :country, type: String
|
34
29
|
field :region, type: String
|
35
30
|
field :city, type: String
|
31
|
+
field :latitude, type: Float
|
32
|
+
field :longitude, type: Float
|
36
33
|
|
37
34
|
# utm parameters
|
38
35
|
field :utm_source, type: String
|
@@ -2,7 +2,7 @@
|
|
2
2
|
* Ahoy.js
|
3
3
|
* Simple, powerful JavaScript analytics
|
4
4
|
* https://github.com/ankane/ahoy.js
|
5
|
-
* v0.3.
|
5
|
+
* v0.3.4
|
6
6
|
* MIT License
|
7
7
|
*/
|
8
8
|
|
@@ -12,48 +12,82 @@
|
|
12
12
|
(global.ahoy = factory());
|
13
13
|
}(this, (function () { 'use strict';
|
14
14
|
|
15
|
-
function isUndefined
|
16
|
-
return value === undefined
|
15
|
+
function isUndefined(value) {
|
16
|
+
return value === undefined;
|
17
17
|
}
|
18
18
|
|
19
|
-
function
|
20
|
-
return value ===
|
19
|
+
function isNull(value) {
|
20
|
+
return value === null;
|
21
21
|
}
|
22
22
|
|
23
|
-
function
|
24
|
-
return
|
23
|
+
function isObject(value) {
|
24
|
+
return value === Object(value);
|
25
25
|
}
|
26
26
|
|
27
|
-
function
|
28
|
-
return value
|
29
|
-
typeof value.size === 'number' &&
|
30
|
-
typeof value.type === 'string' &&
|
31
|
-
typeof value.slice === 'function'
|
27
|
+
function isArray(value) {
|
28
|
+
return Array.isArray(value);
|
32
29
|
}
|
33
30
|
|
34
|
-
function
|
35
|
-
return
|
36
|
-
typeof value.lastModified === 'number' &&
|
37
|
-
typeof value.name === 'string'
|
31
|
+
function isDate(value) {
|
32
|
+
return value instanceof Date;
|
38
33
|
}
|
39
34
|
|
40
|
-
function
|
41
|
-
return
|
35
|
+
function isBlob(value) {
|
36
|
+
return (
|
37
|
+
value &&
|
38
|
+
typeof value.size === 'number' &&
|
39
|
+
typeof value.type === 'string' &&
|
40
|
+
typeof value.slice === 'function'
|
41
|
+
);
|
42
42
|
}
|
43
43
|
|
44
|
-
function
|
44
|
+
function isFile(value) {
|
45
|
+
return (
|
46
|
+
isBlob(value) &&
|
47
|
+
(typeof value.lastModifiedDate === 'object' ||
|
48
|
+
typeof value.lastModified === 'number') &&
|
49
|
+
typeof value.name === 'string'
|
50
|
+
);
|
51
|
+
}
|
52
|
+
|
53
|
+
function isFormData(value) {
|
54
|
+
return value instanceof FormData;
|
55
|
+
}
|
56
|
+
|
57
|
+
function objectToFormData(obj, cfg, fd, pre) {
|
58
|
+
if (isFormData(cfg)) {
|
59
|
+
pre = fd;
|
60
|
+
fd = cfg;
|
61
|
+
cfg = null;
|
62
|
+
}
|
63
|
+
|
64
|
+
cfg = cfg || {};
|
65
|
+
cfg.indices = isUndefined(cfg.indices) ? false : cfg.indices;
|
66
|
+
cfg.nulls = isUndefined(cfg.nulls) ? true : cfg.nulls;
|
45
67
|
fd = fd || new FormData();
|
46
68
|
|
47
69
|
if (isUndefined(obj)) {
|
48
|
-
return fd
|
70
|
+
return fd;
|
71
|
+
} else if (isNull(obj)) {
|
72
|
+
if (cfg.nulls) {
|
73
|
+
fd.append(pre, '');
|
74
|
+
}
|
49
75
|
} else if (isArray(obj)) {
|
50
|
-
obj.
|
76
|
+
if (!obj.length) {
|
51
77
|
var key = pre + '[]';
|
52
78
|
|
53
|
-
|
54
|
-
}
|
55
|
-
|
56
|
-
|
79
|
+
fd.append(key, '');
|
80
|
+
} else {
|
81
|
+
obj.forEach(function(value, index) {
|
82
|
+
var key = pre + '[' + (cfg.indices ? index : '') + ']';
|
83
|
+
|
84
|
+
objectToFormData(value, cfg, fd, key);
|
85
|
+
});
|
86
|
+
}
|
87
|
+
} else if (isDate(obj)) {
|
88
|
+
fd.append(pre, obj.toISOString());
|
89
|
+
} else if (isObject(obj) && !isFile(obj) && !isBlob(obj)) {
|
90
|
+
Object.keys(obj).forEach(function(prop) {
|
57
91
|
var value = obj[prop];
|
58
92
|
|
59
93
|
if (isArray(value)) {
|
@@ -62,20 +96,20 @@
|
|
62
96
|
}
|
63
97
|
}
|
64
98
|
|
65
|
-
var key = pre ?
|
99
|
+
var key = pre ? pre + '[' + prop + ']' : prop;
|
66
100
|
|
67
|
-
objectToFormData(value, fd, key);
|
101
|
+
objectToFormData(value, cfg, fd, key);
|
68
102
|
});
|
69
103
|
} else {
|
70
104
|
fd.append(pre, obj);
|
71
105
|
}
|
72
106
|
|
73
|
-
return fd
|
107
|
+
return fd;
|
74
108
|
}
|
75
109
|
|
76
110
|
var objectToFormdata = objectToFormData;
|
77
111
|
|
78
|
-
//
|
112
|
+
// https://www.quirksmode.org/js/cookies.html
|
79
113
|
|
80
114
|
var Cookies = {
|
81
115
|
set: function (name, value, ttl, domain) {
|
@@ -112,13 +146,16 @@
|
|
112
146
|
urlPrefix: "",
|
113
147
|
visitsUrl: "/ahoy/visits",
|
114
148
|
eventsUrl: "/ahoy/events",
|
115
|
-
cookieDomain: null,
|
116
149
|
page: null,
|
117
150
|
platform: "Web",
|
118
151
|
useBeacon: true,
|
119
152
|
startOnReady: true,
|
120
153
|
trackVisits: true,
|
121
|
-
cookies: true
|
154
|
+
cookies: true,
|
155
|
+
cookieDomain: null,
|
156
|
+
headers: {},
|
157
|
+
visitParams: {},
|
158
|
+
withCredentials: false
|
122
159
|
};
|
123
160
|
|
124
161
|
var ahoy = window.ahoy || window.Ahoy || {};
|
@@ -151,8 +188,12 @@
|
|
151
188
|
return config.urlPrefix + config.eventsUrl;
|
152
189
|
}
|
153
190
|
|
191
|
+
function isEmpty(obj) {
|
192
|
+
return Object.keys(obj).length === 0;
|
193
|
+
}
|
194
|
+
|
154
195
|
function canTrackNow() {
|
155
|
-
return (config.useBeacon || config.trackNow) && canStringify && typeof(window.navigator.sendBeacon) !== "undefined";
|
196
|
+
return (config.useBeacon || config.trackNow) && isEmpty(config.headers) && canStringify && typeof(window.navigator.sendBeacon) !== "undefined" && !config.withCredentials;
|
156
197
|
}
|
157
198
|
|
158
199
|
// cookies
|
@@ -220,7 +261,7 @@
|
|
220
261
|
document.readyState === "interactive" || document.readyState === "complete" ? callback() : document.addEventListener("DOMContentLoaded", callback);
|
221
262
|
}
|
222
263
|
|
223
|
-
//
|
264
|
+
// https://stackoverflow.com/a/2117523/1177228
|
224
265
|
function generateId() {
|
225
266
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
226
267
|
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
|
@@ -261,12 +302,22 @@
|
|
261
302
|
contentType: "application/json; charset=utf-8",
|
262
303
|
dataType: "json",
|
263
304
|
beforeSend: CSRFProtection,
|
264
|
-
success: success
|
305
|
+
success: success,
|
306
|
+
headers: config.headers,
|
307
|
+
xhrFields: {
|
308
|
+
withCredentials: config.withCredentials
|
309
|
+
}
|
265
310
|
});
|
266
311
|
} else {
|
267
312
|
var xhr = new XMLHttpRequest();
|
268
313
|
xhr.open("POST", url, true);
|
314
|
+
xhr.withCredentials = config.withCredentials;
|
269
315
|
xhr.setRequestHeader("Content-Type", "application/json");
|
316
|
+
for (var header in config.headers) {
|
317
|
+
if (config.headers.hasOwnProperty(header)) {
|
318
|
+
xhr.setRequestHeader(header, config.headers[header]);
|
319
|
+
}
|
320
|
+
}
|
270
321
|
xhr.onload = function() {
|
271
322
|
if (xhr.status === 200) {
|
272
323
|
success();
|
@@ -285,7 +336,8 @@
|
|
285
336
|
if (config.cookies) {
|
286
337
|
data.visit_token = event.visit_token;
|
287
338
|
data.visitor_token = event.visitor_token;
|
288
|
-
}
|
339
|
+
}
|
340
|
+
delete event.visit_token;
|
289
341
|
delete event.visitor_token;
|
290
342
|
return data;
|
291
343
|
}
|
@@ -402,6 +454,12 @@
|
|
402
454
|
data.referrer = document.referrer;
|
403
455
|
}
|
404
456
|
|
457
|
+
for (var key in config.visitParams) {
|
458
|
+
if (config.visitParams.hasOwnProperty(key)) {
|
459
|
+
data[key] = config.visitParams[key];
|
460
|
+
}
|
461
|
+
}
|
462
|
+
|
405
463
|
log(data);
|
406
464
|
|
407
465
|
sendRequest(visitsUrl(), data, function () {
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ahoy_matey
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.2.
|
4
|
+
version: 2.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-05-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: railties
|
@@ -295,8 +295,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
295
295
|
- !ruby/object:Gem::Version
|
296
296
|
version: '0'
|
297
297
|
requirements: []
|
298
|
-
|
299
|
-
rubygems_version: 2.7.6
|
298
|
+
rubygems_version: 3.0.3
|
300
299
|
signing_key:
|
301
300
|
specification_version: 4
|
302
301
|
summary: Simple, powerful analytics for Rails
|