land 0.1.3
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 +6 -0
- data/.ruby-version +1 -0
- data/Appraisals +15 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +20 -0
- data/MIT-LICENSE +25 -0
- data/README.md +55 -0
- data/Rakefile +61 -0
- data/TODO.md +1 -0
- data/app/helpers/land/helper.rb +5 -0
- data/app/jobs/land/application_job.rb +4 -0
- data/app/lookups/land/ad_group.rb +9 -0
- data/app/lookups/land/ad_type.rb +9 -0
- data/app/lookups/land/affiliate.rb +9 -0
- data/app/lookups/land/app.rb +9 -0
- data/app/lookups/land/bid_match_type.rb +9 -0
- data/app/lookups/land/brand.rb +9 -0
- data/app/lookups/land/browser.rb +9 -0
- data/app/lookups/land/campaign.rb +10 -0
- data/app/lookups/land/content.rb +9 -0
- data/app/lookups/land/creative.rb +9 -0
- data/app/lookups/land/device.rb +9 -0
- data/app/lookups/land/device_type.rb +9 -0
- data/app/lookups/land/domain.rb +9 -0
- data/app/lookups/land/event_type.rb +9 -0
- data/app/lookups/land/experiment.rb +9 -0
- data/app/lookups/land/http_method.rb +9 -0
- data/app/lookups/land/keyword.rb +9 -0
- data/app/lookups/land/match_type.rb +9 -0
- data/app/lookups/land/medium.rb +9 -0
- data/app/lookups/land/mime_type.rb +9 -0
- data/app/lookups/land/network.rb +9 -0
- data/app/lookups/land/path.rb +10 -0
- data/app/lookups/land/placement.rb +9 -0
- data/app/lookups/land/platform.rb +10 -0
- data/app/lookups/land/position.rb +9 -0
- data/app/lookups/land/query_string.rb +10 -0
- data/app/lookups/land/search_term.rb +9 -0
- data/app/lookups/land/source.rb +9 -0
- data/app/lookups/land/subsource.rb +9 -0
- data/app/lookups/land/target.rb +9 -0
- data/app/lookups/land/user_agent_type.rb +9 -0
- data/app/models/concerns/land/table_name.rb +11 -0
- data/app/models/land/application_record.rb +5 -0
- data/app/models/land/attribution.rb +63 -0
- data/app/models/land/cookie.rb +14 -0
- data/app/models/land/event.rb +10 -0
- data/app/models/land/owner.rb +10 -0
- data/app/models/land/ownership.rb +8 -0
- data/app/models/land/pageview.rb +21 -0
- data/app/models/land/referer.rb +19 -0
- data/app/models/land/user_agent.rb +16 -0
- data/app/models/land/visit.rb +18 -0
- data/bin/console +13 -0
- data/bin/rails +22 -0
- data/bin/setup +11 -0
- data/config.ru +13 -0
- data/db/migrate/20200103012916_create_land_schema.rb +227 -0
- data/gemfiles/rails_5.0.gemfile +13 -0
- data/gemfiles/rails_5.1.gemfile +13 -0
- data/gemfiles/rails_5.2.gemfile +13 -0
- data/gemfiles/rails_6.0.gemfile +13 -0
- data/land.gemspec +56 -0
- data/lib/generators/land/install_generator.rb +17 -0
- data/lib/generators/templates/land.rb +29 -0
- data/lib/land.rb +30 -0
- data/lib/land/action.rb +62 -0
- data/lib/land/config.rb +62 -0
- data/lib/land/engine.rb +18 -0
- data/lib/land/tracker.rb +293 -0
- data/lib/land/trackers/noop_tracker.rb +8 -0
- data/lib/land/trackers/user_tracker.rb +79 -0
- data/lib/land/version.rb +5 -0
- data/lib/tasks/land_tasks.rake +4 -0
- metadata +233 -0
data/land.gemspec
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
$:.push File.expand_path("lib", __dir__)
|
|
2
|
+
|
|
3
|
+
# Maintain your gem's version:
|
|
4
|
+
require "land/version"
|
|
5
|
+
|
|
6
|
+
# Describe your gem and declare its dependencies:
|
|
7
|
+
Gem::Specification.new do |gem|
|
|
8
|
+
gem.name = "land"
|
|
9
|
+
gem.version = Land::VERSION
|
|
10
|
+
|
|
11
|
+
gem.authors = ["Erik Peterson"]
|
|
12
|
+
gem.email = ["thecompanygardener@gmail.com"]
|
|
13
|
+
|
|
14
|
+
gem.homepage = "https://github.com/companygardener/land"
|
|
15
|
+
|
|
16
|
+
gem.license = "MIT"
|
|
17
|
+
|
|
18
|
+
gem.summary = %q{Traffic tracking for Rails}
|
|
19
|
+
gem.description = %q{Clickstream tracking for Rails applications.}
|
|
20
|
+
|
|
21
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
|
22
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
|
23
|
+
if gem.respond_to?(:metadata)
|
|
24
|
+
gem.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
25
|
+
else
|
|
26
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
|
27
|
+
"public gem pushes."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
gem.metadata["homepage_uri"] = gem.homepage
|
|
31
|
+
gem.metadata["source_code_uri"] = gem.homepage
|
|
32
|
+
gem.metadata["changelog_uri"] = "https://github.com/companygardener/land/blob/master/CHANGELOG.md"
|
|
33
|
+
|
|
34
|
+
# Specify which files should be added to the gem when it is released.
|
|
35
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
36
|
+
gem.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
|
37
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
gem.bindir = "exe"
|
|
41
|
+
gem.executables = gem.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
42
|
+
gem.require_paths = ["lib"]
|
|
43
|
+
|
|
44
|
+
gem.add_dependency "activerecord", " > 4.0.0"
|
|
45
|
+
gem.add_dependency "lookup_by", "~> 0.11.0"
|
|
46
|
+
|
|
47
|
+
gem.add_development_dependency "pg"
|
|
48
|
+
gem.add_development_dependency "rake"
|
|
49
|
+
|
|
50
|
+
gem.add_development_dependency "appraisal"
|
|
51
|
+
|
|
52
|
+
gem.add_development_dependency "pry"
|
|
53
|
+
|
|
54
|
+
gem.add_development_dependency "rspec"
|
|
55
|
+
gem.add_development_dependency "rspec-rails"
|
|
56
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Land
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path('../../templates', __FILE__)
|
|
9
|
+
|
|
10
|
+
desc "Create the Land initializer"
|
|
11
|
+
|
|
12
|
+
def copy_initializer
|
|
13
|
+
template "land.rb", "config/initializers/land.rb"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'land'
|
|
4
|
+
|
|
5
|
+
Land.configure do |config|
|
|
6
|
+
# Enable land tracking
|
|
7
|
+
config.enabled = true
|
|
8
|
+
|
|
9
|
+
# Uncomment and modify to skip tracking for given paths.
|
|
10
|
+
# config.untracked_paths = %w[
|
|
11
|
+
# /ping
|
|
12
|
+
# /status
|
|
13
|
+
# ]
|
|
14
|
+
|
|
15
|
+
# Uncomment and modify to skip tracking for given IPs
|
|
16
|
+
# config.untracked_ips = %w[
|
|
17
|
+
# 127.0.0.1
|
|
18
|
+
# 192.168.0.1
|
|
19
|
+
# ]
|
|
20
|
+
|
|
21
|
+
# If request.user_agent is blank, this string is saved instead.
|
|
22
|
+
config.blank_user_agent_string = 'user agent missing'
|
|
23
|
+
|
|
24
|
+
# Database schema for land tables
|
|
25
|
+
config.schema = 'land'
|
|
26
|
+
|
|
27
|
+
# Timeout before a new visit is created
|
|
28
|
+
config.visit_timeout = 1.hour
|
|
29
|
+
end
|
data/lib/land.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "land/version"
|
|
4
|
+
require "land/engine"
|
|
5
|
+
|
|
6
|
+
require "lookup_by"
|
|
7
|
+
|
|
8
|
+
module Land
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
autoload :Action, "land/action"
|
|
12
|
+
autoload :Config, "land/config"
|
|
13
|
+
autoload :Tracker, "land/tracker"
|
|
14
|
+
|
|
15
|
+
module Trackers
|
|
16
|
+
autoload :NoopTracker, "land/trackers/noop_tracker"
|
|
17
|
+
autoload :UserTracker, "land/trackers/user_tracker"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.config
|
|
21
|
+
@config ||= Config.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.configure
|
|
25
|
+
|
|
26
|
+
yield config if block_given?
|
|
27
|
+
|
|
28
|
+
config
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/land/action.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Land
|
|
2
|
+
module Action
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
prepend_around_action :track_with_land! if Land.config.enabled
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Use @land to avoid conflicts in controller namespace
|
|
10
|
+
def track_with_land!
|
|
11
|
+
yield && return if untracked?
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
@land = Tracker.for(self)
|
|
15
|
+
@land.track
|
|
16
|
+
rescue => e
|
|
17
|
+
begin
|
|
18
|
+
Rails.logger.error e
|
|
19
|
+
|
|
20
|
+
if defined?(NewRelic::Agent) && NewRelic::Agent.respond_to?(:notice_error)
|
|
21
|
+
NewRelic::Agent.notice_error(e)
|
|
22
|
+
end
|
|
23
|
+
rescue
|
|
24
|
+
# eat this error
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
yield
|
|
30
|
+
rescue => e
|
|
31
|
+
@land.status = ActionDispatch::ExceptionWrapper.status_code_for_exception(e.class.name)
|
|
32
|
+
raise e
|
|
33
|
+
ensure
|
|
34
|
+
begin
|
|
35
|
+
@land.save
|
|
36
|
+
rescue => e
|
|
37
|
+
begin
|
|
38
|
+
Rails.logger.error e
|
|
39
|
+
|
|
40
|
+
if defined?(NewRelic::Agent) && NewRelic::Agent.respond_to?(:notice_error)
|
|
41
|
+
NewRelic::Agent.notice_error(e)
|
|
42
|
+
end
|
|
43
|
+
rescue
|
|
44
|
+
# bubble controller error, not this one
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def untracked?
|
|
51
|
+
untracked_path? || untracked_ip?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def untracked_path?
|
|
55
|
+
Land.config.untracked_paths.include? request.fullpath
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def untracked_ip?
|
|
59
|
+
Land.config.untracked_ips.include? request.remote_ip
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/land/config.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Land
|
|
4
|
+
class Config < HashWithIndifferentAccess
|
|
5
|
+
attr_reader :enabled, :secure_cookie
|
|
6
|
+
|
|
7
|
+
attr_writer :blank_user_agent_string
|
|
8
|
+
attr_writer :schema, :untracked_ips, :untracked_paths
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@enabled = false
|
|
12
|
+
@secure_cookie = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def blank_user_agent_string
|
|
16
|
+
@blank_user_agent_string ||= 'user agent missing'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def enabled=(value)
|
|
20
|
+
raise ArgumentError unless [true, false].include?(value)
|
|
21
|
+
|
|
22
|
+
@enabled = value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def secure_cookie=(value)
|
|
26
|
+
raise ArgumentError unless [true, false].include?(value)
|
|
27
|
+
|
|
28
|
+
@secure_cookie = value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def schema
|
|
32
|
+
@schema ||= 'land'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def untracked_ips
|
|
36
|
+
@untracked_ips ||= []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_untracked_ip(ip)
|
|
40
|
+
@untracked_ips << ip
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def untracked_paths
|
|
44
|
+
@untracked_paths ||= []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add_untracked_path(path)
|
|
48
|
+
@untracked_paths << path
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def visit_timeout
|
|
52
|
+
@visit_timeout ||= 30.minutes
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def visit_timeout=(value)
|
|
56
|
+
raise ArgumentError unless [Integer, ActiveSupport::Duration].include?(value.class)
|
|
57
|
+
raise ArgumentError, "must be positive" unless value.positive?
|
|
58
|
+
|
|
59
|
+
@visit_timeout = value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/land/engine.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
require "action_dispatch"
|
|
5
|
+
|
|
6
|
+
module Land
|
|
7
|
+
class Engine < ::Rails::Engine
|
|
8
|
+
isolate_namespace Land
|
|
9
|
+
|
|
10
|
+
initializer "land.action_controller" do
|
|
11
|
+
ActiveSupport.on_load :action_controller do
|
|
12
|
+
include Land::Action
|
|
13
|
+
|
|
14
|
+
helper Land::Helper
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/land/tracker.rb
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest/sha2"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Land
|
|
7
|
+
class Tracker
|
|
8
|
+
# About ValueTrack parameters
|
|
9
|
+
# https://support.google.com/google-ads/answer/2375447?hl=en&ref_topic=6031980
|
|
10
|
+
#
|
|
11
|
+
# Set up tracking with ValueTrack parameters
|
|
12
|
+
# https://support.google.com/google-ads/answer/6305348?hl=en
|
|
13
|
+
#
|
|
14
|
+
# iOS Campaign Tracker
|
|
15
|
+
# https://developers.google.com/analytics/devguides/collection/ios/v3/campaigns#url-builder
|
|
16
|
+
|
|
17
|
+
# @todo Cake: SUB ID => s1, s2, s3, s4, s5?
|
|
18
|
+
# https://www.affluent.io/blog/affiliate-sub-campaign-sid-tracking-guide/
|
|
19
|
+
#
|
|
20
|
+
# https://strackr.com/subid
|
|
21
|
+
TRACKING_PARAMS = {
|
|
22
|
+
'ad_group' => %w[ad_group adgroup adset_name ovadgrpid ysmadgrpid],
|
|
23
|
+
'ad_type' => %w[ad_type adtype],
|
|
24
|
+
'affiliate' => %w[affiliate aff affid],
|
|
25
|
+
'app' => %w[aid],
|
|
26
|
+
'bid_match_type' => %w[bidmatchtype bid_match_type bmt],
|
|
27
|
+
'brand' => %w[brand brand_name],
|
|
28
|
+
'campaign' => %w[campaign campaign_name utm_campaign ovcampgid ysmcampgid cn],
|
|
29
|
+
'click_id' => %w[click_id clickid dclid fbclid gclid gclsrc msclkid zanpid],
|
|
30
|
+
'content' => %w[content ad_name utm_content cc],
|
|
31
|
+
'creative' => %w[creative adid ovadid],
|
|
32
|
+
'device_type' => %w[device_type devicetype device],
|
|
33
|
+
'experiment' => %w[experiment aceid],
|
|
34
|
+
'keyword' => %w[keyword kw utm_term ovkey ysmkey],
|
|
35
|
+
'match_type' => %w[match_type matchtype match ovmtc ysmmtc],
|
|
36
|
+
'medium' => %w[medium utm_medium cm],
|
|
37
|
+
'network' => %w[network anid],
|
|
38
|
+
'placement' => %w[placement],
|
|
39
|
+
'position' => %w[position adposition ad_position],
|
|
40
|
+
'search_term' => %w[search_term searchterm q querystring ovraw ysmraw],
|
|
41
|
+
'source' => %w[source utm_source cs],
|
|
42
|
+
'subsource' => %w[subsource subid sid effi_id customid afftrack pubref argsite fobs epi ws u1],
|
|
43
|
+
'target' => %w[target]
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
TRACKING_KEYS = TRACKING_PARAMS.values.flatten.freeze
|
|
47
|
+
ATTRIBUTION_KEYS = TRACKING_PARAMS.except('click_id').keys
|
|
48
|
+
|
|
49
|
+
TRACKING_PARAMS_TRANSFORM = {
|
|
50
|
+
'ad_type' => { 'pe' => 'product_extensions',
|
|
51
|
+
'pla' => 'product_listing' },
|
|
52
|
+
|
|
53
|
+
'bid_match_type' => { 'bb' => 'bidded broad',
|
|
54
|
+
'bc' => 'bidded content',
|
|
55
|
+
'be' => 'bidded exact',
|
|
56
|
+
'bp' => 'bidded phrase' },
|
|
57
|
+
|
|
58
|
+
'device_type' => { 'c' => 'computer',
|
|
59
|
+
'm' => 'mobile',
|
|
60
|
+
't' => 'tablet' },
|
|
61
|
+
|
|
62
|
+
'match_type' => { 'b' => 'broad',
|
|
63
|
+
'c' => 'content',
|
|
64
|
+
'e' => 'exact',
|
|
65
|
+
'p' => 'phrase',
|
|
66
|
+
'std' => 'standard',
|
|
67
|
+
'adv' => 'advanced',
|
|
68
|
+
'cnt' => 'content' },
|
|
69
|
+
|
|
70
|
+
'network' => { 'g' => 'google_search',
|
|
71
|
+
's' => 'search_partner',
|
|
72
|
+
'd' => 'display_network' },
|
|
73
|
+
|
|
74
|
+
'source' => { 'fb' => 'facebook',
|
|
75
|
+
'ig' => 'instagram',
|
|
76
|
+
'msg' => 'messenger',
|
|
77
|
+
'an' => 'audience network' }
|
|
78
|
+
}.freeze
|
|
79
|
+
|
|
80
|
+
TRACKED_PARAMS = TRACKING_PARAMS.values.flatten.freeze
|
|
81
|
+
|
|
82
|
+
UUID_REGEX = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\Z/
|
|
83
|
+
UUID_REGEX_V4 = /\A\h{8}-\h{4}-4\h{3}-[89aAbB]\h{3}-\h{12}\Z/
|
|
84
|
+
|
|
85
|
+
# Compact the session by shortening names
|
|
86
|
+
KEYS = {
|
|
87
|
+
visit_id: 'vid',
|
|
88
|
+
visit_time: 'vt',
|
|
89
|
+
user_agent_hash: 'uh',
|
|
90
|
+
attribution_hash: 'ah',
|
|
91
|
+
referer_hash: 'rh'
|
|
92
|
+
}.freeze
|
|
93
|
+
|
|
94
|
+
attr_accessor :status
|
|
95
|
+
|
|
96
|
+
attr_reader :controller, :events, :visit
|
|
97
|
+
|
|
98
|
+
delegate :request, :response, :session, to: :controller
|
|
99
|
+
delegate :headers, :path, :query_parameters, :referer, :remote_ip, to: :request
|
|
100
|
+
|
|
101
|
+
class << self
|
|
102
|
+
def for(controller)
|
|
103
|
+
type = Land::UserAgent[controller.request.user_agent].try(:user_agent_type)
|
|
104
|
+
type = 'noop' unless Land.config.enabled
|
|
105
|
+
type = 'user' if type.nil?
|
|
106
|
+
|
|
107
|
+
# override
|
|
108
|
+
type = 'user' if controller.request.query_parameters.with_indifferent_access.slice(*TRACKING_KEYS).any?
|
|
109
|
+
|
|
110
|
+
"Land::Trackers::#{type.classify}Tracker".constantize.new(controller)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def initialize(controller)
|
|
115
|
+
# Allow subclasses to super from initialize
|
|
116
|
+
fail NotImplementedError, "You must subclass Land::Tracker" if self.class == Tracker
|
|
117
|
+
@controller = controller
|
|
118
|
+
@events = []
|
|
119
|
+
@start_time = Time.now
|
|
120
|
+
@status = nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def create_event(type, meta = {})
|
|
124
|
+
return unless tracking?
|
|
125
|
+
|
|
126
|
+
Event.new(visit_id: @visit_id, event_type: type, meta: meta).tap do |event|
|
|
127
|
+
@events << event
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def track
|
|
132
|
+
fail NotImplementedError, "You must subclass Land::Tracker" if self.class == Tracker
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
protected
|
|
136
|
+
|
|
137
|
+
def cookies
|
|
138
|
+
request.cookie_jar
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def cookie
|
|
142
|
+
validate_cookie
|
|
143
|
+
@cookie_id ||= Cookie.create.id
|
|
144
|
+
set_cookie
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def validate_cookie
|
|
148
|
+
return unless @cookie_id
|
|
149
|
+
return if @cookie_id =~ UUID_REGEX_V4 && Cookie[@cookie_id]
|
|
150
|
+
|
|
151
|
+
# Invalid cookie
|
|
152
|
+
@cookie_id = nil
|
|
153
|
+
@visit_id = nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def set_cookie
|
|
157
|
+
cookies.permanent[:land] = cookie_defaults.merge(value: @cookie_id)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def cookie_defaults
|
|
161
|
+
{ domain: :all, secure: Land.config.secure_cookie, httponly: true }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def referer
|
|
165
|
+
return @referer if @referer
|
|
166
|
+
return unless referer_uri
|
|
167
|
+
|
|
168
|
+
params = Rack::Utils.parse_query referer_uri.query
|
|
169
|
+
attribution = Attribution.lookup params.slice(*ATTRIBUTION_KEYS)
|
|
170
|
+
query = params.except(*ATTRIBUTION_KEYS)
|
|
171
|
+
|
|
172
|
+
begin
|
|
173
|
+
@referer = Referer.where(domain_id: Domain[referer_uri.host],
|
|
174
|
+
path_id: Path[referer_uri.path],
|
|
175
|
+
query_string_id: QueryString[query.to_query],
|
|
176
|
+
attribution_id: attribution.id).first_or_create
|
|
177
|
+
rescue ActiveRecord::RecordNotUnique
|
|
178
|
+
retry
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def referer_changed?
|
|
183
|
+
external_referer? && referer_hash != @referer_hash
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def referer_uri
|
|
187
|
+
@referer_uri ||= URI(URI.encode(request.referer)) if request.referer
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def attribution
|
|
191
|
+
@attribution ||= Attribution.lookup attribution_params
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def attribution_changed?
|
|
195
|
+
attribution? && attribution_hash != @attribution_hash
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def attribution_hash
|
|
199
|
+
Attribution.digest attribution_params
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def user_agent
|
|
203
|
+
return @user_agent if @user_agent
|
|
204
|
+
|
|
205
|
+
user_agent = request.user_agent
|
|
206
|
+
user_agent = Land.config.blank_user_agent_string if user_agent.blank?
|
|
207
|
+
|
|
208
|
+
@user_agent = UserAgent[user_agent]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def user_agent_hash
|
|
212
|
+
Digest::SHA2.base64digest user_agent.user_agent
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def user_agent_changed?
|
|
216
|
+
user_agent_hash != @user_agent_hash
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def referer_hash
|
|
220
|
+
Digest::SHA2.base64digest request.referer
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def attribution?
|
|
224
|
+
attribution_params.any?
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def record_visit
|
|
228
|
+
create_visit if new_visit?
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def create_visit
|
|
232
|
+
@visit = Visit.new
|
|
233
|
+
visit.attribution = attribution
|
|
234
|
+
visit.cookie_id = @cookie_id
|
|
235
|
+
visit.referer_id = referer.try(:id)
|
|
236
|
+
visit.user_agent_id = user_agent.id
|
|
237
|
+
visit.ip_address = remote_ip
|
|
238
|
+
visit.save!
|
|
239
|
+
|
|
240
|
+
@visit_id = @visit.id
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def new_visit?
|
|
244
|
+
@visit_id.nil? || referer_changed? || attribution_changed? || user_agent_changed? || visit_stale?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def external_referer?
|
|
248
|
+
referer_uri && referer_uri.host != request.host
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def extract_tracking(params)
|
|
252
|
+
hash = {}
|
|
253
|
+
|
|
254
|
+
TRACKING_PARAMS.each do |key, names|
|
|
255
|
+
param = names.find { |name| params.key?(name) }
|
|
256
|
+
next unless param
|
|
257
|
+
hash[key] = params[param]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
TRACKING_PARAMS_TRANSFORM.each do |key, transform|
|
|
261
|
+
next unless hash.key? key
|
|
262
|
+
hash[key] = transform[hash[key]] if transform.key? hash[key]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
hash
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def attribution_params
|
|
269
|
+
@attribution_params ||= tracking_params.with_indifferent_access.slice(*ATTRIBUTION_KEYS)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def query_params
|
|
273
|
+
@query_params ||= request.query_parameters.with_indifferent_access
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def tracking?
|
|
277
|
+
!!@visit_id
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def tracking_params
|
|
281
|
+
@tracking_params ||= extract_tracking(query_params)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def untracked_params
|
|
285
|
+
query_params.except(*TRACKED_PARAMS)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def visit_stale?
|
|
289
|
+
return false unless @last_visit_time
|
|
290
|
+
Time.current - @last_visit_time > Land.config.visit_timeout
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|