ahoy_matey 3.0.4 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 141aeb0d7f78023ecefb04f274637e06c68d6d8af9ece0ac583d8cff701b59f9
4
- data.tar.gz: f44945bbdad4be224a57b4e915175ef983c4c24ed28eca4accc8e5fcce171105
3
+ metadata.gz: d09c2a1f7783c94ad857652adfbe8150deb702f9162daf28c2bf9c11454f4277
4
+ data.tar.gz: 0e0a9779762c9ad912ba6d8866f85783c00aa0a96f3c53a4b6b803a3cdf41af2
5
5
  SHA512:
6
- metadata.gz: ee9f88551691cc6be92b5adf9bfd0495065e43510679a61377bc12082c44ac7b0cff544ca88b3157ec92d05432ff58e654b491c7e256eb2c6032b5517573e6e3
7
- data.tar.gz: 45565919c04c0f7c05cf87d0ef0f35fedd867cbd7569ee72a613770efab6a0c7dac1d884d6d0830c4f28efcf475ece191e7d44cb3712b18454c30b0de1658a4d
6
+ metadata.gz: e479ea5345820038c0a4fe4296504781614b53c6cfef5a79856c51073f768dc64082d494ba64d1e03ec41a346832c320565de63c5953f3bdf91e6e2697a1b7c9
7
+ data.tar.gz: 5fc7c4e73835b1ffc7be2d9c125894baccf204b7e7785784887574d80b8d7581d10e1ced1c0562cd7804ad66a22ce05df64ef6b302a981faa567569b618c8557
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## 3.3.0 (2021-08-13)
2
+
3
+ - Added `country_code` to geocoding
4
+ - Updated Ahoy.js to 0.3.9
5
+ - Fixed install generator for MariaDB
6
+
7
+ ## 3.2.0 (2021-03-01)
8
+
9
+ - Disabled geocoding by default for new installations
10
+ - Fixed deprecation warning with Active Record 6.1
11
+
12
+ ## 3.1.0 (2020-12-04)
13
+
14
+ - Added `instance` method
15
+ - Added `request` argument to `user_method`
16
+ - Updated Ahoy.js to 0.3.8
17
+ - Removed `exclude_method` call when geocoding
18
+
19
+ ## 3.0.5 (2020-09-09)
20
+
21
+ - Added `group_prop` method
22
+ - Use `datetime` type in migration
23
+
1
24
  ## 3.0.4 (2020-06-07)
2
25
 
3
26
  - Updated Ahoy.js to 0.3.6
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2019 Andrew Kane
1
+ Copyright (c) 2014-2021 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  :fire: Simple, powerful, first-party analytics for Rails
4
4
 
5
- Track visits and events in Ruby, JavaScript, and native apps. Data is stored in your database by default so you can easily combine it with other data.
5
+ Track visits and events in Ruby, JavaScript, and native apps. Data is stored in your database by default, and you can customize it for any data store as you grow.
6
6
 
7
7
  :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
8
 
9
9
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
10
10
 
11
- [![Build Status](https://travis-ci.org/ankane/ahoy.svg?branch=master)](https://travis-ci.org/ankane/ahoy)
11
+ [![Build Status](https://github.com/ankane/ahoy/workflows/build/badge.svg?branch=master)](https://github.com/ankane/ahoy/actions)
12
12
 
13
13
  ## Installation
14
14
 
@@ -70,6 +70,14 @@ Track an event with:
70
70
  ahoy.track("My second event", {language: "JavaScript"});
71
71
  ```
72
72
 
73
+ ### Native Apps
74
+
75
+ Check out [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android).
76
+
77
+ ### Geocoding Setup
78
+
79
+ To enable geocoding, see the [Geocoding section](#geocoding).
80
+
73
81
  ### GDPR Compliance
74
82
 
75
83
  Ahoy provides a number of options to help with GDPR compliance. See the [GDPR section](#gdpr-compliance-1) for more info.
@@ -145,7 +153,7 @@ See [Ahoy.js](https://github.com/ankane/ahoy.js) for a complete list of features
145
153
 
146
154
  #### Native Apps
147
155
 
148
- For Android, check out [Ahoy Android](https://github.com/instacart/ahoy-android). For other platforms, see the [API spec](#api-spec).
156
+ See the docs for [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android).
149
157
 
150
158
  #### AMP
151
159
 
@@ -181,7 +189,7 @@ Order.joins(:ahoy_visit).group("device_type").count
181
189
  Here’s what the migration to add the `ahoy_visit_id` column should look like:
182
190
 
183
191
  ```ruby
184
- class AddVisitIdToOrders < ActiveRecord::Migration[6.0]
192
+ class AddVisitIdToOrders < ActiveRecord::Migration[6.1]
185
193
  def change
186
194
  add_column :orders, :ahoy_visit_id, :bigint
187
195
  end
@@ -196,7 +204,7 @@ visitable :sign_up_visit
196
204
 
197
205
  ### Users
198
206
 
199
- Ahoy automatically attaches the `current_user` to the visit. With [Devise](https://github.com/plataformatec/devise), it attaches the user even if he or she signs in after the visit starts.
207
+ 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.
200
208
 
201
209
  With other authentication frameworks, add this to the end of your sign in method:
202
210
 
@@ -306,71 +314,112 @@ Set other [cookie options](https://api.rubyonrails.org/classes/ActionDispatch/Co
306
314
  Ahoy.cookie_options = {same_site: :lax}
307
315
  ```
308
316
 
309
- ### Geocoding
317
+ You can also [disable cookies](#anonymity-sets--cookies)
310
318
 
311
- Disable geocoding with:
319
+ ### Token Generation
320
+
321
+ 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).
312
322
 
313
323
  ```ruby
314
- Ahoy.geocode = false
324
+ Ahoy.token_generator = -> { Druuid.gen }
315
325
  ```
316
326
 
317
- The default job queue is `:ahoy`. Change this with:
327
+ ### Throttling
328
+
329
+ You can use [Rack::Attack](https://github.com/kickstarter/rack-attack) to throttle requests to the API.
318
330
 
319
331
  ```ruby
320
- Ahoy.job_queue = :low_priority
332
+ class Rack::Attack
333
+ throttle("ahoy/ip", limit: 20, period: 1.minute) do |req|
334
+ if req.path.start_with?("/ahoy/")
335
+ req.ip
336
+ end
337
+ end
338
+ end
321
339
  ```
322
340
 
323
- #### Geocoding Performance
341
+ ### Exceptions
324
342
 
325
- To avoid calls to a remote API, download the [GeoLite2 City database](https://dev.maxmind.com/geoip/geoip2/geolite2/) and configure Geocoder to use it.
343
+ 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:
326
344
 
327
- Add this line to your application’s Gemfile:
345
+ ```ruby
346
+ Safely.report_exception_method = ->(e) { Rollbar.error(e) }
347
+ ```
348
+
349
+ ## Geocoding
350
+
351
+ 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).
352
+
353
+ To enable geocoding, update `config/initializers/ahoy.rb`:
354
+
355
+ ```ruby
356
+ Ahoy.geocode = true
357
+ ```
358
+
359
+ Geocoding is performed in a background job so it doesn’t slow down web requests. The default job queue is `:ahoy`. Change this with:
360
+
361
+ ```ruby
362
+ Ahoy.job_queue = :low_priority
363
+ ```
364
+
365
+ ### Local Geocoding
366
+
367
+ For privacy and performance, we recommend geocoding locally. Add this line to your application’s Gemfile:
328
368
 
329
369
  ```ruby
330
370
  gem 'maxminddb'
331
371
  ```
332
372
 
333
- And create an initializer at `config/initializers/geocoder.rb` with:
373
+ For city-level geocoding, download the [GeoLite2 City database](https://dev.maxmind.com/geoip/geoip2/geolite2/) and create `config/initializers/geocoder.rb` with:
334
374
 
335
375
  ```ruby
336
376
  Geocoder.configure(
337
377
  ip_lookup: :geoip2,
338
378
  geoip2: {
339
- file: Rails.root.join("lib", "GeoLite2-City.mmdb")
379
+ file: "path/to/GeoLite2-City.mmdb"
340
380
  }
341
381
  )
342
382
  ```
343
383
 
344
- If you use Heroku, you can use an unofficial buildpack like [this one](https://github.com/temedica/heroku-buildpack-maxmind-geolite2) to avoid including the database in your repo.
384
+ For country-level geocoding, install the `geoip-database` package. It’s preinstalled on Heroku. For Ubuntu, use:
345
385
 
346
- ### Token Generation
386
+ ```sh
387
+ sudo apt-get install geoip-database
388
+ ```
347
389
 
348
- 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).
390
+ And create `config/initializers/geocoder.rb` with:
349
391
 
350
392
  ```ruby
351
- Ahoy.token_generator = -> { Druuid.gen }
393
+ Geocoder.configure(
394
+ ip_lookup: :maxmind_local,
395
+ maxmind_local: {
396
+ file: "/usr/share/GeoIP/GeoIP.dat",
397
+ package: :country
398
+ }
399
+ )
352
400
  ```
353
401
 
354
- ### Throttling
402
+ ### Load Balancer Geocoding
355
403
 
356
- You can use [Rack::Attack](https://github.com/kickstarter/rack-attack) to throttle requests to the API.
404
+ Some load balancers can add geocoding information to request headers.
357
405
 
358
- ```ruby
359
- class Rack::Attack
360
- throttle("ahoy/ip", limit: 20, period: 1.minute) do |req|
361
- if req.path.start_with?("/ahoy/")
362
- req.ip
363
- end
364
- end
365
- end
366
- ```
367
-
368
- ### Exceptions
406
+ - [nginx](https://nginx.org/en/docs/http/ngx_http_geoip_module.html)
407
+ - [Google Cloud](https://cloud.google.com/load-balancing/docs/custom-headers)
408
+ - [Cloudflare](https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-Cloudflare-IP-Geolocation)
369
409
 
370
- 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:
410
+ Update `config/initializers/ahoy.rb` with:
371
411
 
372
412
  ```ruby
373
- Safely.report_exception_method = ->(e) { Rollbar.error(e) }
413
+ Ahoy.geocode = false
414
+
415
+ class Ahoy::Store < Ahoy::DatabaseStore
416
+ def track_visit(data)
417
+ data[:country] = request.headers["<country-header>"]
418
+ data[:region] = request.headers["<region-header>"]
419
+ data[:city] = request.headers["<city-header>"]
420
+ super(data)
421
+ end
422
+ end
374
423
  ```
375
424
 
376
425
  ## GDPR Compliance
@@ -431,11 +480,15 @@ Ahoy can switch from cookies to [anonymity sets](https://privacypatterns.org/pat
431
480
  Ahoy.cookies = false
432
481
  ```
433
482
 
434
- Previously set cookies are automatically deleted.
483
+ Previously set cookies are automatically deleted. If you use JavaScript tracking, also set:
484
+
485
+ ```javascript
486
+ ahoy.configure({cookies: false});
487
+ ```
435
488
 
436
489
  ## Data Retention
437
490
 
438
- Delete older data with:
491
+ Data should only be retained for as long as it’s needed. Delete older data with:
439
492
 
440
493
  ```ruby
441
494
  Ahoy::Visit.where("started_at < ?", 2.years.ago).find_in_batches do |visits|
@@ -445,6 +498,12 @@ Ahoy::Visit.where("started_at < ?", 2.years.ago).find_in_batches do |visits|
445
498
  end
446
499
  ```
447
500
 
501
+ You can use [Rollup](https://github.com/ankane/rollup) to aggregate important data before you do.
502
+
503
+ ```ruby
504
+ Ahoy::Visit.rollup("Visits", interval: "hour")
505
+ ```
506
+
448
507
  Delete data for a specific user with:
449
508
 
450
509
  ```ruby
@@ -580,7 +639,7 @@ Ahoy::Visit.group(:referring_domain).count
580
639
 
581
640
  ### Querying Events
582
641
 
583
- Ahoy provides two methods on the event model to make querying easier.
642
+ Ahoy provides a few methods on the event model to make querying easier.
584
643
 
585
644
  To query on both name and properties, you can use:
586
645
 
@@ -591,9 +650,17 @@ Ahoy::Event.where_event("Viewed product", product_id: 123).count
591
650
  Or just query properties with:
592
651
 
593
652
  ```ruby
594
- Ahoy::Event.where_props(product_id: 123).count
653
+ Ahoy::Event.where_props(product_id: 123, category: "Books").count
654
+ ```
655
+
656
+ Group by properties with:
657
+
658
+ ```ruby
659
+ Ahoy::Event.group_prop(:product_id, :category).count
595
660
  ```
596
661
 
662
+ Note: MySQL and MariaDB always return string keys (include `"null"` for `nil`) for `group_prop`.
663
+
597
664
  ### Funnels
598
665
 
599
666
  It’s easy to create funnels.
@@ -606,6 +673,29 @@ viewed_checkout_ids = Ahoy::Event.where(user_id: added_item_ids, name: "Viewed c
606
673
 
607
674
  The same approach also works with visitor tokens.
608
675
 
676
+ ### Rollups
677
+
678
+ Improve query performance by pre-aggregating data with [Rollup](https://github.com/ankane/rollup).
679
+
680
+ ```ruby
681
+ Ahoy::Event.where(name: "Viewed store").rollup("Store views")
682
+ ```
683
+
684
+ This is only needed if you have a lot of data.
685
+
686
+ ### Forecasting
687
+
688
+ To forecast future visits and events, check out [Prophet](https://github.com/ankane/prophet).
689
+
690
+ ```ruby
691
+ daily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate
692
+ Prophet.forecast(daily_visits)
693
+ ```
694
+
695
+ ### Recommendations
696
+
697
+ To make recommendations based on events, check out [Disco](https://github.com/ankane/disco#ahoy).
698
+
609
699
  ## Tutorials
610
700
 
611
701
  - [Tracking Metrics with Ahoy and Blazer](https://gorails.com/episodes/internal-metrics-with-ahoy-and-blazer)
@@ -722,3 +812,20 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
722
812
  - Fix bugs and [submit pull requests](https://github.com/ankane/ahoy/pulls)
723
813
  - Write, clarify, or fix documentation
724
814
  - Suggest or add new features
815
+
816
+ To get started with development:
817
+
818
+ ```sh
819
+ git clone https://github.com/ankane/ahoy.git
820
+ cd ahoy
821
+ bundle install
822
+ bundle exec rake test
823
+ ```
824
+
825
+ To test query methods, start PostgreSQL, MySQL, and MongoDB and use:
826
+
827
+ ```sh
828
+ createdb ahoy_test
829
+ mysqladmin create ahoy_test
830
+ bundle exec rake test:query_methods
831
+ ```
@@ -14,6 +14,7 @@ module Ahoy
14
14
  if location && location.country.present?
15
15
  data = {
16
16
  country: location.country,
17
+ country_code: location.try(:country_code).presence,
17
18
  region: location.try(:state).presence,
18
19
  city: location.try(:city).presence,
19
20
  postal_code: location.try(:postal_code).presence,
data/lib/ahoy.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # stdlib
1
2
  require "ipaddr"
2
3
 
3
4
  # dependencies
@@ -104,6 +105,14 @@ module Ahoy
104
105
  addr.mask(48).to_s
105
106
  end
106
107
  end
108
+
109
+ def self.instance
110
+ Thread.current[:ahoy]
111
+ end
112
+
113
+ def self.instance=(value)
114
+ Thread.current[:ahoy] = value
115
+ end
107
116
  end
108
117
 
109
118
  ActiveSupport.on_load(:action_controller) do
@@ -119,6 +128,10 @@ ActiveSupport.on_load(:action_view) do
119
128
  end
120
129
 
121
130
  # Mongoid
131
+ # TODO use
132
+ # ActiveSupport.on_load(:mongoid) do
133
+ # Mongoid::Document::ClassMethods.include(Ahoy::Model)
134
+ # end
122
135
  if defined?(ActiveModel)
123
136
  ActiveModel::Callbacks.include(Ahoy::Model)
124
137
  end
@@ -24,7 +24,11 @@ module Ahoy
24
24
  def user
25
25
  @user ||= begin
26
26
  if Ahoy.user_method.respond_to?(:call)
27
- Ahoy.user_method.call(controller)
27
+ if Ahoy.user_method.arity == 1
28
+ Ahoy.user_method.call(controller)
29
+ else
30
+ Ahoy.user_method.call(controller, request)
31
+ end
28
32
  else
29
33
  controller.send(Ahoy.user_method) if controller.respond_to?(Ahoy.user_method, true)
30
34
  end
@@ -39,12 +39,12 @@ module Ahoy
39
39
  end
40
40
 
41
41
  def set_ahoy_request_store
42
- previous_value = Thread.current[:ahoy]
42
+ previous_value = Ahoy.instance
43
43
  begin
44
- Thread.current[:ahoy] = ahoy
44
+ Ahoy.instance = ahoy
45
45
  yield
46
46
  ensure
47
- Thread.current[:ahoy] = previous_value
47
+ Ahoy.instance = previous_value
48
48
  end
49
49
  end
50
50
  end
@@ -53,7 +53,7 @@ module Ahoy
53
53
 
54
54
  def visit
55
55
  unless defined?(@visit)
56
- @visit = visit_model.where(visit_token: ahoy.visit_token).first if ahoy.visit_token
56
+ @visit = visit_model.find_by(visit_token: ahoy.visit_token) if ahoy.visit_token
57
57
  end
58
58
  @visit
59
59
  end
data/lib/ahoy/model.rb CHANGED
@@ -7,7 +7,7 @@ module Ahoy
7
7
  end
8
8
  class_eval %{
9
9
  def set_ahoy_visit
10
- self.#{name} ||= Thread.current[:ahoy].try(:visit_or_create)
10
+ self.#{name} ||= Ahoy.instance.try(:visit_or_create)
11
11
  end
12
12
  }
13
13
  end
@@ -31,6 +31,7 @@ module Ahoy
31
31
  end
32
32
  else
33
33
  properties.each do |k, v|
34
+ # TODO cast to json instead
34
35
  relation = relation.where("properties REGEXP ?", "[{,]#{{k.to_s => v}.to_json.sub(/\A\{/, "").sub(/\}\z/, "").gsub("+", "\\\\+")}[,}]")
35
36
  end
36
37
  end
@@ -57,6 +58,7 @@ module Ahoy
57
58
  end
58
59
  else
59
60
  properties.each do |k, v|
61
+ # TODO cast to jsonb instead
60
62
  relation = relation.where("properties SIMILAR TO ?", "%[{,]#{{k.to_s => v}.to_json.sub(/\A\{/, "").sub(/\}\z/, "").gsub("+", "\\\\+")}[,}]%")
61
63
  end
62
64
  end
@@ -66,6 +68,49 @@ module Ahoy
66
68
  relation
67
69
  end
68
70
  alias_method :where_properties, :where_props
71
+
72
+ def group_prop(*props)
73
+ # like with group
74
+ props.flatten!
75
+
76
+ relation = self
77
+ if respond_to?(:columns_hash)
78
+ column_type = columns_hash["properties"].type
79
+ adapter_name = connection.adapter_name.downcase
80
+ else
81
+ adapter_name = "mongoid"
82
+ end
83
+ case adapter_name
84
+ when "mongoid"
85
+ raise "Adapter not supported: #{adapter_name}"
86
+ when /mysql/
87
+ if connection.try(:mariadb?)
88
+ props.each do |prop|
89
+ quoted_prop = connection.quote("$.#{prop}")
90
+ relation = relation.group("JSON_UNQUOTE(JSON_EXTRACT(properties, #{quoted_prop}))")
91
+ end
92
+ else
93
+ column = column_type == :json ? "properties" : "CAST(properties AS JSON)"
94
+ props.each do |prop|
95
+ quoted_prop = connection.quote("$.#{prop}")
96
+ relation = relation.group("JSON_UNQUOTE(JSON_EXTRACT(#{column}, #{quoted_prop}))")
97
+ end
98
+ end
99
+ when /postgres|postgis/
100
+ # convert to jsonb to fix
101
+ # could not identify an equality operator for type json
102
+ # and for text columns
103
+ cast = [:jsonb, :hstore].include?(column_type) ? "" : "::jsonb"
104
+
105
+ props.each do |prop|
106
+ quoted_prop = connection.quote(prop)
107
+ relation = relation.group("properties#{cast} -> #{quoted_prop}")
108
+ end
109
+ else
110
+ raise "Adapter not supported: #{adapter_name}"
111
+ end
112
+ relation
113
+ end
69
114
  end
70
115
  end
71
116
  end
data/lib/ahoy/tracker.rb CHANGED
@@ -67,16 +67,12 @@ module Ahoy
67
67
  end
68
68
 
69
69
  def geocode(data)
70
- if exclude?
71
- debug "Geocode excluded"
72
- else
73
- data = {
74
- visit_token: visit_token
75
- }.merge(data).select { |_, v| v }
70
+ data = {
71
+ visit_token: visit_token
72
+ }.merge(data).select { |_, v| v }
76
73
 
77
- @store.geocode(data)
78
- true
79
- end
74
+ @store.geocode(data)
75
+ true
80
76
  rescue => e
81
77
  report_exception(e)
82
78
  end
data/lib/ahoy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ahoy
2
- VERSION = "3.0.4"
2
+ VERSION = "3.3.0"
3
3
  end
@@ -17,9 +17,7 @@ module Ahoy
17
17
  end
18
18
 
19
19
  def properties_type
20
- # use connection_config instead of connection.adapter
21
- # so database connection isn't needed
22
- case ActiveRecord::Base.connection_config[:adapter].to_s
20
+ case adapter
23
21
  when /postg/i # postgres, postgis
24
22
  "jsonb"
25
23
  when /mysql/i
@@ -29,8 +27,23 @@ module Ahoy
29
27
  end
30
28
  end
31
29
 
30
+ # requires database connection to check for MariaDB
31
+ def serialize_properties?
32
+ properties_type == "text" || (properties_type == "json" && ActiveRecord::Base.connection.try(:mariadb?))
33
+ end
34
+
35
+ # use connection_config instead of connection.adapter
36
+ # so database connection isn't needed
37
+ def adapter
38
+ if ActiveRecord::VERSION::STRING.to_f >= 6.1
39
+ ActiveRecord::Base.connection_db_config.adapter.to_s
40
+ else
41
+ ActiveRecord::Base.connection_config[:adapter].to_s
42
+ end
43
+ end
44
+
32
45
  def rails52?
33
- ActiveRecord::VERSION::STRING >= "5.2"
46
+ ActiveRecord::VERSION::STRING.to_f >= 5.2
34
47
  end
35
48
 
36
49
  def migration_version
@@ -4,7 +4,7 @@ class Ahoy::Event < ApplicationRecord
4
4
  self.table_name = "ahoy_events"
5
5
 
6
6
  belongs_to :visit
7
- belongs_to :user, optional: true<% if properties_type == "text" %>
7
+ belongs_to :user, optional: true<% if serialize_properties? %>
8
8
 
9
9
  serialize :properties, JSON<% end %>
10
10
  end
@@ -41,10 +41,10 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
41
41
  t.string :os_version
42
42
  t.string :platform
43
43
 
44
- t.timestamp :started_at
44
+ t.datetime :started_at
45
45
  end
46
46
 
47
- add_index :ahoy_visits, [:visit_token], unique: true
47
+ add_index :ahoy_visits, :visit_token, unique: true
48
48
 
49
49
  create_table :ahoy_events do |t|
50
50
  t.references :visit
@@ -52,7 +52,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
52
52
 
53
53
  t.string :name
54
54
  t.<%= properties_type %> :properties
55
- t.timestamp :time
55
+ t.datetime :time
56
56
  end
57
57
 
58
58
  add_index :ahoy_events, [:name, :time]<% if properties_type == "jsonb" %><% if rails52? %>
@@ -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
23
+ # we recommend configuring local geocoding first
24
+ # see https://github.com/ankane/ahoy#geocoding
25
+ Ahoy.geocode = false
@@ -3,3 +3,8 @@ end
3
3
 
4
4
  # set to true for JavaScript tracking
5
5
  Ahoy.api = false
6
+
7
+ # set to true for geocoding
8
+ # we recommend configuring local geocoding first
9
+ # see https://github.com/ankane/ahoy#geocoding
10
+ Ahoy.geocode = false
@@ -1,8 +1,8 @@
1
- /*
1
+ /*!
2
2
  * Ahoy.js
3
3
  * Simple, powerful JavaScript analytics
4
4
  * https://github.com/ankane/ahoy.js
5
- * v0.3.6
5
+ * v0.3.9
6
6
  * MIT License
7
7
  */
8
8
 
@@ -12,8 +12,6 @@
12
12
  (global = global || self, global.ahoy = factory());
13
13
  }(this, (function () { 'use strict';
14
14
 
15
- var n=function(n){return void 0===n},e=function(n){return Array.isArray(n)},t=function(n){return n&&"number"==typeof n.size&&"string"==typeof n.type&&"function"==typeof n.slice},s=function(o,i,r,f){return (i=i||{}).indices=!n(i.indices)&&i.indices,i.nullsAsUndefineds=!n(i.nullsAsUndefineds)&&i.nullsAsUndefineds,i.booleansAsIntegers=!n(i.booleansAsIntegers)&&i.booleansAsIntegers,r=r||new FormData,n(o)?r:(null===o?i.nullsAsUndefineds||r.append(f,""):"boolean"!=typeof o?e(o)?o.length&&o.forEach(function(n,e){s(n,i,r,f+"["+(i.indices?e:"")+"]");}):o instanceof Date?r.append(f,o.toISOString()):o!==Object(o)||function(n){return t(n)&&"string"==typeof n.name&&("object"==typeof n.lastModifiedDate||"number"==typeof n.lastModified)}(o)||t(o)?r.append(f,o):Object.keys(o).forEach(function(n){var t=o[n];if(e(t)){ for(;n.length>2&&n.lastIndexOf("[]")===n.length-2;){ n=n.substring(0,n.length-2); } }s(t,i,r,f?f+"["+n+"]":n);}):r.append(f,i.booleansAsIntegers?o?1:0:o),r)};
16
-
17
15
  // https://www.quirksmode.org/js/cookies.html
18
16
 
19
17
  var Cookies = {
@@ -101,6 +99,16 @@
101
99
  return (config.useBeacon || config.trackNow) && isEmpty(config.headers) && canStringify && typeof(window.navigator.sendBeacon) !== "undefined" && !config.withCredentials;
102
100
  }
103
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
+
104
112
  // cookies
105
113
 
106
114
  function setCookie(name, value, ttl) {
@@ -146,17 +154,23 @@
146
154
  element.webkitMatchesSelector;
147
155
 
148
156
  if (matches) {
149
- return matches.apply(element, [selector]);
157
+ if (matches.apply(element, [selector])) {
158
+ return element;
159
+ } else if (element.parentElement) {
160
+ return matchesSelector(element.parentElement, selector);
161
+ }
162
+ return null;
150
163
  } else {
151
164
  log("Unable to match");
152
- return false;
165
+ return null;
153
166
  }
154
167
  }
155
168
 
156
169
  function onEvent(eventName, selector, callback) {
157
170
  document.addEventListener(eventName, function (e) {
158
- if (matchesSelector(e.target, selector)) {
159
- callback(e);
171
+ var matchedElement = matchesSelector(e.target, selector);
172
+ if (matchedElement) {
173
+ callback.call(matchedElement, e);
160
174
  }
161
175
  });
162
176
  }
@@ -275,7 +289,7 @@
275
289
  // stringify so we keep the type
276
290
  data.events_json = JSON.stringify(data.events);
277
291
  delete data.events;
278
- window.navigator.sendBeacon(eventsUrl(), s(data));
292
+ window.navigator.sendBeacon(eventsUrl(), serialize(data));
279
293
  });
280
294
  }
281
295
 
@@ -298,14 +312,13 @@
298
312
  return obj;
299
313
  }
300
314
 
301
- function eventProperties(e) {
302
- var target = e.target;
315
+ function eventProperties() {
303
316
  return cleanObject({
304
- tag: target.tagName.toLowerCase(),
305
- id: presence(target.id),
306
- "class": presence(target.className),
317
+ tag: this.tagName.toLowerCase(),
318
+ id: presence(this.id),
319
+ "class": presence(this.className),
307
320
  page: page(),
308
- section: getClosestSection(target)
321
+ section: getClosestSection(this)
309
322
  });
310
323
  }
311
324
 
@@ -463,35 +476,48 @@
463
476
  ahoy.track("$view", properties);
464
477
  };
465
478
 
466
- ahoy.trackClicks = function () {
467
- onEvent("click", "a, button, input[type=submit]", function (e) {
468
- var target = e.target;
469
- var properties = eventProperties(e);
470
- properties.text = properties.tag == "input" ? target.value : (target.textContent || target.innerText || target.innerHTML).replace(/[\s\r\n]+/g, " ").trim();
471
- properties.href = target.href;
479
+ ahoy.trackClicks = function (selector) {
480
+ if (selector === undefined) {
481
+ log("trackClicks will require a selector in 0.4.0");
482
+ selector = "a, button, input[type=submit]";
483
+ }
484
+ onEvent("click", selector, function (e) {
485
+ var properties = eventProperties.call(this, e);
486
+ properties.text = properties.tag == "input" ? this.value : (this.textContent || this.innerText || this.innerHTML).replace(/[\s\r\n]+/g, " ").trim();
487
+ properties.href = this.href;
472
488
  ahoy.track("$click", properties);
473
489
  });
474
490
  };
475
491
 
476
- ahoy.trackSubmits = function () {
477
- onEvent("submit", "form", function (e) {
478
- var properties = eventProperties(e);
492
+ ahoy.trackSubmits = function (selector) {
493
+ if (selector === undefined) {
494
+ log("trackSubmits will require a selector in 0.4.0");
495
+ selector = "form";
496
+ }
497
+ onEvent("submit", selector, function (e) {
498
+ var properties = eventProperties.call(this, e);
479
499
  ahoy.track("$submit", properties);
480
500
  });
481
501
  };
482
502
 
483
- ahoy.trackChanges = function () {
484
- onEvent("change", "input, textarea, select", function (e) {
485
- var properties = eventProperties(e);
503
+ ahoy.trackChanges = function (selector) {
504
+ if (selector === undefined) {
505
+ // put here instead of above to prevent message with trackAll
506
+ log("trackChanges is deprecated and will be removed in 0.4.0");
507
+ selector = "input, textarea, select";
508
+ }
509
+ onEvent("change", selector, function (e) {
510
+ var properties = eventProperties.call(this, e);
486
511
  ahoy.track("$change", properties);
487
512
  });
488
513
  };
489
514
 
490
515
  ahoy.trackAll = function() {
516
+ log("trackAll is deprecated and will be removed in 0.4.0");
491
517
  ahoy.trackView();
492
- ahoy.trackClicks();
493
- ahoy.trackSubmits();
494
- ahoy.trackChanges();
518
+ ahoy.trackClicks("a, button, input[type=submit]");
519
+ ahoy.trackSubmits("form");
520
+ ahoy.trackChanges("input, textarea, select");
495
521
  };
496
522
 
497
523
  // push events from queue
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ahoy_matey
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.4
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-07 00:00:00.000000000 Z
11
+ date: 2021-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -66,134 +66,8 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  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
- description:
196
- email: andrew@chartkick.com
69
+ description:
70
+ email: andrew@ankane.org
197
71
  executables: []
198
72
  extensions: []
199
73
  extra_rdoc_files: []
@@ -238,7 +112,7 @@ homepage: https://github.com/ankane/ahoy
238
112
  licenses:
239
113
  - MIT
240
114
  metadata: {}
241
- post_install_message:
115
+ post_install_message:
242
116
  rdoc_options: []
243
117
  require_paths:
244
118
  - lib
@@ -253,8 +127,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
253
127
  - !ruby/object:Gem::Version
254
128
  version: '0'
255
129
  requirements: []
256
- rubygems_version: 3.1.2
257
- signing_key:
130
+ rubygems_version: 3.2.22
131
+ signing_key:
258
132
  specification_version: 4
259
133
  summary: Simple, powerful, first-party analytics for Rails
260
134
  test_files: []