ahoy_matey 3.2.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/LICENSE.txt +1 -1
- data/README.md +121 -93
- data/app/controllers/ahoy/base_controller.rb +12 -4
- data/app/jobs/ahoy/geocode_v2_job.rb +3 -0
- data/lib/ahoy/database_store.rb +1 -1
- data/lib/ahoy/engine.rb +7 -0
- data/lib/ahoy/query_methods.rb +32 -63
- data/lib/ahoy/tracker.rb +1 -4
- data/lib/ahoy/version.rb +1 -1
- data/lib/ahoy.rb +3 -8
- data/lib/generators/ahoy/activerecord_generator.rb +5 -0
- data/lib/generators/ahoy/templates/active_record_event_model.rb.tt +1 -1
- data/lib/generators/ahoy/templates/base_store_initializer.rb.tt +2 -2
- data/lib/generators/ahoy/templates/database_store_initializer.rb.tt +2 -2
- data/vendor/assets/javascripts/ahoy.js +32 -110
- metadata +6 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 06e00a470a12c20b510cc9f1476e39c2cfd8ec24115b29d5d09489d091465b74
|
4
|
+
data.tar.gz: e19501a6019c94bafc32121122e2832152adb0fe8475cb3b91ed4d50c791ff6e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
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.
|
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
|
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
|
-
|
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 [
|
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
|
-
|
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
|
-
###
|
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
|
-
|
375
|
-
Ahoy.token_generator = -> { Druuid.gen }
|
376
|
-
```
|
418
|
+
Some load balancers can add geocoding information to request headers.
|
377
419
|
|
378
|
-
|
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
|
-
|
424
|
+
Update `config/initializers/ahoy.rb` with:
|
381
425
|
|
382
426
|
```ruby
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
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 (
|
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
|
-
###
|
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
|
-
|
780
|
+
There are two notable changes to geocoding:
|
733
781
|
|
734
|
-
|
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
|
-
|
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
|
-
|
786
|
+
```ruby
|
787
|
+
gem 'geocoder'
|
788
|
+
```
|
741
789
|
|
742
|
-
|
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,
|
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:
|
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/database_store.rb
CHANGED
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
|
data/lib/ahoy/query_methods.rb
CHANGED
@@ -8,64 +8,40 @@ module Ahoy
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def where_props(properties)
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
16
|
+
where(properties.to_h { |k, v| ["properties.#{k}", v] })
|
21
17
|
when /mysql/
|
22
|
-
|
23
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
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 /
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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 =
|
77
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
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
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 =
|
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
|
-
|
132
|
-
|
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
|
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
|
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
|
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.
|
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(),
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
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-
|
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.
|
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.
|
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
|