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 +4 -4
- data/CHANGELOG.md +25 -0
- data/LICENSE.txt +1 -1
- data/README.md +98 -90
- data/app/controllers/ahoy/base_controller.rb +12 -4
- data/app/jobs/ahoy/geocode_v2_job.rb +3 -0
- data/lib/ahoy.rb +4 -8
- data/lib/ahoy/database_store.rb +1 -1
- data/lib/ahoy/query_methods.rb +27 -45
- data/lib/ahoy/tracker.rb +1 -4
- data/lib/ahoy/version.rb +1 -1
- data/lib/generators/ahoy/activerecord_generator.rb +17 -4
- data/lib/generators/ahoy/templates/active_record_event_model.rb.tt +1 -1
- data/lib/generators/ahoy/templates/base_store_initializer.rb.tt +5 -0
- data/lib/generators/ahoy/templates/database_store_initializer.rb.tt +5 -0
- data/vendor/assets/javascripts/ahoy.js +32 -110
- metadata +7 -175
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cea71919e25f8f3b9ce9f3ccea43f8d11ec29d1995b3c5078428a8d4a7948457
|
4
|
+
data.tar.gz: 81316ad1b9fc956bf0ece8c23c63570d5f333e0f7c781406e1b2214e965feebb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
|
@@ -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.
|
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
|
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
|
-
|
313
|
+
You can also [disable cookies](#anonymity-sets--cookies)
|
314
314
|
|
315
|
-
|
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.
|
320
|
+
Ahoy.token_generator = -> { Druuid.gen }
|
319
321
|
```
|
320
322
|
|
321
|
-
|
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
|
-
|
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
|
-
|
337
|
+
### Exceptions
|
328
338
|
|
329
|
-
|
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
|
-
|
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
|
-
|
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:
|
381
|
+
file: "path/to/GeoLite2-City.mmdb"
|
344
382
|
}
|
345
383
|
)
|
346
384
|
```
|
347
385
|
|
348
|
-
|
386
|
+
For country-level geocoding, install the `geoip-database` package. It’s preinstalled on Heroku. For Ubuntu, use:
|
349
387
|
|
350
|
-
|
388
|
+
```sh
|
389
|
+
sudo apt-get install geoip-database
|
390
|
+
```
|
351
391
|
|
352
|
-
|
392
|
+
And create `config/initializers/geocoder.rb` with:
|
353
393
|
|
354
394
|
```ruby
|
355
|
-
|
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
|
-
###
|
404
|
+
### Load Balancer Geocoding
|
359
405
|
|
360
|
-
|
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
|
-
|
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
|
-
|
412
|
+
Update `config/initializers/ahoy.rb` with:
|
375
413
|
|
376
414
|
```ruby
|
377
|
-
|
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
|
-
###
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
756
|
+
```ruby
|
757
|
+
gem 'geocoder'
|
758
|
+
```
|
751
759
|
|
752
|
-
|
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:
|
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 =
|
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
|
-
|
131
|
-
|
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
|
data/lib/ahoy/database_store.rb
CHANGED
data/lib/ahoy/query_methods.rb
CHANGED
@@ -8,7 +8,9 @@ module Ahoy
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def where_props(properties)
|
11
|
-
|
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(
|
22
|
+
relation = where(properties.to_h { |k, v| ["properties.#{k}", v] })
|
21
23
|
when /mysql/
|
22
|
-
|
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
|
-
|
26
|
+
case column_type
|
27
|
+
when :jsonb
|
40
28
|
relation = relation.where("properties @> ?", properties.to_json)
|
41
|
-
|
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
|
61
|
-
|
62
|
-
|
63
|
-
|
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 =
|
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
|
-
|
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
|
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
@@ -17,9 +17,7 @@ module Ahoy
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def properties_type
|
20
|
-
|
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 >=
|
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
|
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
|
@@ -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.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:
|
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@
|
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.
|
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.
|
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
|