ahoy_matey 4.1.0 → 5.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/LICENSE.txt +1 -1
- data/README.md +57 -67
- data/app/controllers/ahoy/events_controller.rb +13 -2
- data/lib/ahoy/base_store.rb +4 -1
- data/lib/ahoy/controller.rb +9 -4
- data/lib/ahoy/database_store.rb +6 -4
- data/lib/ahoy/query_methods.rb +2 -2
- data/lib/ahoy/tracker.rb +15 -19
- data/lib/ahoy/version.rb +1 -1
- data/lib/ahoy.rb +49 -13
- data/lib/ahoy_matey.rb +1 -1
- data/lib/generators/ahoy/activerecord_generator.rb +19 -11
- data/lib/generators/ahoy/templates/active_record_event_model.rb.tt +1 -1
- data/lib/generators/ahoy/templates/active_record_migration.rb.tt +8 -8
- data/lib/generators/ahoy/templates/mongoid_visit_model.rb.tt +1 -0
- data/vendor/assets/javascripts/ahoy.js +36 -29
- metadata +13 -14
- data/app/jobs/ahoy/geocode_job.rb +0 -11
- /data/{app/jobs → lib}/ahoy/geocode_v2_job.rb +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9161dead45561a928523eb7d5ed70be20c67c8629499c35cb428271d714895a7
|
4
|
+
data.tar.gz: f17eb958ee297a4eda86388e0458a04a2a39de5b868076521eb65dd6e4cbc21a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c68fa7030244573a75b72167d163a8a0f2cfb5ef01e3718980ea25e5c13a5936973d0d5f83d3302f9e610508d94758ec0587f6fe4dd19c7c23668181ea1dcfe0
|
7
|
+
data.tar.gz: 7950a3cd2dda3818e21a6da37ab0d1b49b01d22110c76f0ae1abd15abba7aaa6140db24d70cedb4d5af0c5022f2b95a2f7e5a11339425eefb51240bb9ea17578
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,37 @@
|
|
1
|
+
## 5.2.0 (2024-09-04)
|
2
|
+
|
3
|
+
- Improved error handling for invalid API parameters
|
4
|
+
|
5
|
+
## 5.1.0 (2024-03-26)
|
6
|
+
|
7
|
+
- Added support for Trilogy
|
8
|
+
- Updated Ahoy.js to 0.4.4
|
9
|
+
|
10
|
+
## 5.0.2 (2023-10-05)
|
11
|
+
|
12
|
+
- Excluded visits from Rails health check
|
13
|
+
|
14
|
+
## 5.0.1 (2023-10-01)
|
15
|
+
|
16
|
+
- Fixed error with geocoding with anonymity sets
|
17
|
+
|
18
|
+
## 5.0.0 (2023-10-01)
|
19
|
+
|
20
|
+
- Changed visits to expire with anonymity sets
|
21
|
+
- Fixed error when Active Job is not available
|
22
|
+
- Fixed deprecation warning with Rails 7.1
|
23
|
+
- Dropped support for Ruby < 3 and Rails < 6.1
|
24
|
+
- Dropped support for Mongoid 6
|
25
|
+
|
26
|
+
## 4.2.1 (2023-02-23)
|
27
|
+
|
28
|
+
- Updated Ahoy.js to 0.4.2
|
29
|
+
|
30
|
+
## 4.2.0 (2023-02-07)
|
31
|
+
|
32
|
+
- Added primary key type to generated migration
|
33
|
+
- Updated Ahoy.js to 0.4.1
|
34
|
+
|
1
35
|
## 4.1.0 (2022-06-12)
|
2
36
|
|
3
37
|
- Ensure `exclude_method` is only called once per request
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -4,13 +4,13 @@
|
|
4
4
|
|
5
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
6
|
|
7
|
-
**Ahoy
|
7
|
+
**Ahoy 5.0 was recently released** - see [how to upgrade](#upgrading)
|
8
8
|
|
9
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
|
10
10
|
|
11
11
|
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
|
12
12
|
|
13
|
-
[![Build Status](https://github.com/ankane/ahoy/workflows/build/badge.svg
|
13
|
+
[![Build Status](https://github.com/ankane/ahoy/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/ahoy/actions)
|
14
14
|
|
15
15
|
## Installation
|
16
16
|
|
@@ -48,7 +48,7 @@ And restart your web server.
|
|
48
48
|
|
49
49
|
### JavaScript
|
50
50
|
|
51
|
-
For Rails 7
|
51
|
+
For Importmap (Rails 7 default), add to `config/importmap.rb`:
|
52
52
|
|
53
53
|
```ruby
|
54
54
|
pin "ahoy", to: "ahoy.js"
|
@@ -60,7 +60,7 @@ And add to `app/javascript/application.js`:
|
|
60
60
|
import "ahoy"
|
61
61
|
```
|
62
62
|
|
63
|
-
For Rails 6
|
63
|
+
For Webpacker (Rails 6 default), run:
|
64
64
|
|
65
65
|
```sh
|
66
66
|
yarn add ahoy.js
|
@@ -72,7 +72,7 @@ And add to `app/javascript/packs/application.js`:
|
|
72
72
|
import ahoy from "ahoy.js"
|
73
73
|
```
|
74
74
|
|
75
|
-
For
|
75
|
+
For Sprockets, add to `app/assets/javascripts/application.js`:
|
76
76
|
|
77
77
|
```javascript
|
78
78
|
//= require ahoy
|
@@ -197,7 +197,7 @@ Order.joins(:ahoy_visit).group("device_type").count
|
|
197
197
|
Here’s what the migration to add the `ahoy_visit_id` column should look like:
|
198
198
|
|
199
199
|
```ruby
|
200
|
-
class AddAhoyVisitToOrders < ActiveRecord::Migration[7.
|
200
|
+
class AddAhoyVisitToOrders < ActiveRecord::Migration[7.2]
|
201
201
|
def change
|
202
202
|
add_reference :orders, :ahoy_visit
|
203
203
|
end
|
@@ -212,7 +212,7 @@ visitable :sign_up_visit
|
|
212
212
|
|
213
213
|
### Users
|
214
214
|
|
215
|
-
Ahoy automatically attaches the `current_user` to the visit. With [Devise](https://github.com/
|
215
|
+
Ahoy automatically attaches the `current_user` to the visit. With [Devise](https://github.com/heartcombo/devise), it attaches the user even if they sign in after the visit starts.
|
216
216
|
|
217
217
|
With other authentication frameworks, add this to the end of your sign in method:
|
218
218
|
|
@@ -262,28 +262,6 @@ class ApplicationController < ActionController::Base
|
|
262
262
|
end
|
263
263
|
```
|
264
264
|
|
265
|
-
#### Knock
|
266
|
-
|
267
|
-
To attach the user with [Knock](https://github.com/nsarno/knock), either include `Knock::Authenticable`in `ApplicationController`:
|
268
|
-
|
269
|
-
```ruby
|
270
|
-
class ApplicationController < ActionController::API
|
271
|
-
include Knock::Authenticable
|
272
|
-
end
|
273
|
-
```
|
274
|
-
|
275
|
-
Or include it in Ahoy:
|
276
|
-
|
277
|
-
```ruby
|
278
|
-
Ahoy::BaseController.include Knock::Authenticable
|
279
|
-
```
|
280
|
-
|
281
|
-
And use:
|
282
|
-
|
283
|
-
```ruby
|
284
|
-
Ahoy.user_method = ->(controller) { controller.send(:authenticate_entity, "user") }
|
285
|
-
```
|
286
|
-
|
287
265
|
### Exclusions
|
288
266
|
|
289
267
|
Bots are excluded from tracking by default. To include them, use:
|
@@ -308,6 +286,14 @@ By default, a new visit is created after 4 hours of inactivity. Change this with
|
|
308
286
|
Ahoy.visit_duration = 30.minutes
|
309
287
|
```
|
310
288
|
|
289
|
+
### Visitor Duration
|
290
|
+
|
291
|
+
By default, a new `visitor_token` is generated after 2 years. Change this with:
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
Ahoy.visitor_duration = 30.days
|
295
|
+
```
|
296
|
+
|
311
297
|
### Cookies
|
312
298
|
|
313
299
|
To track visits across multiple subdomains, use:
|
@@ -326,15 +312,15 @@ You can also [disable cookies](#anonymity-sets--cookies)
|
|
326
312
|
|
327
313
|
### Token Generation
|
328
314
|
|
329
|
-
Ahoy uses random UUIDs for visit and visitor tokens by default, but you can use your own generator like [
|
315
|
+
Ahoy uses random UUIDs for visit and visitor tokens by default, but you can use your own generator like [ULID](https://github.com/rafaelsales/ulid).
|
330
316
|
|
331
317
|
```ruby
|
332
|
-
Ahoy.token_generator = -> {
|
318
|
+
Ahoy.token_generator = -> { ULID.generate }
|
333
319
|
```
|
334
320
|
|
335
321
|
### Throttling
|
336
322
|
|
337
|
-
You can use [Rack::Attack](https://github.com/
|
323
|
+
You can use [Rack::Attack](https://github.com/rack/rack-attack) to throttle requests to the API.
|
338
324
|
|
339
325
|
```ruby
|
340
326
|
class Rack::Attack
|
@@ -378,13 +364,17 @@ Ahoy.job_queue = :low_priority
|
|
378
364
|
|
379
365
|
### Local Geocoding
|
380
366
|
|
381
|
-
For privacy and performance, we recommend geocoding locally.
|
367
|
+
For privacy and performance, we recommend geocoding locally.
|
368
|
+
|
369
|
+
For city-level geocoding, download the [GeoLite2 City database](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data).
|
370
|
+
|
371
|
+
Add this line to your application’s Gemfile:
|
382
372
|
|
383
373
|
```ruby
|
384
374
|
gem "maxminddb"
|
385
375
|
```
|
386
376
|
|
387
|
-
|
377
|
+
And create `config/initializers/geocoder.rb` with:
|
388
378
|
|
389
379
|
```ruby
|
390
380
|
Geocoder.configure(
|
@@ -401,6 +391,12 @@ For country-level geocoding, install the `geoip-database` package. It’s preins
|
|
401
391
|
sudo apt-get install geoip-database
|
402
392
|
```
|
403
393
|
|
394
|
+
Add this line to your application’s Gemfile:
|
395
|
+
|
396
|
+
```ruby
|
397
|
+
gem "geoip"
|
398
|
+
```
|
399
|
+
|
404
400
|
And create `config/initializers/geocoder.rb` with:
|
405
401
|
|
406
402
|
```ruby
|
@@ -450,7 +446,7 @@ class Ahoy::Store < Ahoy::DatabaseStore
|
|
450
446
|
end
|
451
447
|
|
452
448
|
Ahoy.mask_ips = true
|
453
|
-
Ahoy.cookies =
|
449
|
+
Ahoy.cookies = :none
|
454
450
|
```
|
455
451
|
|
456
452
|
This:
|
@@ -488,20 +484,20 @@ end
|
|
488
484
|
|
489
485
|
### Anonymity Sets & Cookies
|
490
486
|
|
491
|
-
Ahoy can switch from cookies to [anonymity sets](https://privacypatterns.org/patterns/Anonymity-set). Instead of cookies, visitors with the same IP mask and user agent are grouped together in an
|
487
|
+
Ahoy can switch from cookies to [anonymity sets](https://privacypatterns.org/patterns/Anonymity-set). Instead of cookies, visitors with the same IP mask and user agent are grouped together in an anonymity set.
|
492
488
|
|
493
489
|
```ruby
|
494
|
-
Ahoy.cookies =
|
490
|
+
Ahoy.cookies = :none
|
495
491
|
```
|
496
492
|
|
493
|
+
Note: If Ahoy was installed before v5, [add an index](#50) before making this change.
|
494
|
+
|
497
495
|
Previously set cookies are automatically deleted. If you use JavaScript tracking, also set:
|
498
496
|
|
499
497
|
```javascript
|
500
498
|
ahoy.configure({cookies: false});
|
501
499
|
```
|
502
500
|
|
503
|
-
Note: With anonymity sets, visits no longer expire after 4 hours of inactivity. A new visit is only created when the IP mask or user agent changes (for instance, when a user updates their browser). There are plans to address this in the next major version.
|
504
|
-
|
505
501
|
## Data Retention
|
506
502
|
|
507
503
|
Data should only be retained for as long as it’s needed. Delete older data with:
|
@@ -639,7 +635,7 @@ end
|
|
639
635
|
|
640
636
|
[Blazer](https://github.com/ankane/blazer) is a great tool for exploring your data.
|
641
637
|
|
642
|
-
With
|
638
|
+
With Active Record, you can do:
|
643
639
|
|
644
640
|
```ruby
|
645
641
|
Ahoy::Visit.group(:search_keyword).count
|
@@ -777,19 +773,29 @@ Send a `POST` request to `/ahoy/events` with `Content-Type: application/json` an
|
|
777
773
|
|
778
774
|
## Upgrading
|
779
775
|
|
780
|
-
###
|
776
|
+
### 5.0
|
781
777
|
|
782
|
-
|
778
|
+
Visits now expire with anonymity sets. If using `Ahoy.cookies = false`, a new index is needed.
|
783
779
|
|
784
|
-
|
780
|
+
For Active Record, create a migration with:
|
785
781
|
|
786
|
-
|
782
|
+
```ruby
|
783
|
+
add_index :ahoy_visits, [:visitor_token, :started_at]
|
784
|
+
```
|
787
785
|
|
788
|
-
|
789
|
-
gem "geocoder"
|
790
|
-
```
|
786
|
+
For Mongoid, set:
|
791
787
|
|
792
|
-
|
788
|
+
```ruby
|
789
|
+
class Ahoy::Visit
|
790
|
+
index({visitor_token: 1, started_at: 1})
|
791
|
+
end
|
792
|
+
```
|
793
|
+
|
794
|
+
Create the index before upgrading, and set:
|
795
|
+
|
796
|
+
```ruby
|
797
|
+
Ahoy.cookies = :none
|
798
|
+
```
|
793
799
|
|
794
800
|
## History
|
795
801
|
|
@@ -813,26 +819,10 @@ bundle install
|
|
813
819
|
bundle exec rake test
|
814
820
|
```
|
815
821
|
|
816
|
-
To test
|
822
|
+
To test different adapters, use:
|
817
823
|
|
818
824
|
```sh
|
825
|
+
ADAPTER=postgresql bundle exec rake test
|
826
|
+
ADAPTER=mysql2 bundle exec rake test
|
819
827
|
ADAPTER=mongoid bundle exec rake test
|
820
828
|
```
|
821
|
-
|
822
|
-
To test query methods, use:
|
823
|
-
|
824
|
-
```sh
|
825
|
-
# Postgres
|
826
|
-
createdb ahoy_test
|
827
|
-
bundle exec rake test:query_methods:postgresql
|
828
|
-
|
829
|
-
# SQLite
|
830
|
-
bundle exec rake test:query_methods:sqlite
|
831
|
-
|
832
|
-
# MySQL and MariaDB
|
833
|
-
mysqladmin create ahoy_test
|
834
|
-
bundle exec rake test:query_methods:mysql
|
835
|
-
|
836
|
-
# MongoDB
|
837
|
-
bundle exec rake test:query_methods:mongoid
|
838
|
-
```
|
@@ -17,12 +17,23 @@ module Ahoy
|
|
17
17
|
begin
|
18
18
|
ActiveSupport::JSON.decode(data)
|
19
19
|
rescue ActiveSupport::JSON.parse_error
|
20
|
-
#
|
20
|
+
# TODO change to nil in Ahoy 6
|
21
21
|
[]
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
-
|
25
|
+
max_events_per_request = Ahoy.max_events_per_request
|
26
|
+
|
27
|
+
# check before creating any events
|
28
|
+
unless events.is_a?(Array) && events.first(max_events_per_request).all? { |v| v.is_a?(Hash) }
|
29
|
+
logger.info "[ahoy] Invalid parameters"
|
30
|
+
# :unprocessable_entity is probably more correct
|
31
|
+
# but keep consistent with missing parameters for now
|
32
|
+
render plain: "Invalid parameters\n", status: :bad_request
|
33
|
+
return
|
34
|
+
end
|
35
|
+
|
36
|
+
events.first(max_events_per_request).each do |event|
|
26
37
|
time = Time.zone.parse(event["time"]) rescue nil
|
27
38
|
|
28
39
|
# timestamp is deprecated
|
data/lib/ahoy/base_store.rb
CHANGED
@@ -3,6 +3,7 @@ module Ahoy
|
|
3
3
|
attr_writer :user
|
4
4
|
|
5
5
|
def initialize(options)
|
6
|
+
@user = options[:user]
|
6
7
|
@options = options
|
7
8
|
end
|
8
9
|
|
@@ -36,7 +37,9 @@ module Ahoy
|
|
36
37
|
end
|
37
38
|
|
38
39
|
def exclude?
|
39
|
-
(!Ahoy.track_bots && bot?) ||
|
40
|
+
(!Ahoy.track_bots && bot?) ||
|
41
|
+
exclude_by_method? ||
|
42
|
+
(defined?(Rails::HealthController) && controller.is_a?(Rails::HealthController))
|
40
43
|
end
|
41
44
|
|
42
45
|
def generate_id
|
data/lib/ahoy/controller.rb
CHANGED
@@ -5,9 +5,8 @@ module Ahoy
|
|
5
5
|
base.helper_method :current_visit
|
6
6
|
base.helper_method :ahoy
|
7
7
|
end
|
8
|
-
base.before_action :set_ahoy_cookies, unless: -> { Ahoy.api_only }
|
9
8
|
base.before_action :track_ahoy_visit, unless: -> { Ahoy.api_only }
|
10
|
-
base.around_action :set_ahoy_request_store
|
9
|
+
base.around_action :set_ahoy_request_store, unless: -> { Ahoy.api_only }
|
11
10
|
end
|
12
11
|
|
13
12
|
def ahoy
|
@@ -19,7 +18,7 @@ module Ahoy
|
|
19
18
|
end
|
20
19
|
|
21
20
|
def set_ahoy_cookies
|
22
|
-
if Ahoy.cookies
|
21
|
+
if Ahoy.cookies?
|
23
22
|
ahoy.set_visitor_cookie
|
24
23
|
ahoy.set_visit_cookie
|
25
24
|
else
|
@@ -31,11 +30,17 @@ module Ahoy
|
|
31
30
|
def track_ahoy_visit
|
32
31
|
defer = Ahoy.server_side_visits != true
|
33
32
|
|
34
|
-
if defer && !Ahoy.cookies
|
33
|
+
if defer && !Ahoy.cookies?
|
35
34
|
# avoid calling new_visit?, which triggers a database call
|
35
|
+
elsif !Ahoy.cookies? && ahoy.exclude?
|
36
|
+
# avoid calling new_visit?, which triggers a database call
|
37
|
+
# may or may not be a new visit
|
38
|
+
Ahoy.log("Request excluded")
|
36
39
|
elsif ahoy.new_visit?
|
37
40
|
ahoy.track_visit(defer: defer)
|
38
41
|
end
|
42
|
+
|
43
|
+
set_ahoy_cookies
|
39
44
|
end
|
40
45
|
|
41
46
|
def set_ahoy_request_store
|
data/lib/ahoy/database_store.rb
CHANGED
@@ -53,11 +53,13 @@ module Ahoy
|
|
53
53
|
|
54
54
|
def visit
|
55
55
|
unless defined?(@visit)
|
56
|
-
if
|
57
|
-
# find_by raises error by default when not found
|
58
|
-
@visit = visit_model.where(visit_token: ahoy.visit_token).
|
56
|
+
if ahoy.send(:existing_visit_token) || ahoy.instance_variable_get(:@visit_token)
|
57
|
+
# find_by raises error by default with Mongoid when not found
|
58
|
+
@visit = visit_model.where(visit_token: ahoy.visit_token).take if ahoy.visit_token
|
59
|
+
elsif !Ahoy.cookies? && ahoy.visitor_token
|
60
|
+
@visit = visit_model.where(visitor_token: ahoy.visitor_token).where(started_at: Ahoy.visit_duration.ago..).order(started_at: :desc).first
|
59
61
|
else
|
60
|
-
@visit =
|
62
|
+
@visit = nil
|
61
63
|
end
|
62
64
|
end
|
63
65
|
@visit
|
data/lib/ahoy/query_methods.rb
CHANGED
@@ -14,7 +14,7 @@ module Ahoy
|
|
14
14
|
case adapter_name
|
15
15
|
when "mongoid"
|
16
16
|
where(properties.to_h { |k, v| ["properties.#{k}", v] })
|
17
|
-
when /mysql/
|
17
|
+
when /mysql|trilogy/
|
18
18
|
where("JSON_CONTAINS(properties, ?, '$') = 1", properties.to_json)
|
19
19
|
when /postgres|postgis/
|
20
20
|
case columns_hash["properties"].type
|
@@ -54,7 +54,7 @@ module Ahoy
|
|
54
54
|
case adapter_name
|
55
55
|
when "mongoid"
|
56
56
|
raise "Adapter not supported: #{adapter_name}"
|
57
|
-
when /mysql/
|
57
|
+
when /mysql|trilogy/
|
58
58
|
props.each do |prop|
|
59
59
|
quoted_prop = connection.quote("$.#{prop}")
|
60
60
|
relation = relation.group("JSON_UNQUOTE(JSON_EXTRACT(properties, #{quoted_prop}))")
|
data/lib/ahoy/tracker.rb
CHANGED
@@ -49,7 +49,7 @@ module Ahoy
|
|
49
49
|
visit_token: visit_token,
|
50
50
|
visitor_token: visitor_token,
|
51
51
|
user_id: user.try(:id),
|
52
|
-
started_at: trusted_time(started_at)
|
52
|
+
started_at: trusted_time(started_at)
|
53
53
|
}.merge(visit_properties).select { |_, v| v }
|
54
54
|
|
55
55
|
@store.track_visit(data)
|
@@ -99,7 +99,7 @@ module Ahoy
|
|
99
99
|
end
|
100
100
|
|
101
101
|
def new_visit?
|
102
|
-
Ahoy.cookies ? !existing_visit_token : visit.nil?
|
102
|
+
Ahoy.cookies? ? !existing_visit_token : visit.nil?
|
103
103
|
end
|
104
104
|
|
105
105
|
def new_visitor?
|
@@ -145,6 +145,13 @@ module Ahoy
|
|
145
145
|
delete_cookie("ahoy_track")
|
146
146
|
end
|
147
147
|
|
148
|
+
def exclude?
|
149
|
+
unless defined?(@exclude)
|
150
|
+
@exclude = @store.exclude?
|
151
|
+
end
|
152
|
+
@exclude
|
153
|
+
end
|
154
|
+
|
148
155
|
protected
|
149
156
|
|
150
157
|
def api?
|
@@ -153,7 +160,7 @@ module Ahoy
|
|
153
160
|
|
154
161
|
# private, but used by API
|
155
162
|
def missing_params?
|
156
|
-
if Ahoy.cookies && api? && Ahoy.protect_from_forgery
|
163
|
+
if Ahoy.cookies? && api? && Ahoy.protect_from_forgery
|
157
164
|
!(existing_visit_token && existing_visitor_token)
|
158
165
|
else
|
159
166
|
false
|
@@ -162,7 +169,7 @@ module Ahoy
|
|
162
169
|
|
163
170
|
def set_cookie(name, value, duration = nil, use_domain = true)
|
164
171
|
# safety net
|
165
|
-
return unless Ahoy.cookies && request
|
172
|
+
return unless Ahoy.cookies? && request
|
166
173
|
|
167
174
|
cookie = Ahoy.cookie_options.merge(value: value)
|
168
175
|
cookie[:expires] = duration.from_now if duration
|
@@ -184,13 +191,6 @@ module Ahoy
|
|
184
191
|
end
|
185
192
|
end
|
186
193
|
|
187
|
-
def exclude?
|
188
|
-
unless defined?(@exclude)
|
189
|
-
@exclude = @store.exclude?
|
190
|
-
end
|
191
|
-
@exclude
|
192
|
-
end
|
193
|
-
|
194
194
|
def report_exception(e)
|
195
195
|
if defined?(ActionDispatch::RemoteIp::IpSpoofAttackError) && e.is_a?(ActionDispatch::RemoteIp::IpSpoofAttackError)
|
196
196
|
debug "Tracking excluded due to IP spoofing"
|
@@ -207,7 +207,7 @@ module Ahoy
|
|
207
207
|
def visit_token_helper
|
208
208
|
@visit_token_helper ||= begin
|
209
209
|
token = existing_visit_token
|
210
|
-
token ||=
|
210
|
+
token ||= visit&.visit_token unless Ahoy.cookies?
|
211
211
|
token ||= generate_id unless Ahoy.api_only
|
212
212
|
token
|
213
213
|
end
|
@@ -216,7 +216,7 @@ module Ahoy
|
|
216
216
|
def visitor_token_helper
|
217
217
|
@visitor_token_helper ||= begin
|
218
218
|
token = existing_visitor_token
|
219
|
-
token ||= visitor_anonymity_set unless Ahoy.cookies
|
219
|
+
token ||= visitor_anonymity_set unless Ahoy.cookies?
|
220
220
|
token ||= generate_id unless Ahoy.api_only
|
221
221
|
token
|
222
222
|
end
|
@@ -225,7 +225,7 @@ module Ahoy
|
|
225
225
|
def existing_visit_token
|
226
226
|
@existing_visit_token ||= begin
|
227
227
|
token = visit_header
|
228
|
-
token ||= visit_cookie if Ahoy.cookies && !(api? && Ahoy.protect_from_forgery)
|
228
|
+
token ||= visit_cookie if Ahoy.cookies? && !(api? && Ahoy.protect_from_forgery)
|
229
229
|
token ||= visit_param if api?
|
230
230
|
token
|
231
231
|
end
|
@@ -234,16 +234,12 @@ module Ahoy
|
|
234
234
|
def existing_visitor_token
|
235
235
|
@existing_visitor_token ||= begin
|
236
236
|
token = visitor_header
|
237
|
-
token ||= visitor_cookie if Ahoy.cookies && !(api? && Ahoy.protect_from_forgery)
|
237
|
+
token ||= visitor_cookie if Ahoy.cookies? && !(api? && Ahoy.protect_from_forgery)
|
238
238
|
token ||= visitor_param if api?
|
239
239
|
token
|
240
240
|
end
|
241
241
|
end
|
242
242
|
|
243
|
-
def visit_anonymity_set
|
244
|
-
@visit_anonymity_set ||= Digest::UUID.uuid_v5(UUID_NAMESPACE, ["visit", Ahoy.mask_ip(request.remote_ip), request.user_agent].join("/"))
|
245
|
-
end
|
246
|
-
|
247
243
|
def visitor_anonymity_set
|
248
244
|
@visitor_anonymity_set ||= Digest::UUID.uuid_v5(UUID_NAMESPACE, ["visitor", Ahoy.mask_ip(request.remote_ip), request.user_agent].join("/"))
|
249
245
|
end
|
data/lib/ahoy/version.rb
CHANGED
data/lib/ahoy.rb
CHANGED
@@ -7,27 +7,59 @@ require "active_support/core_ext"
|
|
7
7
|
require "safely/core"
|
8
8
|
|
9
9
|
# modules
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
10
|
+
require_relative "ahoy/utils"
|
11
|
+
require_relative "ahoy/base_store"
|
12
|
+
require_relative "ahoy/controller"
|
13
|
+
require_relative "ahoy/database_store"
|
14
|
+
require_relative "ahoy/helper"
|
15
|
+
require_relative "ahoy/model"
|
16
|
+
require_relative "ahoy/query_methods"
|
17
|
+
require_relative "ahoy/tracker"
|
18
|
+
require_relative "ahoy/version"
|
19
|
+
require_relative "ahoy/visit_properties"
|
20
|
+
|
21
|
+
require_relative "ahoy/engine" if defined?(Rails)
|
22
22
|
|
23
23
|
module Ahoy
|
24
|
+
# activejob optional
|
25
|
+
autoload :GeocodeV2Job, "ahoy/geocode_v2_job"
|
26
|
+
|
24
27
|
mattr_accessor :visit_duration
|
25
28
|
self.visit_duration = 4.hours
|
26
29
|
|
27
30
|
mattr_accessor :visitor_duration
|
28
31
|
self.visitor_duration = 2.years
|
29
32
|
|
30
|
-
|
33
|
+
def self.cookies=(value)
|
34
|
+
if value == false
|
35
|
+
if defined?(Mongoid::Document) && defined?(Ahoy::Visit) && Ahoy::Visit < Mongoid::Document
|
36
|
+
raise <<~EOS
|
37
|
+
This feature requires a new index in Ahoy 5. Set:
|
38
|
+
|
39
|
+
class Ahoy::Visit
|
40
|
+
index({visitor_token: 1, started_at: 1})
|
41
|
+
end
|
42
|
+
|
43
|
+
Create the index before upgrading, and set:
|
44
|
+
|
45
|
+
Ahoy.cookies = :none
|
46
|
+
EOS
|
47
|
+
else
|
48
|
+
raise <<~EOS
|
49
|
+
This feature requires a new index in Ahoy 5. Create a migration with:
|
50
|
+
|
51
|
+
add_index :ahoy_visits, [:visitor_token, :started_at]
|
52
|
+
|
53
|
+
Run it before upgrading, and set:
|
54
|
+
|
55
|
+
Ahoy.cookies = :none
|
56
|
+
EOS
|
57
|
+
end
|
58
|
+
end
|
59
|
+
@@cookies = value
|
60
|
+
end
|
61
|
+
|
62
|
+
mattr_reader :cookies
|
31
63
|
self.cookies = true
|
32
64
|
|
33
65
|
# TODO deprecate in favor of cookie_options
|
@@ -94,6 +126,10 @@ module Ahoy
|
|
94
126
|
logger.info { "[ahoy] #{message}" } if logger
|
95
127
|
end
|
96
128
|
|
129
|
+
def self.cookies?
|
130
|
+
cookies && cookies != :none
|
131
|
+
end
|
132
|
+
|
97
133
|
def self.mask_ip(ip)
|
98
134
|
addr = IPAddr.new(ip)
|
99
135
|
if addr.ipv4?
|
data/lib/ahoy_matey.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
require_relative "ahoy"
|
@@ -21,7 +21,7 @@ module Ahoy
|
|
21
21
|
case adapter
|
22
22
|
when /postg/i # postgres, postgis
|
23
23
|
"jsonb"
|
24
|
-
when /mysql/i
|
24
|
+
when /mysql|trilogy/i
|
25
25
|
"json"
|
26
26
|
else
|
27
27
|
"text"
|
@@ -33,23 +33,31 @@ module Ahoy
|
|
33
33
|
properties_type == "text" || (properties_type == "json" && ActiveRecord::Base.connection.try(:mariadb?))
|
34
34
|
end
|
35
35
|
|
36
|
-
|
37
|
-
|
38
|
-
def adapter
|
39
|
-
if ActiveRecord::VERSION::STRING.to_f >= 6.1
|
40
|
-
ActiveRecord::Base.connection_db_config.adapter.to_s
|
41
|
-
else
|
42
|
-
ActiveRecord::Base.connection_config[:adapter].to_s
|
43
|
-
end
|
36
|
+
def serialize_options
|
37
|
+
ActiveRecord::VERSION::STRING.to_f >= 7.1 ? "coder: JSON" : "JSON"
|
44
38
|
end
|
45
39
|
|
46
|
-
|
47
|
-
|
40
|
+
# use connection_db_config instead of connection.adapter
|
41
|
+
# so database connection isn't needed
|
42
|
+
def adapter
|
43
|
+
ActiveRecord::Base.connection_db_config.adapter.to_s
|
48
44
|
end
|
49
45
|
|
50
46
|
def migration_version
|
51
47
|
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
52
48
|
end
|
49
|
+
|
50
|
+
def primary_key_type
|
51
|
+
", id: :#{key_type}" if key_type
|
52
|
+
end
|
53
|
+
|
54
|
+
def foreign_key_type
|
55
|
+
", type: :#{key_type}" if key_type
|
56
|
+
end
|
57
|
+
|
58
|
+
def key_type
|
59
|
+
Rails.configuration.generators.options.dig(:active_record, :primary_key_type)
|
60
|
+
end
|
53
61
|
end
|
54
62
|
end
|
55
63
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
|
2
2
|
def change
|
3
|
-
create_table :ahoy_visits do |t|
|
3
|
+
create_table :ahoy_visits<%= primary_key_type %> do |t|
|
4
4
|
t.string :visit_token
|
5
5
|
t.string :visitor_token
|
6
6
|
|
@@ -8,7 +8,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
|
|
8
8
|
# simply remove any you don't want
|
9
9
|
|
10
10
|
# user
|
11
|
-
t.references :user
|
11
|
+
t.references :user<%= foreign_key_type %>
|
12
12
|
|
13
13
|
# standard
|
14
14
|
t.string :ip
|
@@ -45,18 +45,18 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
|
|
45
45
|
end
|
46
46
|
|
47
47
|
add_index :ahoy_visits, :visit_token, unique: true
|
48
|
+
add_index :ahoy_visits, [:visitor_token, :started_at]
|
48
49
|
|
49
|
-
create_table :ahoy_events do |t|
|
50
|
-
t.references :visit
|
51
|
-
t.references :user
|
50
|
+
create_table :ahoy_events<%= primary_key_type %> do |t|
|
51
|
+
t.references :visit<%= foreign_key_type %>
|
52
|
+
t.references :user<%= foreign_key_type %>
|
52
53
|
|
53
54
|
t.string :name
|
54
55
|
t.<%= properties_type %> :properties
|
55
56
|
t.datetime :time
|
56
57
|
end
|
57
58
|
|
58
|
-
add_index :ahoy_events, [:name, :time]<% if properties_type == "jsonb"
|
59
|
-
add_index :ahoy_events, :properties, using: :gin, opclass: :jsonb_path_ops<%
|
60
|
-
add_index :ahoy_events, "properties jsonb_path_ops", using: "gin"<% end %><% end %>
|
59
|
+
add_index :ahoy_events, [:name, :time]<% if properties_type == "jsonb" %>
|
60
|
+
add_index :ahoy_events, :properties, using: :gin, opclass: :jsonb_path_ops<% end %>
|
61
61
|
end
|
62
62
|
end
|
@@ -1,16 +1,15 @@
|
|
1
1
|
/*!
|
2
|
-
* Ahoy.js
|
2
|
+
* Ahoy.js v0.4.4
|
3
3
|
* Simple, powerful JavaScript analytics
|
4
4
|
* https://github.com/ankane/ahoy.js
|
5
|
-
* v0.4.0
|
6
5
|
* MIT License
|
7
6
|
*/
|
8
7
|
|
9
8
|
(function (global, factory) {
|
10
9
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
11
10
|
typeof define === 'function' && define.amd ? define(factory) :
|
12
|
-
(global = global || self, global.ahoy = factory());
|
13
|
-
}(this, (function () { 'use strict';
|
11
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ahoy = factory());
|
12
|
+
})(this, (function () { 'use strict';
|
14
13
|
|
15
14
|
// https://www.quirksmode.org/js/cookies.html
|
16
15
|
|
@@ -67,7 +66,7 @@
|
|
67
66
|
|
68
67
|
ahoy.configure = function (options) {
|
69
68
|
for (var key in options) {
|
70
|
-
if (
|
69
|
+
if (Object.prototype.hasOwnProperty.call(options, key)) {
|
71
70
|
config[key] = options[key];
|
72
71
|
}
|
73
72
|
}
|
@@ -102,7 +101,7 @@
|
|
102
101
|
function serialize(object) {
|
103
102
|
var data = new FormData();
|
104
103
|
for (var key in object) {
|
105
|
-
if (
|
104
|
+
if (Object.prototype.hasOwnProperty.call(object, key)) {
|
106
105
|
data.append(key, object[key]);
|
107
106
|
}
|
108
107
|
}
|
@@ -170,6 +169,9 @@
|
|
170
169
|
document.addEventListener(eventName, function (e) {
|
171
170
|
var matchedElement = matchesSelector(e.target, selector);
|
172
171
|
if (matchedElement) {
|
172
|
+
var skip = getClosest(matchedElement, "data-ahoy-skip");
|
173
|
+
if (skip !== null && skip !== "false") { return; }
|
174
|
+
|
173
175
|
callback.call(matchedElement, e);
|
174
176
|
}
|
175
177
|
});
|
@@ -186,8 +188,13 @@
|
|
186
188
|
|
187
189
|
// https://stackoverflow.com/a/2117523/1177228
|
188
190
|
function generateId() {
|
189
|
-
|
190
|
-
|
191
|
+
if (window.crypto && window.crypto.randomUUID) {
|
192
|
+
return window.crypto.randomUUID();
|
193
|
+
}
|
194
|
+
|
195
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
196
|
+
var r = Math.random() * 16 | 0;
|
197
|
+
var v = c === 'x' ? r : (r & 0x3 | 0x8);
|
191
198
|
return v.toString(16);
|
192
199
|
});
|
193
200
|
}
|
@@ -237,11 +244,11 @@
|
|
237
244
|
xhr.withCredentials = config.withCredentials;
|
238
245
|
xhr.setRequestHeader("Content-Type", "application/json");
|
239
246
|
for (var header in config.headers) {
|
240
|
-
if (
|
247
|
+
if (Object.prototype.hasOwnProperty.call(config.headers, header)) {
|
241
248
|
xhr.setRequestHeader(header, config.headers[header]);
|
242
249
|
}
|
243
250
|
}
|
244
|
-
xhr.onload = function() {
|
251
|
+
xhr.onload = function () {
|
245
252
|
if (xhr.status === 200) {
|
246
253
|
success();
|
247
254
|
}
|
@@ -266,11 +273,11 @@
|
|
266
273
|
}
|
267
274
|
|
268
275
|
function trackEvent(event) {
|
269
|
-
ahoy.ready(
|
270
|
-
sendRequest(eventsUrl(), eventData(event), function() {
|
276
|
+
ahoy.ready(function () {
|
277
|
+
sendRequest(eventsUrl(), eventData(event), function () {
|
271
278
|
// remove from queue
|
272
279
|
for (var i = 0; i < eventQueue.length; i++) {
|
273
|
-
if (eventQueue[i].id
|
280
|
+
if (eventQueue[i].id === event.id) {
|
274
281
|
eventQueue.splice(i, 1);
|
275
282
|
break;
|
276
283
|
}
|
@@ -281,7 +288,7 @@
|
|
281
288
|
}
|
282
289
|
|
283
290
|
function trackEventNow(event) {
|
284
|
-
ahoy.ready(
|
291
|
+
ahoy.ready(function () {
|
285
292
|
var data = eventData(event);
|
286
293
|
var param = csrfParam();
|
287
294
|
var token = csrfToken();
|
@@ -303,7 +310,7 @@
|
|
303
310
|
|
304
311
|
function cleanObject(obj) {
|
305
312
|
for (var key in obj) {
|
306
|
-
if (
|
313
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
307
314
|
if (obj[key] === null) {
|
308
315
|
delete obj[key];
|
309
316
|
}
|
@@ -318,14 +325,14 @@
|
|
318
325
|
id: presence(this.id),
|
319
326
|
"class": presence(this.className),
|
320
327
|
page: page(),
|
321
|
-
section:
|
328
|
+
section: getClosest(this, "data-section")
|
322
329
|
});
|
323
330
|
}
|
324
331
|
|
325
|
-
function
|
326
|
-
for (
|
327
|
-
if (element.hasAttribute(
|
328
|
-
return element.getAttribute(
|
332
|
+
function getClosest(element, attribute) {
|
333
|
+
for (; element && element !== document; element = element.parentNode) {
|
334
|
+
if (element.hasAttribute(attribute)) {
|
335
|
+
return element.getAttribute(attribute);
|
329
336
|
}
|
330
337
|
}
|
331
338
|
|
@@ -377,7 +384,7 @@
|
|
377
384
|
}
|
378
385
|
|
379
386
|
for (var key in config.visitParams) {
|
380
|
-
if (
|
387
|
+
if (Object.prototype.hasOwnProperty.call(config.visitParams, key)) {
|
381
388
|
data[key] = config.visitParams[key];
|
382
389
|
}
|
383
390
|
}
|
@@ -431,12 +438,12 @@
|
|
431
438
|
js: true
|
432
439
|
};
|
433
440
|
|
434
|
-
ahoy.ready(
|
441
|
+
ahoy.ready(function () {
|
435
442
|
if (config.cookies && !ahoy.getVisitId()) {
|
436
443
|
createVisit();
|
437
444
|
}
|
438
445
|
|
439
|
-
ahoy.ready(
|
446
|
+
ahoy.ready(function () {
|
440
447
|
log(event);
|
441
448
|
|
442
449
|
event.visit_token = ahoy.getVisitId();
|
@@ -449,7 +456,7 @@
|
|
449
456
|
saveEventQueue();
|
450
457
|
|
451
458
|
// wait in case navigating to reduce duplicate events
|
452
|
-
setTimeout(
|
459
|
+
setTimeout(function () {
|
453
460
|
trackEvent(event);
|
454
461
|
}, 1000);
|
455
462
|
}
|
@@ -467,8 +474,8 @@
|
|
467
474
|
};
|
468
475
|
|
469
476
|
if (additionalProperties) {
|
470
|
-
for(var propName in additionalProperties) {
|
471
|
-
if (
|
477
|
+
for (var propName in additionalProperties) {
|
478
|
+
if (Object.prototype.hasOwnProperty.call(additionalProperties, propName)) {
|
472
479
|
properties[propName] = additionalProperties[propName];
|
473
480
|
}
|
474
481
|
}
|
@@ -482,7 +489,7 @@
|
|
482
489
|
}
|
483
490
|
onEvent("click", selector, function (e) {
|
484
491
|
var properties = eventProperties.call(this, e);
|
485
|
-
properties.text = properties.tag
|
492
|
+
properties.text = properties.tag === "input" ? this.value : (this.textContent || this.innerText || this.innerHTML).replace(/[\s\r\n]+/g, " ").trim();
|
486
493
|
properties.href = this.href;
|
487
494
|
ahoy.track("$click", properties);
|
488
495
|
});
|
@@ -526,7 +533,7 @@
|
|
526
533
|
ahoy.start = function () {};
|
527
534
|
};
|
528
535
|
|
529
|
-
documentReady(function() {
|
536
|
+
documentReady(function () {
|
530
537
|
if (config.startOnReady) {
|
531
538
|
ahoy.start();
|
532
539
|
}
|
@@ -534,4 +541,4 @@
|
|
534
541
|
|
535
542
|
return ahoy;
|
536
543
|
|
537
|
-
}))
|
544
|
+
}));
|
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: 5.2.0
|
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: 2024-09-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -16,42 +16,42 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '6.1'
|
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: '
|
26
|
+
version: '6.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: device_detector
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: '1'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: '1'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: safely_block
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
47
|
+
version: '0.4'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
54
|
+
version: '0.4'
|
55
55
|
description:
|
56
56
|
email: andrew@ankane.org
|
57
57
|
executables: []
|
@@ -65,14 +65,13 @@ files:
|
|
65
65
|
- app/controllers/ahoy/base_controller.rb
|
66
66
|
- app/controllers/ahoy/events_controller.rb
|
67
67
|
- app/controllers/ahoy/visits_controller.rb
|
68
|
-
- app/jobs/ahoy/geocode_job.rb
|
69
|
-
- app/jobs/ahoy/geocode_v2_job.rb
|
70
68
|
- config/routes.rb
|
71
69
|
- lib/ahoy.rb
|
72
70
|
- lib/ahoy/base_store.rb
|
73
71
|
- lib/ahoy/controller.rb
|
74
72
|
- lib/ahoy/database_store.rb
|
75
73
|
- lib/ahoy/engine.rb
|
74
|
+
- lib/ahoy/geocode_v2_job.rb
|
76
75
|
- lib/ahoy/helper.rb
|
77
76
|
- lib/ahoy/model.rb
|
78
77
|
- lib/ahoy/query_methods.rb
|
@@ -106,14 +105,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
106
105
|
requirements:
|
107
106
|
- - ">="
|
108
107
|
- !ruby/object:Gem::Version
|
109
|
-
version: '
|
108
|
+
version: '3'
|
110
109
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
110
|
requirements:
|
112
111
|
- - ">="
|
113
112
|
- !ruby/object:Gem::Version
|
114
113
|
version: '0'
|
115
114
|
requirements: []
|
116
|
-
rubygems_version: 3.
|
115
|
+
rubygems_version: 3.5.11
|
117
116
|
signing_key:
|
118
117
|
specification_version: 4
|
119
118
|
summary: Simple, powerful, first-party analytics for Rails
|
File without changes
|