sojourn 0.0.7
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 +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:
|