ahoy_matey 3.2.0 → 4.0.2

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: 51e11f3086c12a1418ea7436e291ca9d2f0c7e0aa3091c48c904d05b4be6a210
4
- data.tar.gz: 5b5c6afc94c0db23f2f1616f6e9edc0ae95ff334e33704c26417705d06d04670
3
+ metadata.gz: 06e00a470a12c20b510cc9f1476e39c2cfd8ec24115b29d5d09489d091465b74
4
+ data.tar.gz: e19501a6019c94bafc32121122e2832152adb0fe8475cb3b91ed4d50c791ff6e
5
5
  SHA512:
6
- metadata.gz: fd75680d3d04b2383c30b19848f88fcb838f1498e8819d2d758aec6443e85bfb2f73a421da968cb54628097631c777ac1f95dd1ca4bae6d6b22d748a44dcb84d
7
- data.tar.gz: 2d336bd12312cddf4b76f213b1f7f84b1fc2f0b67302e75096d1eb21013982f532064b31894ba217f7ccbafdbaf5eb4c691a3a86350b8abf1449374f66015909
6
+ metadata.gz: b0fe7c188d2165aae96a6a5d2def6658bcf41158477d5d9aa79448b889f87c3b88ef3f6aee19309c56af3587110fff71bfcd60fae0a46562e87e1a22ae070073
7
+ data.tar.gz: 285deb63fb81a4cc423c3eab394e324d5ebe8572c5500750cefe88d615bbd869228d04a725f62cde2868730d7dbd345d3d3787bbaf0acb75f04aa2e231669a81
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## 4.0.2 (2021-11-06)
2
+
3
+ - Added experimental support for `importmap-rails`
4
+
5
+ ## 4.0.1 (2021-08-18)
6
+
7
+ - Added support for `where_event`, `where_props`, and `where_group` for SQLite
8
+ - Fixed results with `where_event` for MySQL, MariaDB, and Postgres `hstore`
9
+ - Fixed results with `where_props` and `where_group` when used with other scopes for MySQL, MariaDB, and Postgres `hstore`
10
+
11
+ ## 4.0.0 (2021-08-14)
12
+
13
+ - Disabled geocoding by default (this was already the case for new installations with 3.2.0+)
14
+ - Made the `geocoder` gem an optional dependency
15
+ - Updated Ahoy.js to 0.4.0
16
+ - Updated API to return 400 status code when missing required parameters
17
+ - Dropped support for Ruby < 2.6 and Rails < 5.2
18
+
19
+ ## 3.3.0 (2021-08-13)
20
+
21
+ - Added `country_code` to geocoding
22
+ - Updated Ahoy.js to 0.3.9
23
+ - Fixed install generator for MariaDB
24
+
1
25
  ## 3.2.0 (2021-03-01)
2
26
 
3
27
  - Disabled geocoding by default for new installations
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
 
@@ -55,7 +57,7 @@ yarn add ahoy.js
55
57
  And add to `app/javascript/packs/application.js`:
56
58
 
57
59
  ```javascript
58
- import ahoy from "ahoy.js";
60
+ import ahoy from "ahoy.js"
59
61
  ```
60
62
 
61
63
  For Rails 5 / Sprockets, add to `app/assets/javascripts/application.js`:
@@ -64,6 +66,18 @@ For Rails 5 / Sprockets, add to `app/assets/javascripts/application.js`:
64
66
  //= require ahoy
65
67
  ```
66
68
 
69
+ For Rails 7 / Importmap (experimental), add to `config/importmap.rb`:
70
+
71
+ ```ruby
72
+ pin "ahoy", to: "ahoy.js"
73
+ ```
74
+
75
+ And add to `app/javascript/application.js`:
76
+
77
+ ```javascript
78
+ import "ahoy"
79
+ ```
80
+
67
81
  Track an event with:
68
82
 
69
83
  ```javascript
@@ -74,14 +88,14 @@ ahoy.track("My second event", {language: "JavaScript"});
74
88
 
75
89
  Check out [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android).
76
90
 
77
- ### GDPR Compliance
78
-
79
- Ahoy provides a number of options to help with GDPR compliance. See the [GDPR section](#gdpr-compliance-1) for more info.
80
-
81
91
  ### Geocoding Setup
82
92
 
83
93
  To enable geocoding, see the [Geocoding section](#geocoding).
84
94
 
95
+ ### GDPR Compliance
96
+
97
+ Ahoy provides a number of options to help with GDPR compliance. See the [GDPR section](#gdpr-compliance-1) for more info.
98
+
85
99
  ## How It Works
86
100
 
87
101
  ### Visits
@@ -143,12 +157,6 @@ end
143
157
  ahoy.track("Viewed book", {title: "The World is Flat"});
144
158
  ```
145
159
 
146
- Track events automatically with:
147
-
148
- ```javascript
149
- ahoy.trackAll();
150
- ```
151
-
152
160
  See [Ahoy.js](https://github.com/ankane/ahoy.js) for a complete list of features.
153
161
 
154
162
  #### Native Apps
@@ -189,7 +197,7 @@ Order.joins(:ahoy_visit).group("device_type").count
189
197
  Here’s what the migration to add the `ahoy_visit_id` column should look like:
190
198
 
191
199
  ```ruby
192
- class AddVisitIdToOrders < ActiveRecord::Migration[6.0]
200
+ class AddVisitIdToOrders < ActiveRecord::Migration[6.1]
193
201
  def change
194
202
  add_column :orders, :ahoy_visit_id, :bigint
195
203
  end
@@ -204,7 +212,7 @@ visitable :sign_up_visit
204
212
 
205
213
  ### Users
206
214
 
207
- 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.
215
+ 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.
208
216
 
209
217
  With other authentication frameworks, add this to the end of your sign in method:
210
218
 
@@ -314,11 +322,49 @@ Set other [cookie options](https://api.rubyonrails.org/classes/ActionDispatch/Co
314
322
  Ahoy.cookie_options = {same_site: :lax}
315
323
  ```
316
324
 
317
- ### Geocoding
325
+ You can also [disable cookies](#anonymity-sets--cookies)
326
+
327
+ ### Token Generation
328
+
329
+ 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).
330
+
331
+ ```ruby
332
+ Ahoy.token_generator = -> { Druuid.gen }
333
+ ```
334
+
335
+ ### Throttling
336
+
337
+ You can use [Rack::Attack](https://github.com/kickstarter/rack-attack) to throttle requests to the API.
338
+
339
+ ```ruby
340
+ class Rack::Attack
341
+ throttle("ahoy/ip", limit: 20, period: 1.minute) do |req|
342
+ if req.path.start_with?("/ahoy/")
343
+ req.ip
344
+ end
345
+ end
346
+ end
347
+ ```
348
+
349
+ ### Exceptions
318
350
 
319
- Ahoy uses [Geocoder](https://github.com/alexreisner/geocoder) for geocoding. We recommend configuring [local geocoding](#local-geocoding) so IP addresses are not sent to a 3rd party service. If you do use a 3rd party service, be sure to add it to your GDPR subprocessor list. If Ahoy is configured to [mask ips](#ip-masking), the masked IP is used (this increases privacy but can reduce accuracy).
351
+ 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:
320
352
 
321
- To enable geocoding, update `config/initializers/ahoy.rb`:
353
+ ```ruby
354
+ Safely.report_exception_method = ->(e) { Rollbar.error(e) }
355
+ ```
356
+
357
+ ## Geocoding
358
+
359
+ 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).
360
+
361
+ To enable geocoding, add this line to your application’s Gemfile:
362
+
363
+ ```ruby
364
+ gem 'geocoder'
365
+ ```
366
+
367
+ And update `config/initializers/ahoy.rb`:
322
368
 
323
369
  ```ruby
324
370
  Ahoy.geocode = true
@@ -367,36 +413,29 @@ Geocoder.configure(
367
413
  )
368
414
  ```
369
415
 
370
- ### Token Generation
371
-
372
- 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).
416
+ ### Load Balancer Geocoding
373
417
 
374
- ```ruby
375
- Ahoy.token_generator = -> { Druuid.gen }
376
- ```
418
+ Some load balancers can add geocoding information to request headers.
377
419
 
378
- ### Throttling
420
+ - [nginx](https://nginx.org/en/docs/http/ngx_http_geoip_module.html)
421
+ - [Google Cloud](https://cloud.google.com/load-balancing/docs/custom-headers)
422
+ - [Cloudflare](https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-Cloudflare-IP-Geolocation)
379
423
 
380
- You can use [Rack::Attack](https://github.com/kickstarter/rack-attack) to throttle requests to the API.
424
+ Update `config/initializers/ahoy.rb` with:
381
425
 
382
426
  ```ruby
383
- class Rack::Attack
384
- throttle("ahoy/ip", limit: 20, period: 1.minute) do |req|
385
- if req.path.start_with?("/ahoy/")
386
- req.ip
387
- end
427
+ Ahoy.geocode = false
428
+
429
+ class Ahoy::Store < Ahoy::DatabaseStore
430
+ def track_visit(data)
431
+ data[:country] = request.headers["<country-header>"]
432
+ data[:region] = request.headers["<region-header>"]
433
+ data[:city] = request.headers["<city-header>"]
434
+ super(data)
388
435
  end
389
436
  end
390
437
  ```
391
438
 
392
- ### Exceptions
393
-
394
- 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:
395
-
396
- ```ruby
397
- Safely.report_exception_method = ->(e) { Rollbar.error(e) }
398
- ```
399
-
400
439
  ## GDPR Compliance
401
440
 
402
441
  Ahoy provides a number of options to help with [GDPR compliance](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation).
@@ -455,7 +494,11 @@ Ahoy can switch from cookies to [anonymity sets](https://privacypatterns.org/pat
455
494
  Ahoy.cookies = false
456
495
  ```
457
496
 
458
- Previously set cookies are automatically deleted.
497
+ Previously set cookies are automatically deleted. If you use JavaScript tracking, also set:
498
+
499
+ ```javascript
500
+ ahoy.configure({cookies: false});
501
+ ```
459
502
 
460
503
  ## Data Retention
461
504
 
@@ -630,7 +673,7 @@ Group by properties with:
630
673
  Ahoy::Event.group_prop(:product_id, :category).count
631
674
  ```
632
675
 
633
- Note: MySQL and MariaDB always return string keys (include `"null"` for `nil`) for `group_prop`.
676
+ Note: MySQL and MariaDB always return string keys (including `"null"` for `nil`) for `group_prop`.
634
677
 
635
678
  ### Funnels
636
679
 
@@ -663,6 +706,24 @@ daily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate
663
706
  Prophet.forecast(daily_visits)
664
707
  ```
665
708
 
709
+ ### Anomaly Detection
710
+
711
+ To detect anomalies in visits and events, check out [AnomalyDetection.rb](https://github.com/ankane/AnomalyDetection.rb).
712
+
713
+ ```ruby
714
+ daily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate
715
+ AnomalyDetection.detect(daily_visits, period: 7)
716
+ ```
717
+
718
+ ### Breakout Detection
719
+
720
+ To detect breakouts in visits and events, check out [Breakout](https://github.com/ankane/breakout).
721
+
722
+ ```ruby
723
+ daily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate
724
+ Breakout.detect(daily_visits)
725
+ ```
726
+
666
727
  ### Recommendations
667
728
 
668
729
  To make recommendations based on events, check out [Disco](https://github.com/ankane/disco#ahoy).
@@ -714,62 +775,19 @@ Send a `POST` request to `/ahoy/events` with `Content-Type: application/json` an
714
775
 
715
776
  ## Upgrading
716
777
 
717
- ### 3.0
718
-
719
- If you installed Ahoy before 2.1 and want to keep legacy user agent parsing and bot detection, add to your Gemfile:
720
-
721
- ```ruby
722
- gem "browser", "~> 2.0"
723
- gem "user_agent_parser"
724
- ```
725
-
726
- And add to `config/initializers/ahoy.rb`:
727
-
728
- ```ruby
729
- Ahoy.user_agent_parser = :legacy
730
- ```
778
+ ### 4.0
731
779
 
732
- ### 2.2
780
+ There are two notable changes to geocoding:
733
781
 
734
- 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`:
782
+ 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).
735
783
 
736
- ```ruby
737
- Ahoy.bot_detection_version = 2
738
- ```
784
+ 2. The `geocoder` gem is now an optional dependency. To use geocoding, add it to your Gemfile:
739
785
 
740
- ### 2.1
786
+ ```ruby
787
+ gem 'geocoder'
788
+ ```
741
789
 
742
- 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`:
743
-
744
- ```ruby
745
- Ahoy.user_agent_parser = :device_detector
746
- ```
747
-
748
- Backfill existing records with:
749
-
750
- ```ruby
751
- Ahoy::Visit.find_each do |visit|
752
- client = DeviceDetector.new(visit.user_agent)
753
- device_type =
754
- case client.device_type
755
- when "smartphone"
756
- "Mobile"
757
- when "tv"
758
- "TV"
759
- else
760
- client.device_type.try(:titleize)
761
- end
762
-
763
- visit.browser = client.name
764
- visit.os = client.os_name
765
- visit.device_type = device_type
766
- visit.save(validate: false) if visit.changed?
767
- end
768
- ```
769
-
770
- ### 2.0
771
-
772
- See the [upgrade guide](docs/Ahoy-2-Upgrade.md)
790
+ Also, check out the [upgrade notes](https://github.com/ankane/ahoy.js#upgrading) for Ahoy.js.
773
791
 
774
792
  ## History
775
793
 
@@ -793,10 +811,20 @@ bundle install
793
811
  bundle exec rake test
794
812
  ```
795
813
 
796
- To test query methods, start PostgreSQL, MySQL, and MongoDB and use:
814
+ To test query methods, use:
797
815
 
798
816
  ```sh
817
+ # Postgres
799
818
  createdb ahoy_test
819
+ bundle exec rake test:query_methods:postgresql
820
+
821
+ # SQLite
822
+ bundle exec rake test:query_methods:sqlite
823
+
824
+ # MySQL and MariaDB
800
825
  mysqladmin create ahoy_test
801
- bundle exec rake test:query_methods
826
+ bundle exec rake test:query_methods:mysql
827
+
828
+ # MongoDB
829
+ bundle exec rake test:query_methods:mongoid
802
830
  ```
@@ -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,
@@ -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/engine.rb CHANGED
@@ -26,5 +26,12 @@ module Ahoy
26
26
  alias_method :call, :call_with_quiet_ahoy
27
27
  end
28
28
  end
29
+
30
+ # for importmap
31
+ if defined?(Importmap)
32
+ initializer "ahoy.importmap", after: "importmap" do |app|
33
+ app.config.assets.precompile << "ahoy.js"
34
+ end
35
+ end
29
36
  end
30
37
  end
@@ -8,64 +8,40 @@ module Ahoy
8
8
  end
9
9
 
10
10
  def where_props(properties)
11
- relation = self
12
- if respond_to?(:columns_hash)
13
- column_type = columns_hash["properties"].type
14
- adapter_name = connection.adapter_name.downcase
15
- else
16
- adapter_name = "mongoid"
17
- end
11
+ return all if properties.empty?
12
+
13
+ adapter_name = respond_to?(:connection) ? connection.adapter_name.downcase : "mongoid"
18
14
  case adapter_name
19
15
  when "mongoid"
20
- relation = where(Hash[properties.map { |k, v| ["properties.#{k}", v] }])
16
+ where(properties.to_h { |k, v| ["properties.#{k}", v] })
21
17
  when /mysql/
22
- if column_type == :json
23
- properties.each do |k, v|
18
+ where("JSON_CONTAINS(properties, ?, '$') = 1", properties.to_json)
19
+ when /postgres|postgis/
20
+ case columns_hash["properties"].type
21
+ when :hstore
22
+ properties.inject(all) do |relation, (k, v)|
24
23
  if v.nil?
25
- v = "null"
26
- elsif v == true
27
- v = "true"
24
+ relation.where("properties -> ? IS NULL", k.to_s)
25
+ else
26
+ relation.where("properties -> ? = ?", k.to_s, v.to_s)
28
27
  end
29
-
30
- relation = relation.where("JSON_UNQUOTE(properties -> ?) = ?", "$.#{k}", v.as_json)
31
28
  end
29
+ when :jsonb
30
+ where("properties @> ?", properties.to_json)
32
31
  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
32
+ where("properties::jsonb @> ?", properties.to_json)
37
33
  end
38
- when /postgres|postgis/
39
- if column_type == :jsonb
40
- 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
51
- properties.each do |k, v|
52
- relation =
53
- if v.nil?
54
- relation.where("properties -> ? IS NULL", k.to_s)
55
- else
56
- relation.where("properties -> ? = ?", k.to_s, v.to_s)
57
- end
58
- end
59
- 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("+", "\\\\+")}[,}]%")
34
+ when /sqlite/
35
+ properties.inject(all) do |relation, (k, v)|
36
+ if v.nil?
37
+ relation.where("JSON_EXTRACT(properties, ?) IS NULL", "$.#{k}")
38
+ else
39
+ relation.where("JSON_EXTRACT(properties, ?) = ?", "$.#{k}", v.as_json)
63
40
  end
64
41
  end
65
42
  else
66
43
  raise "Adapter not supported: #{adapter_name}"
67
44
  end
68
- relation
69
45
  end
70
46
  alias_method :where_properties, :where_props
71
47
 
@@ -73,39 +49,32 @@ module Ahoy
73
49
  # like with group
74
50
  props.flatten!
75
51
 
76
- relation = self
77
- if respond_to?(:columns_hash)
78
- column_type = columns_hash["properties"].type
79
- adapter_name = connection.adapter_name.downcase
80
- else
81
- adapter_name = "mongoid"
82
- end
52
+ relation = all
53
+ adapter_name = respond_to?(:connection) ? connection.adapter_name.downcase : "mongoid"
83
54
  case adapter_name
84
55
  when "mongoid"
85
56
  raise "Adapter not supported: #{adapter_name}"
86
57
  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
58
+ props.each do |prop|
59
+ quoted_prop = connection.quote("$.#{prop}")
60
+ relation = relation.group("JSON_UNQUOTE(JSON_EXTRACT(properties, #{quoted_prop}))")
98
61
  end
99
62
  when /postgres|postgis/
100
63
  # convert to jsonb to fix
101
64
  # could not identify an equality operator for type json
102
65
  # and for text columns
66
+ column_type = columns_hash["properties"].type
103
67
  cast = [:jsonb, :hstore].include?(column_type) ? "" : "::jsonb"
104
68
 
105
69
  props.each do |prop|
106
70
  quoted_prop = connection.quote(prop)
107
71
  relation = relation.group("properties#{cast} -> #{quoted_prop}")
108
72
  end
73
+ when /sqlite/
74
+ props.each do |prop|
75
+ quoted_prop = connection.quote("$.#{prop}")
76
+ relation = relation.group("JSON_EXTRACT(properties, #{quoted_prop})")
77
+ end
109
78
  else
110
79
  raise "Adapter not supported: #{adapter_name}"
111
80
  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.2.0"
2
+ VERSION = "4.0.2"
3
3
  end
data/lib/ahoy.rb CHANGED
@@ -4,7 +4,6 @@ require "ipaddr"
4
4
  # dependencies
5
5
  require "active_support"
6
6
  require "active_support/core_ext"
7
- require "geocoder"
8
7
  require "safely/core"
9
8
 
10
9
  # modules
@@ -44,7 +43,7 @@ module Ahoy
44
43
  self.quiet = true
45
44
 
46
45
  mattr_accessor :geocode
47
- self.geocode = true
46
+ self.geocode = false
48
47
 
49
48
  mattr_accessor :max_content_length
50
49
  self.max_content_length = 8192
@@ -128,10 +127,6 @@ ActiveSupport.on_load(:action_view) do
128
127
  end
129
128
 
130
129
  # Mongoid
131
- # TODO use
132
- # ActiveSupport.on_load(:mongoid) do
133
- # Mongoid::Document::ClassMethods.include(Ahoy::Model)
134
- # end
135
- if defined?(ActiveModel)
136
- ActiveModel::Callbacks.include(Ahoy::Model)
130
+ ActiveSupport.on_load(:mongoid) do
131
+ Mongoid::Document::ClassMethods.include(Ahoy::Model)
137
132
  end
@@ -27,6 +27,11 @@ module Ahoy
27
27
  end
28
28
  end
29
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
+
30
35
  # use connection_config instead of connection.adapter
31
36
  # so database connection isn't needed
32
37
  def adapter
@@ -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
@@ -19,7 +19,7 @@ end
19
19
  # set to true for JavaScript tracking
20
20
  Ahoy.api = false
21
21
 
22
- # set to true for geocoding
23
- # we recommend configuring local geocoding first
22
+ # set to true for geocoding (and add the geocoder gem to your Gemfile)
23
+ # we recommend configuring local geocoding as well
24
24
  # see https://github.com/ankane/ahoy#geocoding
25
25
  Ahoy.geocode = false
@@ -4,7 +4,7 @@ end
4
4
  # set to true for JavaScript tracking
5
5
  Ahoy.api = false
6
6
 
7
- # set to true for geocoding
8
- # we recommend configuring local geocoding first
7
+ # set to true for geocoding (and add the geocoder gem to your Gemfile)
8
+ # we recommend configuring local geocoding as well
9
9
  # see https://github.com/ankane/ahoy#geocoding
10
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.2.0
4
+ version: 4.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-02 00:00:00.000000000 Z
11
+ date: 2021-11-06 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
@@ -120,14 +106,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
120
106
  requirements:
121
107
  - - ">="
122
108
  - !ruby/object:Gem::Version
123
- version: '2.4'
109
+ version: '2.6'
124
110
  required_rubygems_version: !ruby/object:Gem::Requirement
125
111
  requirements:
126
112
  - - ">="
127
113
  - !ruby/object:Gem::Version
128
114
  version: '0'
129
115
  requirements: []
130
- rubygems_version: 3.2.3
116
+ rubygems_version: 3.2.22
131
117
  signing_key:
132
118
  specification_version: 4
133
119
  summary: Simple, powerful, first-party analytics for Rails