ahoy_matey 3.1.0 → 4.0.1

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: c98a3beef3f7d137de52572c02e410709d603028021a25b5e325e47fd6b2fe8c
4
- data.tar.gz: c9304029a1f3828baac05af4397083243113019e52add5da3439fac9c111f2ac
3
+ metadata.gz: cea71919e25f8f3b9ce9f3ccea43f8d11ec29d1995b3c5078428a8d4a7948457
4
+ data.tar.gz: 81316ad1b9fc956bf0ece8c23c63570d5f333e0f7c781406e1b2214e965feebb
5
5
  SHA512:
6
- metadata.gz: 2b4d2504fda7d21993196093617a55b31bf38b58b33a744b64db6f187f9e6da5b570858ecb67aeb550b76ce623742adebbb5369b2edd2f501166dd539682b14c
7
- data.tar.gz: e8ecbc02ed485f8856f34527351b2c293fe77afb07d4b6011c050e246c50ed92d8262639109696feadd386145633778620d3159b023a0ca27d963a2ffb83aef3
6
+ metadata.gz: c52e5f32c8fd7f9d070867e8d53868a58365430ed6d7bfa3baa1dd826f042875d42fadddc7991bf5c1c0b836021dd1b2634e8c2e43fa88a055dff9b8aeae70e5
7
+ data.tar.gz: e3cdb04028a3277f678f65bf713d3a93339be369d4ed8af73a87bddd549b2ffd7983abd91443858e4411459442b8e292612381ea3ff731ce53d8f0b50f995857
data/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ ## 4.0.1 (2021-08-18)
2
+
3
+ - Added support for `where_event`, `where_props`, and `where_group` for SQLite
4
+ - Fixed results with `where_event`
5
+ - Fixed results with `where_props` and `where_group` when used with other scopes
6
+
7
+ ## 4.0.0 (2021-08-14)
8
+
9
+ - Disabled geocoding by default (this was already the case for new installations with 3.2.0+)
10
+ - Made the `geocoder` gem an optional dependency
11
+ - Updated Ahoy.js to 0.4.0
12
+ - Updated API to return 400 status code when missing required parameters
13
+ - Dropped support for Ruby < 2.6 and Rails < 5.2
14
+
15
+ ## 3.3.0 (2021-08-13)
16
+
17
+ - Added `country_code` to geocoding
18
+ - Updated Ahoy.js to 0.3.9
19
+ - Fixed install generator for MariaDB
20
+
21
+ ## 3.2.0 (2021-03-01)
22
+
23
+ - Disabled geocoding by default for new installations
24
+ - Fixed deprecation warning with Active Record 6.1
25
+
1
26
  ## 3.1.0 (2020-12-04)
2
27
 
3
28
  - Added `instance` 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,7 +2,9 @@
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
 
@@ -74,6 +76,10 @@ ahoy.track("My second event", {language: "JavaScript"});
74
76
 
75
77
  Check out [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android).
76
78
 
79
+ ### Geocoding Setup
80
+
81
+ To enable geocoding, see the [Geocoding section](#geocoding).
82
+
77
83
  ### GDPR Compliance
78
84
 
79
85
  Ahoy provides a number of options to help with GDPR compliance. See the [GDPR section](#gdpr-compliance-1) for more info.
@@ -139,12 +145,6 @@ end
139
145
  ahoy.track("Viewed book", {title: "The World is Flat"});
140
146
  ```
141
147
 
142
- Track events automatically with:
143
-
144
- ```javascript
145
- ahoy.trackAll();
146
- ```
147
-
148
148
  See [Ahoy.js](https://github.com/ankane/ahoy.js) for a complete list of features.
149
149
 
150
150
  #### Native Apps
@@ -185,7 +185,7 @@ Order.joins(:ahoy_visit).group("device_type").count
185
185
  Here’s what the migration to add the `ahoy_visit_id` column should look like:
186
186
 
187
187
  ```ruby
188
- class AddVisitIdToOrders < ActiveRecord::Migration[6.0]
188
+ class AddVisitIdToOrders < ActiveRecord::Migration[6.1]
189
189
  def change
190
190
  add_column :orders, :ahoy_visit_id, :bigint
191
191
  end
@@ -200,7 +200,7 @@ visitable :sign_up_visit
200
200
 
201
201
  ### Users
202
202
 
203
- 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.
204
204
 
205
205
  With other authentication frameworks, add this to the end of your sign in method:
206
206
 
@@ -310,71 +310,118 @@ Set other [cookie options](https://api.rubyonrails.org/classes/ActionDispatch/Co
310
310
  Ahoy.cookie_options = {same_site: :lax}
311
311
  ```
312
312
 
313
- ### Geocoding
313
+ You can also [disable cookies](#anonymity-sets--cookies)
314
314
 
315
- 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).
316
318
 
317
319
  ```ruby
318
- Ahoy.geocode = false
320
+ Ahoy.token_generator = -> { Druuid.gen }
319
321
  ```
320
322
 
321
- 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.
322
326
 
323
327
  ```ruby
324
- 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
325
335
  ```
326
336
 
327
- #### Geocoding Performance
337
+ ### Exceptions
328
338
 
329
- 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:
330
340
 
331
- 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:
332
370
 
333
371
  ```ruby
334
372
  gem 'maxminddb'
335
373
  ```
336
374
 
337
- 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:
338
376
 
339
377
  ```ruby
340
378
  Geocoder.configure(
341
379
  ip_lookup: :geoip2,
342
380
  geoip2: {
343
- file: Rails.root.join("lib", "GeoLite2-City.mmdb")
381
+ file: "path/to/GeoLite2-City.mmdb"
344
382
  }
345
383
  )
346
384
  ```
347
385
 
348
- 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:
349
387
 
350
- ### Token Generation
388
+ ```sh
389
+ sudo apt-get install geoip-database
390
+ ```
351
391
 
352
- 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:
353
393
 
354
394
  ```ruby
355
- 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
+ )
356
402
  ```
357
403
 
358
- ### Throttling
404
+ ### Load Balancer Geocoding
359
405
 
360
- You can use [Rack::Attack](https://github.com/kickstarter/rack-attack) to throttle requests to the API.
361
-
362
- ```ruby
363
- class Rack::Attack
364
- throttle("ahoy/ip", limit: 20, period: 1.minute) do |req|
365
- if req.path.start_with?("/ahoy/")
366
- req.ip
367
- end
368
- end
369
- end
370
- ```
406
+ Some load balancers can add geocoding information to request headers.
371
407
 
372
- ### Exceptions
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)
373
411
 
374
- 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:
375
413
 
376
414
  ```ruby
377
- 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
378
425
  ```
379
426
 
380
427
  ## GDPR Compliance
@@ -435,7 +482,11 @@ Ahoy can switch from cookies to [anonymity sets](https://privacypatterns.org/pat
435
482
  Ahoy.cookies = false
436
483
  ```
437
484
 
438
- 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
+ ```
439
490
 
440
491
  ## Data Retention
441
492
 
@@ -694,62 +745,19 @@ Send a `POST` request to `/ahoy/events` with `Content-Type: application/json` an
694
745
 
695
746
  ## Upgrading
696
747
 
697
- ### 3.0
698
-
699
- If you installed Ahoy before 2.1 and want to keep legacy user agent parsing and bot detection, add to your Gemfile:
700
-
701
- ```ruby
702
- gem "browser", "~> 2.0"
703
- gem "user_agent_parser"
704
- ```
705
-
706
- And add to `config/initializers/ahoy.rb`:
707
-
708
- ```ruby
709
- Ahoy.user_agent_parser = :legacy
710
- ```
711
-
712
- ### 2.2
713
-
714
- 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
715
749
 
716
- ```ruby
717
- Ahoy.bot_detection_version = 2
718
- ```
719
-
720
- ### 2.1
721
-
722
- 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`:
723
-
724
- ```ruby
725
- Ahoy.user_agent_parser = :device_detector
726
- ```
750
+ There are two notable changes to geocoding:
727
751
 
728
- 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).
729
753
 
730
- ```ruby
731
- Ahoy::Visit.find_each do |visit|
732
- client = DeviceDetector.new(visit.user_agent)
733
- device_type =
734
- case client.device_type
735
- when "smartphone"
736
- "Mobile"
737
- when "tv"
738
- "TV"
739
- else
740
- client.device_type.try(:titleize)
741
- end
742
-
743
- visit.browser = client.name
744
- visit.os = client.os_name
745
- visit.device_type = device_type
746
- visit.save(validate: false) if visit.changed?
747
- end
748
- ```
754
+ 2. The `geocoder` gem is now an optional dependency. To use geocoding, add it to your Gemfile:
749
755
 
750
- ### 2.0
756
+ ```ruby
757
+ gem 'geocoder'
758
+ ```
751
759
 
752
- 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.
753
761
 
754
762
  ## History
755
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
@@ -127,10 +127,6 @@ ActiveSupport.on_load(:action_view) do
127
127
  end
128
128
 
129
129
  # Mongoid
130
- # TODO use
131
- # ActiveSupport.on_load(:mongoid) do
132
- # Mongoid::Document::ClassMethods.include(Ahoy::Model)
133
- # end
134
- if defined?(ActiveModel)
135
- ActiveModel::Callbacks.include(Ahoy::Model)
130
+ ActiveSupport.on_load(:mongoid) do
131
+ Mongoid::Document::ClassMethods.include(Ahoy::Model)
136
132
  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
@@ -8,7 +8,9 @@ module Ahoy
8
8
  end
9
9
 
10
10
  def where_props(properties)
11
- relation = self
11
+ return all if properties.empty?
12
+
13
+ relation = all
12
14
  if respond_to?(:columns_hash)
13
15
  column_type = columns_hash["properties"].type
14
16
  adapter_name = connection.adapter_name.downcase
@@ -17,37 +19,14 @@ module Ahoy
17
19
  end
18
20
  case adapter_name
19
21
  when "mongoid"
20
- relation = where(Hash[properties.map { |k, v| ["properties.#{k}", v] }])
22
+ relation = where(properties.to_h { |k, v| ["properties.#{k}", v] })
21
23
  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("+", "\\\\+")}[,}]")
36
- end
37
- end
24
+ relation = relation.where("JSON_CONTAINS(properties, ?, '$') = 1", properties.to_json)
38
25
  when /postgres|postgis/
39
- if column_type == :jsonb
26
+ case column_type
27
+ when :jsonb
40
28
  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
29
+ when :hstore
51
30
  properties.each do |k, v|
52
31
  relation =
53
32
  if v.nil?
@@ -57,10 +36,16 @@ module Ahoy
57
36
  end
58
37
  end
59
38
  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
39
+ relation = relation.where("properties::jsonb @> ?", properties.to_json)
40
+ end
41
+ when /sqlite/
42
+ properties.each do |k, v|
43
+ relation =
44
+ if v.nil?
45
+ relation.where("JSON_EXTRACT(properties, ?) IS NULL", "$.#{k}")
46
+ else
47
+ relation.where("JSON_EXTRACT(properties, ?) = ?", "$.#{k}", v.as_json)
48
+ end
64
49
  end
65
50
  else
66
51
  raise "Adapter not supported: #{adapter_name}"
@@ -73,7 +58,7 @@ module Ahoy
73
58
  # like with group
74
59
  props.flatten!
75
60
 
76
- relation = self
61
+ relation = all
77
62
  if respond_to?(:columns_hash)
78
63
  column_type = columns_hash["properties"].type
79
64
  adapter_name = connection.adapter_name.downcase
@@ -84,17 +69,9 @@ module Ahoy
84
69
  when "mongoid"
85
70
  raise "Adapter not supported: #{adapter_name}"
86
71
  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
72
+ props.each do |prop|
73
+ quoted_prop = connection.quote("$.#{prop}")
74
+ relation = relation.group("JSON_UNQUOTE(JSON_EXTRACT(properties, #{quoted_prop}))")
98
75
  end
99
76
  when /postgres|postgis/
100
77
  # convert to jsonb to fix
@@ -106,6 +83,11 @@ module Ahoy
106
83
  quoted_prop = connection.quote(prop)
107
84
  relation = relation.group("properties#{cast} -> #{quoted_prop}")
108
85
  end
86
+ when /sqlite/
87
+ props.each do |prop|
88
+ quoted_prop = connection.quote("$.#{prop}")
89
+ relation = relation.group("JSON_EXTRACT(properties, #{quoted_prop})")
90
+ end
109
91
  else
110
92
  raise "Adapter not supported: #{adapter_name}"
111
93
  end
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)
@@ -155,6 +151,7 @@ module Ahoy
155
151
  @options[:api]
156
152
  end
157
153
 
154
+ # private, but used by API
158
155
  def missing_params?
159
156
  if Ahoy.cookies && api? && Ahoy.protect_from_forgery
160
157
  !(existing_visit_token && existing_visitor_token)
data/lib/ahoy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ahoy
2
- VERSION = "3.1.0"
2
+ VERSION = "4.0.1"
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.8
5
+ * v0.4.0
6
6
  * MIT License
7
7
  */
8
8
 
@@ -12,97 +12,6 @@
12
12
  (global = global || self, global.ahoy = factory());
13
13
  }(this, (function () { 'use strict';
14
14
 
15
- var isUndefined = function (value) { return value === undefined; };
16
-
17
- var isNull = function (value) { return value === null; };
18
-
19
- var isBoolean = function (value) { return typeof value === 'boolean'; };
20
-
21
- var isObject = function (value) { return value === Object(value); };
22
-
23
- var isArray = function (value) { return Array.isArray(value); };
24
-
25
- var isDate = function (value) { return value instanceof Date; };
26
-
27
- var isBlob = function (value) { return value &&
28
- typeof value.size === 'number' &&
29
- typeof value.type === 'string' &&
30
- typeof value.slice === 'function'; };
31
-
32
- var isFile = function (value) { return isBlob(value) &&
33
- typeof value.name === 'string' &&
34
- (typeof value.lastModifiedDate === 'object' ||
35
- typeof value.lastModified === 'number'); };
36
-
37
- var serialize = function (obj, cfg, fd, pre) {
38
- cfg = cfg || {};
39
-
40
- cfg.indices = isUndefined(cfg.indices) ? false : cfg.indices;
41
-
42
- cfg.nullsAsUndefineds = isUndefined(cfg.nullsAsUndefineds)
43
- ? false
44
- : cfg.nullsAsUndefineds;
45
-
46
- cfg.booleansAsIntegers = isUndefined(cfg.booleansAsIntegers)
47
- ? false
48
- : cfg.booleansAsIntegers;
49
-
50
- cfg.allowEmptyArrays = isUndefined(cfg.allowEmptyArrays)
51
- ? false
52
- : cfg.allowEmptyArrays;
53
-
54
- fd = fd || new FormData();
55
-
56
- if (isUndefined(obj)) {
57
- return fd;
58
- } else if (isNull(obj)) {
59
- if (!cfg.nullsAsUndefineds) {
60
- fd.append(pre, '');
61
- }
62
- } else if (isBoolean(obj)) {
63
- if (cfg.booleansAsIntegers) {
64
- fd.append(pre, obj ? 1 : 0);
65
- } else {
66
- fd.append(pre, obj);
67
- }
68
- } else if (isArray(obj)) {
69
- if (obj.length) {
70
- obj.forEach(function (value, index) {
71
- var key = pre + '[' + (cfg.indices ? index : '') + ']';
72
-
73
- serialize(value, cfg, fd, key);
74
- });
75
- } else if (cfg.allowEmptyArrays) {
76
- fd.append(pre + '[]', '');
77
- }
78
- } else if (isDate(obj)) {
79
- fd.append(pre, obj.toISOString());
80
- } else if (isObject(obj) && !isFile(obj) && !isBlob(obj)) {
81
- Object.keys(obj).forEach(function (prop) {
82
- var value = obj[prop];
83
-
84
- if (isArray(value)) {
85
- while (prop.length > 2 && prop.lastIndexOf('[]') === prop.length - 2) {
86
- prop = prop.substring(0, prop.length - 2);
87
- }
88
- }
89
-
90
- var key = pre ? pre + '[' + prop + ']' : prop;
91
-
92
- serialize(value, cfg, fd, key);
93
- });
94
- } else {
95
- fd.append(pre, obj);
96
- }
97
-
98
- return fd;
99
- };
100
-
101
- var index_module = {
102
- serialize: serialize,
103
- };
104
- var index_module_1 = index_module.serialize;
105
-
106
15
  // https://www.quirksmode.org/js/cookies.html
107
16
 
108
17
  var Cookies = {
@@ -117,7 +26,7 @@
117
26
  if (domain) {
118
27
  cookieDomain = "; domain=" + domain;
119
28
  }
120
- document.cookie = name + "=" + escape(value) + expires + cookieDomain + "; path=/";
29
+ document.cookie = name + "=" + escape(value) + expires + cookieDomain + "; path=/; samesite=lax";
121
30
  },
122
31
  get: function (name) {
123
32
  var i, c;
@@ -190,6 +99,16 @@
190
99
  return (config.useBeacon || config.trackNow) && isEmpty(config.headers) && canStringify && typeof(window.navigator.sendBeacon) !== "undefined" && !config.withCredentials;
191
100
  }
192
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
+
193
112
  // cookies
194
113
 
195
114
  function setCookie(name, value, ttl) {
@@ -238,7 +157,7 @@
238
157
  if (matches.apply(element, [selector])) {
239
158
  return element;
240
159
  } else if (element.parentElement) {
241
- return matchesSelector(element.parentElement, selector)
160
+ return matchesSelector(element.parentElement, selector);
242
161
  }
243
162
  return null;
244
163
  } else {
@@ -370,7 +289,7 @@
370
289
  // stringify so we keep the type
371
290
  data.events_json = JSON.stringify(data.events);
372
291
  delete data.events;
373
- window.navigator.sendBeacon(eventsUrl(), index_module_1(data));
292
+ window.navigator.sendBeacon(eventsUrl(), serialize(data));
374
293
  });
375
294
  }
376
295
 
@@ -393,7 +312,7 @@
393
312
  return obj;
394
313
  }
395
314
 
396
- function eventProperties(e) {
315
+ function eventProperties() {
397
316
  return cleanObject({
398
317
  tag: this.tagName.toLowerCase(),
399
318
  id: presence(this.id),
@@ -557,8 +476,11 @@
557
476
  ahoy.track("$view", properties);
558
477
  };
559
478
 
560
- ahoy.trackClicks = function () {
561
- onEvent("click", "a, button, input[type=submit]", function (e) {
479
+ ahoy.trackClicks = function (selector) {
480
+ if (selector === undefined) {
481
+ throw new Error("Missing selector");
482
+ }
483
+ onEvent("click", selector, function (e) {
562
484
  var properties = eventProperties.call(this, e);
563
485
  properties.text = properties.tag == "input" ? this.value : (this.textContent || this.innerText || this.innerHTML).replace(/[\s\r\n]+/g, " ").trim();
564
486
  properties.href = this.href;
@@ -566,27 +488,27 @@
566
488
  });
567
489
  };
568
490
 
569
- ahoy.trackSubmits = function () {
570
- onEvent("submit", "form", function (e) {
491
+ ahoy.trackSubmits = function (selector) {
492
+ if (selector === undefined) {
493
+ throw new Error("Missing selector");
494
+ }
495
+ onEvent("submit", selector, function (e) {
571
496
  var properties = eventProperties.call(this, e);
572
497
  ahoy.track("$submit", properties);
573
498
  });
574
499
  };
575
500
 
576
- ahoy.trackChanges = function () {
577
- onEvent("change", "input, textarea, select", function (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) {
578
507
  var properties = eventProperties.call(this, e);
579
508
  ahoy.track("$change", properties);
580
509
  });
581
510
  };
582
511
 
583
- ahoy.trackAll = function() {
584
- ahoy.trackView();
585
- ahoy.trackClicks();
586
- ahoy.trackSubmits();
587
- ahoy.trackChanges();
588
- };
589
-
590
512
  // push events from queue
591
513
  try {
592
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.1.0
4
+ version: 4.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-04 00:00:00.000000000 Z
11
+ date: 2021-08-18 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
55
  description:
224
- email: andrew@chartkick.com
56
+ email: andrew@ankane.org
225
57
  executables: []
226
58
  extensions: []
227
59
  extra_rdoc_files: []
@@ -274,14 +106,14 @@ 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.4
116
+ rubygems_version: 3.2.22
285
117
  signing_key:
286
118
  specification_version: 4
287
119
  summary: Simple, powerful, first-party analytics for Rails