ahoy_matey 3.0.5 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17572400fdaab440040c753cf23084f2a47868e5c1ed12229e85fd1904e9c937
4
- data.tar.gz: cc917af046906e308830fa1ffa9543db1bc1fe0f79488ccaddef090e6366433d
3
+ metadata.gz: 1fb8aecda5242614f518d06dbdc9449305f0ed30677d667e01eb1edefc627bc7
4
+ data.tar.gz: 8f52fbe0bab0a0ac1c5722b06f342add3d31c0c441230b77f03ae566e07f746b
5
5
  SHA512:
6
- metadata.gz: 325caa06934a20bf7a58f573fff7359b7058cf63e3e7576c48faf1bd5d08ac2564810a28e5a775c3ac324170754db4de35d46f43c0b421f8c4977760af19d153
7
- data.tar.gz: e7af674522e09e3fb472b52546f30d10b743e2b2814efd97162d05cb1d4f9b13615dce211cd83e291da16e370a86cf7d8d384b16ad60d8af07c8ced8fbd17f59
6
+ metadata.gz: '059927d7fe2344882860ae81ff23e142a09ed955da818da8d3e679b03173b39e64ffc611e4e682a912901678cd9b790b461805f788c423cd2b4331c2a340c226'
7
+ data.tar.gz: f2d53c35500ee4d30485cfbf6c5d444cbf03c50cec4762783a972411343e19b8baf53b61286cc4aa78bbbe98a69df21da701989422263780447341fdce756db0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ ## 4.0.0 (2021-08-14)
2
+
3
+ - Disabled geocoding by default
4
+ - Made the `geocoder` gem an optional dependency
5
+ - Updated Ahoy.js to 0.4.0
6
+ - Updated API to return 400 status code when missing required parameters
7
+ - Dropped support for Ruby < 2.6 and Rails < 5.2
8
+
9
+ ## 3.3.0 (2021-08-13)
10
+
11
+ - Added `country_code` to geocoding
12
+ - Updated Ahoy.js to 0.3.9
13
+ - Fixed install generator for MariaDB
14
+
15
+ ## 3.2.0 (2021-03-01)
16
+
17
+ - Disabled geocoding by default for new installations
18
+ - Fixed deprecation warning with Active Record 6.1
19
+
20
+ ## 3.1.0 (2020-12-04)
21
+
22
+ - Added `instance` method
23
+ - Added `request` argument to `user_method`
24
+ - Updated Ahoy.js to 0.3.8
25
+ - Removed `exclude_method` call when geocoding
26
+
1
27
  ## 3.0.5 (2020-09-09)
2
28
 
3
29
  - Added `group_prop` method
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2020 Andrew Kane
1
+ Copyright (c) 2014-2021 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  :fire: Simple, powerful, first-party analytics for Rails
4
4
 
5
- Track visits and events in Ruby, JavaScript, and native apps. Data is stored in your database by default so you can easily combine it with other data.
5
+ Track visits and events in Ruby, JavaScript, and native apps. Data is stored in your database by default, and you can customize it for any data store as you grow.
6
+
7
+ **Ahoy 4.0 was recently released** - see [how to upgrade](#upgrading)
6
8
 
7
9
  :postbox: Check out [Ahoy Email](https://github.com/ankane/ahoy_email) for emails and [Field Test](https://github.com/ankane/field_test) for A/B testing
8
10
 
9
11
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
10
12
 
11
- [![Build Status](https://travis-ci.org/ankane/ahoy.svg?branch=master)](https://travis-ci.org/ankane/ahoy)
13
+ [![Build Status](https://github.com/ankane/ahoy/workflows/build/badge.svg?branch=master)](https://github.com/ankane/ahoy/actions)
12
14
 
13
15
  ## Installation
14
16
 
@@ -70,6 +72,14 @@ Track an event with:
70
72
  ahoy.track("My second event", {language: "JavaScript"});
71
73
  ```
72
74
 
75
+ ### Native Apps
76
+
77
+ Check out [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android).
78
+
79
+ ### Geocoding Setup
80
+
81
+ To enable geocoding, see the [Geocoding section](#geocoding).
82
+
73
83
  ### GDPR Compliance
74
84
 
75
85
  Ahoy provides a number of options to help with GDPR compliance. See the [GDPR section](#gdpr-compliance-1) for more info.
@@ -135,17 +145,11 @@ end
135
145
  ahoy.track("Viewed book", {title: "The World is Flat"});
136
146
  ```
137
147
 
138
- Track events automatically with:
139
-
140
- ```javascript
141
- ahoy.trackAll();
142
- ```
143
-
144
148
  See [Ahoy.js](https://github.com/ankane/ahoy.js) for a complete list of features.
145
149
 
146
150
  #### Native Apps
147
151
 
148
- For Android, check out [Ahoy Android](https://github.com/instacart/ahoy-android). For other platforms, see the [API spec](#api-spec).
152
+ See the docs for [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android).
149
153
 
150
154
  #### AMP
151
155
 
@@ -181,7 +185,7 @@ Order.joins(:ahoy_visit).group("device_type").count
181
185
  Here’s what the migration to add the `ahoy_visit_id` column should look like:
182
186
 
183
187
  ```ruby
184
- class AddVisitIdToOrders < ActiveRecord::Migration[6.0]
188
+ class AddVisitIdToOrders < ActiveRecord::Migration[6.1]
185
189
  def change
186
190
  add_column :orders, :ahoy_visit_id, :bigint
187
191
  end
@@ -196,7 +200,7 @@ visitable :sign_up_visit
196
200
 
197
201
  ### Users
198
202
 
199
- Ahoy automatically attaches the `current_user` to the visit. With [Devise](https://github.com/plataformatec/devise), it attaches the user even if he or she signs in after the visit starts.
203
+ Ahoy automatically attaches the `current_user` to the visit. With [Devise](https://github.com/plataformatec/devise), it attaches the user even if they sign in after the visit starts.
200
204
 
201
205
  With other authentication frameworks, add this to the end of your sign in method:
202
206
 
@@ -306,71 +310,118 @@ Set other [cookie options](https://api.rubyonrails.org/classes/ActionDispatch/Co
306
310
  Ahoy.cookie_options = {same_site: :lax}
307
311
  ```
308
312
 
309
- ### Geocoding
313
+ You can also [disable cookies](#anonymity-sets--cookies)
310
314
 
311
- Disable geocoding with:
315
+ ### Token Generation
316
+
317
+ Ahoy uses random UUIDs for visit and visitor tokens by default, but you can use your own generator like [Druuid](https://github.com/recurly/druuid).
312
318
 
313
319
  ```ruby
314
- Ahoy.geocode = false
320
+ Ahoy.token_generator = -> { Druuid.gen }
315
321
  ```
316
322
 
317
- The default job queue is `:ahoy`. Change this with:
323
+ ### Throttling
324
+
325
+ You can use [Rack::Attack](https://github.com/kickstarter/rack-attack) to throttle requests to the API.
318
326
 
319
327
  ```ruby
320
- Ahoy.job_queue = :low_priority
328
+ class Rack::Attack
329
+ throttle("ahoy/ip", limit: 20, period: 1.minute) do |req|
330
+ if req.path.start_with?("/ahoy/")
331
+ req.ip
332
+ end
333
+ end
334
+ end
321
335
  ```
322
336
 
323
- #### Geocoding Performance
337
+ ### Exceptions
324
338
 
325
- To avoid calls to a remote API, download the [GeoLite2 City database](https://dev.maxmind.com/geoip/geoip2/geolite2/) and configure Geocoder to use it.
339
+ Exceptions are rescued so analytics do not break your app. Ahoy uses [Safely](https://github.com/ankane/safely) to try to report them to a service by default. To customize this, use:
326
340
 
327
- Add this line to your application’s Gemfile:
341
+ ```ruby
342
+ Safely.report_exception_method = ->(e) { Rollbar.error(e) }
343
+ ```
344
+
345
+ ## Geocoding
346
+
347
+ Ahoy uses [Geocoder](https://github.com/alexreisner/geocoder) for geocoding. We recommend configuring [local geocoding](#local-geocoding) or [load balancer geocoding](#load-balancer-geocoding) so IP addresses are not sent to a 3rd party service. If you do use a 3rd party service and adhere to GDPR, be sure to add it to your subprocessor list. If Ahoy is configured to [mask IPs](#ip-masking), the masked IP is used (this can reduce accuracy but is better for privacy).
348
+
349
+ To enable geocoding, add this line to your application’s Gemfile:
350
+
351
+ ```ruby
352
+ gem 'geocoder'
353
+ ```
354
+
355
+ And update `config/initializers/ahoy.rb`:
356
+
357
+ ```ruby
358
+ Ahoy.geocode = true
359
+ ```
360
+
361
+ Geocoding is performed in a background job so it doesn’t slow down web requests. The default job queue is `:ahoy`. Change this with:
362
+
363
+ ```ruby
364
+ Ahoy.job_queue = :low_priority
365
+ ```
366
+
367
+ ### Local Geocoding
368
+
369
+ For privacy and performance, we recommend geocoding locally. Add this line to your application’s Gemfile:
328
370
 
329
371
  ```ruby
330
372
  gem 'maxminddb'
331
373
  ```
332
374
 
333
- And create an initializer at `config/initializers/geocoder.rb` with:
375
+ For city-level geocoding, download the [GeoLite2 City database](https://dev.maxmind.com/geoip/geoip2/geolite2/) and create `config/initializers/geocoder.rb` with:
334
376
 
335
377
  ```ruby
336
378
  Geocoder.configure(
337
379
  ip_lookup: :geoip2,
338
380
  geoip2: {
339
- file: Rails.root.join("lib", "GeoLite2-City.mmdb")
381
+ file: "path/to/GeoLite2-City.mmdb"
340
382
  }
341
383
  )
342
384
  ```
343
385
 
344
- If you use Heroku, you can use an unofficial buildpack like [this one](https://github.com/temedica/heroku-buildpack-maxmind-geolite2) to avoid including the database in your repo.
386
+ For country-level geocoding, install the `geoip-database` package. It’s preinstalled on Heroku. For Ubuntu, use:
345
387
 
346
- ### Token Generation
388
+ ```sh
389
+ sudo apt-get install geoip-database
390
+ ```
347
391
 
348
- Ahoy uses random UUIDs for visit and visitor tokens by default, but you can use your own generator like [Druuid](https://github.com/recurly/druuid).
392
+ And create `config/initializers/geocoder.rb` with:
349
393
 
350
394
  ```ruby
351
- Ahoy.token_generator = -> { Druuid.gen }
395
+ Geocoder.configure(
396
+ ip_lookup: :maxmind_local,
397
+ maxmind_local: {
398
+ file: "/usr/share/GeoIP/GeoIP.dat",
399
+ package: :country
400
+ }
401
+ )
352
402
  ```
353
403
 
354
- ### Throttling
404
+ ### Load Balancer Geocoding
355
405
 
356
- You can use [Rack::Attack](https://github.com/kickstarter/rack-attack) to throttle requests to the API.
406
+ Some load balancers can add geocoding information to request headers.
357
407
 
358
- ```ruby
359
- class Rack::Attack
360
- throttle("ahoy/ip", limit: 20, period: 1.minute) do |req|
361
- if req.path.start_with?("/ahoy/")
362
- req.ip
363
- end
364
- end
365
- end
366
- ```
408
+ - [nginx](https://nginx.org/en/docs/http/ngx_http_geoip_module.html)
409
+ - [Google Cloud](https://cloud.google.com/load-balancing/docs/custom-headers)
410
+ - [Cloudflare](https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-Cloudflare-IP-Geolocation)
367
411
 
368
- ### Exceptions
369
-
370
- Exceptions are rescued so analytics do not break your app. Ahoy uses [Safely](https://github.com/ankane/safely) to try to report them to a service by default. To customize this, use:
412
+ Update `config/initializers/ahoy.rb` with:
371
413
 
372
414
  ```ruby
373
- Safely.report_exception_method = ->(e) { Rollbar.error(e) }
415
+ Ahoy.geocode = false
416
+
417
+ class Ahoy::Store < Ahoy::DatabaseStore
418
+ def track_visit(data)
419
+ data[:country] = request.headers["<country-header>"]
420
+ data[:region] = request.headers["<region-header>"]
421
+ data[:city] = request.headers["<city-header>"]
422
+ super(data)
423
+ end
424
+ end
374
425
  ```
375
426
 
376
427
  ## GDPR Compliance
@@ -431,7 +482,11 @@ Ahoy can switch from cookies to [anonymity sets](https://privacypatterns.org/pat
431
482
  Ahoy.cookies = false
432
483
  ```
433
484
 
434
- Previously set cookies are automatically deleted.
485
+ Previously set cookies are automatically deleted. If you use JavaScript tracking, also set:
486
+
487
+ ```javascript
488
+ ahoy.configure({cookies: false});
489
+ ```
435
490
 
436
491
  ## Data Retention
437
492
 
@@ -690,62 +745,19 @@ Send a `POST` request to `/ahoy/events` with `Content-Type: application/json` an
690
745
 
691
746
  ## Upgrading
692
747
 
693
- ### 3.0
694
-
695
- If you installed Ahoy before 2.1 and want to keep legacy user agent parsing and bot detection, add to your Gemfile:
696
-
697
- ```ruby
698
- gem "browser", "~> 2.0"
699
- gem "user_agent_parser"
700
- ```
701
-
702
- And add to `config/initializers/ahoy.rb`:
703
-
704
- ```ruby
705
- Ahoy.user_agent_parser = :legacy
706
- ```
707
-
708
- ### 2.2
709
-
710
- Ahoy now ships with better bot detection if you use Device Detector. This should be more accurate but can significantly reduce the number of visits recorded. For existing installs, it’s opt-in to start. To use it, add to `config/initializers/ahoy.rb`:
748
+ ### 4.0
711
749
 
712
- ```ruby
713
- Ahoy.bot_detection_version = 2
714
- ```
715
-
716
- ### 2.1
717
-
718
- Ahoy recommends [Device Detector](https://github.com/podigee/device_detector) for user agent parsing and makes it the default for new installations. To switch, add to `config/initializers/ahoy.rb`:
719
-
720
- ```ruby
721
- Ahoy.user_agent_parser = :device_detector
722
- ```
750
+ There are two notable changes to geocoding:
723
751
 
724
- Backfill existing records with:
752
+ 1. Geocoding is now disabled by default (this was already the case for new installations with 3.2.0+). Check out the instructions for [how to enable it](#geocoding).
725
753
 
726
- ```ruby
727
- Ahoy::Visit.find_each do |visit|
728
- client = DeviceDetector.new(visit.user_agent)
729
- device_type =
730
- case client.device_type
731
- when "smartphone"
732
- "Mobile"
733
- when "tv"
734
- "TV"
735
- else
736
- client.device_type.try(:titleize)
737
- end
738
-
739
- visit.browser = client.name
740
- visit.os = client.os_name
741
- visit.device_type = device_type
742
- visit.save(validate: false) if visit.changed?
743
- end
744
- ```
754
+ 2. The `geocoder` gem is now an optional dependency. To use geocoding, add it to your Gemfile:
745
755
 
746
- ### 2.0
756
+ ```ruby
757
+ gem 'geocoder'
758
+ ```
747
759
 
748
- See the [upgrade guide](docs/Ahoy-2-Upgrade.md)
760
+ Also, check out the [upgrade notes](https://github.com/ankane/ahoy.js#upgrading) for Ahoy.js.
749
761
 
750
762
  ## History
751
763
 
@@ -5,19 +5,27 @@ module Ahoy
5
5
  skip_after_action(*filters, raise: false)
6
6
  skip_around_action(*filters, raise: false)
7
7
 
8
- before_action :verify_request_size
9
- before_action :renew_cookies
10
-
11
8
  if respond_to?(:protect_from_forgery)
12
9
  protect_from_forgery with: :null_session, if: -> { Ahoy.protect_from_forgery }
13
10
  end
14
11
 
12
+ before_action :verify_request_size
13
+ before_action :check_params
14
+ before_action :renew_cookies
15
+
15
16
  protected
16
17
 
17
18
  def ahoy
18
19
  @ahoy ||= Ahoy::Tracker.new(controller: self, api: true)
19
20
  end
20
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
+
21
29
  # set proper ttl if cookie generated from JavaScript
22
30
  # approach is not perfect, as user must reload the page
23
31
  # for new cookie settings to take effect
@@ -28,7 +36,7 @@ module Ahoy
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 plain: "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
@@ -6,6 +6,8 @@ module Ahoy
6
6
  location =
7
7
  begin
8
8
  Geocoder.search(ip).first
9
+ rescue NameError
10
+ raise "Add the geocoder gem to your Gemfile to use geocoding"
9
11
  rescue => e
10
12
  Ahoy.log "Geocode error: #{e.class.name}: #{e.message}"
11
13
  nil
@@ -14,6 +16,7 @@ module Ahoy
14
16
  if location && location.country.present?
15
17
  data = {
16
18
  country: location.country,
19
+ country_code: location.try(:country_code).presence,
17
20
  region: location.try(:state).presence,
18
21
  city: location.try(:city).presence,
19
22
  postal_code: location.try(:postal_code).presence,
data/lib/ahoy.rb CHANGED
@@ -1,9 +1,9 @@
1
+ # stdlib
1
2
  require "ipaddr"
2
3
 
3
4
  # dependencies
4
5
  require "active_support"
5
6
  require "active_support/core_ext"
6
- require "geocoder"
7
7
  require "safely/core"
8
8
 
9
9
  # modules
@@ -43,7 +43,7 @@ module Ahoy
43
43
  self.quiet = true
44
44
 
45
45
  mattr_accessor :geocode
46
- self.geocode = true
46
+ self.geocode = false
47
47
 
48
48
  mattr_accessor :max_content_length
49
49
  self.max_content_length = 8192
@@ -104,6 +104,14 @@ module Ahoy
104
104
  addr.mask(48).to_s
105
105
  end
106
106
  end
107
+
108
+ def self.instance
109
+ Thread.current[:ahoy]
110
+ end
111
+
112
+ def self.instance=(value)
113
+ Thread.current[:ahoy] = value
114
+ end
107
115
  end
108
116
 
109
117
  ActiveSupport.on_load(:action_controller) do
@@ -119,10 +127,6 @@ ActiveSupport.on_load(:action_view) do
119
127
  end
120
128
 
121
129
  # Mongoid
122
- # TODO use
123
- # ActiveSupport.on_load(:mongoid) do
124
- # Mongoid::Document::ClassMethods.include(Ahoy::Model)
125
- # end
126
- if defined?(ActiveModel)
127
- ActiveModel::Callbacks.include(Ahoy::Model)
130
+ ActiveSupport.on_load(:mongoid) do
131
+ Mongoid::Document::ClassMethods.include(Ahoy::Model)
128
132
  end
@@ -24,7 +24,11 @@ module Ahoy
24
24
  def user
25
25
  @user ||= begin
26
26
  if Ahoy.user_method.respond_to?(:call)
27
- Ahoy.user_method.call(controller)
27
+ if Ahoy.user_method.arity == 1
28
+ Ahoy.user_method.call(controller)
29
+ else
30
+ Ahoy.user_method.call(controller, request)
31
+ end
28
32
  else
29
33
  controller.send(Ahoy.user_method) if controller.respond_to?(Ahoy.user_method, true)
30
34
  end
@@ -39,12 +39,12 @@ module Ahoy
39
39
  end
40
40
 
41
41
  def set_ahoy_request_store
42
- previous_value = Thread.current[:ahoy]
42
+ previous_value = Ahoy.instance
43
43
  begin
44
- Thread.current[:ahoy] = ahoy
44
+ Ahoy.instance = ahoy
45
45
  yield
46
46
  ensure
47
- Thread.current[:ahoy] = previous_value
47
+ Ahoy.instance = previous_value
48
48
  end
49
49
  end
50
50
  end
@@ -53,7 +53,7 @@ module Ahoy
53
53
 
54
54
  def visit
55
55
  unless defined?(@visit)
56
- @visit = visit_model.where(visit_token: ahoy.visit_token).first if ahoy.visit_token
56
+ @visit = visit_model.find_by(visit_token: ahoy.visit_token) if ahoy.visit_token
57
57
  end
58
58
  @visit
59
59
  end
data/lib/ahoy/model.rb CHANGED
@@ -7,7 +7,7 @@ module Ahoy
7
7
  end
8
8
  class_eval %{
9
9
  def set_ahoy_visit
10
- self.#{name} ||= Thread.current[:ahoy].try(:visit_or_create)
10
+ self.#{name} ||= Ahoy.instance.try(:visit_or_create)
11
11
  end
12
12
  }
13
13
  end
@@ -19,35 +19,21 @@ module Ahoy
19
19
  when "mongoid"
20
20
  relation = where(Hash[properties.map { |k, v| ["properties.#{k}", v] }])
21
21
  when /mysql/
22
- if column_type == :json
23
- properties.each do |k, v|
24
- if v.nil?
25
- v = "null"
26
- elsif v == true
27
- v = "true"
28
- end
29
-
30
- relation = relation.where("JSON_UNQUOTE(properties -> ?) = ?", "$.#{k}", v.as_json)
31
- end
32
- else
33
- properties.each do |k, v|
34
- # TODO cast to json instead
35
- relation = relation.where("properties REGEXP ?", "[{,]#{{k.to_s => v}.to_json.sub(/\A\{/, "").sub(/\}\z/, "").gsub("+", "\\\\+")}[,}]")
22
+ column = column_type == :json || connection.try(:mariadb?) ? "properties" : "CAST(properties AS JSON)"
23
+ properties.each do |k, v|
24
+ if v.nil?
25
+ v = "null"
26
+ elsif v == true
27
+ v = "true"
36
28
  end
29
+
30
+ relation = relation.where("JSON_UNQUOTE(JSON_EXTRACT(#{column}, ?)) = ?", "$.#{k}", v.as_json)
37
31
  end
38
32
  when /postgres|postgis/
39
- if column_type == :jsonb
33
+ case column_type
34
+ when :jsonb
40
35
  relation = relation.where("properties @> ?", properties.to_json)
41
- elsif column_type == :json
42
- properties.each do |k, v|
43
- relation =
44
- if v.nil?
45
- relation.where("properties ->> ? IS NULL", k.to_s)
46
- else
47
- relation.where("properties ->> ? = ?", k.to_s, v.as_json.to_s)
48
- end
49
- end
50
- elsif column_type == :hstore
36
+ when :hstore
51
37
  properties.each do |k, v|
52
38
  relation =
53
39
  if v.nil?
@@ -57,10 +43,7 @@ module Ahoy
57
43
  end
58
44
  end
59
45
  else
60
- properties.each do |k, v|
61
- # TODO cast to jsonb instead
62
- relation = relation.where("properties SIMILAR TO ?", "%[{,]#{{k.to_s => v}.to_json.sub(/\A\{/, "").sub(/\}\z/, "").gsub("+", "\\\\+")}[,}]%")
63
- end
46
+ relation = relation.where("properties::jsonb @> ?", properties.to_json)
64
47
  end
65
48
  else
66
49
  raise "Adapter not supported: #{adapter_name}"
@@ -84,17 +67,10 @@ module Ahoy
84
67
  when "mongoid"
85
68
  raise "Adapter not supported: #{adapter_name}"
86
69
  when /mysql/
87
- if connection.try(:mariadb?)
88
- props.each do |prop|
89
- quoted_prop = connection.quote("$.#{prop}")
90
- relation = relation.group("JSON_UNQUOTE(JSON_EXTRACT(properties, #{quoted_prop}))")
91
- end
92
- else
93
- column = column_type == :json ? "properties" : "CAST(properties AS JSON)"
94
- props.each do |prop|
95
- quoted_prop = connection.quote("$.#{prop}")
96
- relation = relation.group("JSON_UNQUOTE(JSON_EXTRACT(#{column}, #{quoted_prop}))")
97
- end
70
+ column = column_type == :json || connection.try(:mariadb?) ? "properties" : "CAST(properties AS JSON)"
71
+ props.each do |prop|
72
+ quoted_prop = connection.quote("$.#{prop}")
73
+ relation = relation.group("JSON_UNQUOTE(JSON_EXTRACT(#{column}, #{quoted_prop}))")
98
74
  end
99
75
  when /postgres|postgis/
100
76
  # convert to jsonb to fix
data/lib/ahoy/tracker.rb CHANGED
@@ -19,8 +19,6 @@ module Ahoy
19
19
  def track(name, properties = {}, options = {})
20
20
  if exclude?
21
21
  debug "Event excluded"
22
- elsif missing_params?
23
- debug "Missing required parameters"
24
22
  else
25
23
  data = {
26
24
  visit_token: visit_token,
@@ -41,8 +39,6 @@ module Ahoy
41
39
  def track_visit(defer: false, started_at: nil)
42
40
  if exclude?
43
41
  debug "Visit excluded"
44
- elsif missing_params?
45
- debug "Missing required parameters"
46
42
  else
47
43
  if defer
48
44
  set_cookie("ahoy_track", true, nil, false)
@@ -67,16 +63,12 @@ module Ahoy
67
63
  end
68
64
 
69
65
  def geocode(data)
70
- if exclude?
71
- debug "Geocode excluded"
72
- else
73
- data = {
74
- visit_token: visit_token
75
- }.merge(data).select { |_, v| v }
66
+ data = {
67
+ visit_token: visit_token
68
+ }.merge(data).select { |_, v| v }
76
69
 
77
- @store.geocode(data)
78
- true
79
- end
70
+ @store.geocode(data)
71
+ true
80
72
  rescue => e
81
73
  report_exception(e)
82
74
  end
@@ -159,6 +151,7 @@ module Ahoy
159
151
  @options[:api]
160
152
  end
161
153
 
154
+ # private, but used by API
162
155
  def missing_params?
163
156
  if Ahoy.cookies && api? && Ahoy.protect_from_forgery
164
157
  !(existing_visit_token && existing_visitor_token)
data/lib/ahoy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ahoy
2
- VERSION = "3.0.5"
2
+ VERSION = "4.0.0"
3
3
  end
@@ -17,9 +17,7 @@ module Ahoy
17
17
  end
18
18
 
19
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
20
+ case adapter
23
21
  when /postg/i # postgres, postgis
24
22
  "jsonb"
25
23
  when /mysql/i
@@ -29,8 +27,23 @@ module Ahoy
29
27
  end
30
28
  end
31
29
 
30
+ # requires database connection to check for MariaDB
31
+ def serialize_properties?
32
+ properties_type == "text" || (properties_type == "json" && ActiveRecord::Base.connection.try(:mariadb?))
33
+ end
34
+
35
+ # use connection_config instead of connection.adapter
36
+ # so database connection isn't needed
37
+ def adapter
38
+ if ActiveRecord::VERSION::STRING.to_f >= 6.1
39
+ ActiveRecord::Base.connection_db_config.adapter.to_s
40
+ else
41
+ ActiveRecord::Base.connection_config[:adapter].to_s
42
+ end
43
+ end
44
+
32
45
  def rails52?
33
- ActiveRecord::VERSION::STRING >= "5.2"
46
+ ActiveRecord::VERSION::STRING.to_f >= 5.2
34
47
  end
35
48
 
36
49
  def migration_version
@@ -4,7 +4,7 @@ class Ahoy::Event < ApplicationRecord
4
4
  self.table_name = "ahoy_events"
5
5
 
6
6
  belongs_to :visit
7
- belongs_to :user, optional: true<% if properties_type == "text" %>
7
+ belongs_to :user, optional: true<% if serialize_properties? %>
8
8
 
9
9
  serialize :properties, JSON<% end %>
10
10
  end
@@ -18,3 +18,8 @@ end
18
18
 
19
19
  # set to true for JavaScript tracking
20
20
  Ahoy.api = false
21
+
22
+ # set to true for geocoding (and add the geocoder gem to your Gemfile)
23
+ # we recommend configuring local geocoding as well
24
+ # see https://github.com/ankane/ahoy#geocoding
25
+ Ahoy.geocode = false
@@ -3,3 +3,8 @@ end
3
3
 
4
4
  # set to true for JavaScript tracking
5
5
  Ahoy.api = false
6
+
7
+ # set to true for geocoding (and add the geocoder gem to your Gemfile)
8
+ # we recommend configuring local geocoding as well
9
+ # see https://github.com/ankane/ahoy#geocoding
10
+ Ahoy.geocode = false
@@ -1,8 +1,8 @@
1
- /*
1
+ /*!
2
2
  * Ahoy.js
3
3
  * Simple, powerful JavaScript analytics
4
4
  * https://github.com/ankane/ahoy.js
5
- * v0.3.6
5
+ * v0.4.0
6
6
  * MIT License
7
7
  */
8
8
 
@@ -12,8 +12,6 @@
12
12
  (global = global || self, global.ahoy = factory());
13
13
  }(this, (function () { 'use strict';
14
14
 
15
- var n=function(n){return void 0===n},e=function(n){return Array.isArray(n)},t=function(n){return n&&"number"==typeof n.size&&"string"==typeof n.type&&"function"==typeof n.slice},s=function(o,i,r,f){return (i=i||{}).indices=!n(i.indices)&&i.indices,i.nullsAsUndefineds=!n(i.nullsAsUndefineds)&&i.nullsAsUndefineds,i.booleansAsIntegers=!n(i.booleansAsIntegers)&&i.booleansAsIntegers,r=r||new FormData,n(o)?r:(null===o?i.nullsAsUndefineds||r.append(f,""):"boolean"!=typeof o?e(o)?o.length&&o.forEach(function(n,e){s(n,i,r,f+"["+(i.indices?e:"")+"]");}):o instanceof Date?r.append(f,o.toISOString()):o!==Object(o)||function(n){return t(n)&&"string"==typeof n.name&&("object"==typeof n.lastModifiedDate||"number"==typeof n.lastModified)}(o)||t(o)?r.append(f,o):Object.keys(o).forEach(function(n){var t=o[n];if(e(t)){ for(;n.length>2&&n.lastIndexOf("[]")===n.length-2;){ n=n.substring(0,n.length-2); } }s(t,i,r,f?f+"["+n+"]":n);}):r.append(f,i.booleansAsIntegers?o?1:0:o),r)};
16
-
17
15
  // https://www.quirksmode.org/js/cookies.html
18
16
 
19
17
  var Cookies = {
@@ -28,7 +26,7 @@
28
26
  if (domain) {
29
27
  cookieDomain = "; domain=" + domain;
30
28
  }
31
- document.cookie = name + "=" + escape(value) + expires + cookieDomain + "; path=/";
29
+ document.cookie = name + "=" + escape(value) + expires + cookieDomain + "; path=/; samesite=lax";
32
30
  },
33
31
  get: function (name) {
34
32
  var i, c;
@@ -101,6 +99,16 @@
101
99
  return (config.useBeacon || config.trackNow) && isEmpty(config.headers) && canStringify && typeof(window.navigator.sendBeacon) !== "undefined" && !config.withCredentials;
102
100
  }
103
101
 
102
+ function serialize(object) {
103
+ var data = new FormData();
104
+ for (var key in object) {
105
+ if (object.hasOwnProperty(key)) {
106
+ data.append(key, object[key]);
107
+ }
108
+ }
109
+ return data;
110
+ }
111
+
104
112
  // cookies
105
113
 
106
114
  function setCookie(name, value, ttl) {
@@ -146,17 +154,23 @@
146
154
  element.webkitMatchesSelector;
147
155
 
148
156
  if (matches) {
149
- return matches.apply(element, [selector]);
157
+ if (matches.apply(element, [selector])) {
158
+ return element;
159
+ } else if (element.parentElement) {
160
+ return matchesSelector(element.parentElement, selector);
161
+ }
162
+ return null;
150
163
  } else {
151
164
  log("Unable to match");
152
- return false;
165
+ return null;
153
166
  }
154
167
  }
155
168
 
156
169
  function onEvent(eventName, selector, callback) {
157
170
  document.addEventListener(eventName, function (e) {
158
- if (matchesSelector(e.target, selector)) {
159
- callback(e);
171
+ var matchedElement = matchesSelector(e.target, selector);
172
+ if (matchedElement) {
173
+ callback.call(matchedElement, e);
160
174
  }
161
175
  });
162
176
  }
@@ -275,7 +289,7 @@
275
289
  // stringify so we keep the type
276
290
  data.events_json = JSON.stringify(data.events);
277
291
  delete data.events;
278
- window.navigator.sendBeacon(eventsUrl(), s(data));
292
+ window.navigator.sendBeacon(eventsUrl(), serialize(data));
279
293
  });
280
294
  }
281
295
 
@@ -298,14 +312,13 @@
298
312
  return obj;
299
313
  }
300
314
 
301
- function eventProperties(e) {
302
- var target = e.target;
315
+ function eventProperties() {
303
316
  return cleanObject({
304
- tag: target.tagName.toLowerCase(),
305
- id: presence(target.id),
306
- "class": presence(target.className),
317
+ tag: this.tagName.toLowerCase(),
318
+ id: presence(this.id),
319
+ "class": presence(this.className),
307
320
  page: page(),
308
- section: getClosestSection(target)
321
+ section: getClosestSection(this)
309
322
  });
310
323
  }
311
324
 
@@ -463,37 +476,39 @@
463
476
  ahoy.track("$view", properties);
464
477
  };
465
478
 
466
- ahoy.trackClicks = function () {
467
- onEvent("click", "a, button, input[type=submit]", function (e) {
468
- var target = e.target;
469
- var properties = eventProperties(e);
470
- properties.text = properties.tag == "input" ? target.value : (target.textContent || target.innerText || target.innerHTML).replace(/[\s\r\n]+/g, " ").trim();
471
- properties.href = target.href;
479
+ ahoy.trackClicks = function (selector) {
480
+ if (selector === undefined) {
481
+ throw new Error("Missing selector");
482
+ }
483
+ onEvent("click", selector, function (e) {
484
+ var properties = eventProperties.call(this, e);
485
+ properties.text = properties.tag == "input" ? this.value : (this.textContent || this.innerText || this.innerHTML).replace(/[\s\r\n]+/g, " ").trim();
486
+ properties.href = this.href;
472
487
  ahoy.track("$click", properties);
473
488
  });
474
489
  };
475
490
 
476
- ahoy.trackSubmits = function () {
477
- onEvent("submit", "form", function (e) {
478
- var properties = eventProperties(e);
491
+ ahoy.trackSubmits = function (selector) {
492
+ if (selector === undefined) {
493
+ throw new Error("Missing selector");
494
+ }
495
+ onEvent("submit", selector, function (e) {
496
+ var properties = eventProperties.call(this, e);
479
497
  ahoy.track("$submit", properties);
480
498
  });
481
499
  };
482
500
 
483
- ahoy.trackChanges = function () {
484
- onEvent("change", "input, textarea, select", function (e) {
485
- var properties = eventProperties(e);
501
+ ahoy.trackChanges = function (selector) {
502
+ log("trackChanges is deprecated and will be removed in 0.5.0");
503
+ if (selector === undefined) {
504
+ throw new Error("Missing selector");
505
+ }
506
+ onEvent("change", selector, function (e) {
507
+ var properties = eventProperties.call(this, e);
486
508
  ahoy.track("$change", properties);
487
509
  });
488
510
  };
489
511
 
490
- ahoy.trackAll = function() {
491
- ahoy.trackView();
492
- ahoy.trackClicks();
493
- ahoy.trackSubmits();
494
- ahoy.trackChanges();
495
- };
496
-
497
512
  // push events from queue
498
513
  try {
499
514
  eventQueue = JSON.parse(getCookie("ahoy_events") || "[]");
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ahoy_matey
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.5
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-10 00:00:00.000000000 Z
11
+ date: 2021-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,28 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5'
19
+ version: '5.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '5'
27
- - !ruby/object:Gem::Dependency
28
- name: geocoder
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: 1.4.5
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: 1.4.5
26
+ version: '5.2'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: safely_block
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -66,162 +52,8 @@ dependencies:
66
52
  - - ">="
67
53
  - !ruby/object:Gem::Version
68
54
  version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: bundler
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: rake
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: minitest
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: combustion
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: rails
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: sqlite3
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
- - !ruby/object:Gem::Dependency
154
- name: pg
155
- requirement: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - ">="
158
- - !ruby/object:Gem::Version
159
- version: '0'
160
- type: :development
161
- prerelease: false
162
- version_requirements: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - ">="
165
- - !ruby/object:Gem::Version
166
- version: '0'
167
- - !ruby/object:Gem::Dependency
168
- name: mysql2
169
- requirement: !ruby/object:Gem::Requirement
170
- requirements:
171
- - - ">="
172
- - !ruby/object:Gem::Version
173
- version: '0'
174
- type: :development
175
- prerelease: false
176
- version_requirements: !ruby/object:Gem::Requirement
177
- requirements:
178
- - - ">="
179
- - !ruby/object:Gem::Version
180
- version: '0'
181
- - !ruby/object:Gem::Dependency
182
- name: mongoid
183
- requirement: !ruby/object:Gem::Requirement
184
- requirements:
185
- - - ">="
186
- - !ruby/object:Gem::Version
187
- version: '0'
188
- type: :development
189
- prerelease: false
190
- version_requirements: !ruby/object:Gem::Requirement
191
- requirements:
192
- - - ">="
193
- - !ruby/object:Gem::Version
194
- version: '0'
195
- - !ruby/object:Gem::Dependency
196
- name: browser
197
- requirement: !ruby/object:Gem::Requirement
198
- requirements:
199
- - - "~>"
200
- - !ruby/object:Gem::Version
201
- version: '2.0'
202
- type: :development
203
- prerelease: false
204
- version_requirements: !ruby/object:Gem::Requirement
205
- requirements:
206
- - - "~>"
207
- - !ruby/object:Gem::Version
208
- version: '2.0'
209
- - !ruby/object:Gem::Dependency
210
- name: user_agent_parser
211
- requirement: !ruby/object:Gem::Requirement
212
- requirements:
213
- - - ">="
214
- - !ruby/object:Gem::Version
215
- version: '0'
216
- type: :development
217
- prerelease: false
218
- version_requirements: !ruby/object:Gem::Requirement
219
- requirements:
220
- - - ">="
221
- - !ruby/object:Gem::Version
222
- version: '0'
223
- description:
224
- email: andrew@chartkick.com
55
+ description:
56
+ email: andrew@ankane.org
225
57
  executables: []
226
58
  extensions: []
227
59
  extra_rdoc_files: []
@@ -266,7 +98,7 @@ homepage: https://github.com/ankane/ahoy
266
98
  licenses:
267
99
  - MIT
268
100
  metadata: {}
269
- post_install_message:
101
+ post_install_message:
270
102
  rdoc_options: []
271
103
  require_paths:
272
104
  - lib
@@ -274,15 +106,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
274
106
  requirements:
275
107
  - - ">="
276
108
  - !ruby/object:Gem::Version
277
- version: '2.4'
109
+ version: '2.6'
278
110
  required_rubygems_version: !ruby/object:Gem::Requirement
279
111
  requirements:
280
112
  - - ">="
281
113
  - !ruby/object:Gem::Version
282
114
  version: '0'
283
115
  requirements: []
284
- rubygems_version: 3.1.2
285
- signing_key:
116
+ rubygems_version: 3.2.22
117
+ signing_key:
286
118
  specification_version: 4
287
119
  summary: Simple, powerful, first-party analytics for Rails
288
120
  test_files: []