ahoy_matey 1.5.5 → 4.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +184 -34
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +464 -407
  6. data/app/controllers/ahoy/base_controller.rb +23 -15
  7. data/app/controllers/ahoy/events_controller.rb +8 -2
  8. data/app/controllers/ahoy/visits_controller.rb +8 -1
  9. data/app/jobs/ahoy/geocode_job.rb +11 -0
  10. data/app/jobs/ahoy/geocode_v2_job.rb +31 -0
  11. data/config/routes.rb +1 -1
  12. data/lib/ahoy/base_store.rb +101 -0
  13. data/lib/ahoy/controller.rb +23 -16
  14. data/lib/ahoy/database_store.rb +94 -0
  15. data/lib/ahoy/engine.rb +14 -7
  16. data/lib/ahoy/helper.rb +40 -0
  17. data/lib/ahoy/model.rb +5 -27
  18. data/lib/ahoy/query_methods.rb +88 -0
  19. data/lib/ahoy/tracker.rb +105 -51
  20. data/lib/ahoy/utils.rb +7 -0
  21. data/lib/ahoy/version.rb +1 -1
  22. data/lib/ahoy/visit_properties.rb +99 -37
  23. data/lib/ahoy.rb +83 -93
  24. data/lib/ahoy_matey.rb +1 -1
  25. data/lib/generators/ahoy/activerecord_generator.rb +67 -0
  26. data/lib/generators/ahoy/base_generator.rb +13 -0
  27. data/lib/generators/ahoy/install_generator.rb +44 -0
  28. data/lib/generators/ahoy/mongoid_generator.rb +16 -0
  29. data/lib/generators/ahoy/templates/active_record_event_model.rb.tt +10 -0
  30. data/lib/generators/ahoy/templates/active_record_migration.rb.tt +62 -0
  31. data/lib/generators/ahoy/templates/active_record_visit_model.rb.tt +6 -0
  32. data/lib/generators/ahoy/templates/base_store_initializer.rb.tt +25 -0
  33. data/lib/generators/ahoy/templates/database_store_initializer.rb.tt +10 -0
  34. data/lib/generators/ahoy/{stores/templates/mongoid_event_model.rb → templates/mongoid_event_model.rb.tt} +4 -2
  35. data/lib/generators/ahoy/{stores/templates/mongoid_visit_model.rb → templates/mongoid_visit_model.rb.tt} +15 -9
  36. data/vendor/assets/javascripts/ahoy.js +271 -133
  37. metadata +37 -273
  38. data/.gitignore +0 -17
  39. data/Gemfile +0 -6
  40. data/Rakefile +0 -8
  41. data/ahoy_matey.gemspec +0 -38
  42. data/lib/ahoy/deckhands/location_deckhand.rb +0 -49
  43. data/lib/ahoy/deckhands/request_deckhand.rb +0 -52
  44. data/lib/ahoy/deckhands/technology_deckhand.rb +0 -47
  45. data/lib/ahoy/deckhands/traffic_source_deckhand.rb +0 -22
  46. data/lib/ahoy/deckhands/utm_parameter_deckhand.rb +0 -23
  47. data/lib/ahoy/geocode_job.rb +0 -13
  48. data/lib/ahoy/logger_silencer.rb +0 -75
  49. data/lib/ahoy/properties.rb +0 -58
  50. data/lib/ahoy/stores/active_record_store.rb +0 -61
  51. data/lib/ahoy/stores/active_record_token_store.rb +0 -114
  52. data/lib/ahoy/stores/base_store.rb +0 -88
  53. data/lib/ahoy/stores/bunny_store.rb +0 -33
  54. data/lib/ahoy/stores/fluentd_store.rb +0 -17
  55. data/lib/ahoy/stores/kafka_store.rb +0 -40
  56. data/lib/ahoy/stores/kinesis_firehose_store.rb +0 -42
  57. data/lib/ahoy/stores/log_store.rb +0 -53
  58. data/lib/ahoy/stores/mongoid_store.rb +0 -63
  59. data/lib/ahoy/stores/nats_store.rb +0 -34
  60. data/lib/ahoy/stores/nsq_store.rb +0 -36
  61. data/lib/ahoy/subscribers/active_record.rb +0 -19
  62. data/lib/ahoy/throttle.rb +0 -17
  63. data/lib/generators/ahoy/stores/active_record_events_generator.rb +0 -53
  64. data/lib/generators/ahoy/stores/active_record_generator.rb +0 -16
  65. data/lib/generators/ahoy/stores/active_record_visits_generator.rb +0 -43
  66. data/lib/generators/ahoy/stores/bunny_generator.rb +0 -15
  67. data/lib/generators/ahoy/stores/custom_generator.rb +0 -15
  68. data/lib/generators/ahoy/stores/fluentd_generator.rb +0 -15
  69. data/lib/generators/ahoy/stores/kafka_generator.rb +0 -15
  70. data/lib/generators/ahoy/stores/kinesis_firehose_generator.rb +0 -15
  71. data/lib/generators/ahoy/stores/log_generator.rb +0 -15
  72. data/lib/generators/ahoy/stores/mongoid_events_generator.rb +0 -19
  73. data/lib/generators/ahoy/stores/mongoid_generator.rb +0 -14
  74. data/lib/generators/ahoy/stores/mongoid_visits_generator.rb +0 -27
  75. data/lib/generators/ahoy/stores/nats_generator.rb +0 -15
  76. data/lib/generators/ahoy/stores/nsq_generator.rb +0 -15
  77. data/lib/generators/ahoy/stores/templates/active_record_event_model.rb +0 -12
  78. data/lib/generators/ahoy/stores/templates/active_record_events_migration.rb +0 -19
  79. data/lib/generators/ahoy/stores/templates/active_record_initializer.rb +0 -3
  80. data/lib/generators/ahoy/stores/templates/active_record_visit_model.rb +0 -4
  81. data/lib/generators/ahoy/stores/templates/active_record_visits_migration.rb +0 -57
  82. data/lib/generators/ahoy/stores/templates/bunny_initializer.rb +0 -9
  83. data/lib/generators/ahoy/stores/templates/custom_initializer.rb +0 -10
  84. data/lib/generators/ahoy/stores/templates/fluentd_initializer.rb +0 -3
  85. data/lib/generators/ahoy/stores/templates/kafka_initializer.rb +0 -9
  86. data/lib/generators/ahoy/stores/templates/kinesis_firehose_initializer.rb +0 -17
  87. data/lib/generators/ahoy/stores/templates/log_initializer.rb +0 -3
  88. data/lib/generators/ahoy/stores/templates/mongoid_initializer.rb +0 -3
  89. data/lib/generators/ahoy/stores/templates/nats_initializer.rb +0 -9
  90. data/lib/generators/ahoy/stores/templates/nsq_initializer.rb +0 -9
  91. data/test/properties/mysql_json_test.rb +0 -18
  92. data/test/properties/mysql_text_test.rb +0 -19
  93. data/test/properties/postgresql_hstore_test.rb +0 -18
  94. data/test/properties/postgresql_json_test.rb +0 -18
  95. data/test/properties/postgresql_jsonb_test.rb +0 -18
  96. data/test/properties/postgresql_text_test.rb +0 -19
  97. data/test/test_helper.rb +0 -99
  98. data/test/visit_properties_test.rb +0 -44
@@ -1,34 +1,42 @@
1
1
  module Ahoy
2
2
  class BaseController < ApplicationController
3
- # skip all filters except for authlogic
4
- filters = _process_action_callbacks.map(&:filter) - [:load_authlogic]
5
- if Rails::VERSION::MAJOR >= 5
6
- skip_before_action(*filters, raise: false)
7
- skip_after_action(*filters, raise: false)
8
- skip_around_action(*filters, raise: false)
9
- before_action :verify_request_size
10
- elsif respond_to?(:skip_action_callback)
11
- skip_action_callback *filters
12
- before_action :verify_request_size
13
- else
14
- skip_filter *filters
15
- before_filter :verify_request_size
16
- end
3
+ filters = _process_action_callbacks.map(&:filter) - Ahoy.preserve_callbacks
4
+ skip_before_action(*filters, raise: false)
5
+ skip_after_action(*filters, raise: false)
6
+ skip_around_action(*filters, raise: false)
17
7
 
18
8
  if respond_to?(:protect_from_forgery)
19
9
  protect_from_forgery with: :null_session, if: -> { Ahoy.protect_from_forgery }
20
10
  end
21
11
 
12
+ before_action :verify_request_size
13
+ before_action :check_params
14
+ before_action :renew_cookies
15
+
22
16
  protected
23
17
 
24
18
  def ahoy
25
19
  @ahoy ||= Ahoy::Tracker.new(controller: self, api: true)
26
20
  end
27
21
 
22
+ def check_params
23
+ if ahoy.send(:missing_params?)
24
+ logger.info "[ahoy] Missing required parameters"
25
+ render plain: "Missing required parameters\n", status: :bad_request
26
+ end
27
+ end
28
+
29
+ # set proper ttl if cookie generated from JavaScript
30
+ # approach is not perfect, as user must reload the page
31
+ # for new cookie settings to take effect
32
+ def renew_cookies
33
+ set_ahoy_cookies if params[:js] && !Ahoy.api_only
34
+ end
35
+
28
36
  def verify_request_size
29
37
  if request.content_length > Ahoy.max_content_length
30
38
  logger.info "[ahoy] Payload too large"
31
- render text: "Payload too large\n", status: 413
39
+ render plain: "Payload too large\n", status: :payload_too_large
32
40
  end
33
41
  end
34
42
  end
@@ -3,13 +3,19 @@ module Ahoy
3
3
  def create
4
4
  events =
5
5
  if params[:name]
6
- # legacy API
6
+ # legacy API and AMP
7
7
  [request.params]
8
8
  elsif params[:events]
9
9
  request.params[:events]
10
10
  else
11
+ data =
12
+ if params[:events_json]
13
+ request.params[:events_json]
14
+ else
15
+ request.body.read
16
+ end
11
17
  begin
12
- ActiveSupport::JSON.decode(request.body.read)
18
+ ActiveSupport::JSON.decode(data)
13
19
  rescue ActiveSupport::JSON.parse_error
14
20
  # do nothing
15
21
  []
@@ -2,7 +2,14 @@ module Ahoy
2
2
  class VisitsController < BaseController
3
3
  def create
4
4
  ahoy.track_visit
5
- render json: {visit_id: ahoy.visit_id, visitor_id: ahoy.visitor_id}
5
+
6
+ render json: {
7
+ visit_token: ahoy.visit_token,
8
+ visitor_token: ahoy.visitor_token,
9
+ # legacy
10
+ visit_id: ahoy.visit_token,
11
+ visitor_id: ahoy.visitor_token
12
+ }
6
13
  end
7
14
  end
8
15
  end
@@ -0,0 +1,11 @@
1
+ # for smooth update from Ahoy 1 -> 2
2
+ # TODO remove in 5.0
3
+ module Ahoy
4
+ class GeocodeJob < ActiveJob::Base
5
+ queue_as { Ahoy.job_queue }
6
+
7
+ def perform(visit)
8
+ Ahoy::GeocodeV2Job.perform_now(visit.visit_token, visit.ip)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ module Ahoy
2
+ class GeocodeV2Job < ActiveJob::Base
3
+ queue_as { Ahoy.job_queue }
4
+
5
+ def perform(visit_token, ip)
6
+ location =
7
+ begin
8
+ Geocoder.search(ip).first
9
+ rescue NameError
10
+ raise "Add the geocoder gem to your Gemfile to use geocoding"
11
+ rescue => e
12
+ Ahoy.log "Geocode error: #{e.class.name}: #{e.message}"
13
+ nil
14
+ end
15
+
16
+ if location && location.country.present?
17
+ data = {
18
+ country: location.country,
19
+ country_code: location.try(:country_code).presence,
20
+ region: location.try(:state).presence,
21
+ city: location.try(:city).presence,
22
+ postal_code: location.try(:postal_code).presence,
23
+ latitude: location.try(:latitude).presence,
24
+ longitude: location.try(:longitude).presence
25
+ }
26
+
27
+ Ahoy::Tracker.new(visit_token: visit_token).geocode(data)
28
+ end
29
+ end
30
+ end
31
+ end
data/config/routes.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  Rails.application.routes.draw do
2
- mount Ahoy::Engine => "/ahoy" if Ahoy.mount
2
+ mount Ahoy::Engine => "/ahoy" if Ahoy.api
3
3
  end
4
4
 
5
5
  Ahoy::Engine.routes.draw do
@@ -0,0 +1,101 @@
1
+ module Ahoy
2
+ class BaseStore
3
+ attr_writer :user
4
+
5
+ def initialize(options)
6
+ @options = options
7
+ end
8
+
9
+ def track_visit(data)
10
+ end
11
+
12
+ def track_event(data)
13
+ end
14
+
15
+ def geocode(data)
16
+ end
17
+
18
+ def authenticate(data)
19
+ end
20
+
21
+ def visit
22
+ end
23
+
24
+ def user
25
+ @user ||= begin
26
+ if Ahoy.user_method.respond_to?(:call)
27
+ if Ahoy.user_method.arity == 1
28
+ Ahoy.user_method.call(controller)
29
+ else
30
+ Ahoy.user_method.call(controller, request)
31
+ end
32
+ else
33
+ controller.send(Ahoy.user_method) if controller.respond_to?(Ahoy.user_method, true)
34
+ end
35
+ end
36
+ end
37
+
38
+ def exclude?
39
+ (!Ahoy.track_bots && bot?) || exclude_by_method?
40
+ end
41
+
42
+ def generate_id
43
+ Ahoy.token_generator.call
44
+ end
45
+
46
+ def visit_or_create
47
+ visit
48
+ end
49
+
50
+ protected
51
+
52
+ def bot?
53
+ unless defined?(@bot)
54
+ @bot = begin
55
+ if request
56
+ if Ahoy.user_agent_parser == :device_detector
57
+ detector = DeviceDetector.new(request.user_agent)
58
+ if Ahoy.bot_detection_version == 2
59
+ detector.bot? || (detector.device_type.nil? && detector.os_name.nil?)
60
+ else
61
+ detector.bot?
62
+ end
63
+ else
64
+ # no need to throw friendly error if browser isn't defined
65
+ # since will error in visit_properties
66
+ Browser.new(request.user_agent).bot?
67
+ end
68
+ else
69
+ false
70
+ end
71
+ end
72
+ end
73
+
74
+ @bot
75
+ end
76
+
77
+ def exclude_by_method?
78
+ if Ahoy.exclude_method
79
+ if Ahoy.exclude_method.arity == 1
80
+ Ahoy.exclude_method.call(controller)
81
+ else
82
+ Ahoy.exclude_method.call(controller, request)
83
+ end
84
+ else
85
+ false
86
+ end
87
+ end
88
+
89
+ def request
90
+ @request ||= @options[:request] || controller.try(:request)
91
+ end
92
+
93
+ def controller
94
+ @controller ||= @options[:controller]
95
+ end
96
+
97
+ def ahoy
98
+ @ahoy ||= @options[:ahoy]
99
+ end
100
+ end
101
+ end
@@ -1,5 +1,3 @@
1
- require "request_store"
2
-
3
1
  module Ahoy
4
2
  module Controller
5
3
  def self.included(base)
@@ -7,15 +5,9 @@ module Ahoy
7
5
  base.helper_method :current_visit
8
6
  base.helper_method :ahoy
9
7
  end
10
- if base.respond_to?(:before_action)
11
- base.before_action :set_ahoy_cookies, unless: -> { Ahoy.api_only }
12
- base.before_action :track_ahoy_visit, unless: -> { Ahoy.api_only }
13
- base.before_action :set_ahoy_request_store
14
- else
15
- base.before_filter :set_ahoy_cookies, unless: -> { Ahoy.api_only }
16
- base.before_filter :track_ahoy_visit, unless: -> { Ahoy.api_only }
17
- base.before_filter :set_ahoy_request_store
18
- end
8
+ base.before_action :set_ahoy_cookies, unless: -> { Ahoy.api_only }
9
+ base.before_action :track_ahoy_visit, unless: -> { Ahoy.api_only }
10
+ base.around_action :set_ahoy_request_store
19
11
  end
20
12
 
21
13
  def ahoy
@@ -27,18 +19,33 @@ module Ahoy
27
19
  end
28
20
 
29
21
  def set_ahoy_cookies
30
- ahoy.set_visitor_cookie
31
- ahoy.set_visit_cookie
22
+ if Ahoy.cookies
23
+ ahoy.set_visitor_cookie
24
+ ahoy.set_visit_cookie
25
+ else
26
+ # delete cookies if exist
27
+ ahoy.reset
28
+ end
32
29
  end
33
30
 
34
31
  def track_ahoy_visit
35
- if ahoy.new_visit?
36
- ahoy.track_visit(defer: !Ahoy.track_visits_immediately)
32
+ defer = Ahoy.server_side_visits != true
33
+
34
+ if defer && !Ahoy.cookies
35
+ # avoid calling new_visit?, which triggers a database call
36
+ elsif ahoy.new_visit?
37
+ ahoy.track_visit(defer: defer)
37
38
  end
38
39
  end
39
40
 
40
41
  def set_ahoy_request_store
41
- RequestStore.store[:ahoy] ||= ahoy
42
+ previous_value = Ahoy.instance
43
+ begin
44
+ Ahoy.instance = ahoy
45
+ yield
46
+ ensure
47
+ Ahoy.instance = previous_value
48
+ end
42
49
  end
43
50
  end
44
51
  end
@@ -0,0 +1,94 @@
1
+ module Ahoy
2
+ class DatabaseStore < BaseStore
3
+ def track_visit(data)
4
+ @visit = visit_model.create!(slice_data(visit_model, data))
5
+ rescue => e
6
+ raise e unless unique_exception?(e)
7
+
8
+ # so next call to visit will try to fetch from DB
9
+ if defined?(@visit)
10
+ remove_instance_variable(:@visit)
11
+ end
12
+ end
13
+
14
+ def track_event(data)
15
+ visit = visit_or_create(started_at: data[:time])
16
+ if visit
17
+ event = event_model.new(slice_data(event_model, data))
18
+ event.visit = visit
19
+ event.time = visit.started_at if event.time < visit.started_at
20
+ begin
21
+ event.save!
22
+ rescue => e
23
+ raise e unless unique_exception?(e)
24
+ end
25
+ else
26
+ Ahoy.log "Event excluded since visit not created: #{data[:visit_token]}"
27
+ end
28
+ end
29
+
30
+ def geocode(data)
31
+ visit_token = data.delete(:visit_token)
32
+ data = slice_data(visit_model, data)
33
+ if defined?(Mongoid::Document) && visit_model < Mongoid::Document
34
+ # upsert since visit might not be found due to eventual consistency
35
+ visit_model.where(visit_token: visit_token).find_one_and_update({"$set": data}, {upsert: true})
36
+ elsif visit
37
+ visit.update!(data)
38
+ else
39
+ Ahoy.log "Visit for geocode not found: #{visit_token}"
40
+ end
41
+ end
42
+
43
+ def authenticate(_)
44
+ if visit && visit.respond_to?(:user) && !visit.user
45
+ begin
46
+ visit.user = user
47
+ visit.save!
48
+ rescue ActiveRecord::AssociationTypeMismatch
49
+ # do nothing
50
+ end
51
+ end
52
+ end
53
+
54
+ def visit
55
+ unless defined?(@visit)
56
+ if defined?(Mongoid::Document) && visit_model < Mongoid::Document
57
+ # find_by raises error by default when not found
58
+ @visit = visit_model.where(visit_token: ahoy.visit_token).first if ahoy.visit_token
59
+ else
60
+ @visit = visit_model.find_by(visit_token: ahoy.visit_token) if ahoy.visit_token
61
+ end
62
+ end
63
+ @visit
64
+ end
65
+
66
+ # if we don't have a visit, let's try to create one first
67
+ def visit_or_create(started_at: nil)
68
+ ahoy.track_visit(started_at: started_at) if !visit && Ahoy.server_side_visits
69
+ visit
70
+ end
71
+
72
+ protected
73
+
74
+ def visit_model
75
+ ::Ahoy::Visit
76
+ end
77
+
78
+ def event_model
79
+ ::Ahoy::Event
80
+ end
81
+
82
+ def slice_data(model, data)
83
+ column_names = model.try(:column_names) || model.attribute_names
84
+ data.slice(*column_names.map(&:to_sym)).select { |_, v| !v.nil? }
85
+ end
86
+
87
+ def unique_exception?(e)
88
+ return true if defined?(ActiveRecord::RecordNotUnique) && e.is_a?(ActiveRecord::RecordNotUnique)
89
+ return true if defined?(PG::UniqueViolation) && e.is_a?(PG::UniqueViolation)
90
+ return true if defined?(Mongo::Error::OperationFailure) && e.is_a?(Mongo::Error::OperationFailure) && e.message.include?("duplicate key error")
91
+ false
92
+ end
93
+ end
94
+ end
data/lib/ahoy/engine.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  module Ahoy
2
2
  class Engine < ::Rails::Engine
3
- initializer "ahoy.middleware", after: "sprockets.environment" do |app|
4
- if Ahoy.throttle
5
- require "ahoy/throttle"
6
- app.middleware.use Ahoy::Throttle
7
- end
3
+ initializer "ahoy", after: "sprockets.environment" do
4
+ Ahoy.logger ||= Rails.logger
5
+
6
+ # allow Devise to be loaded after Ahoy
7
+ require "ahoy/warden" if defined?(Warden)
8
8
 
9
9
  next unless Ahoy.quiet
10
10
 
@@ -14,8 +14,8 @@ module Ahoy
14
14
  # Just create an alias for call in middleware
15
15
  Rails::Rack::Logger.class_eval do
16
16
  def call_with_quiet_ahoy(env)
17
- if env["PATH_INFO"].start_with?(AHOY_PREFIX) && logger.respond_to?(:silence_logger)
18
- logger.silence_logger do
17
+ if env["PATH_INFO"].start_with?(AHOY_PREFIX) && logger.respond_to?(:silence)
18
+ logger.silence do
19
19
  call_without_quiet_ahoy(env)
20
20
  end
21
21
  else
@@ -26,5 +26,12 @@ module Ahoy
26
26
  alias_method :call, :call_with_quiet_ahoy
27
27
  end
28
28
  end
29
+
30
+ # for importmap
31
+ initializer "ahoy.importmap" do |app|
32
+ if defined?(Importmap)
33
+ app.config.assets.precompile << "ahoy.js"
34
+ end
35
+ end
29
36
  end
30
37
  end
@@ -0,0 +1,40 @@
1
+ module Ahoy
2
+ module Helper
3
+ def amp_event(name, properties = {})
4
+ url = Ahoy::Engine.routes.url_helpers.events_url(
5
+ url_options.slice(:host, :port, :protocol).merge(
6
+ name: name,
7
+ properties: properties,
8
+ screen_width: "SCREEN_WIDTH",
9
+ screen_height: "SCREEN_HEIGHT",
10
+ platform: "Web",
11
+ landing_page: "AMPDOC_URL",
12
+ referrer: "DOCUMENT_REFERRER",
13
+ random: "RANDOM"
14
+ )
15
+ )
16
+ url = "#{url}&visit_token=${clientId(ahoy_visit)}&visitor_token=${clientId(ahoy_visitor)}"
17
+
18
+ content_tag "amp-analytics" do
19
+ content_tag "script", type: "application/json" do
20
+ json_escape({
21
+ requests: {
22
+ pageview: url
23
+ },
24
+ triggers: {
25
+ trackPageview: {
26
+ on: "visible",
27
+ request: "pageview"
28
+ }
29
+ },
30
+ transport: {
31
+ beacon: true,
32
+ xhrpost: true,
33
+ image: false
34
+ }
35
+ }.to_json).html_safe
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/ahoy/model.rb CHANGED
@@ -1,37 +1,15 @@
1
1
  module Ahoy
2
2
  module Model
3
- def visitable(name = nil, options = {})
4
- if name.is_a?(Hash)
5
- name = nil
6
- options = name
7
- end
8
- name ||= :visit
3
+ def visitable(name = :visit, **options)
9
4
  class_eval do
10
- belongs_to name, options
11
- before_create :set_visit
5
+ belongs_to(name, class_name: "Ahoy::Visit", optional: true, **options)
6
+ before_create :set_ahoy_visit
12
7
  end
13
8
  class_eval %{
14
- def set_visit
15
- self.#{name} ||= RequestStore.store[:ahoy].try(:visit)
9
+ def set_ahoy_visit
10
+ self.#{name} ||= Ahoy.instance.try(:visit_or_create)
16
11
  end
17
12
  }
18
13
  end
19
-
20
- # deprecated
21
-
22
- def ahoy_visit
23
- class_eval do
24
- warn "[DEPRECATION] ahoy_visit is deprecated"
25
-
26
- belongs_to :user, polymorphic: true
27
-
28
- def landing_params
29
- @landing_params ||= begin
30
- warn "[DEPRECATION] landing_params is deprecated"
31
- Deckhands::UtmParameterDeckhand.new(landing_page).landing_params
32
- end
33
- end
34
- end
35
- end
36
14
  end
37
15
  end
@@ -0,0 +1,88 @@
1
+ module Ahoy
2
+ module QueryMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def where_event(name, properties = {})
7
+ where(name: name).where_props(properties)
8
+ end
9
+
10
+ def where_props(properties)
11
+ return all if properties.empty?
12
+
13
+ adapter_name = respond_to?(:connection) ? connection.adapter_name.downcase : "mongoid"
14
+ case adapter_name
15
+ when "mongoid"
16
+ where(properties.to_h { |k, v| ["properties.#{k}", v] })
17
+ when /mysql/
18
+ where("JSON_CONTAINS(properties, ?, '$') = 1", properties.to_json)
19
+ when /postgres|postgis/
20
+ case columns_hash["properties"].type
21
+ when :hstore
22
+ properties.inject(all) do |relation, (k, v)|
23
+ if v.nil?
24
+ relation.where("properties -> ? IS NULL", k.to_s)
25
+ else
26
+ relation.where("properties -> ? = ?", k.to_s, v.to_s)
27
+ end
28
+ end
29
+ when :jsonb
30
+ where("properties @> ?", properties.to_json)
31
+ else
32
+ where("properties::jsonb @> ?", properties.to_json)
33
+ end
34
+ when /sqlite/
35
+ properties.inject(all) do |relation, (k, v)|
36
+ if v.nil?
37
+ relation.where("JSON_EXTRACT(properties, ?) IS NULL", "$.#{k}")
38
+ else
39
+ relation.where("JSON_EXTRACT(properties, ?) = ?", "$.#{k}", v.as_json)
40
+ end
41
+ end
42
+ else
43
+ raise "Adapter not supported: #{adapter_name}"
44
+ end
45
+ end
46
+ alias_method :where_properties, :where_props
47
+
48
+ def group_prop(*props)
49
+ # like with group
50
+ props.flatten!
51
+
52
+ relation = all
53
+ adapter_name = respond_to?(:connection) ? connection.adapter_name.downcase : "mongoid"
54
+ case adapter_name
55
+ when "mongoid"
56
+ raise "Adapter not supported: #{adapter_name}"
57
+ when /mysql/
58
+ props.each do |prop|
59
+ quoted_prop = connection.quote("$.#{prop}")
60
+ relation = relation.group("JSON_UNQUOTE(JSON_EXTRACT(properties, #{quoted_prop}))")
61
+ end
62
+ when /postgres|postgis/
63
+ # convert to jsonb to fix
64
+ # could not identify an equality operator for type json
65
+ # and for text columns
66
+ column_type = columns_hash["properties"].type
67
+ cast = [:jsonb, :hstore].include?(column_type) ? "" : "::jsonb"
68
+
69
+ props.each do |prop|
70
+ quoted_prop = connection.quote(prop)
71
+ relation = relation.group("properties#{cast} -> #{quoted_prop}")
72
+ end
73
+ when /sqlite/
74
+ props.each do |prop|
75
+ quoted_prop = connection.quote("$.#{prop}")
76
+ relation = relation.group("JSON_EXTRACT(properties, #{quoted_prop})")
77
+ end
78
+ else
79
+ raise "Adapter not supported: #{adapter_name}"
80
+ end
81
+ relation
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ # backward compatibility
88
+ Ahoy::Properties = Ahoy::QueryMethods