source_track 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.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +8 -0
- data/lib/source_track.rb +34 -0
- data/lib/source_track/configuration.rb +76 -0
- data/lib/source_track/rails.rb +26 -0
- data/lib/source_track/rails/controller_methods.rb +51 -0
- data/lib/source_track/railtie.rb +25 -0
- data/lib/source_track/token_parser.rb +70 -0
- data/lib/source_track/version.rb +3 -0
- data/source_track.gemspec +22 -0
- data/spec/lib/configuration_spec.rb +15 -0
- data/spec/lib/source_track_spec.rb +8 -0
- data/spec/lib/token_parser_spec.rb +44 -0
- data/spec/spec_helper.rb +9 -0
- metadata +100 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Bryan Rehbein
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# SourceTrack
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'source_track'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install source_track
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/source_track.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'source_track/version'
|
2
|
+
require 'source_track/configuration'
|
3
|
+
require 'source_track/token_parser'
|
4
|
+
|
5
|
+
require 'source_track/railtie' if defined?(Rails::Railtie)
|
6
|
+
|
7
|
+
module SourceTrack
|
8
|
+
class << self
|
9
|
+
|
10
|
+
# Configuration object. Must act like a hash and return sensible
|
11
|
+
# values for all configuration options.
|
12
|
+
attr_writer :configuration
|
13
|
+
|
14
|
+
# Call this method to modify defaults in your initializers.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# SourceTrack.configure do |config|
|
18
|
+
# config.use_dates = true
|
19
|
+
# config.signed_cookies = false
|
20
|
+
# end
|
21
|
+
def configure(silent = false)
|
22
|
+
yield(configuration)
|
23
|
+
end
|
24
|
+
|
25
|
+
def configuration
|
26
|
+
@configuration ||= Configuration.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def parser
|
30
|
+
@parser ||= TokenParser.new
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module SourceTrack
|
4
|
+
# Used to set up and modify settings for the notifier.
|
5
|
+
class Configuration
|
6
|
+
|
7
|
+
DEFAULT_SEPARATOR = "|"
|
8
|
+
DEFAULT_EPOCH = Date.new(2012,1,1)
|
9
|
+
DEFAULT_DATE_LENGTH = 4
|
10
|
+
OPTIONS = [:use_dates, :separator, :epoch, :date_length, :cookie_name, :http_only_cookies,
|
11
|
+
:cookie_path, :environment, :app_root, :logger].freeze
|
12
|
+
|
13
|
+
# Add a date component to the source token stored
|
14
|
+
attr_writer :use_dates
|
15
|
+
|
16
|
+
# Separator (can't be 0-9 A-F or other undesireables in cookies), default to pipe (|)
|
17
|
+
attr_writer :separator
|
18
|
+
|
19
|
+
|
20
|
+
# Epoch date to use for hexdates
|
21
|
+
attr_accessor :epoch
|
22
|
+
|
23
|
+
# Length of hexdate (defaults to 4)
|
24
|
+
attr_accessor :date_length
|
25
|
+
|
26
|
+
# HTTP cookies
|
27
|
+
attr_accessor :cookie_name
|
28
|
+
attr_accessor :http_only_cookies
|
29
|
+
attr_accessor :cookie_path
|
30
|
+
|
31
|
+
# Application specifics
|
32
|
+
attr_accessor :environment
|
33
|
+
attr_accessor :app_root
|
34
|
+
attr_accessor :logger
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@epoch = DEFAULT_EPOCH
|
38
|
+
@date_length = DEFAULT_DATE_LENGTH
|
39
|
+
@cookie_name = 'sct'
|
40
|
+
@http_only_cookies = true
|
41
|
+
@cookie_path = '/'
|
42
|
+
end
|
43
|
+
|
44
|
+
# Track data along with the source token
|
45
|
+
def use_dates
|
46
|
+
@use_dates || true
|
47
|
+
end
|
48
|
+
alias_method :use_dates?, :use_dates
|
49
|
+
|
50
|
+
def separator
|
51
|
+
@separator || DEFAULT_SEPARATOR
|
52
|
+
end
|
53
|
+
|
54
|
+
# Allows config options to be read like a hash
|
55
|
+
#
|
56
|
+
# @param [Symbol] option Key for a given attribute
|
57
|
+
def [](option)
|
58
|
+
send(option)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns a hash of all configurable options
|
62
|
+
def to_hash
|
63
|
+
OPTIONS.inject({}) do |hash, option|
|
64
|
+
hash[option.to_sym] = self.send(option)
|
65
|
+
hash
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a hash of all configurable options merged with +hash+
|
70
|
+
#
|
71
|
+
# @param [Hash] hash A set of configuration options that will take precedence over the defaults
|
72
|
+
def merge(hash)
|
73
|
+
to_hash.merge(hash)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'source_track'
|
2
|
+
require 'source_track/rails/controller_methods'
|
3
|
+
|
4
|
+
module SourceTrack
|
5
|
+
module Rails
|
6
|
+
|
7
|
+
def self.initialize
|
8
|
+
if defined?(ActionController::Base)
|
9
|
+
ActionController::Base.send(:include, SourceTrack::Rails::ControllerMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
track_logger = if defined?(::Rails.logger)
|
13
|
+
::Rails.logger
|
14
|
+
elsif defined?(RAILS_DEFAULT_LOGGER)
|
15
|
+
RAILS_DEFAULT_LOGGER
|
16
|
+
end
|
17
|
+
|
18
|
+
SourceTrack.configure(true) do |config|
|
19
|
+
config.logger = track_logger
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
SourceTrack::Rails.initialize
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module SourceTrack
|
2
|
+
module Rails
|
3
|
+
module ControllerMethods
|
4
|
+
|
5
|
+
# TODO: It would be nice on page load (or before filter) to load an array of tokens accessible
|
6
|
+
# inside the controller (maybe with extra methods like has_token?)
|
7
|
+
# it would get parsed at the start of the request cycle (in the controller), any modifications would be
|
8
|
+
# stored in an instance variable (i.e. track, clear, etc), state of dirty? would become true, then
|
9
|
+
# when response is being generated, set the cookie if dirty? is true
|
10
|
+
|
11
|
+
# it would look similar to this (we would send a cookie with A9212 source token with today's date)
|
12
|
+
# before_filter :scene_track
|
13
|
+
# def show
|
14
|
+
# track_source("A9212")
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# def signup
|
18
|
+
# @user.sources = source_tokens
|
19
|
+
# @user.super_important_person = true if source_tokens.has_source("VIP") (should we also be able to filter by date?)
|
20
|
+
# @user.save!
|
21
|
+
# source_tokens.clear
|
22
|
+
# redirect root_url
|
23
|
+
# end
|
24
|
+
|
25
|
+
# TODO - make sure the cookie is a far future expires
|
26
|
+
def track_source(token, options = {})
|
27
|
+
tokens = source_tokens
|
28
|
+
tokens << {:token => token, :date => Date.today}
|
29
|
+
cookies[SourceTrack.configuration.cookie_name] = {
|
30
|
+
:value => SourceTrack.parser.encode(tokens),
|
31
|
+
:http_only => SourceTrack.configuration.http_only_cookies,
|
32
|
+
:expires => 10.years.from_now
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def source_tokens
|
37
|
+
@tokens ||= SourceTrack.parser.parse(cookies[SourceTrack.configuration.cookie_name])
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear_tokens
|
41
|
+
cookies.delete SourceTrack.configuration.cookie_name
|
42
|
+
end
|
43
|
+
|
44
|
+
def has_source(token)
|
45
|
+
source_tokens.map{|m| m[:token]}.include?(token)
|
46
|
+
end
|
47
|
+
alias_method :has_source?, :has_source
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'airbrake'
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
module SourceTrack
|
5
|
+
class Railtie < ::Rails::Railtie
|
6
|
+
rake_tasks do
|
7
|
+
#require 'source_track/rails/rake_tasks'
|
8
|
+
end
|
9
|
+
|
10
|
+
config.after_initialize do
|
11
|
+
SourceTrack.configure(true) do |config|
|
12
|
+
config.logger ||= ::Rails.logger
|
13
|
+
config.environment ||= ::Rails.env
|
14
|
+
config.app_root ||= ::Rails.root
|
15
|
+
end
|
16
|
+
|
17
|
+
ActiveSupport.on_load(:action_controller) do
|
18
|
+
# Lazily load action_controller methods
|
19
|
+
#
|
20
|
+
require 'source_track/rails/controller_methods'
|
21
|
+
include SourceTrack::Rails::ControllerMethods
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'open-uri'
|
3
|
+
|
4
|
+
module SourceTrack
|
5
|
+
class TokenParser
|
6
|
+
|
7
|
+
# Take the string from the cookie and return the source tokens (token and optional date)
|
8
|
+
# ABC12300FA|CODE2300FB|CD2380 #( with dates, last 4 characters are hex )
|
9
|
+
# ABBC123|CODE23|CD #( no dates )
|
10
|
+
# we MUST preserve order
|
11
|
+
def parse(v)
|
12
|
+
return [] if v.nil? || v.strip.empty?
|
13
|
+
|
14
|
+
tokens = []
|
15
|
+
v.split(config.separator).each do |t|
|
16
|
+
# validate the token is valid
|
17
|
+
|
18
|
+
if config.use_dates?
|
19
|
+
token, token_date = split_token_date(t, config.date_length)
|
20
|
+
tokens << {:token => token, :date => get_date(token_date, config.epoch)}
|
21
|
+
else
|
22
|
+
tokens << {:token => t}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
tokens
|
27
|
+
end
|
28
|
+
|
29
|
+
# returns a string that can be set in the cookie
|
30
|
+
# we may or may not need to encode so the cookies work in all situations
|
31
|
+
def encode(tokens)
|
32
|
+
return nil if tokens.nil?
|
33
|
+
tokens = [tokens] unless tokens.is_a?(Array)
|
34
|
+
|
35
|
+
# TODO: tokens need to be unique (duplicates need to remain at the end), remember
|
36
|
+
# the order from left to right is oldest to newest - any duplicates should always be newest
|
37
|
+
# (i.e. if we 'track' a user using the same token on the same day 2x - the second visit's position
|
38
|
+
# should be meaningful) -- i.e. order of tracking => A, F, B, F, C should encode as A|B|F|C, NOT A|F|B|C
|
39
|
+
tokens.map{|t| encode_token(t) }.join(config.separator)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def encode_token(token)
|
45
|
+
encoded_token = token[:token]
|
46
|
+
encoded_token += get_hex_date(token[:date], config.epoch) if (!token[:date].nil? && config.use_dates?)
|
47
|
+
encoded_token
|
48
|
+
end
|
49
|
+
|
50
|
+
def split_token_date(v, n)
|
51
|
+
# validate token
|
52
|
+
date_part = v[-n,n]
|
53
|
+
tok_part = v[0..(-n - 1)]
|
54
|
+
[tok_part, date_part]
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_date(hex_date, epoch)
|
58
|
+
# validate hex_date
|
59
|
+
epoch + hex_date.hex
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_hex_date(date, epoch)
|
63
|
+
sprintf("%04X", (date - epoch))
|
64
|
+
end
|
65
|
+
|
66
|
+
def config
|
67
|
+
SourceTrack.configuration
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/source_track/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Bryan Rehbein"]
|
6
|
+
gem.email = ["bryan@siliconsea.net"]
|
7
|
+
gem.description = %q{Gem to help understand a user's interaction with your site
|
8
|
+
and the influence of various referrals, promos, campaigns, etc.}
|
9
|
+
gem.summary = %q{Track the influence that your marketing campaigns and referrals
|
10
|
+
have on your users.}
|
11
|
+
gem.homepage = "https://github.com/redbeard0x0a/source_track"
|
12
|
+
|
13
|
+
gem.files = `git ls-files`.split($\)
|
14
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
16
|
+
gem.name = "source_track"
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
gem.version = SourceTrack::VERSION
|
19
|
+
|
20
|
+
gem.add_development_dependency 'rake'
|
21
|
+
gem.add_development_dependency 'rspec'
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SourceTrack::Configuration do
|
4
|
+
let(:config) { SourceTrack.configuration }
|
5
|
+
|
6
|
+
it "should initialize with the defaults" do
|
7
|
+
config.use_dates.should be_true
|
8
|
+
config.separator.should eq '|'
|
9
|
+
config.epoch.should eq Date.new(2012,1,1)
|
10
|
+
config.date_length.should eq 4
|
11
|
+
config.cookie_name.should eq 'sct'
|
12
|
+
config.http_only_cookies.should be_true
|
13
|
+
config.cookie_path.should eq '/'
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SourceTrack::TokenParser do
|
4
|
+
|
5
|
+
context "with use_dates" do
|
6
|
+
before(:each) do
|
7
|
+
@parser = SourceTrack.parser
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should encode a single token value" do
|
11
|
+
@parser.encode({:token => "ABC", :date => Date.new(2012,9,7)}).should eq "ABC00FA"
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should encode an array of tokens" do
|
15
|
+
@parser.encode([
|
16
|
+
{:token => "BBC", :date => Date.new(2012,9,7)},
|
17
|
+
{:token => "WOWZA9828", :date => Date.new(2012,10,9)}
|
18
|
+
]).should eq "BBC00FA|WOWZA9828011A"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "without use_dates" do
|
23
|
+
before(:each) do
|
24
|
+
@parser = SourceTrack.parser
|
25
|
+
end
|
26
|
+
|
27
|
+
it "encode a single token value" do
|
28
|
+
@parser.encode({:token => "ABC"}).should eq "ABC"
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should encode an array of tokens" do
|
32
|
+
@parser.encode([
|
33
|
+
{:token => "39828ABO"}, {:token => "HOLF392"}
|
34
|
+
]).should eq "39828ABO|HOLF392"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should encode an array of tokens" do
|
38
|
+
# @parser.encode([
|
39
|
+
# {:token => "39828ABO", :date => Date.new(2012,9,7)},
|
40
|
+
# {:token => "HOLF392", :date => Date.new(2012,10,9)}
|
41
|
+
# ]).should eq "39828ABO|HOLF392"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: source_track
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Bryan Rehbein
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: ! "Gem to help understand a user's interaction with your site \n and
|
47
|
+
the influence of various referrals, promos, campaigns, etc."
|
48
|
+
email:
|
49
|
+
- bryan@siliconsea.net
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- LICENSE
|
57
|
+
- README.md
|
58
|
+
- Rakefile
|
59
|
+
- lib/source_track.rb
|
60
|
+
- lib/source_track/configuration.rb
|
61
|
+
- lib/source_track/rails.rb
|
62
|
+
- lib/source_track/rails/controller_methods.rb
|
63
|
+
- lib/source_track/railtie.rb
|
64
|
+
- lib/source_track/token_parser.rb
|
65
|
+
- lib/source_track/version.rb
|
66
|
+
- source_track.gemspec
|
67
|
+
- spec/lib/configuration_spec.rb
|
68
|
+
- spec/lib/source_track_spec.rb
|
69
|
+
- spec/lib/token_parser_spec.rb
|
70
|
+
- spec/spec_helper.rb
|
71
|
+
homepage: https://github.com/redbeard0x0a/source_track
|
72
|
+
licenses: []
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
requirements: []
|
90
|
+
rubyforge_project:
|
91
|
+
rubygems_version: 1.8.23
|
92
|
+
signing_key:
|
93
|
+
specification_version: 3
|
94
|
+
summary: Track the influence that your marketing campaigns and referrals have on
|
95
|
+
your users.
|
96
|
+
test_files:
|
97
|
+
- spec/lib/configuration_spec.rb
|
98
|
+
- spec/lib/source_track_spec.rb
|
99
|
+
- spec/lib/token_parser_spec.rb
|
100
|
+
- spec/spec_helper.rb
|