ahoy_matey 3.0.1 → 3.1.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: d90d64ae8641aa0bae1bc44acf29379a585ebbad0bbb87325380508d5a8ffcc7
4
- data.tar.gz: 7c01ce8cbd2e92e3555479b251889d171b54be9d5c369fca51f413fdc912ab01
3
+ metadata.gz: c98a3beef3f7d137de52572c02e410709d603028021a25b5e325e47fd6b2fe8c
4
+ data.tar.gz: c9304029a1f3828baac05af4397083243113019e52add5da3439fac9c111f2ac
5
5
  SHA512:
6
- metadata.gz: fe62cce407d17f736b616f11a742fefe338cdedd2457665cb3e578bafe0bd6fbb889dd315382dcb70d563a35dd5c5e05de908c069e12d21ab92e0cee637e42d7
7
- data.tar.gz: f399c7708f6c1071c72db3d9164ab77bd3377fdc0eab7064707fb1202b240156be03669070763bf2f13bcf0bfcac134286eb9e9f9fb988d78267c71da9603386
6
+ metadata.gz: 2b4d2504fda7d21993196093617a55b31bf38b58b33a744b64db6f187f9e6da5b570858ecb67aeb550b76ce623742adebbb5369b2edd2f501166dd539682b14c
7
+ data.tar.gz: e8ecbc02ed485f8856f34527351b2c293fe77afb07d4b6011c050e246c50ed92d8262639109696feadd386145633778620d3159b023a0ca27d963a2ffb83aef3
@@ -1,11 +1,35 @@
1
- ## 3.0.1
1
+ ## 3.1.0 (2020-12-04)
2
+
3
+ - Added `instance` method
4
+ - Added `request` argument to `user_method`
5
+ - Updated Ahoy.js to 0.3.8
6
+ - Removed `exclude_method` call when geocoding
7
+
8
+ ## 3.0.5 (2020-09-09)
9
+
10
+ - Added `group_prop` method
11
+ - Use `datetime` type in migration
12
+
13
+ ## 3.0.4 (2020-06-07)
14
+
15
+ - Updated Ahoy.js to 0.3.6
16
+
17
+ ## 3.0.3 (2020-04-17)
18
+
19
+ - Updated Ahoy.js to 0.3.5
20
+
21
+ ## 3.0.2 (2020-04-03)
22
+
23
+ - Added `cookie_options`
24
+
25
+ ## 3.0.1 (2019-09-21)
2
26
 
3
27
  - Made `Ahoy::Tracker` work outside of requests
4
28
  - Fixed storage of `false` values with customized store
5
29
  - Fixed error with `user_method` and `Rails::InfoController`
6
30
  - Gracefully handle `ActionDispatch::RemoteIp::IpSpoofAttackError`
7
31
 
8
- ## 3.0.0
32
+ ## 3.0.0 (2019-05-29)
9
33
 
10
34
  - Made Device Detector the default user agent parser
11
35
  - Made v2 the default bot detection version
@@ -13,18 +37,18 @@
13
37
  - Removed search keyword detection (most search engines today prevent this)
14
38
  - Removed support for Rails < 5
15
39
 
16
- ## 2.2.1
40
+ ## 2.2.1 (2019-05-26)
17
41
 
18
42
  - Updated Ahoy.js to 0.3.4
19
43
  - Fixed v2 bot detection
20
44
  - Added latitude and longitude to installation
21
45
 
22
- ## 2.2.0
46
+ ## 2.2.0 (2019-01-04)
23
47
 
24
48
  - Added `amp_event` helper
25
49
  - Improved bot detection for Device Detector
26
50
 
27
- ## 2.1.0
51
+ ## 2.1.0 (2018-05-18)
28
52
 
29
53
  - Added option for IP masking
30
54
  - Added option to use anonymity sets instead of cookies
@@ -32,19 +56,19 @@
32
56
  - Fixed `visitable` for Rails 4.2
33
57
  - Removed `search_keyword` from new installs
34
58
 
35
- ## 2.0.2
59
+ ## 2.0.2 (2018-03-14)
36
60
 
37
61
  - Fixed error on duplicate records
38
62
  - Fixed message when visit not found for geocoding
39
63
  - Better compatibility with GeoLite2
40
64
  - Better browser compatibility for Ahoy.js
41
65
 
42
- ## 2.0.1
66
+ ## 2.0.1 (2018-02-26)
43
67
 
44
68
  - Added `Ahoy.server_side_visits = :when_needed` to automatically create visits server-side when needed for events and `visitable`
45
69
  - Better handling of visit duration and expiration in JavaScript
46
70
 
47
- ## 2.0.0
71
+ ## 2.0.0 (2018-02-25)
48
72
 
49
73
  - Removed dependency on jQuery
50
74
  - Use `navigator.sendBeacon` by default in supported browsers
@@ -66,58 +90,58 @@ Breaking changes
66
90
  - Removed most built-in stores
67
91
  - Removed support for Rails < 4.2
68
92
 
69
- ## 1.6.1
93
+ ## 1.6.1 (2018-02-02)
70
94
 
71
95
  - Added `gin` index on properties for events
72
96
  - Fixed `visitable` options when name not provided
73
97
 
74
- ## 1.6.0
98
+ ## 1.6.0 (2017-05-01)
75
99
 
76
100
  - Added support for Rails 5.1
77
101
 
78
- ## 1.5.5
102
+ ## 1.5.5 (2017-03-23)
79
103
 
80
104
  - Added support for Rails API
81
105
  - Added NATS and NSQ stores
82
106
 
83
- ## 1.5.4
107
+ ## 1.5.4 (2017-01-22)
84
108
 
85
109
  - Fixed issue with duplicate events
86
110
  - Added support for PostGIS for `where_properties`
87
111
 
88
- ## 1.5.3
112
+ ## 1.5.3 (2016-10-31)
89
113
 
90
114
  - Fixed error with Rails 5 and Mongoid 6
91
115
  - Fixed regression with server not generating visit and visitor tokens
92
116
  - Accept UTM parameters as request parameters (for native apps)
93
117
 
94
- ## 1.5.2
118
+ ## 1.5.2 (2016-08-26)
95
119
 
96
120
  - Better support for Rails 5
97
121
 
98
- ## 1.5.1
122
+ ## 1.5.1 (2016-08-19)
99
123
 
100
124
  - Restored throttling after removing side effects
101
125
 
102
- ## 1.5.0
126
+ ## 1.5.0 (2016-08-19)
103
127
 
104
128
  - Removed throttling due to unintended side effects with its implementation
105
129
  - Ensure basic token requirements
106
130
  - Fixed visit recreation on cookie expiration
107
131
  - Fixed issue where `/ahoy/visits` is called indefinitely when `Ahoy.cookie_domain = :all`
108
132
 
109
- ## 1.4.2
133
+ ## 1.4.2 (2016-06-21)
110
134
 
111
135
  - Fixed issues with `where_properties`
112
136
 
113
- ## 1.4.1
137
+ ## 1.4.1 (2016-06-20)
114
138
 
115
139
  - Added `where_properties` method
116
140
  - Added Kafka store
117
141
  - Added `mount` option
118
142
  - Use less intrusive version of `safely`
119
143
 
120
- ## 1.4.0
144
+ ## 1.4.0 (2016-03-23)
121
145
 
122
146
  - Use `ActiveRecordTokenStore` by default (integer instead of uuid for id)
123
147
  - Detect database for `rails g ahoy:stores:active_record` for easier installation
@@ -125,55 +149,55 @@ Breaking changes
125
149
  - Fixed issue with log silencer
126
150
  - Use multi-column indexes on `ahoy_events` table creation
127
151
 
128
- ## 1.3.1
152
+ ## 1.3.1 (2016-03-22)
129
153
 
130
154
  - Raise errors in test environment
131
155
 
132
- ## 1.3.0
156
+ ## 1.3.0 (2016-03-06)
133
157
 
134
158
  - Added throttling
135
159
  - Added `max_content_length` and `max_events_per_request`
136
160
 
137
- ## 1.2.2
161
+ ## 1.2.2 (2016-03-05)
138
162
 
139
163
  - Fixed issue with latest version of `browser` gem
140
164
  - Added support for RabbitMQ
141
165
  - Added support for Amazon Kinesis Firehose
142
166
  - Fixed deprecation warnings in Rails 5
143
167
 
144
- ## 1.2.1
168
+ ## 1.2.1 (2015-08-14)
145
169
 
146
170
  - Fixed `SystemStackError: stack level too deep` when used with `activerecord-session_store`
147
171
 
148
- ## 1.2.0
172
+ ## 1.2.0 (2015-06-07)
149
173
 
150
174
  - Added support for PostgreSQL `jsonb` column type
151
175
  - Added Fluentd store
152
176
  - Added latitude, longitude, and postal_code to visits
153
177
  - Log exclusions
154
178
 
155
- ## 1.1.1
179
+ ## 1.1.1 (2015-01-05)
156
180
 
157
181
  - Better support for Authlogic
158
182
  - Added `screen_height` and `screen_width`
159
183
 
160
- ## 1.1.0
184
+ ## 1.1.0 (2014-11-02)
161
185
 
162
186
  - Added `geocode` option
163
187
  - Report errors to service by default
164
188
  - Fixed association mismatch
165
189
 
166
- ## 1.0.2
190
+ ## 1.0.2 (2014-07-10)
167
191
 
168
192
  - Fixed BSON for Mongoid 3
169
193
  - Fixed Doorkeeper integration
170
194
  - Fixed user tracking in overridden authenticate method
171
195
 
172
- ## 1.0.1
196
+ ## 1.0.1 (2014-06-27)
173
197
 
174
198
  - Fixed `visitable` outside of requests
175
199
 
176
- ## 1.0.0
200
+ ## 1.0.0 (2014-06-18)
177
201
 
178
202
  - Added support for any data store, and Mongoid out of the box
179
203
  - Added `track_visits_immediately` option
@@ -181,17 +205,17 @@ Breaking changes
181
205
  - Visits expire after inactivity, not fixed interval
182
206
  - Added `visit_duration` and `visitor_duration` options
183
207
 
184
- ## 0.3.2
208
+ ## 0.3.2 (2014-06-15)
185
209
 
186
210
  - Fixed bot exclusion for visits
187
211
  - Fixed user method
188
212
 
189
- ## 0.3.1
213
+ ## 0.3.1 (2014-06-12)
190
214
 
191
215
  - Fixed visitor cookies when set on server
192
216
  - Added `domain` option for server cookies
193
217
 
194
- ## 0.3.0
218
+ ## 0.3.0 (2014-06-11)
195
219
 
196
220
  - Added `current_visit_token` and `current_visitor_token` method
197
221
  - Switched to UUIDs
@@ -199,47 +223,47 @@ Breaking changes
199
223
  - Skip server-side bot events
200
224
  - Added `request` argument to `exclude_method`
201
225
 
202
- ## 0.2.2
226
+ ## 0.2.2 (2014-05-26)
203
227
 
204
228
  - Added `exclude_method` option
205
229
  - Added support for batch events
206
230
  - Fixed cookie encoding
207
231
  - Fixed `options` variable from being modified
208
232
 
209
- ## 0.2.1
233
+ ## 0.2.1 (2014-05-16)
210
234
 
211
235
  - Fixed IE 8 error
212
236
  - Added `track_bots` option
213
237
  - Added `$authenticate` event
214
238
 
215
- ## 0.2.0
239
+ ## 0.2.0 (2014-05-13)
216
240
 
217
241
  - Added event tracking (merged ahoy_events)
218
242
  - Added ahoy.js
219
243
 
220
- ## 0.1.8
244
+ ## 0.1.8 (2014-05-11)
221
245
 
222
246
  - Fixed bug with `user_type` set to `false` instead of `nil`
223
247
 
224
- ## 0.1.7
248
+ ## 0.1.7 (2014-05-11)
225
249
 
226
250
  - Made cookie functions public for ahoy_events
227
251
 
228
- ## 0.1.6
252
+ ## 0.1.6 (2014-05-07)
229
253
 
230
254
  - Better user agent parser
231
255
 
232
- ## 0.1.5
256
+ ## 0.1.5 (2014-05-01)
233
257
 
234
258
  - Added support for Doorkeeper
235
259
  - Added options to `visitable`
236
260
  - Added `landing_params` method
237
261
 
238
- ## 0.1.4
262
+ ## 0.1.4 (2014-04-27)
239
263
 
240
264
  - Added `ahoy.ready()` and `ahoy.log()` for events
241
265
 
242
- ## 0.1.3
266
+ ## 0.1.3 (2014-04-24)
243
267
 
244
268
  - Supports `current_user` from `ApplicationController`
245
269
  - Added `ahoy.reset()`
@@ -247,16 +271,16 @@ Breaking changes
247
271
  - Added experimental support for native apps
248
272
  - Prefer `ahoy` over `Ahoy`
249
273
 
250
- ## 0.1.2
274
+ ## 0.1.2 (2014-04-15)
251
275
 
252
276
  - Attach user on Devise sign up
253
277
  - Ability to specify visit model
254
278
 
255
- ## 0.1.1
279
+ ## 0.1.1 (2014-03-20)
256
280
 
257
281
  - Made most database columns optional
258
282
  - Performance hack for referer-parser
259
283
 
260
- ## 0.1.0
284
+ ## 0.1.0 (2014-03-19)
261
285
 
262
286
  - First major release
@@ -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
@@ -1,14 +1,14 @@
1
1
  # Ahoy
2
2
 
3
- :fire: Simple, powerful analytics for Rails
3
+ :fire: Simple, powerful, first-party analytics for Rails
4
4
 
5
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.
6
6
 
7
- :postbox: To track emails, check out [Ahoy Email](https://github.com/ankane/ahoy_email), and for A/B testing, check out [Field Test](https://github.com/ankane/field_test)
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,10 @@ 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.
@@ -145,7 +149,7 @@ See [Ahoy.js](https://github.com/ankane/ahoy.js) for a complete list of features
145
149
 
146
150
  #### Native Apps
147
151
 
148
- For Android, check out [Ahoy Android](https://github.com/instacart/ahoy-android). For other platforms, see the [API spec](#api-spec).
152
+ See the docs for [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android).
149
153
 
150
154
  #### AMP
151
155
 
@@ -292,7 +296,7 @@ By default, a new visit is created after 4 hours of inactivity. Change this with
292
296
  Ahoy.visit_duration = 30.minutes
293
297
  ```
294
298
 
295
- ### Multiple Subdomains
299
+ ### Cookies
296
300
 
297
301
  To track visits across multiple subdomains, use:
298
302
 
@@ -300,6 +304,12 @@ To track visits across multiple subdomains, use:
300
304
  Ahoy.cookie_domain = :all
301
305
  ```
302
306
 
307
+ Set other [cookie options](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html) with:
308
+
309
+ ```ruby
310
+ Ahoy.cookie_options = {same_site: :lax}
311
+ ```
312
+
303
313
  ### Geocoding
304
314
 
305
315
  Disable geocoding with:
@@ -308,7 +318,7 @@ Disable geocoding with:
308
318
  Ahoy.geocode = false
309
319
  ```
310
320
 
311
- Change the job queue with:
321
+ The default job queue is `:ahoy`. Change this with:
312
322
 
313
323
  ```ruby
314
324
  Ahoy.job_queue = :low_priority
@@ -427,6 +437,34 @@ Ahoy.cookies = false
427
437
 
428
438
  Previously set cookies are automatically deleted.
429
439
 
440
+ ## Data Retention
441
+
442
+ Data should only be retained for as long as it’s needed. Delete older data with:
443
+
444
+ ```ruby
445
+ Ahoy::Visit.where("started_at < ?", 2.years.ago).find_in_batches do |visits|
446
+ visit_ids = visits.map(&:id)
447
+ Ahoy::Event.where(visit_id: visit_ids).delete_all
448
+ Ahoy::Visit.where(id: visit_ids).delete_all
449
+ end
450
+ ```
451
+
452
+ You can use [Rollup](https://github.com/ankane/rollup) to aggregate important data before you do.
453
+
454
+ ```ruby
455
+ Ahoy::Visit.rollup("Visits", interval: "hour")
456
+ ```
457
+
458
+ Delete data for a specific user with:
459
+
460
+ ```ruby
461
+ user_id = 123
462
+ visit_ids = Ahoy::Visit.where(user_id: user_id).pluck(:id)
463
+ Ahoy::Event.where(visit_id: visit_ids).delete_all
464
+ Ahoy::Visit.where(id: visit_ids).delete_all
465
+ Ahoy::Event.where(user_id: user_id).delete_all
466
+ ```
467
+
430
468
  ## Development
431
469
 
432
470
  Ahoy is built with developers in mind. You can run the following code in your browser’s console.
@@ -552,7 +590,7 @@ Ahoy::Visit.group(:referring_domain).count
552
590
 
553
591
  ### Querying Events
554
592
 
555
- Ahoy provides two methods on the event model to make querying easier.
593
+ Ahoy provides a few methods on the event model to make querying easier.
556
594
 
557
595
  To query on both name and properties, you can use:
558
596
 
@@ -563,9 +601,17 @@ Ahoy::Event.where_event("Viewed product", product_id: 123).count
563
601
  Or just query properties with:
564
602
 
565
603
  ```ruby
566
- Ahoy::Event.where_props(product_id: 123).count
604
+ Ahoy::Event.where_props(product_id: 123, category: "Books").count
605
+ ```
606
+
607
+ Group by properties with:
608
+
609
+ ```ruby
610
+ Ahoy::Event.group_prop(:product_id, :category).count
567
611
  ```
568
612
 
613
+ Note: MySQL and MariaDB always return string keys (include `"null"` for `nil`) for `group_prop`.
614
+
569
615
  ### Funnels
570
616
 
571
617
  It’s easy to create funnels.
@@ -578,6 +624,29 @@ viewed_checkout_ids = Ahoy::Event.where(user_id: added_item_ids, name: "Viewed c
578
624
 
579
625
  The same approach also works with visitor tokens.
580
626
 
627
+ ### Rollups
628
+
629
+ Improve query performance by pre-aggregating data with [Rollup](https://github.com/ankane/rollup).
630
+
631
+ ```ruby
632
+ Ahoy::Event.where(name: "Viewed store").rollup("Store views")
633
+ ```
634
+
635
+ This is only needed if you have a lot of data.
636
+
637
+ ### Forecasting
638
+
639
+ To forecast future visits and events, check out [Prophet](https://github.com/ankane/prophet).
640
+
641
+ ```ruby
642
+ daily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate
643
+ Prophet.forecast(daily_visits)
644
+ ```
645
+
646
+ ### Recommendations
647
+
648
+ To make recommendations based on events, check out [Disco](https://github.com/ankane/disco#ahoy).
649
+
581
650
  ## Tutorials
582
651
 
583
652
  - [Tracking Metrics with Ahoy and Blazer](https://gorails.com/episodes/internal-metrics-with-ahoy-and-blazer)
@@ -694,3 +763,20 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
694
763
  - Fix bugs and [submit pull requests](https://github.com/ankane/ahoy/pulls)
695
764
  - Write, clarify, or fix documentation
696
765
  - Suggest or add new features
766
+
767
+ To get started with development:
768
+
769
+ ```sh
770
+ git clone https://github.com/ankane/ahoy.git
771
+ cd ahoy
772
+ bundle install
773
+ bundle exec rake test
774
+ ```
775
+
776
+ To test query methods, start PostgreSQL, MySQL, and MongoDB and use:
777
+
778
+ ```sh
779
+ createdb ahoy_test
780
+ mysqladmin create ahoy_test
781
+ bundle exec rake test:query_methods
782
+ ```
@@ -30,8 +30,12 @@ module Ahoy
30
30
  mattr_accessor :cookies
31
31
  self.cookies = true
32
32
 
33
+ # TODO deprecate in favor of cookie_options
33
34
  mattr_accessor :cookie_domain
34
35
 
36
+ mattr_accessor :cookie_options
37
+ self.cookie_options = {}
38
+
35
39
  mattr_accessor :server_side_visits
36
40
  self.server_side_visits = true
37
41
 
@@ -100,6 +104,14 @@ module Ahoy
100
104
  addr.mask(48).to_s
101
105
  end
102
106
  end
107
+
108
+ def self.instance
109
+ Thread.current[:ahoy]
110
+ end
111
+
112
+ def self.instance=(value)
113
+ Thread.current[:ahoy] = value
114
+ end
103
115
  end
104
116
 
105
117
  ActiveSupport.on_load(:action_controller) do
@@ -115,6 +127,10 @@ ActiveSupport.on_load(:action_view) do
115
127
  end
116
128
 
117
129
  # Mongoid
130
+ # TODO use
131
+ # ActiveSupport.on_load(:mongoid) do
132
+ # Mongoid::Document::ClassMethods.include(Ahoy::Model)
133
+ # end
118
134
  if defined?(ActiveModel)
119
135
  ActiveModel::Callbacks.include(Ahoy::Model)
120
136
  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
@@ -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
@@ -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
@@ -171,12 +167,11 @@ module Ahoy
171
167
  # safety net
172
168
  return unless Ahoy.cookies && request
173
169
 
174
- cookie = {
175
- value: value
176
- }
170
+ cookie = Ahoy.cookie_options.merge(value: value)
177
171
  cookie[:expires] = duration.from_now if duration
178
- domain = Ahoy.cookie_domain
179
- cookie[:domain] = domain if domain && use_domain
172
+ # prefer cookie_options[:domain] over cookie_domain
173
+ cookie[:domain] ||= Ahoy.cookie_domain if Ahoy.cookie_domain
174
+ cookie.delete(:domain) unless use_domain
180
175
  request.cookie_jar[name] = cookie
181
176
  end
182
177
 
@@ -1,3 +1,3 @@
1
1
  module Ahoy
2
- VERSION = "3.0.1"
2
+ VERSION = "3.1.0"
3
3
  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? %>
@@ -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.1
4
+ version: 3.1.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: 2019-09-22 00:00:00.000000000 Z
11
+ date: 2020-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -192,7 +192,35 @@ dependencies:
192
192
  - - ">="
193
193
  - !ruby/object:Gem::Version
194
194
  version: '0'
195
- description:
195
+ - !ruby/object:Gem::Dependency
196
+ name: browser
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '2.0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '2.0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: user_agent_parser
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ description:
196
224
  email: andrew@chartkick.com
197
225
  executables: []
198
226
  extensions: []
@@ -238,7 +266,7 @@ homepage: https://github.com/ankane/ahoy
238
266
  licenses:
239
267
  - MIT
240
268
  metadata: {}
241
- post_install_message:
269
+ post_install_message:
242
270
  rdoc_options: []
243
271
  require_paths:
244
272
  - lib
@@ -253,8 +281,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
253
281
  - !ruby/object:Gem::Version
254
282
  version: '0'
255
283
  requirements: []
256
- rubygems_version: 3.0.3
257
- signing_key:
284
+ rubygems_version: 3.1.4
285
+ signing_key:
258
286
  specification_version: 4
259
- summary: Simple, powerful analytics for Rails
287
+ summary: Simple, powerful, first-party analytics for Rails
260
288
  test_files: []