sojourn 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/README.md +118 -0
- data/Rakefile +1 -0
- data/lib/generators/sojourn/install_generator.rb +35 -0
- data/lib/generators/sojourn/templates/config_initializer.rb +37 -0
- data/lib/generators/sojourn/templates/create_sojourn_browsers.rb +13 -0
- data/lib/generators/sojourn/templates/create_sojourn_campaigns.rb +10 -0
- data/lib/generators/sojourn/templates/create_sojourn_events.rb +17 -0
- data/lib/generators/sojourn/templates/create_sojourn_requests.rb +19 -0
- data/lib/sojourn.rb +24 -0
- data/lib/sojourn/browser.rb +27 -0
- data/lib/sojourn/campaign.rb +27 -0
- data/lib/sojourn/configuration.rb +28 -0
- data/lib/sojourn/controller.rb +26 -0
- data/lib/sojourn/event.rb +22 -0
- data/lib/sojourn/request.rb +42 -0
- data/lib/sojourn/serializers/symbol.rb +13 -0
- data/lib/sojourn/session_stores/cookie.rb +49 -0
- data/lib/sojourn/tracker.rb +76 -0
- data/lib/sojourn/version.rb +3 -0
- data/sojourn.gemspec +28 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 05e30ba3490b5cb18a66e3ecf4bdb0048e0ce529
|
4
|
+
data.tar.gz: 055a2accda2519d306ed020f9df334694cea68c0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8c8eaa338c482230664097aae7910ebae14f9ebace498dc9feff819d96fce1b66e65546c1416f3e23e3dee6342c0936fe209e081eb11a1a0aeff62de82a74947
|
7
|
+
data.tar.gz: 70f60a8e7ba6a81055acd0306736b4feff026eed0f2dbac10d18510bd2e6e4dec6711a9c96eacac5eef6767ef12e1ce2b042d0739f31285ec95bdb9286f25f24
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# Sojourn
|
2
|
+
|
3
|
+
Simple source & event tracking for Rails. This gem automatically tracks *sojourners*
|
4
|
+
(i.e. unique visitors) based on:
|
5
|
+
|
6
|
+
* Referer
|
7
|
+
* UTM parameters
|
8
|
+
* Browser (User Agent)
|
9
|
+
* The currently logged-in user (i.e. `current_user`)
|
10
|
+
* Various other request data
|
11
|
+
|
12
|
+
## How It Works
|
13
|
+
|
14
|
+
**Everything is tracked in the form of events.** Yep, events all the way down.
|
15
|
+
(See 'Why Events?' below for the reasoning behind this.)
|
16
|
+
|
17
|
+
Sojourn assigns each *sojourner* a UUID, which is tracked across requests. All events are
|
18
|
+
associated with this UUID and with the current user's ID (if logged-in).
|
19
|
+
|
20
|
+
Events (`Sojourn::Event`) consist of a name, a set of properties (key-value hash) and information
|
21
|
+
about the request. In the current ActiveRecord implementation, requests (`Sojourn::Request`) can
|
22
|
+
be queried separately and may have many events. Requests also track browser (`Sojourn::Browser`)
|
23
|
+
and campaign (`Sojourn::Campaign`) info as unique models. See 'Usage' below for the details
|
24
|
+
of these models.
|
25
|
+
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
# Track a custom event (highly encouraged!):
|
31
|
+
sojourn.track! 'clicked call-to-action', plan_choice: 'enterprise'
|
32
|
+
|
33
|
+
e = Sojourn::Event.last
|
34
|
+
e.name # event name (e.g. 'clicked call-to-action')
|
35
|
+
e.sojourner_uuid # uuid tracked across requests, stored in cookie
|
36
|
+
e.user # User or nil
|
37
|
+
e.properties # key-value hash (e.g. "{ plan_choice: 'enterprise' }")
|
38
|
+
e.request # Sojourn::Request object
|
39
|
+
|
40
|
+
r = Sojourn::Request.last
|
41
|
+
r.referer
|
42
|
+
r.host
|
43
|
+
r.path
|
44
|
+
r.controller
|
45
|
+
r.action
|
46
|
+
r.params
|
47
|
+
r.method
|
48
|
+
r.ip_address
|
49
|
+
r.campaign # Sojourn::Campaign object (nil if no campaign detected)
|
50
|
+
r.browser # Sojourn::Browser object
|
51
|
+
|
52
|
+
b = Sojourn::Browser.last
|
53
|
+
b.known? # whether or not the browser was detected successfully
|
54
|
+
b.user_agent
|
55
|
+
b.name
|
56
|
+
b.version
|
57
|
+
b.platform
|
58
|
+
b.bot?
|
59
|
+
|
60
|
+
c = Sojourn::Campaign.last
|
61
|
+
c.path # Base path (e.g. '/posts/2')
|
62
|
+
c.params # Notable (tracked) params, sorted. (Typically utm-style, but configurable.)
|
63
|
+
|
64
|
+
```
|
65
|
+
|
66
|
+
## Default Events
|
67
|
+
|
68
|
+
The three built-in events are as follows:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
'!sojourning' # The sojourner has arrived from an external source.
|
72
|
+
'!logged_in' # The sojourner has logged-in.
|
73
|
+
'!logged_out' # The sojourner has logged-out.
|
74
|
+
```
|
75
|
+
|
76
|
+
A `'!sojourning'` event takes place whenever any of the following is true:
|
77
|
+
|
78
|
+
* The sojourner has never been seen before (i.e. direct traffic of some kind)
|
79
|
+
* The referer is from an external source (i.e. not the current `request.host`)
|
80
|
+
* The request contains tracked (utm-style) parameters. (These can be configured in the `sojourn.rb`
|
81
|
+
initializer.)
|
82
|
+
|
83
|
+
|
84
|
+
## Installation
|
85
|
+
|
86
|
+
Add this line to your application's Gemfile:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
gem 'sojourn'
|
90
|
+
```
|
91
|
+
|
92
|
+
And then execute:
|
93
|
+
|
94
|
+
$ bundle
|
95
|
+
|
96
|
+
To install migrations and the `sojourn.rb` initializer, execute:
|
97
|
+
|
98
|
+
$ rails g sojourn:install
|
99
|
+
|
100
|
+
## Why Events? Why not track visits/visitors as their own objects?
|
101
|
+
|
102
|
+
The idea is that, at a certain scale, this kind of tracking should be dumped directly into
|
103
|
+
append-only logs (or an event bus / messaging queue) for asynchronous processing.
|
104
|
+
|
105
|
+
This is made easier when everything can be represented, at a basic level, as discrete events.
|
106
|
+
In theory, it works with just about any data store, and makes for easy time series and funnel
|
107
|
+
analysis. I'd like to move away from ActiveRecord at some point and open up the door for other,
|
108
|
+
highly scalable data backends.
|
109
|
+
|
110
|
+
## Current Limitations (i.e. the 'todo' list)
|
111
|
+
|
112
|
+
* Tested only on rails 3.2.18 and ruby 2.0.0 with ActiveRecord and PostgreSQL.
|
113
|
+
* Assumes `User` and `current_user` convention for user tracking.
|
114
|
+
* Assumes that if `request.referer` does not match `request.host`, the referer is external to your
|
115
|
+
website.
|
116
|
+
* Relies solely on cookies to track visitor UUID across requests (no JS, fingerprinting, etc)
|
117
|
+
* Relies on ActiveRecord for storage. (At a bigger scale, append-only logs are preferred)
|
118
|
+
* There are no tests.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
module Sojourn
|
3
|
+
module Generators
|
4
|
+
class InstallGenerator < Rails::Generators::Base
|
5
|
+
include Rails::Generators::Migration
|
6
|
+
|
7
|
+
desc 'Copies sojourn migrations to your file.'
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
|
10
|
+
def self.next_migration_number(dirname)
|
11
|
+
next_migration_number = current_migration_number(dirname) + 1
|
12
|
+
if ::ActiveRecord::Base.timestamped_migrations
|
13
|
+
[Time.now.utc.strftime('%Y%m%d%H%M%S'), format('%.14d', next_migration_number)].max
|
14
|
+
else
|
15
|
+
format('%.3d', next_migration_number)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_config_file
|
20
|
+
template 'config_initializer.rb', 'config/initializers/sojourn.rb'
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_migrations
|
24
|
+
%w(campaigns browsers requests events).map { |m| "create_sojourn_#{m}" }.each do |name|
|
25
|
+
if self.class.migration_exists?('db/migrate', name)
|
26
|
+
say " #{set_color('skip', :yellow)} #{name}.rb (migration already exists)"
|
27
|
+
else
|
28
|
+
migration_template "#{name}.rb", "db/migrate/#{name}.rb"
|
29
|
+
sleep 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
Sojourn.configure do |config|
|
2
|
+
|
3
|
+
# A new '!sojourning' event is created whenever:
|
4
|
+
# 1. The 'referer' is external
|
5
|
+
# 2. There are utm-style parameters in the request.
|
6
|
+
# 3. The visitor is new and has never been assigned a 'sojourner_id' (i.e. direct traffic)
|
7
|
+
# The two events '!logged_in' and '!logged_out' are created whenever sojourn detects a
|
8
|
+
# change to `current_user`.
|
9
|
+
|
10
|
+
# To disable all automatic event tracking, uncomment the following line:
|
11
|
+
# config.tracking_enabled = false
|
12
|
+
|
13
|
+
# You may customize the list of tracked (utm-style) parameters here.
|
14
|
+
# Note: Using tracked params on internal links within your site is NOT recommended, as this will
|
15
|
+
# result in many repeated '!sojourning' events. Instead, you should use a different set
|
16
|
+
# of parameters (such as 'from', 'source', or 'referer') and NOT include them here.
|
17
|
+
# Default: [:utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign]
|
18
|
+
#
|
19
|
+
# config.campaign_params += [:my_custom_tracking_param]
|
20
|
+
|
21
|
+
# By default, sojourn will attach `user_id`, `sojourner_uuid`, and a `created_at` timestamp
|
22
|
+
# to every event. If you would like to add additional properties, you may do so as follows.
|
23
|
+
# Note: This block will be called in the context of your controller.
|
24
|
+
#
|
25
|
+
# config.default_properties do |p|
|
26
|
+
# p[:rails_env] = Rails.env
|
27
|
+
# p[:my_custom_property] = method_defined_in_controller
|
28
|
+
# end
|
29
|
+
|
30
|
+
# By default, sojourn uses a signed, permanent cookie to store the sojourner uuid. You
|
31
|
+
# may specifiy an alternate/custom session_store, or change the name of the cookie that
|
32
|
+
# gets created. (default is `:_sojourn`)
|
33
|
+
#
|
34
|
+
# config.session_store = Sojourn::SessionStores::Cookie
|
35
|
+
# config.cookie_name = :my_custom_cookie_name
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateSojournBrowsers < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :sojourn_browsers do |t|
|
4
|
+
t.text :user_agent, unique: true
|
5
|
+
t.string :name
|
6
|
+
t.string :version
|
7
|
+
t.string :platform
|
8
|
+
t.boolean :known
|
9
|
+
t.boolean :bot
|
10
|
+
end
|
11
|
+
add_index :sojourn_browsers, [:user_agent]
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class CreateSojournCampaigns < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :sojourn_campaigns do |t|
|
4
|
+
t.string :path, limit: 2048
|
5
|
+
t.string :params, limit: 2048
|
6
|
+
t.timestamp :created_at
|
7
|
+
end
|
8
|
+
add_index :sojourn_campaigns, [:path, :params]
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateSojournEvents < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :sojourn_events do |t|
|
4
|
+
t.string :sojourner_uuid, limit: 36, null: false
|
5
|
+
t.string :name
|
6
|
+
t.text :properties
|
7
|
+
t.references :sojourn_request
|
8
|
+
t.references :user
|
9
|
+
t.timestamp :created_at
|
10
|
+
end
|
11
|
+
add_index :sojourn_events, [:sojourner_uuid]
|
12
|
+
add_index :sojourn_events, [:sojourn_request_id]
|
13
|
+
add_index :sojourn_events, [:user_id]
|
14
|
+
add_index :sojourn_events, [:created_at]
|
15
|
+
add_index :sojourn_events, [:name]
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateSojournRequests < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :sojourn_requests do |t|
|
4
|
+
t.references :sojourn_campaign
|
5
|
+
t.references :sojourn_browser
|
6
|
+
t.string :host, limit: 2048
|
7
|
+
t.string :path, limit: 2048
|
8
|
+
t.string :method
|
9
|
+
t.string :controller
|
10
|
+
t.string :action
|
11
|
+
t.string :ip_address
|
12
|
+
t.text :params
|
13
|
+
t.text :referer
|
14
|
+
t.timestamp :created_at
|
15
|
+
end
|
16
|
+
add_index :sojourn_requests, [:sojourn_campaign_id]
|
17
|
+
add_index :sojourn_requests, [:sojourn_browser_id]
|
18
|
+
end
|
19
|
+
end
|
data/lib/sojourn.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'sojourn/version'
|
2
|
+
require_relative 'sojourn/configuration'
|
3
|
+
require_relative 'sojourn/controller'
|
4
|
+
|
5
|
+
module Sojourn
|
6
|
+
def self.table_name_prefix
|
7
|
+
'sojourn_'
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.configure(&block)
|
11
|
+
block.call(config)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.config
|
15
|
+
@config ||= Configuration.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.tables_exist?
|
19
|
+
@tables_exist ||= %w(sojourn_events sojourn_requests sojourn_browsers sojourn_campaigns)
|
20
|
+
.map { |t| ActiveRecord::Base.connection.table_exists?(t) }.all?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
ActionController::Base.send :include, Sojourn::Controller
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'browser'
|
2
|
+
|
3
|
+
module Sojourn
|
4
|
+
class Browser < ActiveRecord::Base
|
5
|
+
has_many :requests, foreign_key: :sojourn_browser_id
|
6
|
+
has_many :events, through: :requests
|
7
|
+
|
8
|
+
def self.from_request(request)
|
9
|
+
where(user_agent: request.user_agent).first_or_initialize
|
10
|
+
end
|
11
|
+
|
12
|
+
before_validation do
|
13
|
+
self.name ||= browser.name
|
14
|
+
self.version ||= browser.version
|
15
|
+
self.platform ||= browser.platform
|
16
|
+
self.known ||= browser.known?
|
17
|
+
self.bot ||= browser.bot?
|
18
|
+
true # otherwise .valid? will return false
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def browser
|
24
|
+
@browser ||= ::Browser.new(user_agent: user_agent)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Sojourn
|
2
|
+
class Campaign < ActiveRecord::Base
|
3
|
+
|
4
|
+
has_many :requests, foreign_key: :sojourn_campaign_id
|
5
|
+
has_many :events, through: :requests
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def from_request(request)
|
10
|
+
return unless (params = tracked_params(request.params)).any?
|
11
|
+
where(path: request.path.downcase.try(:truncate, 2048))
|
12
|
+
.where(params: params.to_param.try(:truncate, 2048)).first_or_initialize
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def tracked_params(params)
|
18
|
+
Hash[filter_params(params).sort.map { |k, v| [k.downcase, v.downcase] }]
|
19
|
+
end
|
20
|
+
|
21
|
+
def filter_params(params)
|
22
|
+
params.slice(*Sojourn.config.campaign_params).delete_if { |_, v| v.blank? }
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'session_stores/cookie'
|
2
|
+
|
3
|
+
class Configuration
|
4
|
+
|
5
|
+
attr_accessor :campaign_params
|
6
|
+
attr_accessor :session_store
|
7
|
+
attr_accessor :cookie_name
|
8
|
+
attr_accessor :tracking_enabled
|
9
|
+
attr_accessor :default_properties_block
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
set_defaults
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_properties(&block)
|
16
|
+
self.default_properties_block = block
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def set_defaults
|
22
|
+
self.campaign_params = [:utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign]
|
23
|
+
self.session_store = Sojourn::SessionStores::Cookie
|
24
|
+
self.cookie_name = :_sojourn
|
25
|
+
self.tracking_enabled = true
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'tracker'
|
2
|
+
|
3
|
+
module Sojourn
|
4
|
+
module Controller
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.before_filter :track_sojourning
|
8
|
+
base.before_filter :save_sojourn_session
|
9
|
+
end
|
10
|
+
|
11
|
+
def sojourn
|
12
|
+
@sojourn ||= Tracker.new(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def track_sojourning
|
18
|
+
sojourn.sojourning!
|
19
|
+
end
|
20
|
+
|
21
|
+
def save_sojourn_session
|
22
|
+
sojourn.update_session!
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Sojourn
|
2
|
+
class Event < ActiveRecord::Base
|
3
|
+
DEFAULT_FIELDS = [:id, :sojourner_uuid, :name, :properties, :sojourn_request_id, :user_id, :created_at]
|
4
|
+
|
5
|
+
belongs_to :request, foreign_key: :sojourn_request_id
|
6
|
+
belongs_to :user
|
7
|
+
has_one :campaign, through: :request
|
8
|
+
has_one :browser, through: :request
|
9
|
+
|
10
|
+
serialize :properties
|
11
|
+
|
12
|
+
before_save do
|
13
|
+
properties.keys.map(&:to_sym).each do |key|
|
14
|
+
send("#{key}=", properties[key]) if self.class.available_fields.include?(key)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.available_fields
|
19
|
+
@available_fields ||= column_names.map(&:to_sym) - DEFAULT_FIELDS
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative 'campaign'
|
2
|
+
require_relative 'browser'
|
3
|
+
require_relative 'serializers/symbol'
|
4
|
+
require 'addressable/uri'
|
5
|
+
|
6
|
+
module Sojourn
|
7
|
+
class Request < ActiveRecord::Base
|
8
|
+
attr_accessor :user_agent
|
9
|
+
|
10
|
+
serialize :method, Serializers::Symbol
|
11
|
+
serialize :params
|
12
|
+
|
13
|
+
belongs_to :campaign, foreign_key: :sojourn_campaign_id
|
14
|
+
belongs_to :browser, foreign_key: :sojourn_browser_id
|
15
|
+
has_many :events, foreign_key: :sojourn_request_id
|
16
|
+
|
17
|
+
def self.from_request(request)
|
18
|
+
new referer: request.referer.try(:truncate, 2048),
|
19
|
+
host: request.host.try(:truncate, 2048),
|
20
|
+
path: request.path.try(:truncate, 2048),
|
21
|
+
controller: request.params[:controller],
|
22
|
+
action: request.params[:action],
|
23
|
+
params: request.filtered_parameters.with_indifferent_access.except(:controller, :action),
|
24
|
+
method: request.request_method_symbol,
|
25
|
+
ip_address: request.remote_ip,
|
26
|
+
user_agent: request.user_agent
|
27
|
+
end
|
28
|
+
|
29
|
+
before_validation do
|
30
|
+
self.campaign ||= Campaign.from_request(self)
|
31
|
+
self.browser ||= Browser.from_request(self) if user_agent
|
32
|
+
end
|
33
|
+
|
34
|
+
def outside_referer?
|
35
|
+
referer.present? && Addressable::URI.parse(referer).host != host
|
36
|
+
end
|
37
|
+
|
38
|
+
def any_utm_data?
|
39
|
+
Sojourn.config.campaign_params.map { |p| params[p].present? }.any?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Sojourn
|
2
|
+
module SessionStores
|
3
|
+
class Cookie
|
4
|
+
|
5
|
+
def initialize(ctx)
|
6
|
+
@ctx = ctx
|
7
|
+
end
|
8
|
+
|
9
|
+
def sojourner_uuid
|
10
|
+
cookie_data[:uuid]
|
11
|
+
end
|
12
|
+
|
13
|
+
def sojourner_uuid=(value)
|
14
|
+
cookies.permanent.signed[cookie_name] = { value: cookie_data.merge(uuid: value) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def sojourner_tracked?
|
18
|
+
cookie_data.key?(:uuid)
|
19
|
+
end
|
20
|
+
|
21
|
+
def user_id
|
22
|
+
cookie_data[:user_id]
|
23
|
+
end
|
24
|
+
|
25
|
+
def user_id=(value)
|
26
|
+
cookies.permanent.signed[cookie_name] = { value: cookie_data.merge(user_id: value) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def user_tracked?
|
30
|
+
cookie_data.key?(:user_id)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def cookie_data
|
36
|
+
cookies.signed[cookie_name] || {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def cookies
|
40
|
+
@ctx.send(:cookies)
|
41
|
+
end
|
42
|
+
|
43
|
+
def cookie_name
|
44
|
+
@cookie_name ||= Sojourn.config.cookie_name
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative 'request'
|
2
|
+
require_relative 'event'
|
3
|
+
|
4
|
+
module Sojourn
|
5
|
+
class Tracker
|
6
|
+
attr_accessor :ctx
|
7
|
+
delegate :current_user, to: :ctx
|
8
|
+
|
9
|
+
def initialize(ctx)
|
10
|
+
self.ctx = ctx
|
11
|
+
end
|
12
|
+
|
13
|
+
def track!(event_name, properties = {}, user_id = current_user_id)
|
14
|
+
return unless Sojourn.tables_exist?
|
15
|
+
properties = default_event_properties.merge(properties)
|
16
|
+
Event.create! sojourner_uuid: sojourner_uuid, name: event_name, request: request,
|
17
|
+
properties: properties, user_id: user_id
|
18
|
+
end
|
19
|
+
|
20
|
+
def sojourning!
|
21
|
+
return unless Sojourn.config.tracking_enabled && Sojourn.tables_exist?
|
22
|
+
track!('!sojourning') if sojourning?
|
23
|
+
track_user_change! if user_changed?
|
24
|
+
end
|
25
|
+
|
26
|
+
def track_user_change!
|
27
|
+
return unless user_changed?
|
28
|
+
track!('!logged_out', {}, session.user_id) if session.user_id
|
29
|
+
track!('!logged_in', {}, current_user_id) if current_user_id
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_session!
|
33
|
+
session.sojourner_uuid ||= sojourner_uuid
|
34
|
+
session.user_id = current_user_id
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def request
|
40
|
+
@request ||= Request.from_request(ctx.request)
|
41
|
+
end
|
42
|
+
|
43
|
+
def session
|
44
|
+
@session ||= Sojourn.config.session_store.new(ctx)
|
45
|
+
end
|
46
|
+
|
47
|
+
def sojourner_uuid
|
48
|
+
@sojourner_uuid ||= session.sojourner_uuid || SecureRandom.uuid
|
49
|
+
end
|
50
|
+
|
51
|
+
def sojourning?
|
52
|
+
request.outside_referer? || request.any_utm_data? || !session.sojourner_tracked?
|
53
|
+
end
|
54
|
+
|
55
|
+
def user_changed?
|
56
|
+
session.user_tracked? && session.user_id != current_user_id
|
57
|
+
end
|
58
|
+
|
59
|
+
def current_user_id
|
60
|
+
current_user.try(:id)
|
61
|
+
end
|
62
|
+
|
63
|
+
def default_event_properties
|
64
|
+
@default_event_properties ||= fetch_default_properties
|
65
|
+
end
|
66
|
+
|
67
|
+
def fetch_default_properties(properties = {})
|
68
|
+
if Sojourn.config.default_properties_block
|
69
|
+
@ctx.define_singleton_method :sojourn_event_properties,
|
70
|
+
Sojourn.config.default_properties_block
|
71
|
+
end
|
72
|
+
@ctx.sojourn_event_properties(properties) if @ctx.respond_to? :sojourn_event_properties
|
73
|
+
properties
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/sojourn.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'sojourn/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'sojourn'
|
7
|
+
spec.version = Sojourn::VERSION
|
8
|
+
spec.authors = ['Smudge']
|
9
|
+
spec.email = ['nathan@ngriffith.com']
|
10
|
+
spec.summary = 'Simple user source tracking for Rails.'
|
11
|
+
spec.description = %(
|
12
|
+
Sojourn tracks site visitors and sources, with the ability to recognise
|
13
|
+
multiple sources per visitor. Each time a new source is detected,
|
14
|
+
sojourn tracks the referer, utm data, and logged-in user (if any)).gsub("\n", ' ')
|
15
|
+
spec.homepage = ''
|
16
|
+
spec.license = ''
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0")
|
19
|
+
spec.executables = spec.files.grep(/^bin/) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)/)
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
24
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
25
|
+
|
26
|
+
spec.add_dependency 'browser', '>= 0.8.0'
|
27
|
+
spec.add_dependency 'addressable', '>= 2.3.1'
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sojourn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.7
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Smudge
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: browser
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.8.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.8.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: addressable
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.3.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.3.1
|
69
|
+
description: " Sojourn tracks site visitors and sources, with the ability to recognise
|
70
|
+
multiple sources per visitor. Each time a new source is detected, sojourn tracks
|
71
|
+
the referer, utm data, and logged-in user (if any)"
|
72
|
+
email:
|
73
|
+
- nathan@ngriffith.com
|
74
|
+
executables: []
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- ".gitignore"
|
79
|
+
- Gemfile
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- lib/generators/sojourn/install_generator.rb
|
83
|
+
- lib/generators/sojourn/templates/config_initializer.rb
|
84
|
+
- lib/generators/sojourn/templates/create_sojourn_browsers.rb
|
85
|
+
- lib/generators/sojourn/templates/create_sojourn_campaigns.rb
|
86
|
+
- lib/generators/sojourn/templates/create_sojourn_events.rb
|
87
|
+
- lib/generators/sojourn/templates/create_sojourn_requests.rb
|
88
|
+
- lib/sojourn.rb
|
89
|
+
- lib/sojourn/browser.rb
|
90
|
+
- lib/sojourn/campaign.rb
|
91
|
+
- lib/sojourn/configuration.rb
|
92
|
+
- lib/sojourn/controller.rb
|
93
|
+
- lib/sojourn/event.rb
|
94
|
+
- lib/sojourn/request.rb
|
95
|
+
- lib/sojourn/serializers/symbol.rb
|
96
|
+
- lib/sojourn/session_stores/cookie.rb
|
97
|
+
- lib/sojourn/tracker.rb
|
98
|
+
- lib/sojourn/version.rb
|
99
|
+
- sojourn.gemspec
|
100
|
+
homepage: ''
|
101
|
+
licenses:
|
102
|
+
- ''
|
103
|
+
metadata: {}
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 2.2.2
|
121
|
+
signing_key:
|
122
|
+
specification_version: 4
|
123
|
+
summary: Simple user source tracking for Rails.
|
124
|
+
test_files: []
|
125
|
+
has_rdoc:
|