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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 05e30ba3490b5cb18a66e3ecf4bdb0048e0ce529
4
- data.tar.gz: 055a2accda2519d306ed020f9df334694cea68c0
3
+ metadata.gz: a5bcd732bdfc1df4fdd68744f4c807f894104848
4
+ data.tar.gz: 927a8ba15bd0948cb8c390c15b7c900785051a1f
5
5
  SHA512:
6
- metadata.gz: 8c8eaa338c482230664097aae7910ebae14f9ebace498dc9feff819d96fce1b66e65546c1416f3e23e3dee6342c0936fe209e081eb11a1a0aeff62de82a74947
7
- data.tar.gz: 70f60a8e7ba6a81055acd0306736b4feff026eed0f2dbac10d18510bd2e6e4dec6711a9c96eacac5eef6767ef12e1ce2b042d0739f31285ec95bdb9286f25f24
6
+ metadata.gz: 469867e717db9bc51b25e1b414a711db552631225524e781a2c9a5d62e3faefdf2d8e0f77acd426aaa7ac094b62e35b7361ec5c761ed4dadaddf7ce8bba2d32b
7
+ data.tar.gz: 44d5346a2ccabc605e220af036077b2f9969213580bc04188852f5d6ee6ff4a2b3a6ea1bf566cefdaa0cde87177092d3892247448bafdc19703cf8f9c96dc3f4
@@ -0,0 +1,11 @@
1
+ ---
2
+ engines:
3
+ fixme:
4
+ enabled: true
5
+ rubocop:
6
+ enabled: true
7
+ ratings:
8
+ paths:
9
+ - "**.rb"
10
+ exclude_paths:
11
+ - spec/**/*
data/.gitignore CHANGED
@@ -12,3 +12,4 @@
12
12
  *.o
13
13
  *.a
14
14
  mkmf.log
15
+ .DS_Store
@@ -0,0 +1,6 @@
1
+ Metrics/LineLength:
2
+ Max: 99
3
+ Style/AccessModifierIndentation:
4
+ EnforcedStyle: outdent
5
+ Style/Documentation:
6
+ Enabled: false
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.4
4
+ - 2.1.8
5
+ - 2.0.0-p648
6
+ addons:
7
+ code_climate:
8
+ repo_token: f8a0ab3c3b30ca6a3091f30a9e344344a80f919837de817d576c0e3f174a4e2c
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Sojourn
1
+ # Sojourn [![Build Status](https://img.shields.io/travis/smudge/sojourn.svg)](https://travis-ci.org/smudge/sojourn) [![Code Climate](https://img.shields.io/codeclimate/github/smudge/sojourn.svg)](https://codeclimate.com/github/smudge/sojourn) [![Test Coverage](https://img.shields.io/codeclimate/coverage/github/smudge/sojourn.svg)](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. 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.
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
@@ -1 +1,9 @@
1
1
  require 'bundler/gem_tasks'
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task default: :spec
7
+ rescue LoadError
8
+ puts 'rspec rake tasks not found'
9
+ end
@@ -21,7 +21,7 @@ module Sojourn
21
21
  end
22
22
 
23
23
  def create_migrations
24
- %w(campaigns browsers requests events).map { |m| "create_sojourn_#{m}" }.each do |name|
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.text :properties
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]
@@ -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 sojourn_browsers sojourn_campaigns)
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
@@ -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
@@ -1,13 +1,12 @@
1
+ require_relative 'serializers/indifferent_json'
2
+
1
3
  module Sojourn
2
4
  class Event < ActiveRecord::Base
3
- DEFAULT_FIELDS = [:id, :sojourner_uuid, :name, :properties, :sojourn_request_id, :user_id, :created_at]
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|
@@ -1,42 +1,123 @@
1
- require_relative 'campaign'
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 < ActiveRecord::Base
8
- attr_accessor :user_agent
7
+ class Request
8
+ KEYS = %w(uuid referer host path controller action params method ip_address user_agent)
9
9
 
10
- serialize :method, Serializers::Symbol
11
- serialize :params
10
+ attr_reader :request
12
11
 
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
12
+ def initialize(request)
13
+ @request = request
14
+ end
16
15
 
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
16
+ def outside_referer?
17
+ referer.present? && referer_host != host
27
18
  end
28
19
 
29
- before_validation do
30
- self.campaign ||= Campaign.from_request(self)
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 outside_referer?
35
- referer.present? && Addressable::URI.parse(referer).host != host
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 any_utm_data?
39
- Sojourn.config.campaign_params.map { |p| params[p].present? }.any?
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
@@ -0,0 +1,13 @@
1
+ module Sojourn
2
+ module Serializers
3
+ class IndifferentJSON
4
+ def self.load(string)
5
+ JSON.parse(string || '{}').with_indifferent_access
6
+ end
7
+
8
+ def self.dump(hash)
9
+ (hash || {}).to_json
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,7 +1,6 @@
1
1
  module Sojourn
2
2
  module SessionStores
3
3
  class Cookie
4
-
5
4
  def initialize(ctx)
6
5
  @ctx = ctx
7
6
  end
@@ -43,7 +42,6 @@ module Sojourn
43
42
  def cookie_name
44
43
  @cookie_name ||= Sojourn.config.cookie_name
45
44
  end
46
-
47
45
  end
48
46
  end
49
47
  end
@@ -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, request: request,
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.from_request(ctx.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
@@ -1,3 +1,3 @@
1
1
  module Sojourn
2
- VERSION = '0.0.7'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -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
@@ -0,0 +1,9 @@
1
+ module Mocks
2
+ class User
3
+ attr_accessor :id
4
+
5
+ def initialize(id = rand(10_000))
6
+ self.id = id
7
+ end
8
+ end
9
+ end
@@ -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.7
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: 2015-03-02 00:00:00.000000000 Z
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/symbol.rb
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.2.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
- has_rdoc:
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
@@ -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
@@ -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
@@ -1,13 +0,0 @@
1
- module Sojourn
2
- module Serializers
3
- class Symbol
4
- def self.load(string)
5
- string.to_sym
6
- end
7
-
8
- def self.dump(symbol)
9
- symbol.to_s
10
- end
11
- end
12
- end
13
- end