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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.ruby-version +1 -0
  4. data/Appraisals +15 -0
  5. data/CHANGELOG.md +6 -0
  6. data/Gemfile +20 -0
  7. data/MIT-LICENSE +25 -0
  8. data/README.md +55 -0
  9. data/Rakefile +61 -0
  10. data/TODO.md +1 -0
  11. data/app/helpers/land/helper.rb +5 -0
  12. data/app/jobs/land/application_job.rb +4 -0
  13. data/app/lookups/land/ad_group.rb +9 -0
  14. data/app/lookups/land/ad_type.rb +9 -0
  15. data/app/lookups/land/affiliate.rb +9 -0
  16. data/app/lookups/land/app.rb +9 -0
  17. data/app/lookups/land/bid_match_type.rb +9 -0
  18. data/app/lookups/land/brand.rb +9 -0
  19. data/app/lookups/land/browser.rb +9 -0
  20. data/app/lookups/land/campaign.rb +10 -0
  21. data/app/lookups/land/content.rb +9 -0
  22. data/app/lookups/land/creative.rb +9 -0
  23. data/app/lookups/land/device.rb +9 -0
  24. data/app/lookups/land/device_type.rb +9 -0
  25. data/app/lookups/land/domain.rb +9 -0
  26. data/app/lookups/land/event_type.rb +9 -0
  27. data/app/lookups/land/experiment.rb +9 -0
  28. data/app/lookups/land/http_method.rb +9 -0
  29. data/app/lookups/land/keyword.rb +9 -0
  30. data/app/lookups/land/match_type.rb +9 -0
  31. data/app/lookups/land/medium.rb +9 -0
  32. data/app/lookups/land/mime_type.rb +9 -0
  33. data/app/lookups/land/network.rb +9 -0
  34. data/app/lookups/land/path.rb +10 -0
  35. data/app/lookups/land/placement.rb +9 -0
  36. data/app/lookups/land/platform.rb +10 -0
  37. data/app/lookups/land/position.rb +9 -0
  38. data/app/lookups/land/query_string.rb +10 -0
  39. data/app/lookups/land/search_term.rb +9 -0
  40. data/app/lookups/land/source.rb +9 -0
  41. data/app/lookups/land/subsource.rb +9 -0
  42. data/app/lookups/land/target.rb +9 -0
  43. data/app/lookups/land/user_agent_type.rb +9 -0
  44. data/app/models/concerns/land/table_name.rb +11 -0
  45. data/app/models/land/application_record.rb +5 -0
  46. data/app/models/land/attribution.rb +63 -0
  47. data/app/models/land/cookie.rb +14 -0
  48. data/app/models/land/event.rb +10 -0
  49. data/app/models/land/owner.rb +10 -0
  50. data/app/models/land/ownership.rb +8 -0
  51. data/app/models/land/pageview.rb +21 -0
  52. data/app/models/land/referer.rb +19 -0
  53. data/app/models/land/user_agent.rb +16 -0
  54. data/app/models/land/visit.rb +18 -0
  55. data/bin/console +13 -0
  56. data/bin/rails +22 -0
  57. data/bin/setup +11 -0
  58. data/config.ru +13 -0
  59. data/db/migrate/20200103012916_create_land_schema.rb +227 -0
  60. data/gemfiles/rails_5.0.gemfile +13 -0
  61. data/gemfiles/rails_5.1.gemfile +13 -0
  62. data/gemfiles/rails_5.2.gemfile +13 -0
  63. data/gemfiles/rails_6.0.gemfile +13 -0
  64. data/land.gemspec +56 -0
  65. data/lib/generators/land/install_generator.rb +17 -0
  66. data/lib/generators/templates/land.rb +29 -0
  67. data/lib/land.rb +30 -0
  68. data/lib/land/action.rb +62 -0
  69. data/lib/land/config.rb +62 -0
  70. data/lib/land/engine.rb +18 -0
  71. data/lib/land/tracker.rb +293 -0
  72. data/lib/land/trackers/noop_tracker.rb +8 -0
  73. data/lib/land/trackers/user_tracker.rb +79 -0
  74. data/lib/land/version.rb +5 -0
  75. data/lib/tasks/land_tasks.rake +4 -0
  76. metadata +233 -0
@@ -0,0 +1,10 @@
1
+ module Land
2
+ class Path < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_by :path, cache: 50, find_or_create: true
6
+
7
+ has_many :page_views
8
+ has_many :referers
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module Land
2
+ class Placement < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_by :placement, cache: 50, find_or_create: true
6
+
7
+ has_many :attributions
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ module Land
2
+ class Platform < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_by :platform, cache: 50, find_or_create: true
6
+
7
+ has_many :attributions
8
+ has_many :user_agents
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module Land
2
+ class Position < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_by :position, cache: 50, find_or_create: true
6
+
7
+ has_many :attributions
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ module Land
2
+ class QueryString < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_by :query_string, cache: 50, find_or_create: true, allow_blank: true
6
+
7
+ has_many :page_views
8
+ has_many :referers
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module Land
2
+ class SearchTerm < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_by :search_term, cache: 50, find_or_create: true
6
+
7
+ has_many :attributions
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Land
2
+ class Source < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_by :source, cache: 250, find_or_create: true
6
+
7
+ has_many :attributions
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Land
2
+ class Subsource < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_by :subsource, cache: 1000, find_or_create: true
6
+
7
+ has_many :attributions
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Land
2
+ class Target < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_by :target, cache: 50, find_or_create: true
6
+
7
+ has_many :attributions
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Land
2
+ class UserAgentType < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_by :user_agent_type, cache: true
6
+
7
+ has_many :user_agents
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Land
4
+ module TableName
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ self.table_name = "#{Land.config.schema}.#{model_name.element.pluralize}"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module Land
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Land
4
+ class Attribution < ApplicationRecord
5
+ include TableName
6
+
7
+ KEYS = %w[
8
+ ad_type
9
+ ad_group
10
+ affiliate
11
+ app
12
+ bid_match_type
13
+ brand
14
+ campaign
15
+ content
16
+ creative
17
+ device_type
18
+ experiment
19
+ keyword
20
+ match_type
21
+ medium
22
+ network
23
+ placement
24
+ position
25
+ search_term
26
+ source
27
+ subsource
28
+ target
29
+ ]
30
+
31
+ self.record_timestamps = false
32
+
33
+ KEYS.each do |key|
34
+ lookup_for key.to_sym, class_name: "Land::#{key.classify}".constantize
35
+ end
36
+
37
+ has_many :visits
38
+
39
+ class << self
40
+ def transform(params)
41
+ hash = params.slice(*KEYS)
42
+
43
+ filter = {}
44
+
45
+ hash.each do |k, v|
46
+ filter[k.foreign_key] = "Land::#{k.classify}".constantize[v]
47
+ end
48
+
49
+ filter
50
+ end
51
+
52
+ def lookup(params)
53
+ where(transform(params)).first_or_create
54
+ rescue ActiveRecord::RecordNotUnique
55
+ retry
56
+ end
57
+
58
+ def digest(params)
59
+ Digest::SHA2.base64digest transform(params).values.map(&:name).sort.join("\n")
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,14 @@
1
+ module Land
2
+ class Cookie < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_by :cookie_id, cache: 100, find: true
6
+
7
+ has_many :ownerships
8
+ has_many :visits
9
+
10
+ after_initialize do
11
+ self.id ||= SecureRandom.uuid
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ module Land
2
+ class Event < ApplicationRecord
3
+ include TableName
4
+ self.record_timestamps = false
5
+
6
+ lookup_for :event_type, class_name: EventType, symbolize: true
7
+
8
+ belongs_to :pageview, required: false
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Land
2
+ class Owner < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_by :owner, cache: 50, find_or_create: true
6
+
7
+ has_many :ownerships
8
+ has_many :visits
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ module Land
2
+ class Ownership < ApplicationRecord
3
+ include TableName
4
+
5
+ lookup_for :cookie, class_name: Cookie
6
+ lookup_for :owner, class_name: Owner
7
+ end
8
+ end
@@ -0,0 +1,21 @@
1
+ module Land
2
+ class Pageview < ApplicationRecord
3
+ include TableName
4
+ self.record_timestamps = false
5
+
6
+ # optional: true is misleading
7
+ # visit_id is actually required, but its validity is enforced by the db.
8
+ # Without this Rails loads the visit record when saving the pageview.
9
+ # This removes the unnecessary query.
10
+ belongs_to :visit, optional: true
11
+
12
+ lookup_for :mime_type, class_name: MimeType
13
+ lookup_for :http_method, class_name: HttpMethod
14
+ lookup_for :path, class_name: Path
15
+ lookup_for :query_string, class_name: QueryString
16
+
17
+ after_initialize do
18
+ self.id ||= SecureRandom.uuid
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Land
4
+ class Referer < ApplicationRecord
5
+ include TableName
6
+
7
+ lookup_for :domain, class_name: Domain
8
+ lookup_for :path, class_name: Path
9
+ lookup_for :query_string, class_name: QueryString
10
+
11
+ def url
12
+ uri.to_s
13
+ end
14
+
15
+ def uri
16
+ URI.join("https://#{domain}", path)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module Land
2
+ class UserAgent < ApplicationRecord
3
+ include TableName
4
+
5
+ self.record_timestamps = false
6
+
7
+ lookup_by :user_agent, cache: 50, find_or_create: true
8
+
9
+ lookup_for :user_agent_type, class_name: UserAgentType
10
+ lookup_for :device, class_name: Device
11
+ lookup_for :platform, class_name: Platform
12
+ lookup_for :browser, class_name: Browser
13
+
14
+ has_many :visits
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ module Land
2
+ class Visit < ApplicationRecord
3
+ include TableName
4
+
5
+ belongs_to :attribution
6
+ belongs_to :cookie
7
+ belongs_to :user_agent
8
+ belongs_to :referer, optional: true
9
+
10
+ lookup_for :owner, class_name: Owner
11
+
12
+ has_many :pageviews
13
+
14
+ after_initialize do
15
+ self.id ||= SecureRandom.uuid
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "land"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # require "irb"
10
+ # IRB.start(__FILE__)
11
+
12
+ require "pry"
13
+ Pry.start
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENV['RAILS_ENV'] ||= 'development'
6
+
7
+ ENGINE_ROOT = File.expand_path('..', __dir__)
8
+ ENGINE_PATH = File.expand_path('../lib/land/engine', __dir__)
9
+
10
+ # Set up gems listed in the Gemfile.
11
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
12
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
13
+
14
+ require 'combustion'
15
+
16
+ Combustion.initialize! :active_record,
17
+ database_reset: false,
18
+ load_schema: false,
19
+ database_migrate: false
20
+
21
+ require 'rails/all'
22
+ require 'rails/engine/commands'
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+ rails db:reset
8
+
9
+ rspec
10
+
11
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "bundler"
5
+
6
+ Bundler.require :default, :development
7
+
8
+ Combustion.initialize! :active_record,
9
+ database_reset: false,
10
+ load_schema: false,
11
+ database_migrate: false
12
+
13
+ run Combustion::Application
@@ -0,0 +1,227 @@
1
+ class CreateLandSchema < ActiveRecord::Migration[5.0]
2
+ QUERY_PARAMS = %w[
3
+ ad_type
4
+ ad_group
5
+ affiliate
6
+ app
7
+ bid_match_type
8
+ brand
9
+ campaign
10
+ content
11
+ creative
12
+ device_type
13
+ experiment
14
+ keyword
15
+ match_type
16
+ medium
17
+ network
18
+ placement
19
+ position
20
+ search_term
21
+ source
22
+ subsource
23
+ target
24
+ ]
25
+
26
+ def schema
27
+ Land.config.schema
28
+ end
29
+
30
+ def up
31
+ execute "CREATE SCHEMA #{schema};"
32
+
33
+ with_options schema: schema do |t|
34
+ # Query params
35
+ t.create_lookup_tables(*QUERY_PARAMS.map(&:pluralize))
36
+
37
+ # User agent
38
+ t.create_lookup_table :devices
39
+ t.create_lookup_tables :user_agent_types, :browsers, :platforms, small: true
40
+
41
+ # HTTP
42
+ t.create_lookup_tables :domains, :paths, :query_strings
43
+ t.create_lookup_tables :http_methods, :mime_types, small: true
44
+
45
+ t.create_lookup_table :event_types, small: true
46
+ end
47
+
48
+ execute %[CREATE EXTENSION "uuid-ossp";]
49
+
50
+ enable_extension "uuid-ossp"
51
+
52
+ execute %[ALTER EXTENSION "uuid-ossp" SET SCHEMA "public";]
53
+
54
+ execute <<~SQL
55
+ SET search_path TO #{schema},public;
56
+
57
+ INSERT INTO user_agent_types (user_agent_type) VALUES ('user'), ('ping'), ('crawl'), ('scrape'), ('scan');
58
+
59
+ CREATE TABLE user_agents (
60
+ user_agent_id SERIAL PRIMARY KEY
61
+
62
+ , user_agent_type_id SMALLINT REFERENCES user_agent_types
63
+
64
+ , device_id INTEGER REFERENCES devices
65
+ , platform_id SMALLINT REFERENCES platforms
66
+ , browser_id SMALLINT REFERENCES browsers
67
+ , browser_version TEXT
68
+
69
+ , user_agent TEXT NOT NULL UNIQUE
70
+
71
+ , created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
72
+ );
73
+
74
+ CREATE INDEX ON user_agents (device_id);
75
+ CREATE INDEX ON user_agents (platform_id);
76
+ CREATE INDEX ON user_agents (browser_id);
77
+
78
+ ALTER TABLE ad_types ALTER COLUMN ad_type_id SET DATA TYPE SMALLINT;
79
+ ALTER TABLE bid_match_types ALTER COLUMN bid_match_type_id SET DATA TYPE SMALLINT;
80
+ ALTER TABLE device_types ALTER COLUMN device_type_id SET DATA TYPE SMALLINT;
81
+ ALTER TABLE match_types ALTER COLUMN match_type_id SET DATA TYPE SMALLINT;
82
+ ALTER TABLE networks ALTER COLUMN network_id SET DATA TYPE SMALLINT;
83
+ ALTER TABLE positions ALTER COLUMN position_id SET DATA TYPE SMALLINT;
84
+
85
+ CREATE TABLE attributions (
86
+ attribution_id SERIAL PRIMARY KEY
87
+
88
+ , #{QUERY_PARAMS.map { |name| format('%s INTEGER REFERENCES %s', name.foreign_key, name.pluralize) }.join(',') }
89
+
90
+ , created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
91
+
92
+ , UNIQUE (#{QUERY_PARAMS.map(&:foreign_key).join(',')})
93
+ );
94
+
95
+ ALTER TABLE attributions
96
+ ALTER COLUMN ad_type_id SET DATA TYPE SMALLINT
97
+ , ALTER COLUMN bid_match_type_id SET DATA TYPE SMALLINT
98
+ , ALTER COLUMN device_type_id SET DATA TYPE SMALLINT
99
+ , ALTER COLUMN match_type_id SET DATA TYPE SMALLINT
100
+ , ALTER COLUMN network_id SET DATA TYPE SMALLINT
101
+ , ALTER COLUMN position_id SET DATA TYPE SMALLINT
102
+ ;
103
+
104
+ #{QUERY_PARAMS.map { |p| "CREATE INDEX ON attributions (#{p.foreign_key});" }.join("\n")}
105
+
106
+ CREATE TABLE referers (
107
+ referer_id SERIAL PRIMARY KEY
108
+
109
+ , domain_id INTEGER NOT NULL REFERENCES domains
110
+ , path_id INTEGER NOT NULL REFERENCES paths
111
+ , query_string_id INTEGER NOT NULL REFERENCES query_strings
112
+
113
+ , attribution_id INTEGER NOT NULL REFERENCES attributions
114
+
115
+ , UNIQUE (domain_id, path_id, query_string_id, attribution_id)
116
+ );
117
+
118
+ CREATE INDEX ON referers (domain_id);
119
+ CREATE INDEX ON referers (path_id);
120
+ CREATE INDEX ON referers (query_string_id);
121
+
122
+ CREATE INDEX ON referers (attribution_id);
123
+
124
+ CREATE TABLE cookies (
125
+ cookie_id UUID PRIMARY KEY DEFAULT uuid_generate_v4()
126
+ );
127
+
128
+ CREATE TABLE owners (
129
+ owner_id SERIAL PRIMARY KEY
130
+ , owner TEXT NOT NULL UNIQUE
131
+ );
132
+
133
+ CREATE TABLE ownerships (
134
+ ownership_id SERIAL PRIMARY KEY
135
+
136
+ , owner_id INTEGER NOT NULL REFERENCES owners
137
+ , cookie_id UUID NOT NULL REFERENCES cookies
138
+
139
+ , UNIQUE (owner_id, cookie_id)
140
+ );
141
+
142
+ CREATE TABLE visits (
143
+ visit_id UUID PRIMARY KEY DEFAULT uuid_generate_v4()
144
+
145
+ , cookie_id UUID NOT NULL REFERENCES cookies
146
+
147
+ , user_agent_id INTEGER NOT NULL REFERENCES user_agents
148
+ , attribution_id INTEGER NOT NULL REFERENCES attributions
149
+
150
+ , referer_id INTEGER REFERENCES referers
151
+ , owner_id INTEGER REFERENCES owners
152
+
153
+ , ip_address INET NOT NULL
154
+
155
+ , created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
156
+ , updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
157
+ );
158
+
159
+ CREATE INDEX ON visits (cookie_id);
160
+ CREATE INDEX ON visits (user_agent_id);
161
+ CREATE INDEX ON visits (attribution_id);
162
+ CREATE INDEX ON visits (referer_id);
163
+ CREATE INDEX ON visits (owner_id);
164
+
165
+ CREATE TABLE pageviews (
166
+ pageview_id UUID PRIMARY KEY DEFAULT uuid_generate_v4()
167
+
168
+ , visit_id UUID NOT NULL REFERENCES visits
169
+ , path_id INTEGER NOT NULL REFERENCES paths
170
+ , query_string_id INTEGER NOT NULL REFERENCES query_strings
171
+
172
+ , mime_type_id SMALLINT REFERENCES mime_types
173
+ , http_method_id SMALLINT NOT NULL REFERENCES http_methods
174
+
175
+ , request_id UUID
176
+
177
+ , click_id TEXT
178
+
179
+ , http_status INTEGER
180
+ , response_time INTEGER
181
+
182
+ , created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
183
+ );
184
+
185
+ CREATE INDEX ON pageviews (visit_id);
186
+ CREATE INDEX ON pageviews (path_id);
187
+ CREATE INDEX ON pageviews (query_string_id);
188
+ CREATE INDEX ON pageviews (request_id);
189
+ CREATE INDEX ON pageviews (click_id);
190
+
191
+ CREATE TABLE events (
192
+ event_id UUID PRIMARY KEY DEFAULT uuid_generate_v4()
193
+
194
+ , event_type_id SMALLINT NOT NULL REFERENCES event_types
195
+ , visit_id UUID NOT NULL REFERENCES visits
196
+
197
+ , meta JSON
198
+
199
+ , created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
200
+ );
201
+
202
+ CREATE INDEX ON events (event_type_id);
203
+ CREATE INDEX ON events (visit_id);
204
+
205
+
206
+ INSERT INTO bid_match_types (bid_match_type) VALUES ('bidded broad'), ('bidded content'), ('bidded exact'), ('bidded phrase');
207
+ INSERT INTO device_types (device_type) VALUES ('computer'), ('mobile'), ('tablet');
208
+ INSERT INTO match_types (match_type) VALUES ('broad'), ('phrase'), ('exact');
209
+ INSERT INTO networks (network) VALUES ('google_search'), ('search_partner'), ('display_network');
210
+
211
+ CREATE VIEW response_times_by_path AS SELECT *
212
+ FROM (
213
+ SELECT path_id
214
+ , path
215
+ , ROUND(AVG(response_time), 3) AS "avg response time (ms)"
216
+
217
+ FROM land.pageviews pv
218
+ JOIN land.paths p USING (path_id)
219
+
220
+ GROUP BY path_id, path
221
+ ) agg
222
+
223
+ ORDER BY agg."avg response time (ms)" DESC
224
+ ;
225
+ SQL
226
+ end
227
+ end