ahoy_matey 2.2.1 → 3.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,13 +1,10 @@
1
1
  module Ahoy
2
2
  class BaseController < ApplicationController
3
3
  filters = _process_action_callbacks.map(&:filter) - Ahoy.preserve_callbacks
4
- if Rails::VERSION::MAJOR >= 5
5
- skip_before_action(*filters, raise: false)
6
- skip_after_action(*filters, raise: false)
7
- skip_around_action(*filters, raise: false)
8
- else
9
- skip_action_callback(*filters)
10
- end
4
+ skip_before_action(*filters, raise: false)
5
+ skip_after_action(*filters, raise: false)
6
+ skip_around_action(*filters, raise: false)
7
+
11
8
  before_action :verify_request_size
12
9
  before_action :renew_cookies
13
10
 
@@ -31,7 +28,7 @@ module Ahoy
31
28
  def verify_request_size
32
29
  if request.content_length > Ahoy.max_content_length
33
30
  logger.info "[ahoy] Payload too large"
34
- render text: "Payload too large\n", status: 413
31
+ render plain: "Payload too large\n", status: 413
35
32
  end
36
33
  end
37
34
  end
@@ -1,11 +1,12 @@
1
1
  require "ipaddr"
2
2
 
3
+ # dependencies
3
4
  require "active_support"
4
5
  require "active_support/core_ext"
5
- require "addressable/uri"
6
6
  require "geocoder"
7
7
  require "safely/core"
8
8
 
9
+ # modules
9
10
  require "ahoy/utils"
10
11
  require "ahoy/base_store"
11
12
  require "ahoy/controller"
@@ -29,8 +30,12 @@ module Ahoy
29
30
  mattr_accessor :cookies
30
31
  self.cookies = true
31
32
 
33
+ # TODO deprecate in favor of cookie_options
32
34
  mattr_accessor :cookie_domain
33
35
 
36
+ mattr_accessor :cookie_options
37
+ self.cookie_options = {}
38
+
34
39
  mattr_accessor :server_side_visits
35
40
  self.server_side_visits = true
36
41
 
@@ -72,7 +77,7 @@ module Ahoy
72
77
  self.track_bots = false
73
78
 
74
79
  mattr_accessor :bot_detection_version
75
- self.bot_detection_version = 1
80
+ self.bot_detection_version = 2
76
81
 
77
82
  mattr_accessor :token_generator
78
83
  self.token_generator = -> { SecureRandom.uuid }
@@ -81,10 +86,12 @@ module Ahoy
81
86
  self.mask_ips = false
82
87
 
83
88
  mattr_accessor :user_agent_parser
84
- self.user_agent_parser = :legacy
89
+ self.user_agent_parser = :device_detector
90
+
91
+ mattr_accessor :logger
85
92
 
86
93
  def self.log(message)
87
- Rails.logger.info { "[ahoy] #{message}" }
94
+ logger.info { "[ahoy] #{message}" } if logger
88
95
  end
89
96
 
90
97
  def self.mask_ip(ip)
@@ -26,7 +26,7 @@ module Ahoy
26
26
  if Ahoy.user_method.respond_to?(:call)
27
27
  Ahoy.user_method.call(controller)
28
28
  else
29
- controller.send(Ahoy.user_method)
29
+ controller.send(Ahoy.user_method) if controller.respond_to?(Ahoy.user_method, true)
30
30
  end
31
31
  end
32
32
  end
@@ -52,11 +52,13 @@ module Ahoy
52
52
  if Ahoy.user_agent_parser == :device_detector
53
53
  detector = DeviceDetector.new(request.user_agent)
54
54
  if Ahoy.bot_detection_version == 2
55
- detector.bot? || detector.device_type.nil?
55
+ detector.bot? || (detector.device_type.nil? && detector.os_name.nil?)
56
56
  else
57
57
  detector.bot?
58
58
  end
59
59
  else
60
+ # no need to throw friendly error if browser isn't defined
61
+ # since will error in visit_properties
60
62
  Browser.new(request.user_agent).bot?
61
63
  end
62
64
  else
@@ -1,5 +1,3 @@
1
- require "request_store"
2
-
3
1
  module Ahoy
4
2
  module Controller
5
3
  def self.included(base)
@@ -9,7 +7,7 @@ module Ahoy
9
7
  end
10
8
  base.before_action :set_ahoy_cookies, unless: -> { Ahoy.api_only }
11
9
  base.before_action :track_ahoy_visit, unless: -> { Ahoy.api_only }
12
- base.before_action :set_ahoy_request_store
10
+ base.around_action :set_ahoy_request_store
13
11
  end
14
12
 
15
13
  def ahoy
@@ -41,7 +39,13 @@ module Ahoy
41
39
  end
42
40
 
43
41
  def set_ahoy_request_store
44
- RequestStore.store[:ahoy] ||= ahoy
42
+ previous_value = Thread.current[:ahoy]
43
+ begin
44
+ Thread.current[:ahoy] = ahoy
45
+ yield
46
+ ensure
47
+ Thread.current[:ahoy] = previous_value
48
+ end
45
49
  end
46
50
  end
47
51
  end
@@ -34,7 +34,7 @@ module Ahoy
34
34
  # upsert since visit might not be found due to eventual consistency
35
35
  visit_model.where(visit_token: visit_token).find_one_and_update({"$set": data}, {upsert: true})
36
36
  elsif visit
37
- visit.update_attributes(data)
37
+ visit.update!(data)
38
38
  else
39
39
  Ahoy.log "Visit for geocode not found: #{visit_token}"
40
40
  end
@@ -76,7 +76,7 @@ module Ahoy
76
76
 
77
77
  def slice_data(model, data)
78
78
  column_names = model.try(:column_names) || model.attribute_names
79
- data.slice(*column_names.map(&:to_sym)).select { |_, v| v }
79
+ data.slice(*column_names.map(&:to_sym)).select { |_, v| !v.nil? }
80
80
  end
81
81
 
82
82
  def unique_exception?(e)
@@ -1,6 +1,8 @@
1
1
  module Ahoy
2
2
  class Engine < ::Rails::Engine
3
3
  initializer "ahoy", after: "sprockets.environment" do
4
+ Ahoy.logger ||= Rails.logger
5
+
4
6
  # allow Devise to be loaded after Ahoy
5
7
  require "ahoy/warden" if defined?(Warden)
6
8
 
@@ -2,14 +2,12 @@ module Ahoy
2
2
  module Model
3
3
  def visitable(name = :visit, **options)
4
4
  class_eval do
5
- safe_options = options.dup
6
- safe_options[:optional] = true if Rails::VERSION::MAJOR >= 5
7
- belongs_to(name, class_name: "Ahoy::Visit", **safe_options)
5
+ belongs_to(name, class_name: "Ahoy::Visit", optional: true, **options)
8
6
  before_create :set_ahoy_visit
9
7
  end
10
8
  class_eval %{
11
9
  def set_ahoy_visit
12
- self.#{name} ||= RequestStore.store[:ahoy].try(:visit_or_create)
10
+ self.#{name} ||= Thread.current[:ahoy].try(:visit_or_create)
13
11
  end
14
12
  }
15
13
  end
@@ -11,6 +11,7 @@ module Ahoy
11
11
  @controller = options[:controller]
12
12
  @request = options[:request] || @controller.try(:request)
13
13
  @visit_token = options[:visit_token]
14
+ @user = options[:user]
14
15
  @options = options
15
16
  end
16
17
 
@@ -57,7 +58,7 @@ module Ahoy
57
58
 
58
59
  @store.track_visit(data)
59
60
 
60
- 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]
61
62
  end
62
63
  end
63
64
  true
@@ -127,9 +128,8 @@ module Ahoy
127
128
  @user ||= @store.user
128
129
  end
129
130
 
130
- # TODO better name
131
131
  def visit_properties
132
- @visit_properties ||= Ahoy::VisitProperties.new(request, api: api?).generate
132
+ @visit_properties ||= request ? Ahoy::VisitProperties.new(request, api: api?).generate : {}
133
133
  end
134
134
 
135
135
  def visit_token
@@ -169,24 +169,23 @@ module Ahoy
169
169
 
170
170
  def set_cookie(name, value, duration = nil, use_domain = true)
171
171
  # safety net
172
- return unless Ahoy.cookies
172
+ return unless Ahoy.cookies && request
173
173
 
174
- cookie = {
175
- value: value
176
- }
174
+ cookie = Ahoy.cookie_options.merge(value: value)
177
175
  cookie[:expires] = duration.from_now if duration
178
- domain = Ahoy.cookie_domain
179
- cookie[:domain] = domain if domain && use_domain
176
+ # prefer cookie_options[:domain] over cookie_domain
177
+ cookie[:domain] ||= Ahoy.cookie_domain if Ahoy.cookie_domain
178
+ cookie.delete(:domain) unless use_domain
180
179
  request.cookie_jar[name] = cookie
181
180
  end
182
181
 
183
182
  def delete_cookie(name)
184
- request.cookie_jar.delete(name) if request.cookie_jar[name]
183
+ request.cookie_jar.delete(name) if request && request.cookie_jar[name]
185
184
  end
186
185
 
187
186
  def trusted_time(time = nil)
188
187
  if !time || (api? && !(1.minute.ago..Time.now).cover?(time))
189
- Time.zone.now
188
+ Time.current
190
189
  else
191
190
  time
192
191
  end
@@ -197,8 +196,12 @@ module Ahoy
197
196
  end
198
197
 
199
198
  def report_exception(e)
200
- raise e if Rails.env.development? || Rails.env.test?
201
- Safely.report_exception(e)
199
+ if defined?(ActionDispatch::RemoteIp::IpSpoofAttackError) && e.is_a?(ActionDispatch::RemoteIp::IpSpoofAttackError)
200
+ debug "Tracking excluded due to IP spoofing"
201
+ else
202
+ raise e if !defined?(Rails) || Rails.env.development? || Rails.env.test?
203
+ Safely.report_exception(e)
204
+ end
202
205
  end
203
206
 
204
207
  def generate_id
@@ -1,3 +1,3 @@
1
1
  module Ahoy
2
- VERSION = "2.2.1"
2
+ VERSION = "3.0.4"
3
3
  end
@@ -1,7 +1,6 @@
1
- require "browser"
1
+ require "cgi"
2
2
  require "device_detector"
3
- require "referer-parser"
4
- require "user_agent_parser"
3
+ require "uri"
5
4
 
6
5
  module Ahoy
7
6
  class VisitProperties
@@ -21,23 +20,26 @@ module Ahoy
21
20
  private
22
21
 
23
22
  def utm_properties
24
- landing_uri = Addressable::URI.parse(landing_page) rescue nil
25
- 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
26
31
 
27
32
  props = {}
28
33
  %w(utm_source utm_medium utm_term utm_content utm_campaign).each do |name|
29
- props[name.to_sym] = params[name] || landing_params[name]
34
+ props[name.to_sym] = params[name] || landing_params[name].try(:first)
30
35
  end
31
36
  props
32
37
  end
33
38
 
34
39
  def traffic_properties
35
- # cache for performance
36
- @@referrer_parser ||= RefererParser::Parser.new
37
-
40
+ uri = URI.parse(referrer) rescue nil
38
41
  {
39
- referring_domain: (Addressable::URI.parse(referrer).host.first(255) rescue nil),
40
- search_keyword: (@@referrer_parser.parse(@referrer)[:term].first(255) rescue nil).presence
42
+ referring_domain: uri.try(:host).try(:first, 255)
41
43
  }
42
44
  end
43
45
 
@@ -60,6 +62,9 @@ module Ahoy
60
62
  device_type: device_type
61
63
  }
62
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)
67
+
63
68
  # cache for performance
64
69
  @@user_agent_parser ||= UserAgentParser::Parser.new
65
70
 
@@ -1,34 +1,19 @@
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
@@ -44,14 +29,12 @@ module Ahoy
44
29
  end
45
30
  end
46
31
 
47
- def rails5?
48
- Rails::VERSION::MAJOR >= 5
32
+ def rails52?
33
+ ActiveRecord::VERSION::STRING >= "5.2"
49
34
  end
50
35
 
51
36
  def migration_version
52
- if rails5?
53
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
54
- end
37
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
55
38
  end
56
39
  end
57
40
  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"
@@ -3,7 +3,7 @@ require "rails/generators"
3
3
  module Ahoy
4
4
  module Generators
5
5
  class InstallGenerator < 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
  activerecord = defined?(ActiveRecord)
@@ -3,7 +3,7 @@ require "rails/generators"
3
3
  module Ahoy
4
4
  module Generators
5
5
  class MongoidGenerator < 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 "database_store_initializer.rb", "config/initializers/ahoy.rb"
@@ -11,10 +11,6 @@ module Ahoy
11
11
  template "mongoid_event_model.rb", "app/models/ahoy/event.rb"
12
12
  puts "\nAlmost set! Last, run:\n\n rake db:mongoid:create_indexes"
13
13
  end
14
-
15
- def rails5?
16
- Rails::VERSION::MAJOR >= 5
17
- end
18
14
  end
19
15
  end
20
16
  end
@@ -1,10 +1,10 @@
1
- class Ahoy::Event < <%= rails5? ? "ApplicationRecord" : "ActiveRecord::Base" %>
1
+ class Ahoy::Event < ApplicationRecord
2
2
  include Ahoy::QueryMethods
3
3
 
4
4
  self.table_name = "ahoy_events"
5
5
 
6
6
  belongs_to :visit
7
- belongs_to :user<%= rails5? ? ", optional: true" : nil %><% if properties_type == "text" %>
7
+ belongs_to :user, optional: true<% if properties_type == "text" %>
8
8
 
9
9
  serialize :properties, JSON<% end %>
10
10
  end
@@ -26,8 +26,8 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
26
26
  t.string :country
27
27
  t.string :region
28
28
  t.string :city
29
- t.decimal :latitude, precision: 10, scale: 8
30
- t.decimal :longitude, precision: 11, scale: 8
29
+ t.float :latitude
30
+ t.float :longitude
31
31
 
32
32
  # utm parameters
33
33
  t.string :utm_source
@@ -55,7 +55,8 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
55
55
  t.timestamp :time
56
56
  end
57
57
 
58
- add_index :ahoy_events, [:name, :time]<% if properties_type == "jsonb" && rails5? %>
59
- add_index :ahoy_events, "properties jsonb_path_ops", using: "gin"<% end %>
58
+ add_index :ahoy_events, [:name, :time]<% if properties_type == "jsonb" %><% if rails52? %>
59
+ add_index :ahoy_events, :properties, using: :gin, opclass: :jsonb_path_ops<% else %>
60
+ add_index :ahoy_events, "properties jsonb_path_ops", using: "gin"<% end %><% end %>
60
61
  end
61
62
  end
@@ -1,6 +1,6 @@
1
- class Ahoy::Visit < <%= rails5? ? "ApplicationRecord" : "ActiveRecord::Base" %>
1
+ class Ahoy::Visit < ApplicationRecord
2
2
  self.table_name = "ahoy_visits"
3
3
 
4
4
  has_many :events, class_name: "Ahoy::Event"
5
- belongs_to :user<%= rails5? ? ", optional: true" : nil %>
5
+ belongs_to :user, optional: true
6
6
  end