ahoy_matey 2.0.0 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|