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.
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"