ahoy_matey 2.0.0 → 3.2.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 +5 -5
- data/CHANGELOG.md +112 -37
- data/CONTRIBUTING.md +9 -7
- data/LICENSE.txt +1 -1
- data/README.md +377 -63
- data/app/controllers/ahoy/base_controller.rb +14 -10
- data/app/controllers/ahoy/events_controller.rb +1 -1
- data/app/controllers/ahoy/visits_controller.rb +1 -0
- data/app/jobs/ahoy/geocode_v2_job.rb +3 -4
- data/lib/ahoy.rb +57 -2
- data/lib/ahoy/base_store.rb +32 -3
- data/lib/ahoy/controller.rb +21 -8
- data/lib/ahoy/database_store.rb +33 -16
- data/lib/ahoy/engine.rb +3 -1
- data/lib/ahoy/helper.rb +40 -0
- data/lib/ahoy/model.rb +2 -2
- data/lib/ahoy/query_methods.rb +46 -1
- data/lib/ahoy/tracker.rb +59 -27
- data/lib/ahoy/utils.rb +7 -0
- data/lib/ahoy/version.rb +1 -1
- data/lib/ahoy/visit_properties.rb +73 -37
- data/lib/generators/ahoy/activerecord_generator.rb +17 -26
- data/lib/generators/ahoy/base_generator.rb +1 -1
- data/lib/generators/ahoy/install_generator.rb +1 -1
- data/lib/generators/ahoy/mongoid_generator.rb +1 -5
- data/lib/generators/ahoy/templates/active_record_event_model.rb.tt +10 -0
- data/lib/generators/ahoy/templates/{active_record_migration.rb → active_record_migration.rb.tt} +14 -7
- data/lib/generators/ahoy/templates/active_record_visit_model.rb.tt +6 -0
- data/lib/generators/ahoy/templates/{base_store_initializer.rb → base_store_initializer.rb.tt} +8 -0
- data/lib/generators/ahoy/templates/database_store_initializer.rb.tt +10 -0
- data/lib/generators/ahoy/templates/{mongoid_event_model.rb → mongoid_event_model.rb.tt} +1 -1
- data/lib/generators/ahoy/templates/{mongoid_visit_model.rb → mongoid_visit_model.rb.tt} +9 -7
- data/vendor/assets/javascripts/ahoy.js +539 -552
- metadata +27 -204
- data/.github/ISSUE_TEMPLATE.md +0 -7
- data/.gitignore +0 -17
- data/Gemfile +0 -6
- data/Rakefile +0 -9
- data/ahoy_matey.gemspec +0 -36
- data/docs/Ahoy-2-Upgrade.md +0 -147
- data/docs/Data-Store-Examples.md +0 -240
- data/lib/generators/ahoy/templates/active_record_event_model.rb +0 -10
- data/lib/generators/ahoy/templates/active_record_visit_model.rb +0 -6
- data/lib/generators/ahoy/templates/database_store_initializer.rb +0 -5
- data/test/query_methods/mongoid_test.rb +0 -23
- data/test/query_methods/mysql_json_test.rb +0 -18
- data/test/query_methods/mysql_text_test.rb +0 -19
- data/test/query_methods/postgresql_hstore_test.rb +0 -20
- data/test/query_methods/postgresql_json_test.rb +0 -18
- data/test/query_methods/postgresql_jsonb_test.rb +0 -19
- data/test/query_methods/postgresql_text_test.rb +0 -19
- data/test/test_helper.rb +0 -100
data/lib/ahoy/tracker.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
|
+
require "active_support/core_ext/digest/uuid"
|
2
|
+
|
1
3
|
module Ahoy
|
2
4
|
class Tracker
|
5
|
+
UUID_NAMESPACE = "a82ae811-5011-45ab-a728-569df7499c5f"
|
6
|
+
|
3
7
|
attr_reader :request, :controller
|
4
8
|
|
5
9
|
def initialize(**options)
|
@@ -7,6 +11,7 @@ module Ahoy
|
|
7
11
|
@controller = options[:controller]
|
8
12
|
@request = options[:request] || @controller.try(:request)
|
9
13
|
@visit_token = options[:visit_token]
|
14
|
+
@user = options[:user]
|
10
15
|
@options = options
|
11
16
|
end
|
12
17
|
|
@@ -33,7 +38,7 @@ module Ahoy
|
|
33
38
|
report_exception(e)
|
34
39
|
end
|
35
40
|
|
36
|
-
def track_visit(defer: false)
|
41
|
+
def track_visit(defer: false, started_at: nil)
|
37
42
|
if exclude?
|
38
43
|
debug "Visit excluded"
|
39
44
|
elsif missing_params?
|
@@ -42,16 +47,18 @@ module Ahoy
|
|
42
47
|
if defer
|
43
48
|
set_cookie("ahoy_track", true, nil, false)
|
44
49
|
else
|
50
|
+
delete_cookie("ahoy_track")
|
51
|
+
|
45
52
|
data = {
|
46
53
|
visit_token: visit_token,
|
47
54
|
visitor_token: visitor_token,
|
48
55
|
user_id: user.try(:id),
|
49
|
-
started_at: trusted_time,
|
56
|
+
started_at: trusted_time(started_at),
|
50
57
|
}.merge(visit_properties).select { |_, v| v }
|
51
58
|
|
52
59
|
@store.track_visit(data)
|
53
60
|
|
54
|
-
Ahoy::GeocodeV2Job.perform_later(visit_token, data[:ip]) if Ahoy.geocode
|
61
|
+
Ahoy::GeocodeV2Job.perform_later(visit_token, data[:ip]) if Ahoy.geocode && data[:ip]
|
55
62
|
end
|
56
63
|
end
|
57
64
|
true
|
@@ -60,12 +67,12 @@ module Ahoy
|
|
60
67
|
end
|
61
68
|
|
62
69
|
def geocode(data)
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
70
|
+
data = {
|
71
|
+
visit_token: visit_token
|
72
|
+
}.merge(data).select { |_, v| v }
|
73
|
+
|
74
|
+
@store.geocode(data)
|
75
|
+
true
|
69
76
|
rescue => e
|
70
77
|
report_exception(e)
|
71
78
|
end
|
@@ -91,8 +98,12 @@ module Ahoy
|
|
91
98
|
@visit ||= @store.visit
|
92
99
|
end
|
93
100
|
|
101
|
+
def visit_or_create
|
102
|
+
@visit ||= @store.visit_or_create
|
103
|
+
end
|
104
|
+
|
94
105
|
def new_visit?
|
95
|
-
!existing_visit_token
|
106
|
+
Ahoy.cookies ? !existing_visit_token : visit.nil?
|
96
107
|
end
|
97
108
|
|
98
109
|
def new_visitor?
|
@@ -113,9 +124,8 @@ module Ahoy
|
|
113
124
|
@user ||= @store.user
|
114
125
|
end
|
115
126
|
|
116
|
-
# TODO better name
|
117
127
|
def visit_properties
|
118
|
-
@visit_properties ||= Ahoy::VisitProperties.new(request, api: api?).generate
|
128
|
+
@visit_properties ||= request ? Ahoy::VisitProperties.new(request, api: api?).generate : {}
|
119
129
|
end
|
120
130
|
|
121
131
|
def visit_token
|
@@ -130,13 +140,13 @@ module Ahoy
|
|
130
140
|
|
131
141
|
def reset
|
132
142
|
reset_visit
|
133
|
-
|
143
|
+
delete_cookie("ahoy_visitor")
|
134
144
|
end
|
135
145
|
|
136
146
|
def reset_visit
|
137
|
-
|
138
|
-
|
139
|
-
|
147
|
+
delete_cookie("ahoy_visit")
|
148
|
+
delete_cookie("ahoy_events")
|
149
|
+
delete_cookie("ahoy_track")
|
140
150
|
end
|
141
151
|
|
142
152
|
protected
|
@@ -146,7 +156,7 @@ module Ahoy
|
|
146
156
|
end
|
147
157
|
|
148
158
|
def missing_params?
|
149
|
-
if api? && Ahoy.protect_from_forgery
|
159
|
+
if Ahoy.cookies && api? && Ahoy.protect_from_forgery
|
150
160
|
!(existing_visit_token && existing_visitor_token)
|
151
161
|
else
|
152
162
|
false
|
@@ -154,18 +164,24 @@ module Ahoy
|
|
154
164
|
end
|
155
165
|
|
156
166
|
def set_cookie(name, value, duration = nil, use_domain = true)
|
157
|
-
|
158
|
-
|
159
|
-
|
167
|
+
# safety net
|
168
|
+
return unless Ahoy.cookies && request
|
169
|
+
|
170
|
+
cookie = Ahoy.cookie_options.merge(value: value)
|
160
171
|
cookie[:expires] = duration.from_now if duration
|
161
|
-
domain
|
162
|
-
cookie[:domain]
|
172
|
+
# prefer cookie_options[:domain] over cookie_domain
|
173
|
+
cookie[:domain] ||= Ahoy.cookie_domain if Ahoy.cookie_domain
|
174
|
+
cookie.delete(:domain) unless use_domain
|
163
175
|
request.cookie_jar[name] = cookie
|
164
176
|
end
|
165
177
|
|
178
|
+
def delete_cookie(name)
|
179
|
+
request.cookie_jar.delete(name) if request && request.cookie_jar[name]
|
180
|
+
end
|
181
|
+
|
166
182
|
def trusted_time(time = nil)
|
167
183
|
if !time || (api? && !(1.minute.ago..Time.now).cover?(time))
|
168
|
-
Time.
|
184
|
+
Time.current
|
169
185
|
else
|
170
186
|
time
|
171
187
|
end
|
@@ -176,7 +192,12 @@ module Ahoy
|
|
176
192
|
end
|
177
193
|
|
178
194
|
def report_exception(e)
|
179
|
-
|
195
|
+
if defined?(ActionDispatch::RemoteIp::IpSpoofAttackError) && e.is_a?(ActionDispatch::RemoteIp::IpSpoofAttackError)
|
196
|
+
debug "Tracking excluded due to IP spoofing"
|
197
|
+
else
|
198
|
+
raise e if !defined?(Rails) || Rails.env.development? || Rails.env.test?
|
199
|
+
Safely.report_exception(e)
|
200
|
+
end
|
180
201
|
end
|
181
202
|
|
182
203
|
def generate_id
|
@@ -186,6 +207,7 @@ module Ahoy
|
|
186
207
|
def visit_token_helper
|
187
208
|
@visit_token_helper ||= begin
|
188
209
|
token = existing_visit_token
|
210
|
+
token ||= visit_anonymity_set unless Ahoy.cookies
|
189
211
|
token ||= generate_id unless Ahoy.api_only
|
190
212
|
token
|
191
213
|
end
|
@@ -194,6 +216,7 @@ module Ahoy
|
|
194
216
|
def visitor_token_helper
|
195
217
|
@visitor_token_helper ||= begin
|
196
218
|
token = existing_visitor_token
|
219
|
+
token ||= visitor_anonymity_set unless Ahoy.cookies
|
197
220
|
token ||= generate_id unless Ahoy.api_only
|
198
221
|
token
|
199
222
|
end
|
@@ -202,7 +225,7 @@ module Ahoy
|
|
202
225
|
def existing_visit_token
|
203
226
|
@existing_visit_token ||= begin
|
204
227
|
token = visit_header
|
205
|
-
token ||= visit_cookie
|
228
|
+
token ||= visit_cookie if Ahoy.cookies && !(api? && Ahoy.protect_from_forgery)
|
206
229
|
token ||= visit_param if api?
|
207
230
|
token
|
208
231
|
end
|
@@ -211,12 +234,20 @@ module Ahoy
|
|
211
234
|
def existing_visitor_token
|
212
235
|
@existing_visitor_token ||= begin
|
213
236
|
token = visitor_header
|
214
|
-
token ||= visitor_cookie
|
237
|
+
token ||= visitor_cookie if Ahoy.cookies && !(api? && Ahoy.protect_from_forgery)
|
215
238
|
token ||= visitor_param if api?
|
216
239
|
token
|
217
240
|
end
|
218
241
|
end
|
219
242
|
|
243
|
+
def visit_anonymity_set
|
244
|
+
@visit_anonymity_set ||= Digest::UUID.uuid_v5(UUID_NAMESPACE, ["visit", Ahoy.mask_ip(request.remote_ip), request.user_agent].join("/"))
|
245
|
+
end
|
246
|
+
|
247
|
+
def visitor_anonymity_set
|
248
|
+
@visitor_anonymity_set ||= Digest::UUID.uuid_v5(UUID_NAMESPACE, ["visitor", Ahoy.mask_ip(request.remote_ip), request.user_agent].join("/"))
|
249
|
+
end
|
250
|
+
|
220
251
|
def visit_cookie
|
221
252
|
@visit_cookie ||= request && request.cookies["ahoy_visit"]
|
222
253
|
end
|
@@ -242,11 +273,12 @@ module Ahoy
|
|
242
273
|
end
|
243
274
|
|
244
275
|
def ensure_token(token)
|
276
|
+
token = Ahoy::Utils.ensure_utf8(token)
|
245
277
|
token.to_s.gsub(/[^a-z0-9\-]/i, "").first(64) if token
|
246
278
|
end
|
247
279
|
|
248
280
|
def debug(message)
|
249
|
-
|
281
|
+
Ahoy.log message
|
250
282
|
end
|
251
283
|
end
|
252
284
|
end
|
data/lib/ahoy/utils.rb
ADDED
data/lib/ahoy/version.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
require "
|
2
|
-
require "
|
3
|
-
require "
|
1
|
+
require "cgi"
|
2
|
+
require "device_detector"
|
3
|
+
require "uri"
|
4
4
|
|
5
5
|
module Ahoy
|
6
6
|
class VisitProperties
|
@@ -20,59 +20,95 @@ module Ahoy
|
|
20
20
|
private
|
21
21
|
|
22
22
|
def utm_properties
|
23
|
-
|
24
|
-
|
23
|
+
landing_params = {}
|
24
|
+
begin
|
25
|
+
landing_uri = URI.parse(landing_page)
|
26
|
+
# could also use Rack::Utils.parse_nested_query
|
27
|
+
landing_params = CGI.parse(landing_uri.query) if landing_uri
|
28
|
+
rescue
|
29
|
+
# do nothing
|
30
|
+
end
|
25
31
|
|
26
32
|
props = {}
|
27
33
|
%w(utm_source utm_medium utm_term utm_content utm_campaign).each do |name|
|
28
|
-
props[name.to_sym] = params[name] || landing_params[name]
|
34
|
+
props[name.to_sym] = params[name] || landing_params[name].try(:first)
|
29
35
|
end
|
30
36
|
props
|
31
37
|
end
|
32
38
|
|
33
39
|
def traffic_properties
|
34
|
-
|
35
|
-
@@referrer_parser ||= RefererParser::Parser.new
|
36
|
-
|
40
|
+
uri = URI.parse(referrer) rescue nil
|
37
41
|
{
|
38
|
-
referring_domain:
|
39
|
-
search_keyword: (@@referrer_parser.parse(@referrer)[:term][0..255] rescue nil).presence
|
42
|
+
referring_domain: uri.try(:host).try(:first, 255)
|
40
43
|
}
|
41
44
|
end
|
42
45
|
|
43
46
|
def tech_properties
|
44
|
-
|
45
|
-
|
47
|
+
if Ahoy.user_agent_parser == :device_detector
|
48
|
+
client = DeviceDetector.new(request.user_agent)
|
49
|
+
device_type =
|
50
|
+
case client.device_type
|
51
|
+
when "smartphone"
|
52
|
+
"Mobile"
|
53
|
+
when "tv"
|
54
|
+
"TV"
|
55
|
+
else
|
56
|
+
client.device_type.try(:titleize)
|
57
|
+
end
|
46
58
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
elsif browser.device.console?
|
56
|
-
"Console"
|
57
|
-
elsif browser.device.tablet?
|
58
|
-
"Tablet"
|
59
|
-
elsif browser.device.mobile?
|
60
|
-
"Mobile"
|
61
|
-
else
|
62
|
-
"Desktop"
|
63
|
-
end
|
59
|
+
{
|
60
|
+
browser: client.name,
|
61
|
+
os: client.os_name,
|
62
|
+
device_type: device_type
|
63
|
+
}
|
64
|
+
else
|
65
|
+
raise "Add browser to your Gemfile to use legacy user agent parsing" unless defined?(Browser)
|
66
|
+
raise "Add user_agent_parser to your Gemfile to use legacy user agent parsing" unless defined?(UserAgentParser)
|
64
67
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
68
|
+
# cache for performance
|
69
|
+
@@user_agent_parser ||= UserAgentParser::Parser.new
|
70
|
+
|
71
|
+
user_agent = request.user_agent
|
72
|
+
agent = @@user_agent_parser.parse(user_agent)
|
73
|
+
browser = Browser.new(user_agent)
|
74
|
+
device_type =
|
75
|
+
if browser.bot?
|
76
|
+
"Bot"
|
77
|
+
elsif browser.device.tv?
|
78
|
+
"TV"
|
79
|
+
elsif browser.device.console?
|
80
|
+
"Console"
|
81
|
+
elsif browser.device.tablet?
|
82
|
+
"Tablet"
|
83
|
+
elsif browser.device.mobile?
|
84
|
+
"Mobile"
|
85
|
+
else
|
86
|
+
"Desktop"
|
87
|
+
end
|
88
|
+
|
89
|
+
{
|
90
|
+
browser: agent.name,
|
91
|
+
os: agent.os.name,
|
92
|
+
device_type: device_type
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# masking based on Google Analytics anonymization
|
98
|
+
# https://support.google.com/analytics/answer/2763052
|
99
|
+
def ip
|
100
|
+
ip = request.remote_ip
|
101
|
+
if ip && Ahoy.mask_ips
|
102
|
+
Ahoy.mask_ip(ip)
|
103
|
+
else
|
104
|
+
ip
|
105
|
+
end
|
70
106
|
end
|
71
107
|
|
72
108
|
def request_properties
|
73
109
|
{
|
74
|
-
ip:
|
75
|
-
user_agent: request.user_agent,
|
110
|
+
ip: ip,
|
111
|
+
user_agent: Ahoy::Utils.ensure_utf8(request.user_agent),
|
76
112
|
referrer: referrer,
|
77
113
|
landing_page: landing_page,
|
78
114
|
platform: params["platform"],
|
@@ -1,40 +1,23 @@
|
|
1
|
-
# taken from https://github.com/collectiveidea/audited/blob/master/lib/generators/audited/install_generator.rb
|
2
|
-
require "rails/generators"
|
3
|
-
require "rails/generators/migration"
|
4
|
-
require "active_record"
|
5
1
|
require "rails/generators/active_record"
|
6
2
|
|
7
3
|
module Ahoy
|
8
4
|
module Generators
|
9
5
|
class ActiverecordGenerator < Rails::Generators::Base
|
10
|
-
include
|
11
|
-
source_root File.
|
6
|
+
include ActiveRecord::Generators::Migration
|
7
|
+
source_root File.join(__dir__, "templates")
|
12
8
|
|
13
9
|
class_option :database, type: :string, aliases: "-d"
|
14
10
|
|
15
|
-
# Implement the required interface for Rails::Generators::Migration.
|
16
|
-
def self.next_migration_number(dirname) #:nodoc:
|
17
|
-
next_migration_number = current_migration_number(dirname) + 1
|
18
|
-
if ::ActiveRecord::Base.timestamped_migrations
|
19
|
-
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
20
|
-
else
|
21
|
-
"%.3d" % next_migration_number
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
11
|
def copy_templates
|
26
12
|
template "database_store_initializer.rb", "config/initializers/ahoy.rb"
|
27
13
|
template "active_record_visit_model.rb", "app/models/ahoy/visit.rb"
|
28
14
|
template "active_record_event_model.rb", "app/models/ahoy/event.rb"
|
29
15
|
migration_template "active_record_migration.rb", "db/migrate/create_ahoy_visits_and_events.rb", migration_version: migration_version
|
30
|
-
|
31
|
-
puts "\nAlmost set! Last, run:\n\n #{migrate_command} db:migrate"
|
16
|
+
puts "\nAlmost set! Last, run:\n\n rails db:migrate"
|
32
17
|
end
|
33
18
|
|
34
19
|
def properties_type
|
35
|
-
|
36
|
-
# so database connection isn't needed
|
37
|
-
case ActiveRecord::Base.connection_config[:adapter].to_s
|
20
|
+
case adapter
|
38
21
|
when /postg/i # postgres, postgis
|
39
22
|
"jsonb"
|
40
23
|
when /mysql/i
|
@@ -44,14 +27,22 @@ module Ahoy
|
|
44
27
|
end
|
45
28
|
end
|
46
29
|
|
47
|
-
|
48
|
-
|
30
|
+
# use connection_config instead of connection.adapter
|
31
|
+
# so database connection isn't needed
|
32
|
+
def adapter
|
33
|
+
if ActiveRecord::VERSION::STRING.to_f >= 6.1
|
34
|
+
ActiveRecord::Base.connection_db_config.adapter.to_s
|
35
|
+
else
|
36
|
+
ActiveRecord::Base.connection_config[:adapter].to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def rails52?
|
41
|
+
ActiveRecord::VERSION::STRING.to_f >= 5.2
|
49
42
|
end
|
50
43
|
|
51
44
|
def migration_version
|
52
|
-
|
53
|
-
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
54
|
-
end
|
45
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
55
46
|
end
|
56
47
|
end
|
57
48
|
end
|
@@ -3,7 +3,7 @@ require "rails/generators"
|
|
3
3
|
module Ahoy
|
4
4
|
module Generators
|
5
5
|
class BaseGenerator < Rails::Generators::Base
|
6
|
-
source_root File.
|
6
|
+
source_root File.join(__dir__, "templates")
|
7
7
|
|
8
8
|
def copy_templates
|
9
9
|
template "base_store_initializer.rb", "config/initializers/ahoy.rb"
|