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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3ef1f6ecc94ec7e579782279c9187ec29be12393
4
- data.tar.gz: 0515d97b0c1916bd94e057de85fb1a1ccb11f8d2
3
+ metadata.gz: c1ddf7a214c83d60f35b1d7a39653ef65d5ab3ee
4
+ data.tar.gz: e3c9af288c825faffda2cb7bf97cd8aa248c41a6
5
5
  SHA512:
6
- metadata.gz: 7fb051cdca6fa1fe250094b03acf393e28c93c93df0a4ca9379fd6aa468478668be22f4cce72316ea5d9db20824d7ea700e627c5f2787f0b9ce24878d2736346
7
- data.tar.gz: 00435c1bb0c2f83fec2eef2bf73d45c908269d793a5c18ebc3de39dd3286c13f33e390a8c83e3d3c05581602836c905be0740a21d381ec695a3bcc968dc584dd
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 visit tracking for Rails
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
- See which campaigns generate the most revenue effortlessly.
14
+ Track events in:
15
15
 
16
- ```ruby
17
- Order.joins(:visit).group("utm_campaign").sum(:revenue)
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
- :seedling: To track events like page views, check out [Ahoy Events](https://github.com/ankane/ahoy_events).
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
- ### Location
138
+ ### Native Apps
137
139
 
138
- Ahoy uses [Geocoder](https://github.com/alexreisner/geocoder) for IP-based geocoding.
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
- ### Multiple Subdomains
147
+ The endpoint will return a JSON response like:
141
148
 
142
- To track visits across multiple subdomains, add this **before** the javascript files.
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
- var ahoy = {"domain": "yourdomain.com"};
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
- ### Development
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
- ### Native Apps [experimental]
171
-
172
- When a user launches the app, create a visit. Send a `POST` request to `/ahoy/visits` with:
257
+ ### More
173
258
 
174
- - platform - `iOS`, `Android`, etc.
175
- - app_version - `1.0.0`
176
- - os_version - `7.0.6`
177
- - visitor_token - if you have one
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
- The endpoint will return a JSON response like:
264
+ ### Doorkeeper
180
265
 
181
- ```json
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
- Send the visit token in the `Ahoy-Visit` header for all requests.
268
+ ```ruby
269
+ class ApplicationController < ActionController::Base
189
270
 
190
- After 4 hours, create another visit and use the updated visit token.
271
+ private
191
272
 
192
- ### More
273
+ def current_resource_owner
274
+ User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
275
+ end
193
276
 
194
- - Excludes bots
195
- - Degrades gracefully when cookies are disabled
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
- Use a different model
282
+ To track visits across multiple subdomains, add this **before** the javascript files.
202
283
 
203
- ```ruby
204
- Ahoy.visit_model = UserVisit
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
- - track emails
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 = current_user if respond_to?(:current_user) and v.respond_to?(: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=)
@@ -0,0 +1,10 @@
1
+ module Ahoy
2
+ class Event < ActiveRecord::Base
3
+ self.table_name = "ahoy_events"
4
+
5
+ belongs_to :visit
6
+ belongs_to :user, polymorphic: true
7
+
8
+ serialize :properties, JSON
9
+ end
10
+ end
data/config/routes.rb CHANGED
@@ -5,5 +5,6 @@ end
5
5
  Ahoy::Engine.routes.draw do
6
6
  scope module: "ahoy" do
7
7
  resources :visits, only: [:create]
8
+ resources :events, only: [:create]
8
9
  end
9
10
  end
@@ -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
- landing_uri = Addressable::URI.parse(landing_page) rescue nil
25
- if landing_uri
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
- browser = Browser.new(ua: user_agent)
32
+ agent = Ahoy.user_agent_parser.parse(user_agent)
37
33
 
38
- self.browser = browser.name if respond_to?(: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 :visit
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
- if !self.class.column_names.include?("visit_id")
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
- end
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Ahoy
2
- VERSION = "0.1.4"
2
+ VERSION = "0.2.0"
3
3
  end
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 && visitToken != "test") {
135
+ if (visitToken && visitorToken) {
71
136
  // TODO keep visit alive?
72
137
  log("Active visit");
73
138
  setReady();
74
139
  } else {
75
- setCookie("ahoy_visit", "test", 1);
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, function(response) {
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.log = log;
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
- ahoy.ready = function (callback) {
126
- if (isReady) {
127
- callback();
128
- } else {
129
- queue.push(callback);
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.1.4
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-04-28 00:00:00.000000000 Z
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