ahoy_matey 3.0.2 → 3.2.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: 0007d4286157e3456e73cd0fbcf9ac46905f7088caabd047f1514b4782a99432
4
- data.tar.gz: 3b036aa183d4a847df7b1c42be88ce968cffaa3ce9b14281fc05abce747addcc
3
+ metadata.gz: 51e11f3086c12a1418ea7436e291ca9d2f0c7e0aa3091c48c904d05b4be6a210
4
+ data.tar.gz: 5b5c6afc94c0db23f2f1616f6e9edc0ae95ff334e33704c26417705d06d04670
5
5
  SHA512:
6
- metadata.gz: 6f74414728cdd0325ebe091894cc3300bfb889cd978cc15491c244682ca73c26294ce32585f9e91613bb69516eb80a867b7f968e249e4492beea3f83f298de6f
7
- data.tar.gz: 927cc957710268438b2b01379fe4f58f5f43840f465033cbac200b183396da14391fe4f90121781cd1c34fa574b1fa09987434259fe71f43c484c6b56ede9512
6
+ metadata.gz: fd75680d3d04b2383c30b19848f88fcb838f1498e8819d2d758aec6443e85bfb2f73a421da968cb54628097631c777ac1f95dd1ca4bae6d6b22d748a44dcb84d
7
+ data.tar.gz: 2d336bd12312cddf4b76f213b1f7f84b1fc2f0b67302e75096d1eb21013982f532064b31894ba217f7ccbafdbaf5eb4c691a3a86350b8abf1449374f66015909
data/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ ## 3.2.0 (2021-03-01)
2
+
3
+ - Disabled geocoding by default for new installations
4
+ - Fixed deprecation warning with Active Record 6.1
5
+
6
+ ## 3.1.0 (2020-12-04)
7
+
8
+ - Added `instance` method
9
+ - Added `request` argument to `user_method`
10
+ - Updated Ahoy.js to 0.3.8
11
+ - Removed `exclude_method` call when geocoding
12
+
13
+ ## 3.0.5 (2020-09-09)
14
+
15
+ - Added `group_prop` method
16
+ - Use `datetime` type in migration
17
+
18
+ ## 3.0.4 (2020-06-07)
19
+
20
+ - Updated Ahoy.js to 0.3.6
21
+
22
+ ## 3.0.3 (2020-04-17)
23
+
24
+ - Updated Ahoy.js to 0.3.5
25
+
1
26
  ## 3.0.2 (2020-04-03)
2
27
 
3
28
  - Added `cookie_options`
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2019 Andrew Kane
1
+ Copyright (c) 2014-2020 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -8,7 +8,7 @@ Track visits and events in Ruby, JavaScript, and native apps. Data is stored in
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,10 +70,18 @@ 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
+
73
77
  ### GDPR Compliance
74
78
 
75
79
  Ahoy provides a number of options to help with GDPR compliance. See the [GDPR section](#gdpr-compliance-1) for more info.
76
80
 
81
+ ### Geocoding Setup
82
+
83
+ To enable geocoding, see the [Geocoding section](#geocoding).
84
+
77
85
  ## How It Works
78
86
 
79
87
  ### Visits
@@ -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
 
@@ -308,40 +316,56 @@ Ahoy.cookie_options = {same_site: :lax}
308
316
 
309
317
  ### Geocoding
310
318
 
311
- Disable geocoding with:
319
+ Ahoy uses [Geocoder](https://github.com/alexreisner/geocoder) for geocoding. We recommend configuring [local geocoding](#local-geocoding) so IP addresses are not sent to a 3rd party service. If you do use a 3rd party service, be sure to add it to your GDPR subprocessor list. If Ahoy is configured to [mask ips](#ip-masking), the masked IP is used (this increases privacy but can reduce accuracy).
320
+
321
+ To enable geocoding, update `config/initializers/ahoy.rb`:
312
322
 
313
323
  ```ruby
314
- Ahoy.geocode = false
324
+ Ahoy.geocode = true
315
325
  ```
316
326
 
317
- The default job queue is `:ahoy`. Change this with:
327
+ Geocoding is performed in a background job so it doesn’t slow down web requests. The default job queue is `:ahoy`. Change this with:
318
328
 
319
329
  ```ruby
320
330
  Ahoy.job_queue = :low_priority
321
331
  ```
322
332
 
323
- #### Geocoding Performance
324
-
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.
333
+ ### Local Geocoding
326
334
 
327
- Add this line to your application’s Gemfile:
335
+ For privacy and performance, we recommend geocoding locally. Add this line to your application’s Gemfile:
328
336
 
329
337
  ```ruby
330
338
  gem 'maxminddb'
331
339
  ```
332
340
 
333
- And create an initializer at `config/initializers/geocoder.rb` with:
341
+ For city-level geocoding, download the [GeoLite2 City database](https://dev.maxmind.com/geoip/geoip2/geolite2/) and create `config/initializers/geocoder.rb` with:
334
342
 
335
343
  ```ruby
336
344
  Geocoder.configure(
337
345
  ip_lookup: :geoip2,
338
346
  geoip2: {
339
- file: Rails.root.join("lib", "GeoLite2-City.mmdb")
347
+ file: "path/to/GeoLite2-City.mmdb"
340
348
  }
341
349
  )
342
350
  ```
343
351
 
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.
352
+ For country-level geocoding, install the `geoip-database` package. It’s preinstalled on Heroku. For Ubuntu, use:
353
+
354
+ ```sh
355
+ sudo apt-get install geoip-database
356
+ ```
357
+
358
+ And create `config/initializers/geocoder.rb` with:
359
+
360
+ ```ruby
361
+ Geocoder.configure(
362
+ ip_lookup: :maxmind_local,
363
+ maxmind_local: {
364
+ file: "/usr/share/GeoIP/GeoIP.dat",
365
+ package: :country
366
+ }
367
+ )
368
+ ```
345
369
 
346
370
  ### Token Generation
347
371
 
@@ -433,6 +457,34 @@ Ahoy.cookies = false
433
457
 
434
458
  Previously set cookies are automatically deleted.
435
459
 
460
+ ## Data Retention
461
+
462
+ Data should only be retained for as long as it’s needed. Delete older data with:
463
+
464
+ ```ruby
465
+ Ahoy::Visit.where("started_at < ?", 2.years.ago).find_in_batches do |visits|
466
+ visit_ids = visits.map(&:id)
467
+ Ahoy::Event.where(visit_id: visit_ids).delete_all
468
+ Ahoy::Visit.where(id: visit_ids).delete_all
469
+ end
470
+ ```
471
+
472
+ You can use [Rollup](https://github.com/ankane/rollup) to aggregate important data before you do.
473
+
474
+ ```ruby
475
+ Ahoy::Visit.rollup("Visits", interval: "hour")
476
+ ```
477
+
478
+ Delete data for a specific user with:
479
+
480
+ ```ruby
481
+ user_id = 123
482
+ visit_ids = Ahoy::Visit.where(user_id: user_id).pluck(:id)
483
+ Ahoy::Event.where(visit_id: visit_ids).delete_all
484
+ Ahoy::Visit.where(id: visit_ids).delete_all
485
+ Ahoy::Event.where(user_id: user_id).delete_all
486
+ ```
487
+
436
488
  ## Development
437
489
 
438
490
  Ahoy is built with developers in mind. You can run the following code in your browser’s console.
@@ -558,7 +610,7 @@ Ahoy::Visit.group(:referring_domain).count
558
610
 
559
611
  ### Querying Events
560
612
 
561
- Ahoy provides two methods on the event model to make querying easier.
613
+ Ahoy provides a few methods on the event model to make querying easier.
562
614
 
563
615
  To query on both name and properties, you can use:
564
616
 
@@ -569,9 +621,17 @@ Ahoy::Event.where_event("Viewed product", product_id: 123).count
569
621
  Or just query properties with:
570
622
 
571
623
  ```ruby
572
- Ahoy::Event.where_props(product_id: 123).count
624
+ Ahoy::Event.where_props(product_id: 123, category: "Books").count
625
+ ```
626
+
627
+ Group by properties with:
628
+
629
+ ```ruby
630
+ Ahoy::Event.group_prop(:product_id, :category).count
573
631
  ```
574
632
 
633
+ Note: MySQL and MariaDB always return string keys (include `"null"` for `nil`) for `group_prop`.
634
+
575
635
  ### Funnels
576
636
 
577
637
  It’s easy to create funnels.
@@ -584,6 +644,29 @@ viewed_checkout_ids = Ahoy::Event.where(user_id: added_item_ids, name: "Viewed c
584
644
 
585
645
  The same approach also works with visitor tokens.
586
646
 
647
+ ### Rollups
648
+
649
+ Improve query performance by pre-aggregating data with [Rollup](https://github.com/ankane/rollup).
650
+
651
+ ```ruby
652
+ Ahoy::Event.where(name: "Viewed store").rollup("Store views")
653
+ ```
654
+
655
+ This is only needed if you have a lot of data.
656
+
657
+ ### Forecasting
658
+
659
+ To forecast future visits and events, check out [Prophet](https://github.com/ankane/prophet).
660
+
661
+ ```ruby
662
+ daily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate
663
+ Prophet.forecast(daily_visits)
664
+ ```
665
+
666
+ ### Recommendations
667
+
668
+ To make recommendations based on events, check out [Disco](https://github.com/ankane/disco#ahoy).
669
+
587
670
  ## Tutorials
588
671
 
589
672
  - [Tracking Metrics with Ahoy and Blazer](https://gorails.com/episodes/internal-metrics-with-ahoy-and-blazer)
@@ -700,3 +783,20 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
700
783
  - Fix bugs and [submit pull requests](https://github.com/ankane/ahoy/pulls)
701
784
  - Write, clarify, or fix documentation
702
785
  - Suggest or add new features
786
+
787
+ To get started with development:
788
+
789
+ ```sh
790
+ git clone https://github.com/ankane/ahoy.git
791
+ cd ahoy
792
+ bundle install
793
+ bundle exec rake test
794
+ ```
795
+
796
+ To test query methods, start PostgreSQL, MySQL, and MongoDB and use:
797
+
798
+ ```sh
799
+ createdb ahoy_test
800
+ mysqladmin create ahoy_test
801
+ bundle exec rake test:query_methods
802
+ ```
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
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.2"
2
+ VERSION = "3.2.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,18 @@ module Ahoy
29
27
  end
30
28
  end
31
29
 
30
+ # use connection_config instead of connection.adapter
31
+ # so database connection isn't needed
32
+ def adapter
33
+ if ActiveRecord::VERSION::STRING.to_f >= 6.1
34
+ ActiveRecord::Base.connection_db_config.adapter.to_s
35
+ else
36
+ ActiveRecord::Base.connection_config[:adapter].to_s
37
+ end
38
+ end
39
+
32
40
  def rails52?
33
- ActiveRecord::VERSION::STRING >= "5.2"
41
+ ActiveRecord::VERSION::STRING.to_f >= 5.2
34
42
  end
35
43
 
36
44
  def migration_version
@@ -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
@@ -2,92 +2,83 @@
2
2
  * Ahoy.js
3
3
  * Simple, powerful JavaScript analytics
4
4
  * https://github.com/ankane/ahoy.js
5
- * v0.3.4
5
+ * v0.3.8
6
6
  * MIT License
7
7
  */
8
8
 
9
9
  (function (global, factory) {
10
10
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
11
11
  typeof define === 'function' && define.amd ? define(factory) :
12
- (global.ahoy = factory());
12
+ (global = global || self, global.ahoy = factory());
13
13
  }(this, (function () { 'use strict';
14
14
 
15
- function isUndefined(value) {
16
- return value === undefined;
17
- }
15
+ var isUndefined = function (value) { return value === undefined; };
18
16
 
19
- function isNull(value) {
20
- return value === null;
21
- }
17
+ var isNull = function (value) { return value === null; };
22
18
 
23
- function isObject(value) {
24
- return value === Object(value);
25
- }
19
+ var isBoolean = function (value) { return typeof value === 'boolean'; };
26
20
 
27
- function isArray(value) {
28
- return Array.isArray(value);
29
- }
21
+ var isObject = function (value) { return value === Object(value); };
30
22
 
31
- function isDate(value) {
32
- return value instanceof Date;
33
- }
23
+ var isArray = function (value) { return Array.isArray(value); };
34
24
 
35
- function isBlob(value) {
36
- return (
37
- value &&
38
- typeof value.size === 'number' &&
39
- typeof value.type === 'string' &&
40
- typeof value.slice === 'function'
41
- );
42
- }
25
+ var isDate = function (value) { return value instanceof Date; };
43
26
 
44
- function isFile(value) {
45
- return (
46
- isBlob(value) &&
47
- (typeof value.lastModifiedDate === 'object' ||
48
- typeof value.lastModified === 'number') &&
49
- typeof value.name === 'string'
50
- );
51
- }
27
+ var isBlob = function (value) { return value &&
28
+ typeof value.size === 'number' &&
29
+ typeof value.type === 'string' &&
30
+ typeof value.slice === 'function'; };
52
31
 
53
- function isFormData(value) {
54
- return value instanceof FormData;
55
- }
56
-
57
- function objectToFormData(obj, cfg, fd, pre) {
58
- if (isFormData(cfg)) {
59
- pre = fd;
60
- fd = cfg;
61
- cfg = null;
62
- }
32
+ var isFile = function (value) { return isBlob(value) &&
33
+ typeof value.name === 'string' &&
34
+ (typeof value.lastModifiedDate === 'object' ||
35
+ typeof value.lastModified === 'number'); };
63
36
 
37
+ var serialize = function (obj, cfg, fd, pre) {
64
38
  cfg = cfg || {};
39
+
65
40
  cfg.indices = isUndefined(cfg.indices) ? false : cfg.indices;
66
- cfg.nulls = isUndefined(cfg.nulls) ? true : cfg.nulls;
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
+
67
54
  fd = fd || new FormData();
68
55
 
69
56
  if (isUndefined(obj)) {
70
57
  return fd;
71
58
  } else if (isNull(obj)) {
72
- if (cfg.nulls) {
59
+ if (!cfg.nullsAsUndefineds) {
73
60
  fd.append(pre, '');
74
61
  }
75
- } else if (isArray(obj)) {
76
- if (!obj.length) {
77
- var key = pre + '[]';
78
-
79
- fd.append(key, '');
62
+ } else if (isBoolean(obj)) {
63
+ if (cfg.booleansAsIntegers) {
64
+ fd.append(pre, obj ? 1 : 0);
80
65
  } else {
81
- obj.forEach(function(value, index) {
66
+ fd.append(pre, obj);
67
+ }
68
+ } else if (isArray(obj)) {
69
+ if (obj.length) {
70
+ obj.forEach(function (value, index) {
82
71
  var key = pre + '[' + (cfg.indices ? index : '') + ']';
83
72
 
84
- objectToFormData(value, cfg, fd, key);
73
+ serialize(value, cfg, fd, key);
85
74
  });
75
+ } else if (cfg.allowEmptyArrays) {
76
+ fd.append(pre + '[]', '');
86
77
  }
87
78
  } else if (isDate(obj)) {
88
79
  fd.append(pre, obj.toISOString());
89
80
  } else if (isObject(obj) && !isFile(obj) && !isBlob(obj)) {
90
- Object.keys(obj).forEach(function(prop) {
81
+ Object.keys(obj).forEach(function (prop) {
91
82
  var value = obj[prop];
92
83
 
93
84
  if (isArray(value)) {
@@ -98,16 +89,19 @@
98
89
 
99
90
  var key = pre ? pre + '[' + prop + ']' : prop;
100
91
 
101
- objectToFormData(value, cfg, fd, key);
92
+ serialize(value, cfg, fd, key);
102
93
  });
103
94
  } else {
104
95
  fd.append(pre, obj);
105
96
  }
106
97
 
107
98
  return fd;
108
- }
99
+ };
109
100
 
110
- var objectToFormdata = objectToFormData;
101
+ var index_module = {
102
+ serialize: serialize,
103
+ };
104
+ var index_module_1 = index_module.serialize;
111
105
 
112
106
  // https://www.quirksmode.org/js/cookies.html
113
107
 
@@ -155,7 +149,9 @@
155
149
  cookieDomain: null,
156
150
  headers: {},
157
151
  visitParams: {},
158
- withCredentials: false
152
+ withCredentials: false,
153
+ visitDuration: 4 * 60, // default 4 hours
154
+ visitorDuration: 2 * 365 * 24 * 60 // default 2 years
159
155
  };
160
156
 
161
157
  var ahoy = window.ahoy || window.Ahoy || {};
@@ -173,8 +169,6 @@
173
169
 
174
170
  var $ = window.jQuery || window.Zepto || window.$;
175
171
  var visitId, visitorId, track;
176
- var visitTtl = 4 * 60; // 4 hours
177
- var visitorTtl = 2 * 365 * 24 * 60; // 2 years
178
172
  var isReady = false;
179
173
  var queue = [];
180
174
  var canStringify = typeof(JSON) !== "undefined" && typeof(JSON.stringify) !== "undefined";
@@ -224,13 +218,13 @@
224
218
  isReady = true;
225
219
  }
226
220
 
227
- function ready(callback) {
221
+ ahoy.ready = function (callback) {
228
222
  if (isReady) {
229
223
  callback();
230
224
  } else {
231
225
  queue.push(callback);
232
226
  }
233
- }
227
+ };
234
228
 
235
229
  function matchesSelector(element, selector) {
236
230
  var matches = element.matches ||
@@ -241,24 +235,34 @@
241
235
  element.webkitMatchesSelector;
242
236
 
243
237
  if (matches) {
244
- return matches.apply(element, [selector]);
238
+ if (matches.apply(element, [selector])) {
239
+ return element;
240
+ } else if (element.parentElement) {
241
+ return matchesSelector(element.parentElement, selector)
242
+ }
243
+ return null;
245
244
  } else {
246
245
  log("Unable to match");
247
- return false;
246
+ return null;
248
247
  }
249
248
  }
250
249
 
251
250
  function onEvent(eventName, selector, callback) {
252
251
  document.addEventListener(eventName, function (e) {
253
- if (matchesSelector(e.target, selector)) {
254
- callback(e);
252
+ var matchedElement = matchesSelector(e.target, selector);
253
+ if (matchedElement) {
254
+ callback.call(matchedElement, e);
255
255
  }
256
256
  });
257
257
  }
258
258
 
259
259
  // http://beeker.io/jquery-document-ready-equivalent-vanilla-javascript
260
260
  function documentReady(callback) {
261
- document.readyState === "interactive" || document.readyState === "complete" ? callback() : document.addEventListener("DOMContentLoaded", callback);
261
+ if (document.readyState === "interactive" || document.readyState === "complete") {
262
+ setTimeout(callback, 0);
263
+ } else {
264
+ document.addEventListener("DOMContentLoaded", callback);
265
+ }
262
266
  }
263
267
 
264
268
  // https://stackoverflow.com/a/2117523/1177228
@@ -294,7 +298,7 @@
294
298
 
295
299
  function sendRequest(url, data, success) {
296
300
  if (canStringify) {
297
- if ($) {
301
+ if ($ && $.ajax) {
298
302
  $.ajax({
299
303
  type: "POST",
300
304
  url: url,
@@ -343,7 +347,7 @@
343
347
  }
344
348
 
345
349
  function trackEvent(event) {
346
- ready( function () {
350
+ ahoy.ready( function () {
347
351
  sendRequest(eventsUrl(), eventData(event), function() {
348
352
  // remove from queue
349
353
  for (var i = 0; i < eventQueue.length; i++) {
@@ -358,7 +362,7 @@
358
362
  }
359
363
 
360
364
  function trackEventNow(event) {
361
- ready( function () {
365
+ ahoy.ready( function () {
362
366
  var data = eventData(event);
363
367
  var param = csrfParam();
364
368
  var token = csrfToken();
@@ -366,7 +370,7 @@
366
370
  // stringify so we keep the type
367
371
  data.events_json = JSON.stringify(data.events);
368
372
  delete data.events;
369
- window.navigator.sendBeacon(eventsUrl(), objectToFormdata(data));
373
+ window.navigator.sendBeacon(eventsUrl(), index_module_1(data));
370
374
  });
371
375
  }
372
376
 
@@ -390,13 +394,12 @@
390
394
  }
391
395
 
392
396
  function eventProperties(e) {
393
- var target = e.target;
394
397
  return cleanObject({
395
- tag: target.tagName.toLowerCase(),
396
- id: presence(target.id),
397
- "class": presence(target.className),
398
+ tag: this.tagName.toLowerCase(),
399
+ id: presence(this.id),
400
+ "class": presence(this.className),
398
401
  page: page(),
399
- section: getClosestSection(target)
402
+ section: getClosestSection(this)
400
403
  });
401
404
  }
402
405
 
@@ -427,7 +430,7 @@
427
430
  } else {
428
431
  if (!visitId) {
429
432
  visitId = generateId();
430
- setCookie("ahoy_visit", visitId, visitTtl);
433
+ setCookie("ahoy_visit", visitId, config.visitDuration);
431
434
  }
432
435
 
433
436
  // make sure cookies are enabled
@@ -436,7 +439,7 @@
436
439
 
437
440
  if (!visitorId) {
438
441
  visitorId = generateId();
439
- setCookie("ahoy_visitor", visitorId, visitorTtl);
442
+ setCookie("ahoy_visitor", visitorId, config.visitorDuration);
440
443
  }
441
444
 
442
445
  var data = {
@@ -509,12 +512,12 @@
509
512
  js: true
510
513
  };
511
514
 
512
- ready( function () {
515
+ ahoy.ready( function () {
513
516
  if (config.cookies && !ahoy.getVisitId()) {
514
517
  createVisit();
515
518
  }
516
519
 
517
- ready( function () {
520
+ ahoy.ready( function () {
518
521
  log(event);
519
522
 
520
523
  event.visit_token = ahoy.getVisitId();
@@ -556,24 +559,23 @@
556
559
 
557
560
  ahoy.trackClicks = function () {
558
561
  onEvent("click", "a, button, input[type=submit]", function (e) {
559
- var target = e.target;
560
- var properties = eventProperties(e);
561
- properties.text = properties.tag == "input" ? target.value : (target.textContent || target.innerText || target.innerHTML).replace(/[\s\r\n]+/g, " ").trim();
562
- properties.href = target.href;
562
+ var properties = eventProperties.call(this, e);
563
+ properties.text = properties.tag == "input" ? this.value : (this.textContent || this.innerText || this.innerHTML).replace(/[\s\r\n]+/g, " ").trim();
564
+ properties.href = this.href;
563
565
  ahoy.track("$click", properties);
564
566
  });
565
567
  };
566
568
 
567
569
  ahoy.trackSubmits = function () {
568
570
  onEvent("submit", "form", function (e) {
569
- var properties = eventProperties(e);
571
+ var properties = eventProperties.call(this, e);
570
572
  ahoy.track("$submit", properties);
571
573
  });
572
574
  };
573
575
 
574
576
  ahoy.trackChanges = function () {
575
577
  onEvent("change", "input, textarea, select", function (e) {
576
- var properties = eventProperties(e);
578
+ var properties = eventProperties.call(this, e);
577
579
  ahoy.track("$change", properties);
578
580
  });
579
581
  };
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.2
4
+ version: 3.2.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-04-04 00:00:00.000000000 Z
11
+ date: 2021-03-02 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.3
131
+ signing_key:
258
132
  specification_version: 4
259
133
  summary: Simple, powerful, first-party analytics for Rails
260
134
  test_files: []