ahoy_matey 1.5.3 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +88 -0
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +369 -380
  6. data/app/controllers/ahoy/base_controller.rb +15 -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 +10 -0
  10. data/app/jobs/ahoy/geocode_v2_job.rb +28 -0
  11. data/config/routes.rb +1 -1
  12. data/lib/ahoy.rb +68 -86
  13. data/lib/ahoy/base_store.rb +97 -0
  14. data/lib/ahoy/controller.rb +26 -17
  15. data/lib/ahoy/database_store.rb +89 -0
  16. data/lib/ahoy/engine.rb +7 -7
  17. data/lib/ahoy/helper.rb +40 -0
  18. data/lib/ahoy/model.rb +5 -27
  19. data/lib/ahoy/{properties.rb → query_methods.rb} +25 -9
  20. data/lib/ahoy/tracker.rb +101 -42
  21. data/lib/ahoy/utils.rb +7 -0
  22. data/lib/ahoy/version.rb +1 -1
  23. data/lib/ahoy/visit_properties.rb +99 -37
  24. data/lib/generators/ahoy/activerecord_generator.rb +41 -0
  25. data/lib/generators/ahoy/base_generator.rb +13 -0
  26. data/lib/generators/ahoy/install_generator.rb +44 -0
  27. data/lib/generators/ahoy/mongoid_generator.rb +16 -0
  28. data/lib/generators/ahoy/templates/active_record_event_model.rb.tt +10 -0
  29. data/lib/generators/ahoy/templates/active_record_migration.rb.tt +62 -0
  30. data/lib/generators/ahoy/templates/active_record_visit_model.rb.tt +6 -0
  31. data/lib/generators/ahoy/templates/base_store_initializer.rb.tt +20 -0
  32. data/lib/generators/ahoy/templates/database_store_initializer.rb.tt +5 -0
  33. data/lib/generators/ahoy/{stores/templates/mongoid_event_model.rb → templates/mongoid_event_model.rb.tt} +4 -2
  34. data/lib/generators/ahoy/{stores/templates/mongoid_visit_model.rb → templates/mongoid_visit_model.rb.tt} +15 -9
  35. data/vendor/assets/javascripts/ahoy.js +336 -113
  36. metadata +54 -144
  37. data/.gitignore +0 -17
  38. data/Gemfile +0 -6
  39. data/Rakefile +0 -8
  40. data/ahoy_matey.gemspec +0 -38
  41. data/lib/ahoy/deckhands/location_deckhand.rb +0 -49
  42. data/lib/ahoy/deckhands/request_deckhand.rb +0 -52
  43. data/lib/ahoy/deckhands/technology_deckhand.rb +0 -47
  44. data/lib/ahoy/deckhands/traffic_source_deckhand.rb +0 -22
  45. data/lib/ahoy/deckhands/utm_parameter_deckhand.rb +0 -23
  46. data/lib/ahoy/geocode_job.rb +0 -13
  47. data/lib/ahoy/logger_silencer.rb +0 -75
  48. data/lib/ahoy/stores/active_record_store.rb +0 -60
  49. data/lib/ahoy/stores/active_record_token_store.rb +0 -113
  50. data/lib/ahoy/stores/base_store.rb +0 -88
  51. data/lib/ahoy/stores/bunny_store.rb +0 -33
  52. data/lib/ahoy/stores/fluentd_store.rb +0 -17
  53. data/lib/ahoy/stores/kafka_store.rb +0 -40
  54. data/lib/ahoy/stores/kinesis_firehose_store.rb +0 -42
  55. data/lib/ahoy/stores/log_store.rb +0 -53
  56. data/lib/ahoy/stores/mongoid_store.rb +0 -63
  57. data/lib/ahoy/subscribers/active_record.rb +0 -19
  58. data/lib/ahoy/throttle.rb +0 -17
  59. data/lib/generators/ahoy/stores/active_record_events_generator.rb +0 -53
  60. data/lib/generators/ahoy/stores/active_record_generator.rb +0 -16
  61. data/lib/generators/ahoy/stores/active_record_visits_generator.rb +0 -43
  62. data/lib/generators/ahoy/stores/bunny_generator.rb +0 -15
  63. data/lib/generators/ahoy/stores/custom_generator.rb +0 -15
  64. data/lib/generators/ahoy/stores/fluentd_generator.rb +0 -15
  65. data/lib/generators/ahoy/stores/kafka_generator.rb +0 -15
  66. data/lib/generators/ahoy/stores/kinesis_firehose_generator.rb +0 -15
  67. data/lib/generators/ahoy/stores/log_generator.rb +0 -15
  68. data/lib/generators/ahoy/stores/mongoid_events_generator.rb +0 -19
  69. data/lib/generators/ahoy/stores/mongoid_generator.rb +0 -14
  70. data/lib/generators/ahoy/stores/mongoid_visits_generator.rb +0 -27
  71. data/lib/generators/ahoy/stores/templates/active_record_event_model.rb +0 -12
  72. data/lib/generators/ahoy/stores/templates/active_record_events_migration.rb +0 -19
  73. data/lib/generators/ahoy/stores/templates/active_record_initializer.rb +0 -3
  74. data/lib/generators/ahoy/stores/templates/active_record_visit_model.rb +0 -4
  75. data/lib/generators/ahoy/stores/templates/active_record_visits_migration.rb +0 -57
  76. data/lib/generators/ahoy/stores/templates/bunny_initializer.rb +0 -9
  77. data/lib/generators/ahoy/stores/templates/custom_initializer.rb +0 -10
  78. data/lib/generators/ahoy/stores/templates/fluentd_initializer.rb +0 -3
  79. data/lib/generators/ahoy/stores/templates/kafka_initializer.rb +0 -9
  80. data/lib/generators/ahoy/stores/templates/kinesis_firehose_initializer.rb +0 -17
  81. data/lib/generators/ahoy/stores/templates/log_initializer.rb +0 -3
  82. data/lib/generators/ahoy/stores/templates/mongoid_initializer.rb +0 -3
  83. data/test/properties/mysql_json_test.rb +0 -18
  84. data/test/properties/mysql_text_test.rb +0 -19
  85. data/test/properties/postgresql_hstore_test.rb +0 -18
  86. data/test/properties/postgresql_json_test.rb +0 -18
  87. data/test/properties/postgresql_jsonb_test.rb +0 -18
  88. data/test/properties/postgresql_text_test.rb +0 -19
  89. data/test/test_helper.rb +0 -99
  90. data/test/visit_properties_test.rb +0 -44
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 = "1.5.3"
2
+ VERSION = "3.0.1"
3
3
  end
@@ -1,60 +1,122 @@
1
+ require "cgi"
2
+ require "device_detector"
3
+ require "uri"
4
+
1
5
  module Ahoy
2
6
  class VisitProperties
3
- REQUEST_KEYS = [:ip, :user_agent, :referrer, :landing_page, :platform, :app_version, :os_version, :screen_height, :screen_width]
4
- TRAFFIC_SOURCE_KEYS = [:referring_domain, :search_keyword]
5
- UTM_PARAMETER_KEYS = [:utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign]
6
- TECHNOLOGY_KEYS = [:browser, :os, :device_type]
7
- LOCATION_KEYS = [:country, :region, :city, :postal_code, :latitude, :longitude]
8
-
9
- KEYS = REQUEST_KEYS + TRAFFIC_SOURCE_KEYS + UTM_PARAMETER_KEYS + TECHNOLOGY_KEYS + LOCATION_KEYS
7
+ attr_reader :request, :params, :referrer, :landing_page
10
8
 
11
- delegate(*REQUEST_KEYS, to: :request_deckhand)
12
- delegate(*TRAFFIC_SOURCE_KEYS, to: :traffic_source_deckhand)
13
- delegate(*(UTM_PARAMETER_KEYS + [:landing_params]), to: :utm_parameter_deckhand)
14
- delegate(*TECHNOLOGY_KEYS, to: :technology_deckhand)
15
- delegate(*LOCATION_KEYS, to: :location_deckhand)
16
-
17
- def initialize(request, options = {})
9
+ def initialize(request, api:)
18
10
  @request = request
19
- @options = options
11
+ @params = request.params
12
+ @referrer = api ? params["referrer"] : request.referer
13
+ @landing_page = api ? params["landing_page"] : request.original_url
20
14
  end
21
15
 
22
- def [](key)
23
- send(key)
16
+ def generate
17
+ @generate ||= request_properties.merge(tech_properties).merge(traffic_properties).merge(utm_properties)
24
18
  end
25
19
 
26
- def keys
27
- if Ahoy.geocode == true # no location keys for :async
28
- KEYS
29
- else
30
- KEYS - LOCATION_KEYS
20
+ private
21
+
22
+ def utm_properties
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
31
+
32
+ props = {}
33
+ %w(utm_source utm_medium utm_term utm_content utm_campaign).each do |name|
34
+ props[name.to_sym] = params[name] || landing_params[name].try(:first)
31
35
  end
36
+ props
32
37
  end
33
38
 
34
- def to_hash
35
- keys.inject({}) { |memo, key| memo[key] = send(key); memo }
39
+ def traffic_properties
40
+ uri = URI.parse(referrer) rescue nil
41
+ {
42
+ referring_domain: uri.try(:host).try(:first, 255)
43
+ }
36
44
  end
37
45
 
38
- protected
46
+ def tech_properties
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
39
58
 
40
- def request_deckhand
41
- @request_deckhand ||= Deckhands::RequestDeckhand.new(@request, @options)
42
- 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)
43
67
 
44
- def traffic_source_deckhand
45
- @traffic_source_deckhand ||= Deckhands::TrafficSourceDeckhand.new(request_deckhand.referrer)
46
- end
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
47
88
 
48
- def utm_parameter_deckhand
49
- @utm_parameter_deckhand ||= Deckhands::UtmParameterDeckhand.new(request_deckhand.landing_page, request_deckhand.params)
89
+ {
90
+ browser: agent.name,
91
+ os: agent.os.name,
92
+ device_type: device_type
93
+ }
94
+ end
50
95
  end
51
96
 
52
- def technology_deckhand
53
- @technology_deckhand ||= Deckhands::TechnologyDeckhand.new(request_deckhand.user_agent)
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
54
106
  end
55
107
 
56
- def location_deckhand
57
- @location_deckhand ||= Deckhands::LocationDeckhand.new(request_deckhand.ip)
108
+ def request_properties
109
+ {
110
+ ip: ip,
111
+ user_agent: Ahoy::Utils.ensure_utf8(request.user_agent),
112
+ referrer: referrer,
113
+ landing_page: landing_page,
114
+ platform: params["platform"],
115
+ app_version: params["app_version"],
116
+ os_version: params["os_version"],
117
+ screen_height: params["screen_height"],
118
+ screen_width: params["screen_width"]
119
+ }
58
120
  end
59
121
  end
60
122
  end
@@ -0,0 +1,41 @@
1
+ require "rails/generators/active_record"
2
+
3
+ module Ahoy
4
+ module Generators
5
+ class ActiverecordGenerator < Rails::Generators::Base
6
+ include ActiveRecord::Generators::Migration
7
+ source_root File.join(__dir__, "templates")
8
+
9
+ class_option :database, type: :string, aliases: "-d"
10
+
11
+ def copy_templates
12
+ template "database_store_initializer.rb", "config/initializers/ahoy.rb"
13
+ template "active_record_visit_model.rb", "app/models/ahoy/visit.rb"
14
+ template "active_record_event_model.rb", "app/models/ahoy/event.rb"
15
+ migration_template "active_record_migration.rb", "db/migrate/create_ahoy_visits_and_events.rb", migration_version: migration_version
16
+ puts "\nAlmost set! Last, run:\n\n rails db:migrate"
17
+ end
18
+
19
+ def properties_type
20
+ # use connection_config instead of connection.adapter
21
+ # so database connection isn't needed
22
+ case ActiveRecord::Base.connection_config[:adapter].to_s
23
+ when /postg/i # postgres, postgis
24
+ "jsonb"
25
+ when /mysql/i
26
+ "json"
27
+ else
28
+ "text"
29
+ end
30
+ end
31
+
32
+ def rails52?
33
+ ActiveRecord::VERSION::STRING >= "5.2"
34
+ end
35
+
36
+ def migration_version
37
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ require "rails/generators"
2
+
3
+ module Ahoy
4
+ module Generators
5
+ class BaseGenerator < Rails::Generators::Base
6
+ source_root File.join(__dir__, "templates")
7
+
8
+ def copy_templates
9
+ template "base_store_initializer.rb", "config/initializers/ahoy.rb"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,44 @@
1
+ require "rails/generators"
2
+
3
+ module Ahoy
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.join(__dir__, "templates")
7
+
8
+ def copy_templates
9
+ activerecord = defined?(ActiveRecord)
10
+ mongoid = defined?(Mongoid)
11
+
12
+ selection =
13
+ if activerecord && mongoid
14
+ puts <<-MSG
15
+
16
+ Which data store would you like to use?
17
+ 1. ActiveRecord (default)
18
+ 2. Mongoid
19
+ 3. Neither
20
+ MSG
21
+
22
+ ask(">")
23
+ elsif activerecord
24
+ "1"
25
+ elsif mongoid
26
+ "2"
27
+ else
28
+ "3"
29
+ end
30
+
31
+ case selection
32
+ when "", "1"
33
+ invoke "ahoy:activerecord"
34
+ when "2"
35
+ invoke "ahoy:mongoid"
36
+ when "3"
37
+ invoke "ahoy:base"
38
+ else
39
+ abort "Error: must enter a number [1-3]"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ require "rails/generators"
2
+
3
+ module Ahoy
4
+ module Generators
5
+ class MongoidGenerator < Rails::Generators::Base
6
+ source_root File.join(__dir__, "templates")
7
+
8
+ def copy_templates
9
+ template "database_store_initializer.rb", "config/initializers/ahoy.rb"
10
+ template "mongoid_visit_model.rb", "app/models/ahoy/visit.rb"
11
+ template "mongoid_event_model.rb", "app/models/ahoy/event.rb"
12
+ puts "\nAlmost set! Last, run:\n\n rake db:mongoid:create_indexes"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ class Ahoy::Event < ApplicationRecord
2
+ include Ahoy::QueryMethods
3
+
4
+ self.table_name = "ahoy_events"
5
+
6
+ belongs_to :visit
7
+ belongs_to :user, optional: true<% if properties_type == "text" %>
8
+
9
+ serialize :properties, JSON<% end %>
10
+ end
@@ -0,0 +1,62 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :ahoy_visits do |t|
4
+ t.string :visit_token
5
+ t.string :visitor_token
6
+
7
+ # the rest are recommended but optional
8
+ # simply remove any you don't want
9
+
10
+ # user
11
+ t.references :user
12
+
13
+ # standard
14
+ t.string :ip
15
+ t.text :user_agent
16
+ t.text :referrer
17
+ t.string :referring_domain
18
+ t.text :landing_page
19
+
20
+ # technology
21
+ t.string :browser
22
+ t.string :os
23
+ t.string :device_type
24
+
25
+ # location
26
+ t.string :country
27
+ t.string :region
28
+ t.string :city
29
+ t.float :latitude
30
+ t.float :longitude
31
+
32
+ # utm parameters
33
+ t.string :utm_source
34
+ t.string :utm_medium
35
+ t.string :utm_term
36
+ t.string :utm_content
37
+ t.string :utm_campaign
38
+
39
+ # native apps
40
+ t.string :app_version
41
+ t.string :os_version
42
+ t.string :platform
43
+
44
+ t.timestamp :started_at
45
+ end
46
+
47
+ add_index :ahoy_visits, [:visit_token], unique: true
48
+
49
+ create_table :ahoy_events do |t|
50
+ t.references :visit
51
+ t.references :user
52
+
53
+ t.string :name
54
+ t.<%= properties_type %> :properties
55
+ t.timestamp :time
56
+ end
57
+
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 %>
61
+ end
62
+ end
@@ -0,0 +1,6 @@
1
+ class Ahoy::Visit < ApplicationRecord
2
+ self.table_name = "ahoy_visits"
3
+
4
+ has_many :events, class_name: "Ahoy::Event"
5
+ belongs_to :user, optional: true
6
+ end
@@ -0,0 +1,20 @@
1
+ class Ahoy::Store < Ahoy::BaseStore
2
+ def track_visit(data)
3
+ # do
4
+ end
5
+
6
+ def track_event(data)
7
+ # something
8
+ end
9
+
10
+ def geocode(data)
11
+ # amazing
12
+ end
13
+
14
+ def authenticate(data)
15
+ # !!!
16
+ end
17
+ end
18
+
19
+ # set to true for JavaScript tracking
20
+ Ahoy.api = false
@@ -0,0 +1,5 @@
1
+ class Ahoy::Store < Ahoy::DatabaseStore
2
+ end
3
+
4
+ # set to true for JavaScript tracking
5
+ Ahoy.api = false
@@ -2,11 +2,13 @@ class Ahoy::Event
2
2
  include Mongoid::Document
3
3
 
4
4
  # associations
5
- belongs_to :visit
6
- belongs_to :user
5
+ belongs_to :visit, index: true
6
+ belongs_to :user, index: true, optional: true
7
7
 
8
8
  # fields
9
9
  field :name, type: String
10
10
  field :properties, type: Hash
11
11
  field :time, type: Time
12
+
13
+ index({name: 1, time: 1})
12
14
  end
@@ -1,11 +1,13 @@
1
- class Visit
1
+ class Ahoy::Visit
2
2
  include Mongoid::Document
3
3
 
4
4
  # associations
5
- belongs_to :user
5
+ has_many :events, class_name: "Ahoy::Event"
6
+ belongs_to :user, index: true, optional: true
6
7
 
7
8
  # required
8
- field :visitor_id, type: <%= @visitor_id_type %>
9
+ field :visit_token, type: String
10
+ field :visitor_token, type: String
9
11
 
10
12
  # the rest are recommended but optional
11
13
  # simply remove the columns you don't want
@@ -14,23 +16,20 @@ class Visit
14
16
  field :ip, type: String
15
17
  field :user_agent, type: String
16
18
  field :referrer, type: String
17
- field :landing_page, type: String
18
-
19
- # traffic source
20
19
  field :referring_domain, type: String
21
- field :search_keyword, type: String
20
+ field :landing_page, type: String
22
21
 
23
22
  # technology
24
23
  field :browser, type: String
25
24
  field :os, type: String
26
25
  field :device_type, type: String
27
- field :screen_height, type: Integer
28
- field :screen_width, type: Integer
29
26
 
30
27
  # location
31
28
  field :country, type: String
32
29
  field :region, type: String
33
30
  field :city, type: String
31
+ field :latitude, type: Float
32
+ field :longitude, type: Float
34
33
 
35
34
  # utm parameters
36
35
  field :utm_source, type: String
@@ -39,5 +38,12 @@ class Visit
39
38
  field :utm_content, type: String
40
39
  field :utm_campaign, type: String
41
40
 
41
+ # native apps
42
+ field :app_version, type: String
43
+ field :os_version, type: String
44
+ field :platform, type: String
45
+
42
46
  field :started_at, type: Time
47
+
48
+ index({visit_token: 1}, {unique: true})
43
49
  end