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.
Files changed (52) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +112 -37
  3. data/CONTRIBUTING.md +9 -7
  4. data/LICENSE.txt +1 -1
  5. data/README.md +377 -63
  6. data/app/controllers/ahoy/base_controller.rb +14 -10
  7. data/app/controllers/ahoy/events_controller.rb +1 -1
  8. data/app/controllers/ahoy/visits_controller.rb +1 -0
  9. data/app/jobs/ahoy/geocode_v2_job.rb +3 -4
  10. data/lib/ahoy.rb +57 -2
  11. data/lib/ahoy/base_store.rb +32 -3
  12. data/lib/ahoy/controller.rb +21 -8
  13. data/lib/ahoy/database_store.rb +33 -16
  14. data/lib/ahoy/engine.rb +3 -1
  15. data/lib/ahoy/helper.rb +40 -0
  16. data/lib/ahoy/model.rb +2 -2
  17. data/lib/ahoy/query_methods.rb +46 -1
  18. data/lib/ahoy/tracker.rb +59 -27
  19. data/lib/ahoy/utils.rb +7 -0
  20. data/lib/ahoy/version.rb +1 -1
  21. data/lib/ahoy/visit_properties.rb +73 -37
  22. data/lib/generators/ahoy/activerecord_generator.rb +17 -26
  23. data/lib/generators/ahoy/base_generator.rb +1 -1
  24. data/lib/generators/ahoy/install_generator.rb +1 -1
  25. data/lib/generators/ahoy/mongoid_generator.rb +1 -5
  26. data/lib/generators/ahoy/templates/active_record_event_model.rb.tt +10 -0
  27. data/lib/generators/ahoy/templates/{active_record_migration.rb → active_record_migration.rb.tt} +14 -7
  28. data/lib/generators/ahoy/templates/active_record_visit_model.rb.tt +6 -0
  29. data/lib/generators/ahoy/templates/{base_store_initializer.rb → base_store_initializer.rb.tt} +8 -0
  30. data/lib/generators/ahoy/templates/database_store_initializer.rb.tt +10 -0
  31. data/lib/generators/ahoy/templates/{mongoid_event_model.rb → mongoid_event_model.rb.tt} +1 -1
  32. data/lib/generators/ahoy/templates/{mongoid_visit_model.rb → mongoid_visit_model.rb.tt} +9 -7
  33. data/vendor/assets/javascripts/ahoy.js +539 -552
  34. metadata +27 -204
  35. data/.github/ISSUE_TEMPLATE.md +0 -7
  36. data/.gitignore +0 -17
  37. data/Gemfile +0 -6
  38. data/Rakefile +0 -9
  39. data/ahoy_matey.gemspec +0 -36
  40. data/docs/Ahoy-2-Upgrade.md +0 -147
  41. data/docs/Data-Store-Examples.md +0 -240
  42. data/lib/generators/ahoy/templates/active_record_event_model.rb +0 -10
  43. data/lib/generators/ahoy/templates/active_record_visit_model.rb +0 -6
  44. data/lib/generators/ahoy/templates/database_store_initializer.rb +0 -5
  45. data/test/query_methods/mongoid_test.rb +0 -23
  46. data/test/query_methods/mysql_json_test.rb +0 -18
  47. data/test/query_methods/mysql_text_test.rb +0 -19
  48. data/test/query_methods/postgresql_hstore_test.rb +0 -20
  49. data/test/query_methods/postgresql_json_test.rb +0 -18
  50. data/test/query_methods/postgresql_jsonb_test.rb +0 -19
  51. data/test/query_methods/postgresql_text_test.rb +0 -19
  52. 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
- if exclude?
64
- debug "Geocode excluded"
65
- else
66
- @store.geocode(data.select { |_, v| v })
67
- true
68
- end
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
- request.cookie_jar.delete("ahoy_visitor")
143
+ delete_cookie("ahoy_visitor")
134
144
  end
135
145
 
136
146
  def reset_visit
137
- request.cookie_jar.delete("ahoy_visit")
138
- request.cookie_jar.delete("ahoy_events")
139
- request.cookie_jar.delete("ahoy_track")
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
- cookie = {
158
- value: value
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 = Ahoy.cookie_domain
162
- cookie[:domain] = domain if domain && use_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.zone.now
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
- Safely.report_exception(e)
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 unless api? && Ahoy.protect_from_forgery
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 unless api? && Ahoy.protect_from_forgery
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
- Rails.logger.debug { "[ahoy] #{message}" }
281
+ Ahoy.log message
250
282
  end
251
283
  end
252
284
  end
data/lib/ahoy/utils.rb ADDED
@@ -0,0 +1,7 @@
1
+ module Ahoy
2
+ module Utils
3
+ def self.ensure_utf8(str)
4
+ str.encode("UTF-8", "binary", invalid: :replace, undef: :replace, replace: "") if str
5
+ end
6
+ end
7
+ end
data/lib/ahoy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ahoy
2
- VERSION = "2.0.0"
2
+ VERSION = "3.2.0"
3
3
  end
@@ -1,6 +1,6 @@
1
- require "browser"
2
- require "referer-parser"
3
- require "user_agent_parser"
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
- landing_uri = Addressable::URI.parse(landing_page) rescue nil
24
- landing_params = (landing_uri && landing_uri.query_values) || {}
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
- # cache for performance
35
- @@referrer_parser ||= RefererParser::Parser.new
36
-
40
+ uri = URI.parse(referrer) rescue nil
37
41
  {
38
- referring_domain: (Addressable::URI.parse(referrer).host.first(255) rescue nil),
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
- # cache for performance
45
- @@user_agent_parser ||= UserAgentParser::Parser.new
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
- user_agent = request.user_agent
48
- agent = @@user_agent_parser.parse(user_agent)
49
- browser = Browser.new(user_agent)
50
- device_type =
51
- if browser.bot?
52
- "Bot"
53
- elsif browser.device.tv?
54
- "TV"
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
- browser: agent.name,
67
- os: agent.os.name,
68
- device_type: device_type,
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: request.remote_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 Rails::Generators::Migration
11
- source_root File.expand_path("../templates", __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
- migrate_command = rails5? ? "rails" : "rake"
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
- # use connection_config instead of connection.adapter
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
- def rails5?
48
- Rails::VERSION::MAJOR >= 5
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
- if rails5?
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.expand_path("../templates", __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"