ahoy_matey 0.1.4 → 0.2.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 +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +207 -40
- data/ahoy_matey.gemspec +1 -0
- data/app/controllers/ahoy/events_controller.rb +14 -0
- data/app/controllers/ahoy/visits_controller.rb +2 -2
- data/app/models/ahoy/event.rb +10 -0
- data/config/routes.rb +1 -0
- data/lib/ahoy/controller.rb +5 -0
- data/lib/ahoy/model.rb +24 -38
- data/lib/ahoy/subscribers/active_record.rb +21 -0
- data/lib/ahoy/tracker.rb +30 -0
- data/lib/ahoy/version.rb +1 -1
- data/lib/ahoy_matey.rb +23 -0
- data/lib/generators/ahoy/events/active_record_generator.rb +36 -0
- data/lib/generators/ahoy/events/templates/create_events.rb +20 -0
- data/lib/generators/ahoy/events/templates/initializer.rb +1 -0
- data/vendor/assets/javascripts/ahoy.js +145 -19
- metadata +23 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1ddf7a214c83d60f35b1d7a39653ef65d5ab3ee
|
|
4
|
+
data.tar.gz: e3c9af288c825faffda2cb7bf97cd8aa248c41a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e3c068287e17bc73b51ce57af242a91013720b64dceefb00330a85ac2c586ba9b11b3fdd6395b9d8adf3d952b9dee1a73557d16a6a8792a49fa62d9c1893ba3c
|
|
7
|
+
data.tar.gz: 137594a001bb62a37274b94fe7776074b0880e74ddfdd0d48cc305d67847d13ca056de3f6fe38161d180d13cf22f79d6d363888a949d2d3abbdea8eed056d7cd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## 0.2.0
|
|
2
|
+
|
|
3
|
+
- Added event tracking (merged ahoy_events)
|
|
4
|
+
- Added ahoy.js
|
|
5
|
+
|
|
6
|
+
## 0.1.8
|
|
7
|
+
|
|
8
|
+
- Fixed bug with `user_type` set to `false` instead of `nil`
|
|
9
|
+
|
|
10
|
+
## 0.1.7
|
|
11
|
+
|
|
12
|
+
- Made cookie functions public for ahoy_events
|
|
13
|
+
|
|
14
|
+
## 0.1.6
|
|
15
|
+
|
|
16
|
+
- Better user agent parser
|
|
17
|
+
|
|
18
|
+
## 0.1.5
|
|
19
|
+
|
|
20
|
+
- Added support for Doorkeeper
|
|
21
|
+
- Added options to `visitable`
|
|
22
|
+
- Added `landing_params` method
|
|
23
|
+
|
|
1
24
|
## 0.1.4
|
|
2
25
|
|
|
3
26
|
- Added `ahoy.ready()` and `ahoy.log()` for events
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ahoy
|
|
2
2
|
|
|
3
|
-
:fire: Simple, powerful
|
|
3
|
+
:fire: Simple, powerful analytics for Rails
|
|
4
4
|
|
|
5
5
|
Visits are stored in **your database** so you can easily combine them with other data.
|
|
6
6
|
|
|
@@ -11,13 +11,17 @@ You get:
|
|
|
11
11
|
- **technology** - browser, OS, and device type
|
|
12
12
|
- **utm parameters** - source, medium, term, content, campaign
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Track events in:
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
- JavaScript
|
|
17
|
+
- Ruby
|
|
18
|
+
- Native apps
|
|
19
|
+
|
|
20
|
+
And store them wherever you’d like - your database, logs, external services, or all of them.
|
|
19
21
|
|
|
20
|
-
:
|
|
22
|
+
:postbox: To track emails, check out [Ahoy Email](https://github.com/ankane/ahoy_email).
|
|
23
|
+
|
|
24
|
+
No Ruby? Check out [Ahoy.js](https://github.com/ankane/ahoy.js).
|
|
21
25
|
|
|
22
26
|
## Installation
|
|
23
27
|
|
|
@@ -91,8 +95,6 @@ Order.joins(:visit).group("device_type").count
|
|
|
91
95
|
|
|
92
96
|
Ahoy automatically attaches the `current_user` to the `current_visit`.
|
|
93
97
|
|
|
94
|
-
If you define your own `current_user` method, be sure to add it to `ActionController::Base`, not `ApplicationController`.
|
|
95
|
-
|
|
96
98
|
With [Devise](https://github.com/plataformatec/devise), it will attach the user even if he / she signs in after the visit starts.
|
|
97
99
|
|
|
98
100
|
With other authentication frameworks, add this to the end of your sign in method:
|
|
@@ -133,19 +135,104 @@ or
|
|
|
133
135
|
http://datakick.org/?utm_medium=twitter&utm_campaign=social&utm_source=tweet123
|
|
134
136
|
```
|
|
135
137
|
|
|
136
|
-
###
|
|
138
|
+
### Native Apps
|
|
137
139
|
|
|
138
|
-
|
|
140
|
+
When a user launches the app, create a visit. Send a `POST` request to `/ahoy/visits` with:
|
|
141
|
+
|
|
142
|
+
- platform - `iOS`, `Android`, etc.
|
|
143
|
+
- app_version - `1.0.0`
|
|
144
|
+
- os_version - `7.0.6`
|
|
145
|
+
- visitor_token - if you have one
|
|
139
146
|
|
|
140
|
-
|
|
147
|
+
The endpoint will return a JSON response like:
|
|
141
148
|
|
|
142
|
-
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"visit_token": "8tx2ziymkwa1WlppnkqxyaBaRlXrEQ3K",
|
|
152
|
+
"visitor_token": "hYBIV0rBfrIUAiArWweiECt4N9pyiygN"
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Send the visit token in the `Ahoy-Visit` header for all requests.
|
|
157
|
+
|
|
158
|
+
After 4 hours, create another visit and use the updated visit token.
|
|
159
|
+
|
|
160
|
+
## Events
|
|
161
|
+
|
|
162
|
+
Each event has a `name` and `properties`.
|
|
163
|
+
|
|
164
|
+
There are three ways to track events.
|
|
165
|
+
|
|
166
|
+
#### JavaScript
|
|
143
167
|
|
|
144
168
|
```javascript
|
|
145
|
-
|
|
169
|
+
ahoy.track("Viewed book", {title: "The World is Flat"});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
or track all views and clicks with:
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
ahoy.trackAll();
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### Ruby
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
ahoy.track "Viewed book", title: "Hot, Flat, and Crowded"
|
|
146
182
|
```
|
|
147
183
|
|
|
148
|
-
|
|
184
|
+
#### Native Apps
|
|
185
|
+
|
|
186
|
+
Send a `POST` request to `/ahoy/events` with:
|
|
187
|
+
|
|
188
|
+
- name
|
|
189
|
+
- properties
|
|
190
|
+
- user token (depends on your authentication framework)
|
|
191
|
+
- `Ahoy-Visit` header
|
|
192
|
+
|
|
193
|
+
Requests should have `Content-Type: application/json`.
|
|
194
|
+
|
|
195
|
+
### Storing Events
|
|
196
|
+
|
|
197
|
+
You choose how to store events.
|
|
198
|
+
|
|
199
|
+
#### ActiveRecord
|
|
200
|
+
|
|
201
|
+
Create an `Ahoy::Event` model to store events.
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
rails generate ahoy:events:active_record
|
|
205
|
+
rake db:migrate
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### Custom
|
|
209
|
+
|
|
210
|
+
Create your own subscribers in `config/initializers/ahoy.rb`.
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
class LogSubscriber
|
|
214
|
+
|
|
215
|
+
def track(name, properties, options = {})
|
|
216
|
+
data = {
|
|
217
|
+
name: name,
|
|
218
|
+
properties: properties,
|
|
219
|
+
time: options[:time].to_i,
|
|
220
|
+
visit_id: options[:visit].try(:id),
|
|
221
|
+
user_id: options[:user].try(:id),
|
|
222
|
+
ip: options[:controller].try(:request).try(:remote_ip)
|
|
223
|
+
}
|
|
224
|
+
Rails.logger.info data.to_json
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# and add it
|
|
230
|
+
Ahoy.subscribers << LogSubscriber.new
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Add as many subscribers as you’d like.
|
|
234
|
+
|
|
235
|
+
## Development
|
|
149
236
|
|
|
150
237
|
Ahoy is built with developers in mind. You can run the following code in your browser’s console.
|
|
151
238
|
|
|
@@ -167,41 +254,35 @@ Turn off logging
|
|
|
167
254
|
ahoy.debug(false);
|
|
168
255
|
```
|
|
169
256
|
|
|
170
|
-
###
|
|
171
|
-
|
|
172
|
-
When a user launches the app, create a visit. Send a `POST` request to `/ahoy/visits` with:
|
|
257
|
+
### More
|
|
173
258
|
|
|
174
|
-
-
|
|
175
|
-
-
|
|
176
|
-
-
|
|
177
|
-
-
|
|
259
|
+
- Excludes bots
|
|
260
|
+
- Degrades gracefully when cookies are disabled
|
|
261
|
+
- Don’t need a field? Just remove it from the migration
|
|
262
|
+
- Visits are 4 hours by default
|
|
178
263
|
|
|
179
|
-
|
|
264
|
+
### Doorkeeper
|
|
180
265
|
|
|
181
|
-
|
|
182
|
-
{
|
|
183
|
-
"visit_token": "8tx2ziymkwa1WlppnkqxyaBaRlXrEQ3K",
|
|
184
|
-
"visitor_token": "hYBIV0rBfrIUAiArWweiECt4N9pyiygN"
|
|
185
|
-
}
|
|
186
|
-
```
|
|
266
|
+
To attach the user with [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper), be sure you have a `current_resource_owner` method in `ApplicationController`.
|
|
187
267
|
|
|
188
|
-
|
|
268
|
+
```ruby
|
|
269
|
+
class ApplicationController < ActionController::Base
|
|
189
270
|
|
|
190
|
-
|
|
271
|
+
private
|
|
191
272
|
|
|
192
|
-
|
|
273
|
+
def current_resource_owner
|
|
274
|
+
User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
|
|
275
|
+
end
|
|
193
276
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
- Don’t need a field? Just remove it from the migration
|
|
197
|
-
- Visits are 4 hours by default
|
|
277
|
+
end
|
|
278
|
+
```
|
|
198
279
|
|
|
199
280
|
## Reference
|
|
200
281
|
|
|
201
|
-
|
|
282
|
+
To track visits across multiple subdomains, add this **before** the javascript files.
|
|
202
283
|
|
|
203
|
-
```
|
|
204
|
-
|
|
284
|
+
```javascript
|
|
285
|
+
var ahoy = {"domain": "yourdomain.com"};
|
|
205
286
|
```
|
|
206
287
|
|
|
207
288
|
Change the platform on the web
|
|
@@ -210,11 +291,97 @@ Change the platform on the web
|
|
|
210
291
|
var ahoy = {"platform": "Mobile Web"}
|
|
211
292
|
```
|
|
212
293
|
|
|
294
|
+
Track additional values
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
class Visit < ActiveRecord::Base
|
|
298
|
+
ahoy_visit
|
|
299
|
+
|
|
300
|
+
before_create :set_gclid
|
|
301
|
+
|
|
302
|
+
def set_gclid
|
|
303
|
+
self.gclid = landing_params["gclid"]
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Use a method besides `current_user`
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
Ahoy.user_method = :true_user
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
or use a Proc
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
Ahoy.user_method = proc {|controller| controller.current_user }
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Customize visitable
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
visitable :sign_up_visit, class_name: "Visit"
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Track view
|
|
328
|
+
|
|
329
|
+
```javascript
|
|
330
|
+
ahoy.trackView();
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Track clicks
|
|
334
|
+
|
|
335
|
+
```javascript
|
|
336
|
+
ahoy.trackClicks();
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Track all Rails actions
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
class ApplicationController < ActionController::Base
|
|
343
|
+
after_filter :track_action
|
|
344
|
+
|
|
345
|
+
protected
|
|
346
|
+
|
|
347
|
+
def track_action
|
|
348
|
+
ahoy.track "Hit action", request.filtered_parameters
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Use a different model for visits
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
Ahoy.visit_model = UserVisit
|
|
357
|
+
|
|
358
|
+
# fix for Rails reloader in development
|
|
359
|
+
ActionDispatch::Reloader.to_prepare do
|
|
360
|
+
Ahoy.visit_model = UserVisit
|
|
361
|
+
end
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Use a different model for events
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
Ahoy.subscribers << Ahoy::Subscribers::ActiveRecord.new(model: Event)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Upgrading
|
|
371
|
+
|
|
372
|
+
In `0.1.6`, a big improvement was made to `browser` and `os`. Update existing visits with:
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
Visit.find_each do |visit|
|
|
376
|
+
visit.set_technology
|
|
377
|
+
visit.save! if visit.changed?
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
|
|
213
381
|
## TODO
|
|
214
382
|
|
|
215
|
-
-
|
|
383
|
+
- better readme
|
|
216
384
|
- simple dashboard
|
|
217
|
-
- hook to store additional fields
|
|
218
385
|
- turn off modules
|
|
219
386
|
|
|
220
387
|
## History
|
data/ahoy_matey.gemspec
CHANGED
|
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
spec.add_dependency "browser", ">= 0.4.0"
|
|
23
23
|
spec.add_dependency "geocoder"
|
|
24
24
|
spec.add_dependency "referer-parser"
|
|
25
|
+
spec.add_dependency "user_agent_parser"
|
|
25
26
|
spec.add_dependency "request_store"
|
|
26
27
|
|
|
27
28
|
spec.add_development_dependency "bundler", "~> 1.5"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Ahoy
|
|
2
|
+
class EventsController < Ahoy::BaseController
|
|
3
|
+
|
|
4
|
+
def create
|
|
5
|
+
options = {}
|
|
6
|
+
if params[:time] and (time = Time.at(params[:time].to_f) rescue nil) and (1.minute.ago..Time.now).cover?(time)
|
|
7
|
+
options[:time] = time
|
|
8
|
+
end
|
|
9
|
+
ahoy.track params[:name], params[:properties], options
|
|
10
|
+
render json: {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -2,7 +2,7 @@ module Ahoy
|
|
|
2
2
|
class VisitsController < BaseController
|
|
3
3
|
|
|
4
4
|
def create
|
|
5
|
-
visit_token = generate_token
|
|
5
|
+
visit_token = params[:visit_token] || generate_token
|
|
6
6
|
visitor_token = params[:visitor_token] || generate_token
|
|
7
7
|
|
|
8
8
|
visit =
|
|
@@ -13,7 +13,7 @@ module Ahoy
|
|
|
13
13
|
v.user_agent = request.user_agent if v.respond_to?(:user_agent=)
|
|
14
14
|
v.referrer = params[:referrer] if v.respond_to?(:referrer=)
|
|
15
15
|
v.landing_page = params[:landing_page] if v.respond_to?(:landing_page=)
|
|
16
|
-
v.user =
|
|
16
|
+
v.user = Ahoy.fetch_user(self) if v.respond_to?(:user=)
|
|
17
17
|
v.platform = params[:platform] if v.respond_to?(:platform=)
|
|
18
18
|
v.app_version = params[:app_version] if v.respond_to?(:app_version=)
|
|
19
19
|
v.os_version = params[:os_version] if v.respond_to?(:os_version=)
|
data/config/routes.rb
CHANGED
data/lib/ahoy/controller.rb
CHANGED
|
@@ -3,6 +3,7 @@ module Ahoy
|
|
|
3
3
|
|
|
4
4
|
def self.included(base)
|
|
5
5
|
base.helper_method :current_visit
|
|
6
|
+
base.helper_method :ahoy
|
|
6
7
|
base.before_filter do
|
|
7
8
|
RequestStore.store[:ahoy_controller] ||= self
|
|
8
9
|
end
|
|
@@ -15,5 +16,9 @@ module Ahoy
|
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
|
|
19
|
+
def ahoy
|
|
20
|
+
@ahoy ||= Ahoy::Tracker.new(controller: self)
|
|
21
|
+
end
|
|
22
|
+
|
|
18
23
|
end
|
|
19
24
|
end
|
data/lib/ahoy/model.rb
CHANGED
|
@@ -21,42 +21,20 @@ module Ahoy
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def set_utm_parameters
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
query_values = landing_uri.query_values || {}
|
|
27
|
-
%w[utm_source utm_medium utm_term utm_content utm_campaign].each do |name|
|
|
28
|
-
self[name] = query_values[name] if respond_to?(:"#{name}=")
|
|
29
|
-
end
|
|
24
|
+
%w[utm_source utm_medium utm_term utm_content utm_campaign].each do |name|
|
|
25
|
+
self[name] = landing_params[name] if respond_to?(:"#{name}=")
|
|
30
26
|
end
|
|
31
27
|
true
|
|
32
28
|
end
|
|
33
29
|
|
|
34
30
|
def set_technology
|
|
35
31
|
if respond_to?(:user_agent)
|
|
36
|
-
|
|
32
|
+
agent = Ahoy.user_agent_parser.parse(user_agent)
|
|
37
33
|
|
|
38
|
-
self.browser =
|
|
39
|
-
|
|
40
|
-
# TODO add more
|
|
41
|
-
self.os =
|
|
42
|
-
if browser.android?
|
|
43
|
-
"Android"
|
|
44
|
-
elsif browser.ios?
|
|
45
|
-
"iOS"
|
|
46
|
-
elsif browser.windows_phone?
|
|
47
|
-
"Windows Phone"
|
|
48
|
-
elsif browser.blackberry?
|
|
49
|
-
"Blackberry"
|
|
50
|
-
elsif browser.chrome_os?
|
|
51
|
-
"Chrome OS"
|
|
52
|
-
elsif browser.mac?
|
|
53
|
-
"Mac"
|
|
54
|
-
elsif browser.windows?
|
|
55
|
-
"Windows"
|
|
56
|
-
elsif browser.linux?
|
|
57
|
-
"Linux"
|
|
58
|
-
end if respond_to?(:os=)
|
|
34
|
+
self.browser = agent.name if respond_to?(:browser=)
|
|
35
|
+
self.os = agent.os.name if respond_to?(:os=)
|
|
59
36
|
|
|
37
|
+
browser = Browser.new(ua: user_agent)
|
|
60
38
|
self.device_type =
|
|
61
39
|
if browser.tv?
|
|
62
40
|
"TV"
|
|
@@ -92,23 +70,31 @@ module Ahoy
|
|
|
92
70
|
true
|
|
93
71
|
end
|
|
94
72
|
|
|
73
|
+
def landing_params
|
|
74
|
+
@landing_params ||= begin
|
|
75
|
+
landing_uri = Addressable::URI.parse(landing_page) rescue nil
|
|
76
|
+
ActiveSupport::HashWithIndifferentAccess.new((landing_uri && landing_uri.query_values) || {})
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
95
80
|
end # end class_eval
|
|
96
81
|
end
|
|
97
82
|
|
|
98
|
-
def visitable
|
|
83
|
+
def visitable(name = nil, options = {})
|
|
84
|
+
if name.is_a?(Hash)
|
|
85
|
+
name = nil
|
|
86
|
+
options = name
|
|
87
|
+
end
|
|
88
|
+
name ||= :visit
|
|
99
89
|
class_eval do
|
|
100
|
-
belongs_to
|
|
101
|
-
|
|
90
|
+
belongs_to name, options
|
|
102
91
|
before_create :set_visit
|
|
103
|
-
|
|
92
|
+
end
|
|
93
|
+
class_eval %Q{
|
|
104
94
|
def set_visit
|
|
105
|
-
|
|
106
|
-
raise "Add a visit_id column to this table to use visitable"
|
|
107
|
-
else
|
|
108
|
-
self.visit ||= RequestStore.store[:ahoy_controller].try(:send, :current_visit)
|
|
109
|
-
end
|
|
95
|
+
self.#{name} ||= RequestStore.store[:ahoy_controller].try(:send, :current_visit)
|
|
110
96
|
end
|
|
111
|
-
|
|
97
|
+
}
|
|
112
98
|
end
|
|
113
99
|
|
|
114
100
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Ahoy
|
|
2
|
+
module Subscribers
|
|
3
|
+
class ActiveRecord
|
|
4
|
+
|
|
5
|
+
def initialize(options = {})
|
|
6
|
+
@model = options[:model] || Ahoy::Event
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def track(name, properties, options = {})
|
|
10
|
+
@model.create! do |e|
|
|
11
|
+
e.visit = options[:visit]
|
|
12
|
+
e.user = options[:user]
|
|
13
|
+
e.name = name
|
|
14
|
+
e.properties = properties
|
|
15
|
+
e.time = options[:time]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/ahoy/tracker.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Ahoy
|
|
2
|
+
class Tracker
|
|
3
|
+
|
|
4
|
+
def initialize(options = {})
|
|
5
|
+
@controller = options[:controller]
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def track(name, properties, options = {})
|
|
9
|
+
# publish to each subscriber
|
|
10
|
+
if @controller
|
|
11
|
+
options[:controller] ||= @controller
|
|
12
|
+
options[:user] ||= Ahoy.fetch_user(@controller)
|
|
13
|
+
if @controller.respond_to?(:current_visit)
|
|
14
|
+
options[:visit] ||= @controller.current_visit
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
options[:time] ||= Time.zone.now
|
|
18
|
+
|
|
19
|
+
subscribers = Ahoy.subscribers
|
|
20
|
+
if subscribers.any?
|
|
21
|
+
subscribers.each do |subscriber|
|
|
22
|
+
subscriber.track(name, properties, options)
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
$stderr.puts "No subscribers"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/ahoy/version.rb
CHANGED
data/lib/ahoy_matey.rb
CHANGED
|
@@ -2,10 +2,13 @@ require "addressable/uri"
|
|
|
2
2
|
require "browser"
|
|
3
3
|
require "geocoder"
|
|
4
4
|
require "referer-parser"
|
|
5
|
+
require "user_agent_parser"
|
|
5
6
|
require "request_store"
|
|
6
7
|
require "ahoy/version"
|
|
8
|
+
require "ahoy/tracker"
|
|
7
9
|
require "ahoy/controller"
|
|
8
10
|
require "ahoy/model"
|
|
11
|
+
require "ahoy/subscribers/active_record"
|
|
9
12
|
require "ahoy/engine"
|
|
10
13
|
|
|
11
14
|
module Ahoy
|
|
@@ -24,6 +27,26 @@ module Ahoy
|
|
|
24
27
|
@referrer_parser ||= RefererParser::Referer.new("https://github.com/ankane/ahoy")
|
|
25
28
|
end
|
|
26
29
|
|
|
30
|
+
# performance
|
|
31
|
+
def self.user_agent_parser
|
|
32
|
+
@user_agent_parser ||= UserAgentParser::Parser.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.fetch_user(controller)
|
|
36
|
+
if user_method.respond_to?(:call)
|
|
37
|
+
user_method.call(controller)
|
|
38
|
+
else
|
|
39
|
+
controller.send(user_method)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
mattr_accessor :user_method
|
|
44
|
+
self.user_method = proc do |controller|
|
|
45
|
+
(controller.respond_to?(:current_user) && controller.current_user) || (controller.respond_to?(:current_resource_owner, true) && controller.send(:current_resource_owner)) || nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
mattr_accessor :subscribers
|
|
49
|
+
self.subscribers = []
|
|
27
50
|
end
|
|
28
51
|
|
|
29
52
|
ActionController::Base.send :include, Ahoy::Controller
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# taken from https://github.com/collectiveidea/audited/blob/master/lib/generators/audited/install_generator.rb
|
|
2
|
+
require "rails/generators"
|
|
3
|
+
require "rails/generators/migration"
|
|
4
|
+
require "active_record"
|
|
5
|
+
require "rails/generators/active_record"
|
|
6
|
+
|
|
7
|
+
module Ahoy
|
|
8
|
+
module Events
|
|
9
|
+
module Generators
|
|
10
|
+
class ActiveRecordGenerator < Rails::Generators::Base
|
|
11
|
+
include Rails::Generators::Migration
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path("../templates", __FILE__)
|
|
14
|
+
|
|
15
|
+
# Implement the required interface for Rails::Generators::Migration.
|
|
16
|
+
def self.next_migration_number(dirname) #:nodoc:
|
|
17
|
+
next_migration_number = current_migration_number(dirname) + 1
|
|
18
|
+
if ActiveRecord::Base.timestamped_migrations
|
|
19
|
+
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
|
20
|
+
else
|
|
21
|
+
"%.3d" % next_migration_number
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def copy_migration
|
|
26
|
+
migration_template "create_events.rb", "db/migrate/create_ahoy_events.rb"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_initializer
|
|
30
|
+
template "initializer.rb", "config/initializers/ahoy.rb"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
|
2
|
+
def change
|
|
3
|
+
create_table :ahoy_events do |t|
|
|
4
|
+
# visit
|
|
5
|
+
t.references :visit
|
|
6
|
+
|
|
7
|
+
# user
|
|
8
|
+
t.integer :user_id
|
|
9
|
+
t.string :user_type
|
|
10
|
+
|
|
11
|
+
t.string :name
|
|
12
|
+
t.text :properties
|
|
13
|
+
t.timestamp :time
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :ahoy_events, [:visit_id]
|
|
17
|
+
add_index :ahoy_events, [:user_id, :user_type]
|
|
18
|
+
add_index :ahoy_events, [:time]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Ahoy.subscribers << Ahoy::Subscribers::ActiveRecord.new
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
var visitorTtl = 2 * 365 * 24 * 60; // 2 years
|
|
11
11
|
var isReady = false;
|
|
12
12
|
var queue = [];
|
|
13
|
+
var canStringify = typeof(JSON) !== "undefined" && typeof(JSON.stringify) !== "undefined";
|
|
14
|
+
var eventQueue = [];
|
|
13
15
|
|
|
14
16
|
// cookies
|
|
15
17
|
|
|
@@ -62,25 +64,98 @@
|
|
|
62
64
|
isReady = true;
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
function ready(callback) {
|
|
68
|
+
if (isReady) {
|
|
69
|
+
callback();
|
|
70
|
+
} else {
|
|
71
|
+
queue.push(callback);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// https://github.com/klughammer/node-randomstring
|
|
76
|
+
function generateId() {
|
|
77
|
+
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghiklmnopqrstuvwxyz';
|
|
78
|
+
var length = 32;
|
|
79
|
+
var string = '';
|
|
80
|
+
|
|
81
|
+
for (var i = 0; i < length; i++) {
|
|
82
|
+
var randomNumber = Math.floor(Math.random() * chars.length);
|
|
83
|
+
string += chars.substring(randomNumber, randomNumber + 1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function saveEventQueue() {
|
|
90
|
+
// TODO add stringify method for IE 7 and under
|
|
91
|
+
if (canStringify) {
|
|
92
|
+
setCookie("ahoy_events", JSON.stringify(eventQueue), 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function trackEvent(event) {
|
|
97
|
+
ready( function () {
|
|
98
|
+
// ensure JSON is defined
|
|
99
|
+
if (canStringify) {
|
|
100
|
+
$.ajax({
|
|
101
|
+
type: "POST",
|
|
102
|
+
url: "/ahoy/events",
|
|
103
|
+
data: JSON.stringify(event),
|
|
104
|
+
contentType: "application/json; charset=utf-8",
|
|
105
|
+
dataType: "json",
|
|
106
|
+
success: function() {
|
|
107
|
+
// remove from queue
|
|
108
|
+
for (var i = 0; i < eventQueue.length; i++) {
|
|
109
|
+
if (eventQueue[i].id == event.id) {
|
|
110
|
+
eventQueue.splice(i, 1);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
saveEventQueue();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function eventProperties(e) {
|
|
122
|
+
var $target = $(e.currentTarget);
|
|
123
|
+
return {
|
|
124
|
+
tag: $target.get(0).tagName.toLowerCase(),
|
|
125
|
+
id: $target.attr("id"),
|
|
126
|
+
class: $target.attr("class")
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
65
130
|
// main
|
|
66
131
|
|
|
67
132
|
visitToken = getCookie("ahoy_visit");
|
|
68
133
|
visitorToken = getCookie("ahoy_visitor");
|
|
69
134
|
|
|
70
|
-
if (visitToken && visitorToken
|
|
135
|
+
if (visitToken && visitorToken) {
|
|
71
136
|
// TODO keep visit alive?
|
|
72
137
|
log("Active visit");
|
|
73
138
|
setReady();
|
|
74
139
|
} else {
|
|
75
|
-
|
|
140
|
+
visitToken = generateId();
|
|
141
|
+
setCookie("ahoy_visit", visitToken, visitTtl);
|
|
76
142
|
|
|
77
143
|
// make sure cookies are enabled
|
|
78
144
|
if (getCookie("ahoy_visit")) {
|
|
79
145
|
log("Visit started");
|
|
80
146
|
|
|
147
|
+
if (!visitorToken) {
|
|
148
|
+
visitorToken = generateId();
|
|
149
|
+
setCookie("ahoy_visitor", visitorToken, visitorTtl);
|
|
150
|
+
}
|
|
151
|
+
|
|
81
152
|
var data = {
|
|
153
|
+
visit_token: visitToken,
|
|
154
|
+
visitor_token: visitorToken,
|
|
82
155
|
platform: ahoy.platform || "Web",
|
|
83
|
-
landing_page: window.location.href
|
|
156
|
+
landing_page: window.location.href,
|
|
157
|
+
screen_width: window.screen.width,
|
|
158
|
+
screen_height: window.screen.height
|
|
84
159
|
};
|
|
85
160
|
|
|
86
161
|
// referrer
|
|
@@ -88,17 +163,9 @@
|
|
|
88
163
|
data.referrer = document.referrer;
|
|
89
164
|
}
|
|
90
165
|
|
|
91
|
-
if (visitorToken) {
|
|
92
|
-
data.visitor_token = visitorToken;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
166
|
log(data);
|
|
96
167
|
|
|
97
|
-
$.post("/ahoy/visits", data,
|
|
98
|
-
setCookie("ahoy_visit", response.visit_token, visitTtl);
|
|
99
|
-
setCookie("ahoy_visitor", response.visitor_token, visitorTtl);
|
|
100
|
-
setReady();
|
|
101
|
-
}, "json");
|
|
168
|
+
$.post("/ahoy/visits", data, setReady, "json");
|
|
102
169
|
} else {
|
|
103
170
|
log("Cookies disabled");
|
|
104
171
|
setReady();
|
|
@@ -120,15 +187,74 @@
|
|
|
120
187
|
return true;
|
|
121
188
|
};
|
|
122
189
|
|
|
123
|
-
ahoy.
|
|
190
|
+
ahoy.track = function (name, properties) {
|
|
191
|
+
// generate unique id
|
|
192
|
+
var event = {
|
|
193
|
+
id: generateId(),
|
|
194
|
+
name: name,
|
|
195
|
+
properties: properties,
|
|
196
|
+
time: (new Date()).getTime() / 1000.0
|
|
197
|
+
};
|
|
198
|
+
log(event);
|
|
124
199
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
200
|
+
eventQueue.push(event);
|
|
201
|
+
saveEventQueue();
|
|
202
|
+
|
|
203
|
+
// wait in case navigating to reduce duplicate events
|
|
204
|
+
setTimeout( function () {
|
|
205
|
+
trackEvent(event);
|
|
206
|
+
}, 1000);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
ahoy.trackView = function () {
|
|
210
|
+
var properties = {
|
|
211
|
+
url: window.location.href,
|
|
212
|
+
title: document.title
|
|
213
|
+
};
|
|
214
|
+
ahoy.track("$view", properties);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
ahoy.trackClicks = function () {
|
|
218
|
+
$(document).on("click", "a, button, input[type=submit]", function (e) {
|
|
219
|
+
var $target = $(e.currentTarget);
|
|
220
|
+
var properties = eventProperties(e);
|
|
221
|
+
properties.text = properties.tag == "input" ? $target.val() : $.trim($target.text());
|
|
222
|
+
properties.href = $target.attr("href");
|
|
223
|
+
ahoy.track("$click", properties);
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
ahoy.trackSubmits = function () {
|
|
228
|
+
$(document).on("submit", "form", function (e) {
|
|
229
|
+
var properties = eventProperties(e);
|
|
230
|
+
ahoy.track("$submit", properties);
|
|
231
|
+
});
|
|
131
232
|
};
|
|
132
233
|
|
|
234
|
+
ahoy.trackChanges = function () {
|
|
235
|
+
$(document).on("change", "input, textarea, select", function (e) {
|
|
236
|
+
var properties = eventProperties(e);
|
|
237
|
+
ahoy.track("$change", properties);
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
ahoy.trackAll = function() {
|
|
242
|
+
ahoy.trackView();
|
|
243
|
+
ahoy.trackClicks();
|
|
244
|
+
ahoy.trackSubmits();
|
|
245
|
+
ahoy.trackChanges();
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// push events from queue
|
|
249
|
+
try {
|
|
250
|
+
eventQueue = JSON.parse(getCookie("ahoy_events") || "[]");
|
|
251
|
+
} catch (e) {
|
|
252
|
+
// do nothing
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (var i = 0; i < eventQueue.length; i++) {
|
|
256
|
+
trackEvent(eventQueue[i]);
|
|
257
|
+
}
|
|
258
|
+
|
|
133
259
|
window.ahoy = ahoy;
|
|
134
260
|
}(window));
|
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: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kane
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2014-
|
|
11
|
+
date: 2014-05-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: addressable
|
|
@@ -66,6 +66,20 @@ dependencies:
|
|
|
66
66
|
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: user_agent_parser
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
69
83
|
- !ruby/object:Gem::Dependency
|
|
70
84
|
name: request_store
|
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -123,13 +137,20 @@ files:
|
|
|
123
137
|
- Rakefile
|
|
124
138
|
- ahoy_matey.gemspec
|
|
125
139
|
- app/controllers/ahoy/base_controller.rb
|
|
140
|
+
- app/controllers/ahoy/events_controller.rb
|
|
126
141
|
- app/controllers/ahoy/visits_controller.rb
|
|
142
|
+
- app/models/ahoy/event.rb
|
|
127
143
|
- config/routes.rb
|
|
128
144
|
- lib/ahoy/controller.rb
|
|
129
145
|
- lib/ahoy/engine.rb
|
|
130
146
|
- lib/ahoy/model.rb
|
|
147
|
+
- lib/ahoy/subscribers/active_record.rb
|
|
148
|
+
- lib/ahoy/tracker.rb
|
|
131
149
|
- lib/ahoy/version.rb
|
|
132
150
|
- lib/ahoy_matey.rb
|
|
151
|
+
- lib/generators/ahoy/events/active_record_generator.rb
|
|
152
|
+
- lib/generators/ahoy/events/templates/create_events.rb
|
|
153
|
+
- lib/generators/ahoy/events/templates/initializer.rb
|
|
133
154
|
- lib/generators/ahoy/install_generator.rb
|
|
134
155
|
- lib/generators/ahoy/templates/install.rb
|
|
135
156
|
- vendor/assets/javascripts/ahoy.js
|