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
|
@@ -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,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
|
data/bin/console
ADDED
|
@@ -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
|
data/bin/rails
ADDED
|
@@ -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'
|
data/bin/setup
ADDED
data/config.ru
ADDED
|
@@ -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
|