sojourn 0.0.7 → 0.1.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/.codeclimate.yml +11 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +6 -0
- data/.travis.yml +8 -0
- data/README.md +44 -31
- data/Rakefile +8 -0
- data/lib/generators/sojourn/install_generator.rb +1 -1
- data/lib/generators/sojourn/templates/create_sojourn_events.rb +1 -3
- data/lib/sojourn.rb +5 -1
- data/lib/sojourn/configuration.rb +0 -2
- data/lib/sojourn/controller.rb +0 -2
- data/lib/sojourn/event.rb +4 -5
- data/lib/sojourn/request.rb +108 -27
- data/lib/sojourn/serializers/indifferent_json.rb +13 -0
- data/lib/sojourn/session_stores/cookie.rb +0 -2
- data/lib/sojourn/tracker.rb +6 -2
- data/lib/sojourn/version.rb +1 -1
- data/sojourn.gemspec +6 -0
- data/spec/mocks/controller.rb +15 -0
- data/spec/mocks/cookie.rb +30 -0
- data/spec/mocks/request.rb +56 -0
- data/spec/mocks/user.rb +9 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/tracker_spec.rb +102 -0
- metadata +104 -11
- data/lib/generators/sojourn/templates/create_sojourn_browsers.rb +0 -13
- data/lib/generators/sojourn/templates/create_sojourn_campaigns.rb +0 -10
- data/lib/generators/sojourn/templates/create_sojourn_requests.rb +0 -19
- data/lib/sojourn/browser.rb +0 -27
- data/lib/sojourn/campaign.rb +0 -27
- data/lib/sojourn/serializers/symbol.rb +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5bcd732bdfc1df4fdd68744f4c807f894104848
|
4
|
+
data.tar.gz: 927a8ba15bd0948cb8c390c15b7c900785051a1f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 469867e717db9bc51b25e1b414a711db552631225524e781a2c9a5d62e3faefdf2d8e0f77acd426aaa7ac094b62e35b7361ec5c761ed4dadaddf7ce8bba2d32b
|
7
|
+
data.tar.gz: 44d5346a2ccabc605e220af036077b2f9969213580bc04188852f5d6ee6ff4a2b3a6ea1bf566cefdaa0cde87177092d3892247448bafdc19703cf8f9c96dc3f4
|
data/.codeclimate.yml
ADDED
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Sojourn
|
1
|
+
# Sojourn [](https://travis-ci.org/smudge/sojourn) [](https://codeclimate.com/github/smudge/sojourn) [](https://codeclimate.com/github/smudge/sojourn/coverage)
|
2
2
|
|
3
3
|
Simple source & event tracking for Rails. This gem automatically tracks *sojourners*
|
4
4
|
(i.e. unique visitors) based on:
|
@@ -19,9 +19,7 @@ associated with this UUID and with the current user's ID (if logged-in).
|
|
19
19
|
|
20
20
|
Events (`Sojourn::Event`) consist of a name, a set of properties (key-value hash) and information
|
21
21
|
about the request. In the current ActiveRecord implementation, requests (`Sojourn::Request`) can
|
22
|
-
be queried separately and may have many events.
|
23
|
-
and campaign (`Sojourn::Campaign`) info as unique models. See 'Usage' below for the details
|
24
|
-
of these models.
|
22
|
+
be queried separately and may have many events. See 'Usage' below for the details of these models.
|
25
23
|
|
26
24
|
|
27
25
|
## Usage
|
@@ -30,37 +28,17 @@ of these models.
|
|
30
28
|
# Track a custom event (highly encouraged!):
|
31
29
|
sojourn.track! 'clicked call-to-action', plan_choice: 'enterprise'
|
32
30
|
|
31
|
+
# If you don't have access to a controller context (i.e. the event is not occurring during a web
|
32
|
+
# request), you can still track a raw event like this:
|
33
|
+
|
34
|
+
Sojourn.track_raw_event! 'subscription expired', plan: 'enterprise', customer_id: 'xyb123'
|
35
|
+
|
36
|
+
# Read events using ActiveRecord
|
33
37
|
e = Sojourn::Event.last
|
34
38
|
e.name # event name (e.g. 'clicked call-to-action')
|
35
39
|
e.sojourner_uuid # uuid tracked across requests, stored in cookie
|
36
40
|
e.user # User or nil
|
37
41
|
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
42
|
```
|
65
43
|
|
66
44
|
## Default Events
|
@@ -81,6 +59,42 @@ A `'!sojourning'` event takes place whenever any of the following is true:
|
|
81
59
|
initializer.)
|
82
60
|
|
83
61
|
|
62
|
+
## Properties
|
63
|
+
|
64
|
+
In addition to properties that you manually add, events will automatically include data about
|
65
|
+
the current web request. An example looks like this:
|
66
|
+
|
67
|
+
```json
|
68
|
+
{
|
69
|
+
"custom_property":"value",
|
70
|
+
"request":{
|
71
|
+
"uuid":"5e698f6ca74a016c49ca6b91a79cada7",
|
72
|
+
"host":"example.com",
|
73
|
+
"path":"/my-news",
|
74
|
+
"controller":"news",
|
75
|
+
"action":"index",
|
76
|
+
"method":"get",
|
77
|
+
"params":{
|
78
|
+
"utm_campaign":"daily_updates",
|
79
|
+
"page":"1"
|
80
|
+
},
|
81
|
+
"referer":"https://mail.google.com",
|
82
|
+
"ip_address":"42.42.42.42",
|
83
|
+
"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.48 Safari/537.36"
|
84
|
+
},
|
85
|
+
"browser":{
|
86
|
+
"bot":false,
|
87
|
+
"name":"Chrome",
|
88
|
+
"known":true,
|
89
|
+
"version":"48",
|
90
|
+
"platform":"mac"
|
91
|
+
},
|
92
|
+
"campaign":{
|
93
|
+
"utm_campaign":"daily_updates"
|
94
|
+
}
|
95
|
+
}
|
96
|
+
```
|
97
|
+
|
84
98
|
## Installation
|
85
99
|
|
86
100
|
Add this line to your application's Gemfile:
|
@@ -115,4 +129,3 @@ highly scalable data backends.
|
|
115
129
|
website.
|
116
130
|
* Relies solely on cookies to track visitor UUID across requests (no JS, fingerprinting, etc)
|
117
131
|
* Relies on ActiveRecord for storage. (At a bigger scale, append-only logs are preferred)
|
118
|
-
* There are no tests.
|
data/Rakefile
CHANGED
@@ -21,7 +21,7 @@ module Sojourn
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def create_migrations
|
24
|
-
%w(
|
24
|
+
%w(events).map { |m| "create_sojourn_#{m}" }.each do |name|
|
25
25
|
if self.class.migration_exists?('db/migrate', name)
|
26
26
|
say " #{set_color('skip', :yellow)} #{name}.rb (migration already exists)"
|
27
27
|
else
|
@@ -3,13 +3,11 @@ class CreateSojournEvents < ActiveRecord::Migration
|
|
3
3
|
create_table :sojourn_events do |t|
|
4
4
|
t.string :sojourner_uuid, limit: 36, null: false
|
5
5
|
t.string :name
|
6
|
-
t.
|
7
|
-
t.references :sojourn_request
|
6
|
+
t.column :properties, :jsonb
|
8
7
|
t.references :user
|
9
8
|
t.timestamp :created_at
|
10
9
|
end
|
11
10
|
add_index :sojourn_events, [:sojourner_uuid]
|
12
|
-
add_index :sojourn_events, [:sojourn_request_id]
|
13
11
|
add_index :sojourn_events, [:user_id]
|
14
12
|
add_index :sojourn_events, [:created_at]
|
15
13
|
add_index :sojourn_events, [:name]
|
data/lib/sojourn.rb
CHANGED
@@ -16,9 +16,13 @@ module Sojourn
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.tables_exist?
|
19
|
-
@tables_exist ||= %w(sojourn_events sojourn_requests
|
19
|
+
@tables_exist ||= %w(sojourn_events sojourn_requests)
|
20
20
|
.map { |t| ActiveRecord::Base.connection.table_exists?(t) }.all?
|
21
21
|
end
|
22
|
+
|
23
|
+
def self.track_raw_event!(name, properties)
|
24
|
+
Event.create! sojourner_uuid: '!unknown', name: name, properties: properties
|
25
|
+
end
|
22
26
|
end
|
23
27
|
|
24
28
|
ActionController::Base.send :include, Sojourn::Controller
|
@@ -1,7 +1,6 @@
|
|
1
1
|
require_relative 'session_stores/cookie'
|
2
2
|
|
3
3
|
class Configuration
|
4
|
-
|
5
4
|
attr_accessor :campaign_params
|
6
5
|
attr_accessor :session_store
|
7
6
|
attr_accessor :cookie_name
|
@@ -24,5 +23,4 @@ private
|
|
24
23
|
self.cookie_name = :_sojourn
|
25
24
|
self.tracking_enabled = true
|
26
25
|
end
|
27
|
-
|
28
26
|
end
|
data/lib/sojourn/controller.rb
CHANGED
@@ -2,7 +2,6 @@ require_relative 'tracker'
|
|
2
2
|
|
3
3
|
module Sojourn
|
4
4
|
module Controller
|
5
|
-
|
6
5
|
def self.included(base)
|
7
6
|
base.before_filter :track_sojourning
|
8
7
|
base.before_filter :save_sojourn_session
|
@@ -21,6 +20,5 @@ module Sojourn
|
|
21
20
|
def save_sojourn_session
|
22
21
|
sojourn.update_session!
|
23
22
|
end
|
24
|
-
|
25
23
|
end
|
26
24
|
end
|
data/lib/sojourn/event.rb
CHANGED
@@ -1,13 +1,12 @@
|
|
1
|
+
require_relative 'serializers/indifferent_json'
|
2
|
+
|
1
3
|
module Sojourn
|
2
4
|
class Event < ActiveRecord::Base
|
3
|
-
DEFAULT_FIELDS =
|
5
|
+
DEFAULT_FIELDS = %i(id sojourner_uuid name properties user_id created_at)
|
4
6
|
|
5
|
-
belongs_to :request, foreign_key: :sojourn_request_id
|
6
7
|
belongs_to :user
|
7
|
-
has_one :campaign, through: :request
|
8
|
-
has_one :browser, through: :request
|
9
8
|
|
10
|
-
serialize :properties
|
9
|
+
serialize :properties, Serializers::IndifferentJSON
|
11
10
|
|
12
11
|
before_save do
|
13
12
|
properties.keys.map(&:to_sym).each do |key|
|
data/lib/sojourn/request.rb
CHANGED
@@ -1,42 +1,123 @@
|
|
1
|
-
|
2
|
-
require_relative 'browser'
|
3
|
-
require_relative 'serializers/symbol'
|
1
|
+
require 'securerandom'
|
4
2
|
require 'addressable/uri'
|
3
|
+
require 'browser'
|
4
|
+
require 'referer-parser'
|
5
5
|
|
6
6
|
module Sojourn
|
7
|
-
class Request
|
8
|
-
|
7
|
+
class Request
|
8
|
+
KEYS = %w(uuid referer host path controller action params method ip_address user_agent)
|
9
9
|
|
10
|
-
|
11
|
-
serialize :params
|
10
|
+
attr_reader :request
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
def initialize(request)
|
13
|
+
@request = request
|
14
|
+
end
|
16
15
|
|
17
|
-
def
|
18
|
-
|
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
|
16
|
+
def outside_referer?
|
17
|
+
referer.present? && referer_host != host
|
27
18
|
end
|
28
19
|
|
29
|
-
|
30
|
-
|
31
|
-
self.browser ||= Browser.from_request(self) if user_agent
|
20
|
+
def any_utm_data?
|
21
|
+
tracked_param_keys.map { |p| downcased_params[p].present? }.any?
|
32
22
|
end
|
33
23
|
|
34
|
-
def
|
35
|
-
|
24
|
+
def tracked_params
|
25
|
+
Hash[downcased_params.slice(*tracked_param_keys).delete_if { |_, v| v.blank? }.sort]
|
36
26
|
end
|
37
27
|
|
38
|
-
def
|
39
|
-
|
28
|
+
def raw_data
|
29
|
+
Hash[KEYS.map { |k| [k, send(k)] }].with_indifferent_access
|
30
|
+
end
|
31
|
+
|
32
|
+
def browser_data
|
33
|
+
return @browser_data if @browser_data
|
34
|
+
@browser_data = {
|
35
|
+
name: browser.name,
|
36
|
+
version: browser.version,
|
37
|
+
platform: browser.platform,
|
38
|
+
bot: browser.bot?,
|
39
|
+
known: browser.known?
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def referer_data
|
44
|
+
return @referer_data if @referer_data
|
45
|
+
p = RefererParser::Parser.new.parse(sanitized_referer)
|
46
|
+
@referer_data = {
|
47
|
+
known: p[:known],
|
48
|
+
host: referer_host,
|
49
|
+
source: p[:source],
|
50
|
+
medium: p[:medium],
|
51
|
+
term: p[:term]
|
52
|
+
}
|
53
|
+
rescue
|
54
|
+
@referer_data = {}
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def uuid
|
60
|
+
@uuid ||= request.uuid || SecureRandom.uuid
|
61
|
+
end
|
62
|
+
|
63
|
+
def referer
|
64
|
+
@referer ||= request.referer.try(:truncate, 2048)
|
65
|
+
end
|
66
|
+
|
67
|
+
def host
|
68
|
+
@host ||= request.host.try(:truncate, 2048)
|
69
|
+
end
|
70
|
+
|
71
|
+
def path
|
72
|
+
@path ||= request.path.try(:truncate, 2048)
|
73
|
+
end
|
74
|
+
|
75
|
+
def controller
|
76
|
+
@controller ||= request.params[:controller]
|
77
|
+
end
|
78
|
+
|
79
|
+
def action
|
80
|
+
@action ||= request.params[:action]
|
81
|
+
end
|
82
|
+
|
83
|
+
def params
|
84
|
+
@params ||= request.filtered_parameters.with_indifferent_access.except(:controller, :action)
|
85
|
+
end
|
86
|
+
|
87
|
+
def method
|
88
|
+
@method ||= request.request_method_symbol
|
89
|
+
end
|
90
|
+
|
91
|
+
def ip_address
|
92
|
+
@ip_address ||= request.remote_ip
|
93
|
+
end
|
94
|
+
|
95
|
+
def user_agent
|
96
|
+
@user_agent ||= request.user_agent
|
97
|
+
end
|
98
|
+
|
99
|
+
def referer_host
|
100
|
+
@referer_host ||= parsed_referer.host
|
101
|
+
end
|
102
|
+
|
103
|
+
def sanitized_referer
|
104
|
+
@sanitized_referer ||= parsed_referer.display_uri
|
105
|
+
end
|
106
|
+
|
107
|
+
def parsed_referer
|
108
|
+
@parsed_referer ||= Addressable::URI.parse(referer)
|
109
|
+
end
|
110
|
+
|
111
|
+
def downcased_params
|
112
|
+
params.each_with_object({}) { |(k, v), h| h[k.to_s.downcase] = v }
|
113
|
+
end
|
114
|
+
|
115
|
+
def tracked_param_keys
|
116
|
+
Sojourn.config.campaign_params.map(&:to_s).map(&:downcase)
|
117
|
+
end
|
118
|
+
|
119
|
+
def browser
|
120
|
+
@browser ||= Browser.new(user_agent: user_agent) if user_agent
|
40
121
|
end
|
41
122
|
end
|
42
123
|
end
|
data/lib/sojourn/tracker.rb
CHANGED
@@ -13,7 +13,7 @@ module Sojourn
|
|
13
13
|
def track!(event_name, properties = {}, user_id = current_user_id)
|
14
14
|
return unless Sojourn.tables_exist?
|
15
15
|
properties = default_event_properties.merge(properties)
|
16
|
-
Event.create! sojourner_uuid: sojourner_uuid, name: event_name,
|
16
|
+
Event.create! sojourner_uuid: sojourner_uuid, name: event_name,
|
17
17
|
properties: properties, user_id: user_id
|
18
18
|
end
|
19
19
|
|
@@ -37,7 +37,7 @@ module Sojourn
|
|
37
37
|
private
|
38
38
|
|
39
39
|
def request
|
40
|
-
@request ||= Request.
|
40
|
+
@request ||= Request.new(ctx.request)
|
41
41
|
end
|
42
42
|
|
43
43
|
def session
|
@@ -70,6 +70,10 @@ module Sojourn
|
|
70
70
|
Sojourn.config.default_properties_block
|
71
71
|
end
|
72
72
|
@ctx.sojourn_event_properties(properties) if @ctx.respond_to? :sojourn_event_properties
|
73
|
+
properties.merge! request: request.raw_data
|
74
|
+
properties.merge! campaign: request.tracked_params if request.tracked_params.any?
|
75
|
+
properties.merge! browser: request.browser_data
|
76
|
+
properties.merge! referer: request.referer_data if request.referer_data.any?
|
73
77
|
properties
|
74
78
|
end
|
75
79
|
end
|
data/lib/sojourn/version.rb
CHANGED
data/sojourn.gemspec
CHANGED
@@ -22,7 +22,13 @@ sojourn tracks the referer, utm data, and logged-in user (if any)).gsub("\n", '
|
|
22
22
|
|
23
23
|
spec.add_development_dependency 'bundler', '~> 1.7'
|
24
24
|
spec.add_development_dependency 'rake', '~> 10.0'
|
25
|
+
spec.add_development_dependency 'rspec', '~> 3.4.0'
|
26
|
+
spec.add_development_dependency 'rspec-its', '~> 1.2.0'
|
27
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3.0'
|
28
|
+
spec.add_development_dependency 'codeclimate-test-reporter', '~> 0.4.8'
|
25
29
|
|
26
30
|
spec.add_dependency 'browser', '>= 0.8.0'
|
27
31
|
spec.add_dependency 'addressable', '>= 2.3.1'
|
32
|
+
spec.add_dependency 'referer-parser', '~> 0.3.0'
|
33
|
+
spec.add_dependency 'rails', '~> 3.2.0'
|
28
34
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'mocks/user'
|
2
|
+
require 'mocks/request'
|
3
|
+
require 'mocks/cookie'
|
4
|
+
|
5
|
+
module Mocks
|
6
|
+
class Controller
|
7
|
+
attr_accessor :current_user, :request, :cookies
|
8
|
+
|
9
|
+
def initialize(user = User.new, request = Request.new, cookies = Cookie.new)
|
10
|
+
self.current_user = user
|
11
|
+
self.request = request
|
12
|
+
self.cookies = cookies
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Credit: https://kylecrum.wordpress.com/2009/07/19/mock-cookies-in-rails-tests-the-easy-way/
|
2
|
+
|
3
|
+
module Mocks
|
4
|
+
class Cookie < Hash
|
5
|
+
def [](name)
|
6
|
+
super(name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def []=(key, options)
|
10
|
+
if options.is_a?(Hash)
|
11
|
+
options.symbolize_keys!
|
12
|
+
else
|
13
|
+
options = { value: options }
|
14
|
+
end
|
15
|
+
super(key, options[:value])
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete(key, _)
|
19
|
+
super(key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def permanent
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def signed
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Mocks
|
4
|
+
class Request
|
5
|
+
CHROME_UA = <<-EOF.squish
|
6
|
+
Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko)
|
7
|
+
Chrome/41.0.2228.0 Safari/537.36
|
8
|
+
EOF
|
9
|
+
|
10
|
+
attr_reader :opts
|
11
|
+
|
12
|
+
def initialize(opts = {})
|
13
|
+
@opts = opts
|
14
|
+
end
|
15
|
+
|
16
|
+
def uuid
|
17
|
+
opts[:uuid] || SecureRandom.uuid
|
18
|
+
end
|
19
|
+
|
20
|
+
def referer
|
21
|
+
opts[:referer]
|
22
|
+
end
|
23
|
+
|
24
|
+
def host
|
25
|
+
opts[:host] || 'http://example.com'
|
26
|
+
end
|
27
|
+
|
28
|
+
def path
|
29
|
+
opts[:path] || '/'
|
30
|
+
end
|
31
|
+
|
32
|
+
def params
|
33
|
+
opts[:params] || {}.with_indifferent_access
|
34
|
+
end
|
35
|
+
|
36
|
+
def method
|
37
|
+
opts[:method] || 'GET'
|
38
|
+
end
|
39
|
+
|
40
|
+
def filtered_parameters
|
41
|
+
@filtered_parameters ||= params.merge(filtered: true)
|
42
|
+
end
|
43
|
+
|
44
|
+
def request_method_symbol
|
45
|
+
@request_method_symbol ||= method.downcase.to_sym
|
46
|
+
end
|
47
|
+
|
48
|
+
def remote_ip
|
49
|
+
opts[:remote_ip] || '192.168.1.1'
|
50
|
+
end
|
51
|
+
|
52
|
+
def user_agent
|
53
|
+
opts[:user_agent] || CHROME_UA
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/spec/mocks/user.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'codeclimate-test-reporter'
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
Bundler.setup
|
6
|
+
|
7
|
+
require 'rspec/its'
|
8
|
+
require 'rails'
|
9
|
+
require 'active_record'
|
10
|
+
require 'sojourn'
|
11
|
+
|
12
|
+
RSpec.configure do |config|
|
13
|
+
config.before(:suite) do
|
14
|
+
ActiveRecord::Base.establish_connection adapter: 'sqlite3',
|
15
|
+
database: ':memory:'
|
16
|
+
|
17
|
+
ActiveRecord::Schema.define do
|
18
|
+
self.verbose = false
|
19
|
+
|
20
|
+
create_table :sojourn_events do |t|
|
21
|
+
t.string :sojourner_uuid, limit: 36, null: false
|
22
|
+
t.string :name
|
23
|
+
t.text :properties
|
24
|
+
t.references :sojourn_request
|
25
|
+
t.references :user
|
26
|
+
t.timestamp :created_at
|
27
|
+
end
|
28
|
+
|
29
|
+
create_table :sojourn_requests do |t|
|
30
|
+
t.string :host, limit: 2048
|
31
|
+
t.string :path, limit: 2048
|
32
|
+
t.string :method
|
33
|
+
t.string :controller
|
34
|
+
t.string :action
|
35
|
+
t.string :ip_address
|
36
|
+
t.text :user_agent
|
37
|
+
t.text :params
|
38
|
+
t.text :referer
|
39
|
+
t.timestamp :created_at
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
config.before do
|
45
|
+
Sojourn::Event.delete_all
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'mocks/controller'
|
4
|
+
|
5
|
+
module Sojourn
|
6
|
+
COOKIE_NAME = Sojourn.config.cookie_name
|
7
|
+
|
8
|
+
describe Tracker do
|
9
|
+
let(:user) { Mocks::User.new }
|
10
|
+
let(:request) { Mocks::Request.new }
|
11
|
+
let(:cookies) { Mocks::Cookie.new }
|
12
|
+
let(:ctx) { Mocks::Controller.new(user, request, cookies) }
|
13
|
+
let(:tracker) { Tracker.new(ctx) }
|
14
|
+
|
15
|
+
describe '#track!' do
|
16
|
+
let(:event_name) { 'foo' }
|
17
|
+
let(:opts) { { bar: true } }
|
18
|
+
before { tracker.track!(event_name, opts) }
|
19
|
+
subject { Event.last }
|
20
|
+
|
21
|
+
its(:user_id) { is_expected.to eq(user.id) }
|
22
|
+
its(:name) { is_expected.to eq(event_name) }
|
23
|
+
|
24
|
+
describe 'request' do
|
25
|
+
subject { Event.last.properties[:request] }
|
26
|
+
|
27
|
+
its([:params]) { is_expected.to eq('filtered' => true) }
|
28
|
+
its([:method]) { is_expected.to eq('get') }
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'properties' do
|
32
|
+
subject { Event.last.properties }
|
33
|
+
|
34
|
+
its(:keys) { is_expected.to eq(%w(request browser bar)) }
|
35
|
+
its([:browser, :name]) { is_expected.to eq('Chrome') }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#sojourning!' do
|
40
|
+
before { tracker.sojourning! }
|
41
|
+
subject { Event.last }
|
42
|
+
|
43
|
+
its(:name) { is_expected.to eq('!sojourning') }
|
44
|
+
|
45
|
+
context 'when already tracked once' do
|
46
|
+
before do
|
47
|
+
tracker.update_session!
|
48
|
+
Sojourn::Event.delete_all
|
49
|
+
tracker.sojourning!
|
50
|
+
end
|
51
|
+
|
52
|
+
it { is_expected.to be_nil }
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when user changes' do
|
56
|
+
before do
|
57
|
+
tracker.update_session!
|
58
|
+
Sojourn::Event.delete_all
|
59
|
+
ctx.current_user = Mocks::User.new
|
60
|
+
tracker.sojourning!
|
61
|
+
end
|
62
|
+
|
63
|
+
its(:name) { is_expected.to eq('!logged_in') }
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'when user logs out' do
|
67
|
+
before do
|
68
|
+
tracker.update_session!
|
69
|
+
Sojourn::Event.delete_all
|
70
|
+
ctx.current_user = nil
|
71
|
+
tracker.sojourning!
|
72
|
+
end
|
73
|
+
|
74
|
+
its(:name) { is_expected.to eq('!logged_out') }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe 'update_session!' do
|
79
|
+
subject { cookies }
|
80
|
+
|
81
|
+
context 'before' do
|
82
|
+
its([COOKIE_NAME]) { is_expected.to be_nil }
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'after running' do
|
86
|
+
before { tracker.update_session! }
|
87
|
+
|
88
|
+
its([COOKIE_NAME, :uuid]) { is_expected.to_not be_nil }
|
89
|
+
its([COOKIE_NAME, :user_id]) { is_expected.to eq(user.id) }
|
90
|
+
|
91
|
+
context 'with existing cookie' do
|
92
|
+
let(:sojourner_uuid) { SecureRandom.uuid }
|
93
|
+
let(:cookie_data) { { user_id: user.id, uuid: sojourner_uuid } }
|
94
|
+
let(:cookies) { Mocks::Cookie[{ COOKIE_NAME => cookie_data }] }
|
95
|
+
|
96
|
+
its([COOKIE_NAME, :uuid]) { is_expected.to eq(sojourner_uuid) }
|
97
|
+
its([COOKIE_NAME, :user_id]) { is_expected.to eq(user.id) }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sojourn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Smudge
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-01-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -38,6 +38,62 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.4.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.4.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-its
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.2.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.2.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.3.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.3.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: codeclimate-test-reporter
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.4.8
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.4.8
|
41
97
|
- !ruby/object:Gem::Dependency
|
42
98
|
name: browser
|
43
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +122,34 @@ dependencies:
|
|
66
122
|
- - ">="
|
67
123
|
- !ruby/object:Gem::Version
|
68
124
|
version: 2.3.1
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: referer-parser
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.3.0
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.3.0
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rails
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 3.2.0
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 3.2.0
|
69
153
|
description: " Sojourn tracks site visitors and sources, with the ability to recognise
|
70
154
|
multiple sources per visitor. Each time a new source is detected, sojourn tracks
|
71
155
|
the referer, utm data, and logged-in user (if any)"
|
@@ -75,28 +159,32 @@ executables: []
|
|
75
159
|
extensions: []
|
76
160
|
extra_rdoc_files: []
|
77
161
|
files:
|
162
|
+
- ".codeclimate.yml"
|
78
163
|
- ".gitignore"
|
164
|
+
- ".rubocop.yml"
|
165
|
+
- ".travis.yml"
|
79
166
|
- Gemfile
|
80
167
|
- README.md
|
81
168
|
- Rakefile
|
82
169
|
- lib/generators/sojourn/install_generator.rb
|
83
170
|
- 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
171
|
- lib/generators/sojourn/templates/create_sojourn_events.rb
|
87
|
-
- lib/generators/sojourn/templates/create_sojourn_requests.rb
|
88
172
|
- lib/sojourn.rb
|
89
|
-
- lib/sojourn/browser.rb
|
90
|
-
- lib/sojourn/campaign.rb
|
91
173
|
- lib/sojourn/configuration.rb
|
92
174
|
- lib/sojourn/controller.rb
|
93
175
|
- lib/sojourn/event.rb
|
94
176
|
- lib/sojourn/request.rb
|
95
|
-
- lib/sojourn/serializers/
|
177
|
+
- lib/sojourn/serializers/indifferent_json.rb
|
96
178
|
- lib/sojourn/session_stores/cookie.rb
|
97
179
|
- lib/sojourn/tracker.rb
|
98
180
|
- lib/sojourn/version.rb
|
99
181
|
- sojourn.gemspec
|
182
|
+
- spec/mocks/controller.rb
|
183
|
+
- spec/mocks/cookie.rb
|
184
|
+
- spec/mocks/request.rb
|
185
|
+
- spec/mocks/user.rb
|
186
|
+
- spec/spec_helper.rb
|
187
|
+
- spec/tracker_spec.rb
|
100
188
|
homepage: ''
|
101
189
|
licenses:
|
102
190
|
- ''
|
@@ -117,9 +205,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
205
|
version: '0'
|
118
206
|
requirements: []
|
119
207
|
rubyforge_project:
|
120
|
-
rubygems_version: 2.
|
208
|
+
rubygems_version: 2.4.5.1
|
121
209
|
signing_key:
|
122
210
|
specification_version: 4
|
123
211
|
summary: Simple user source tracking for Rails.
|
124
|
-
test_files:
|
125
|
-
|
212
|
+
test_files:
|
213
|
+
- spec/mocks/controller.rb
|
214
|
+
- spec/mocks/cookie.rb
|
215
|
+
- spec/mocks/request.rb
|
216
|
+
- spec/mocks/user.rb
|
217
|
+
- spec/spec_helper.rb
|
218
|
+
- spec/tracker_spec.rb
|
@@ -1,13 +0,0 @@
|
|
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
|
@@ -1,10 +0,0 @@
|
|
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
|
@@ -1,19 +0,0 @@
|
|
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/browser.rb
DELETED
@@ -1,27 +0,0 @@
|
|
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
|
data/lib/sojourn/campaign.rb
DELETED
@@ -1,27 +0,0 @@
|
|
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
|