ahoy_matey 2.2.0 → 2.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/ankane/ahoy.svg?branch=master)](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
|