nano-pure-pkg 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/ahoy_matey-5.5.0/CHANGELOG.md +403 -0
  3. data/ahoy_matey-5.5.0/CONTRIBUTING.md +42 -0
  4. data/ahoy_matey-5.5.0/LICENSE.txt +22 -0
  5. data/ahoy_matey-5.5.0/README.md +802 -0
  6. data/ahoy_matey-5.5.0/app/controllers/ahoy/base_controller.rb +44 -0
  7. data/ahoy_matey-5.5.0/app/controllers/ahoy/events_controller.rb +51 -0
  8. data/ahoy_matey-5.5.0/app/controllers/ahoy/visits_controller.rb +15 -0
  9. data/ahoy_matey-5.5.0/config/routes.rb +10 -0
  10. data/ahoy_matey-5.5.0/lib/ahoy/base_store.rb +104 -0
  11. data/ahoy_matey-5.5.0/lib/ahoy/controller.rb +56 -0
  12. data/ahoy_matey-5.5.0/lib/ahoy/database_store.rb +96 -0
  13. data/ahoy_matey-5.5.0/lib/ahoy/engine.rb +37 -0
  14. data/ahoy_matey-5.5.0/lib/ahoy/geocode_v2_job.rb +31 -0
  15. data/ahoy_matey-5.5.0/lib/ahoy/helper.rb +40 -0
  16. data/ahoy_matey-5.5.0/lib/ahoy/model.rb +15 -0
  17. data/ahoy_matey-5.5.0/lib/ahoy/query_methods.rb +88 -0
  18. data/ahoy_matey-5.5.0/lib/ahoy/tracker.rb +287 -0
  19. data/ahoy_matey-5.5.0/lib/ahoy/utils.rb +7 -0
  20. data/ahoy_matey-5.5.0/lib/ahoy/version.rb +3 -0
  21. data/ahoy_matey-5.5.0/lib/ahoy/visit_properties.rb +122 -0
  22. data/ahoy_matey-5.5.0/lib/ahoy/warden.rb +5 -0
  23. data/ahoy_matey-5.5.0/lib/ahoy.rb +167 -0
  24. data/ahoy_matey-5.5.0/lib/ahoy_matey.rb +1 -0
  25. data/ahoy_matey-5.5.0/lib/generators/ahoy/activerecord_generator.rb +59 -0
  26. data/ahoy_matey-5.5.0/lib/generators/ahoy/base_generator.rb +13 -0
  27. data/ahoy_matey-5.5.0/lib/generators/ahoy/install_generator.rb +44 -0
  28. data/ahoy_matey-5.5.0/lib/generators/ahoy/mongoid_generator.rb +16 -0
  29. data/ahoy_matey-5.5.0/lib/generators/ahoy/templates/active_record_event_model.rb.tt +10 -0
  30. data/ahoy_matey-5.5.0/lib/generators/ahoy/templates/active_record_migration.rb.tt +62 -0
  31. data/ahoy_matey-5.5.0/lib/generators/ahoy/templates/active_record_visit_model.rb.tt +6 -0
  32. data/ahoy_matey-5.5.0/lib/generators/ahoy/templates/base_store_initializer.rb.tt +25 -0
  33. data/ahoy_matey-5.5.0/lib/generators/ahoy/templates/database_store_initializer.rb.tt +10 -0
  34. data/ahoy_matey-5.5.0/lib/generators/ahoy/templates/mongoid_event_model.rb.tt +14 -0
  35. data/ahoy_matey-5.5.0/lib/generators/ahoy/templates/mongoid_visit_model.rb.tt +50 -0
  36. data/ahoy_matey-5.5.0/vendor/assets/javascripts/ahoy.js +544 -0
  37. data/nano-pure-pkg.gemspec +12 -0
  38. metadata +77 -0
@@ -0,0 +1,802 @@
1
+ # Ahoy
2
+
3
+ :fire: Simple, powerful, first-party analytics for Rails
4
+
5
+ Track visits and events in Ruby, JavaScript, and native apps. Data is stored in your database by default, and you can customize it for any data store as you grow.
6
+
7
+ :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
+
9
+ :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
10
+
11
+ [![Build Status](https://github.com/ankane/ahoy/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/ahoy/actions)
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application’s Gemfile:
16
+
17
+ ```ruby
18
+ gem "ahoy_matey"
19
+ ```
20
+
21
+ And run:
22
+
23
+ ```sh
24
+ bundle install
25
+ rails generate ahoy:install
26
+ rails db:migrate
27
+ ```
28
+
29
+ Restart your web server, open a page in your browser, and a visit will be created :tada:
30
+
31
+ Track your first event from a controller with:
32
+
33
+ ```ruby
34
+ ahoy.track "My first event", language: "Ruby"
35
+ ```
36
+
37
+ ### JavaScript, Native Apps, & AMP
38
+
39
+ Enable the API in `config/initializers/ahoy.rb`:
40
+
41
+ ```ruby
42
+ Ahoy.api = true
43
+ ```
44
+
45
+ And restart your web server.
46
+
47
+ ### JavaScript
48
+
49
+ For Importmap (Rails default), add to `config/importmap.rb`:
50
+
51
+ ```ruby
52
+ pin "ahoy", to: "ahoy.js"
53
+ ```
54
+
55
+ And add to `app/javascript/application.js`:
56
+
57
+ ```javascript
58
+ import "ahoy"
59
+ ```
60
+
61
+ For Bun, esbuild, rollup.js, or Webpack, run:
62
+
63
+ ```sh
64
+ bun add ahoy.js
65
+ # or
66
+ yarn add ahoy.js
67
+ ```
68
+
69
+ And add to `app/javascript/application.js`:
70
+
71
+ ```javascript
72
+ import ahoy from "ahoy.js"
73
+ ```
74
+
75
+ For Sprockets, add to `app/assets/javascripts/application.js`:
76
+
77
+ ```javascript
78
+ //= require ahoy
79
+ ```
80
+
81
+ Track an event with:
82
+
83
+ ```javascript
84
+ ahoy.track("My second event", {language: "JavaScript"});
85
+ ```
86
+
87
+ ### Native Apps
88
+
89
+ Check out [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android).
90
+
91
+ ### Geocoding Setup
92
+
93
+ To enable geocoding, see the [Geocoding section](#geocoding).
94
+
95
+ ### GDPR Compliance
96
+
97
+ Ahoy provides a number of options to help with GDPR compliance. See the [GDPR section](#gdpr-compliance-1) for more info.
98
+
99
+ ## How It Works
100
+
101
+ ### Visits
102
+
103
+ When someone visits your website, Ahoy creates a visit with lots of useful information.
104
+
105
+ - **traffic source** - referrer, referring domain, landing page
106
+ - **location** - country, region, city, latitude, longitude
107
+ - **technology** - browser, OS, device type
108
+ - **utm parameters** - source, medium, term, content, campaign
109
+
110
+ Use the `current_visit` method to access it.
111
+
112
+ Prevent certain Rails actions from creating visits with:
113
+
114
+ ```ruby
115
+ skip_before_action :track_ahoy_visit
116
+ ```
117
+
118
+ This is typically useful for APIs. If your entire Rails app is an API, you can use:
119
+
120
+ ```ruby
121
+ Ahoy.api_only = true
122
+ ```
123
+
124
+ You can also defer visit tracking to JavaScript. This is useful for preventing bots (that aren’t detected by their user agent) and users with cookies disabled from creating a new visit on each request. `:when_needed` will create visits server-side only when needed by events, and `false` will disable server-side creation completely, discarding events without a visit.
125
+
126
+ ```ruby
127
+ Ahoy.server_side_visits = :when_needed
128
+ ```
129
+
130
+ ### Events
131
+
132
+ Each event has a `name` and `properties`. There are several ways to track events.
133
+
134
+ #### Ruby
135
+
136
+ ```ruby
137
+ ahoy.track "Viewed book", title: "Hot, Flat, and Crowded"
138
+ ```
139
+
140
+ Track actions automatically with:
141
+
142
+ ```ruby
143
+ class ApplicationController < ActionController::Base
144
+ after_action :track_action
145
+
146
+ protected
147
+
148
+ def track_action
149
+ ahoy.track "Ran action", request.path_parameters
150
+ end
151
+ end
152
+ ```
153
+
154
+ #### JavaScript
155
+
156
+ ```javascript
157
+ ahoy.track("Viewed book", {title: "The World is Flat"});
158
+ ```
159
+
160
+ See [Ahoy.js](https://github.com/ankane/ahoy.js) for a complete list of features.
161
+
162
+ #### Native Apps
163
+
164
+ See the docs for [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android).
165
+
166
+ #### AMP
167
+
168
+ ```erb
169
+ <head>
170
+ <script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
171
+ </head>
172
+ <body>
173
+ <%= amp_event "Viewed article", title: "Analytics with Rails" %>
174
+ </body>
175
+ ```
176
+
177
+ ### Associated Models
178
+
179
+ Say we want to associate orders with visits. Just add `visitable` to the model.
180
+
181
+ ```ruby
182
+ class Order < ApplicationRecord
183
+ visitable :ahoy_visit
184
+ end
185
+ ```
186
+
187
+ When a visitor places an order, the `ahoy_visit_id` column is automatically set :tada:
188
+
189
+ See where orders are coming from with simple joins:
190
+
191
+ ```ruby
192
+ Order.joins(:ahoy_visit).group("referring_domain").count
193
+ Order.joins(:ahoy_visit).group("city").count
194
+ Order.joins(:ahoy_visit).group("device_type").count
195
+ ```
196
+
197
+ Here’s what the migration to add the `ahoy_visit_id` column should look like:
198
+
199
+ ```ruby
200
+ class AddAhoyVisitToOrders < ActiveRecord::Migration[8.1]
201
+ def change
202
+ add_reference :orders, :ahoy_visit
203
+ end
204
+ end
205
+ ```
206
+
207
+ Customize the column with:
208
+
209
+ ```ruby
210
+ visitable :sign_up_visit
211
+ ```
212
+
213
+ ### Users
214
+
215
+ Ahoy automatically attaches the `current_user` to the visit. With [Devise](https://github.com/heartcombo/devise), it attaches the user even if they sign in after the visit starts.
216
+
217
+ With other authentication frameworks, add this to the end of your sign in method:
218
+
219
+ ```ruby
220
+ ahoy.authenticate(user)
221
+ ```
222
+
223
+ To see the visits for a given user, create an association:
224
+
225
+ ```ruby
226
+ class User < ApplicationRecord
227
+ has_many :visits, class_name: "Ahoy::Visit"
228
+ end
229
+ ```
230
+
231
+ And use:
232
+
233
+ ```ruby
234
+ User.find(123).visits
235
+ ```
236
+
237
+ #### Custom User Method
238
+
239
+ Use a method besides `current_user`
240
+
241
+ ```ruby
242
+ Ahoy.user_method = :true_user
243
+ ```
244
+
245
+ or use a proc
246
+
247
+ ```ruby
248
+ Ahoy.user_method = ->(controller) { controller.true_user }
249
+ ```
250
+
251
+ #### Doorkeeper
252
+
253
+ To attach the user with [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper), be sure you have a `current_resource_owner` method in `ApplicationController`.
254
+
255
+ ```ruby
256
+ class ApplicationController < ActionController::Base
257
+ private
258
+
259
+ def current_resource_owner
260
+ User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
261
+ end
262
+ end
263
+ ```
264
+
265
+ ### Exclusions
266
+
267
+ Bots are excluded from tracking by default. To include them, use:
268
+
269
+ ```ruby
270
+ Ahoy.track_bots = true
271
+ ```
272
+
273
+ Add your own rules with:
274
+
275
+ ```ruby
276
+ Ahoy.exclude_method = lambda do |controller, request|
277
+ request.ip == "192.168.1.1"
278
+ end
279
+ ```
280
+
281
+ ### Visit Duration
282
+
283
+ By default, a new visit is created after 4 hours of inactivity. Change this with:
284
+
285
+ ```ruby
286
+ Ahoy.visit_duration = 30.minutes
287
+ ```
288
+
289
+ ### Visitor Duration
290
+
291
+ By default, a new `visitor_token` is generated after 2 years. Change this with:
292
+
293
+ ```ruby
294
+ Ahoy.visitor_duration = 30.days
295
+ ```
296
+
297
+ ### Cookies
298
+
299
+ To track visits across multiple subdomains, use:
300
+
301
+ ```ruby
302
+ Ahoy.cookie_domain = :all
303
+ ```
304
+
305
+ Set other [cookie options](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html) with:
306
+
307
+ ```ruby
308
+ Ahoy.cookie_options = {same_site: :lax}
309
+ ```
310
+
311
+ You can also [disable cookies](#anonymity-sets--cookies)
312
+
313
+ ### Token Generation
314
+
315
+ Ahoy uses random UUIDs for visit and visitor tokens by default, but you can use your own generator like [ULID](https://github.com/rafaelsales/ulid).
316
+
317
+ ```ruby
318
+ Ahoy.token_generator = -> { ULID.generate }
319
+ ```
320
+
321
+ ### Throttling
322
+
323
+ You can use [Rack::Attack](https://github.com/rack/rack-attack) to throttle requests to the API.
324
+
325
+ ```ruby
326
+ class Rack::Attack
327
+ throttle("ahoy/ip", limit: 20, period: 1.minute) do |req|
328
+ if req.path.start_with?("/ahoy/")
329
+ req.ip
330
+ end
331
+ end
332
+ end
333
+ ```
334
+
335
+ ### Exceptions
336
+
337
+ 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:
338
+
339
+ ```ruby
340
+ Safely.report_exception_method = ->(e) { Rollbar.error(e) }
341
+ ```
342
+
343
+ ## Geocoding
344
+
345
+ 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).
346
+
347
+ To enable geocoding, add this line to your application’s Gemfile:
348
+
349
+ ```ruby
350
+ gem "geocoder"
351
+ ```
352
+
353
+ And 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.
368
+
369
+ For city-level geocoding, download the [GeoLite2 City database](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data).
370
+
371
+ Add this line to your application’s Gemfile:
372
+
373
+ ```ruby
374
+ gem "maxminddb"
375
+ ```
376
+
377
+ And create `config/initializers/geocoder.rb` with:
378
+
379
+ ```ruby
380
+ Geocoder.configure(
381
+ ip_lookup: :geoip2,
382
+ geoip2: {
383
+ file: "path/to/GeoLite2-City.mmdb"
384
+ }
385
+ )
386
+ ```
387
+
388
+ For country-level geocoding, install the `geoip-database` package. It’s preinstalled on Heroku. For Ubuntu, use:
389
+
390
+ ```sh
391
+ sudo apt-get install geoip-database
392
+ ```
393
+
394
+ Add this line to your application’s Gemfile:
395
+
396
+ ```ruby
397
+ gem "geoip"
398
+ ```
399
+
400
+ And create `config/initializers/geocoder.rb` with:
401
+
402
+ ```ruby
403
+ Geocoder.configure(
404
+ ip_lookup: :maxmind_local,
405
+ maxmind_local: {
406
+ file: "/usr/share/GeoIP/GeoIP.dat",
407
+ package: :country
408
+ }
409
+ )
410
+ ```
411
+
412
+ ### Load Balancer Geocoding
413
+
414
+ Some load balancers can add geocoding information to request headers.
415
+
416
+ - [nginx](https://nginx.org/en/docs/http/ngx_http_geoip_module.html)
417
+ - [Google Cloud](https://cloud.google.com/load-balancing/docs/custom-headers)
418
+ - [Cloudflare](https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-Cloudflare-IP-Geolocation)
419
+
420
+ Update `config/initializers/ahoy.rb` with:
421
+
422
+ ```ruby
423
+ Ahoy.geocode = false
424
+
425
+ class Ahoy::Store < Ahoy::DatabaseStore
426
+ def track_visit(data)
427
+ data[:country] = request.headers["<country-header>"]
428
+ data[:region] = request.headers["<region-header>"]
429
+ data[:city] = request.headers["<city-header>"]
430
+ super(data)
431
+ end
432
+ end
433
+ ```
434
+
435
+ ## GDPR Compliance
436
+
437
+ Ahoy provides a number of options to help with [GDPR compliance](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation).
438
+
439
+ Update `config/initializers/ahoy.rb` with:
440
+
441
+ ```ruby
442
+ class Ahoy::Store < Ahoy::DatabaseStore
443
+ def authenticate(data)
444
+ # disables automatic linking of visits and users
445
+ end
446
+ end
447
+
448
+ Ahoy.mask_ips = true
449
+ Ahoy.cookies = :none
450
+ ```
451
+
452
+ This:
453
+
454
+ - Masks IP addresses
455
+ - Switches from cookies to anonymity sets
456
+ - Disables automatic linking of visits and users
457
+
458
+ If you use JavaScript tracking, also set:
459
+
460
+ ```javascript
461
+ ahoy.configure({cookies: false});
462
+ ```
463
+
464
+ ### IP Masking
465
+
466
+ Ahoy can mask IPs with the same approach [Google Analytics uses for IP anonymization](https://support.google.com/analytics/answer/2763052). This means:
467
+
468
+ - For IPv4, the last octet is set to 0 (`8.8.4.4` becomes `8.8.4.0`)
469
+ - For IPv6, the last 80 bits are set to zeros (`2001:4860:4860:0:0:0:0:8844` becomes `2001:4860:4860::`)
470
+
471
+ ```ruby
472
+ Ahoy.mask_ips = true
473
+ ```
474
+
475
+ IPs are masked before geolocation is performed.
476
+
477
+ To mask previously collected IPs, use:
478
+
479
+ ```ruby
480
+ Ahoy::Visit.find_each do |visit|
481
+ visit.update_column :ip, Ahoy.mask_ip(visit.ip)
482
+ end
483
+ ```
484
+
485
+ ### Anonymity Sets & Cookies
486
+
487
+ Ahoy can switch from cookies to [anonymity sets](https://privacypatterns.org/patterns/Anonymity-set). Instead of cookies, visitors with the same IP mask and user agent are grouped together in an anonymity set.
488
+
489
+ ```ruby
490
+ Ahoy.cookies = :none
491
+ ```
492
+
493
+ Note: If Ahoy was installed before v5, [add an index](https://github.com/ankane/ahoy/tree/v5.0.0?tab=readme-ov-file#50) before making this change.
494
+
495
+ Previously set cookies are automatically deleted. If you use JavaScript tracking, also set:
496
+
497
+ ```javascript
498
+ ahoy.configure({cookies: false});
499
+ ```
500
+
501
+ ## Data Retention
502
+
503
+ Data should only be retained for as long as it’s needed. Delete older data with:
504
+
505
+ ```ruby
506
+ Ahoy::Visit.where("started_at < ?", 2.years.ago).find_in_batches do |visits|
507
+ visit_ids = visits.map(&:id)
508
+ Ahoy::Event.where(visit_id: visit_ids).delete_all
509
+ Ahoy::Visit.where(id: visit_ids).delete_all
510
+ end
511
+ ```
512
+
513
+ You can use [Rollup](https://github.com/ankane/rollup) to aggregate important data before you do.
514
+
515
+ ```ruby
516
+ Ahoy::Visit.rollup("Visits", interval: "hour")
517
+ ```
518
+
519
+ Delete data for a specific user with:
520
+
521
+ ```ruby
522
+ user_id = 123
523
+ visit_ids = Ahoy::Visit.where(user_id: user_id).pluck(:id)
524
+ Ahoy::Event.where(visit_id: visit_ids).delete_all
525
+ Ahoy::Visit.where(id: visit_ids).delete_all
526
+ Ahoy::Event.where(user_id: user_id).delete_all
527
+ ```
528
+
529
+ ## Development
530
+
531
+ Ahoy is built with developers in mind. You can run the following code in your browser’s console.
532
+
533
+ Force a new visit
534
+
535
+ ```javascript
536
+ ahoy.reset(); // then reload the page
537
+ ```
538
+
539
+ Log messages
540
+
541
+ ```javascript
542
+ ahoy.debug();
543
+ ```
544
+
545
+ Turn off logging
546
+
547
+ ```javascript
548
+ ahoy.debug(false);
549
+ ```
550
+
551
+ Debug API requests in Ruby
552
+
553
+ ```ruby
554
+ Ahoy.quiet = false
555
+ ```
556
+
557
+ ## Data Stores
558
+
559
+ Data tracked by Ahoy is sent to your data store. Ahoy ships with a data store that uses your Rails database by default. You can find it in `config/initializers/ahoy.rb`:
560
+
561
+ ```ruby
562
+ class Ahoy::Store < Ahoy::DatabaseStore
563
+ end
564
+ ```
565
+
566
+ There are four events data stores can subscribe to:
567
+
568
+ ```ruby
569
+ class Ahoy::Store < Ahoy::BaseStore
570
+ def track_visit(data)
571
+ # new visit
572
+ end
573
+
574
+ def track_event(data)
575
+ # new event
576
+ end
577
+
578
+ def geocode(data)
579
+ # visit geocoded
580
+ end
581
+
582
+ def authenticate(data)
583
+ # user authenticates
584
+ end
585
+ end
586
+ ```
587
+
588
+ Data stores are designed to be highly customizable so you can scale as you grow. Check out [examples](docs/Data-Store-Examples.md) for Kafka, RabbitMQ, Fluentd, NATS, NSQ, and Amazon Kinesis Firehose.
589
+
590
+ ### Track Additional Data
591
+
592
+ ```ruby
593
+ class Ahoy::Store < Ahoy::DatabaseStore
594
+ def track_visit(data)
595
+ data[:accept_language] = request.headers["Accept-Language"]
596
+ super(data)
597
+ end
598
+ end
599
+ ```
600
+
601
+ Two useful methods you can use are `request` and `controller`.
602
+
603
+ You can pass additional visit data from JavaScript with:
604
+
605
+ ```javascript
606
+ ahoy.configure({visitParams: {referral_code: 123}});
607
+ ```
608
+
609
+ And use:
610
+
611
+ ```ruby
612
+ class Ahoy::Store < Ahoy::DatabaseStore
613
+ def track_visit(data)
614
+ data[:referral_code] = request.parameters[:referral_code]
615
+ super(data)
616
+ end
617
+ end
618
+ ```
619
+
620
+ ### Use Different Models
621
+
622
+ ```ruby
623
+ class Ahoy::Store < Ahoy::DatabaseStore
624
+ def visit_model
625
+ MyVisit
626
+ end
627
+
628
+ def event_model
629
+ MyEvent
630
+ end
631
+ end
632
+ ```
633
+
634
+ ## Explore the Data
635
+
636
+ [Blazer](https://github.com/ankane/blazer) is a great tool for exploring your data.
637
+
638
+ With Active Record, you can do:
639
+
640
+ ```ruby
641
+ Ahoy::Visit.group(:search_keyword).count
642
+ Ahoy::Visit.group(:country).count
643
+ Ahoy::Visit.group(:referring_domain).count
644
+ ```
645
+
646
+ [Chartkick](https://www.chartkick.com/) and [Groupdate](https://github.com/ankane/groupdate) make it easy to visualize the data.
647
+
648
+ ```erb
649
+ <%= line_chart Ahoy::Visit.group_by_day(:started_at).count %>
650
+ ```
651
+
652
+ ### Querying Events
653
+
654
+ Ahoy provides a few methods on the event model to make querying easier.
655
+
656
+ To query on both name and properties, you can use:
657
+
658
+ ```ruby
659
+ Ahoy::Event.where_event("Viewed product", product_id: 123).count
660
+ ```
661
+
662
+ Or just query properties with:
663
+
664
+ ```ruby
665
+ Ahoy::Event.where_props(product_id: 123, category: "Books").count
666
+ ```
667
+
668
+ Group by properties with:
669
+
670
+ ```ruby
671
+ Ahoy::Event.group_prop(:product_id, :category).count
672
+ ```
673
+
674
+ Note: MySQL and MariaDB always return string keys (including `"null"` for `nil`) for `group_prop`.
675
+
676
+ ### Funnels
677
+
678
+ It’s easy to create funnels.
679
+
680
+ ```ruby
681
+ viewed_store_ids = Ahoy::Event.where(name: "Viewed store").distinct.pluck(:user_id)
682
+ added_item_ids = Ahoy::Event.where(user_id: viewed_store_ids, name: "Added item to cart").distinct.pluck(:user_id)
683
+ viewed_checkout_ids = Ahoy::Event.where(user_id: added_item_ids, name: "Viewed checkout").distinct.pluck(:user_id)
684
+ ```
685
+
686
+ The same approach also works with visitor tokens.
687
+
688
+ ### Rollups
689
+
690
+ Improve query performance by pre-aggregating data with [Rollup](https://github.com/ankane/rollup).
691
+
692
+ ```ruby
693
+ Ahoy::Event.where(name: "Viewed store").rollup("Store views")
694
+ ```
695
+
696
+ This is only needed if you have a lot of data.
697
+
698
+ ### Forecasting
699
+
700
+ To forecast future visits and events, check out [Prophet](https://github.com/ankane/prophet).
701
+
702
+ ```ruby
703
+ daily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate
704
+ Prophet.forecast(daily_visits)
705
+ ```
706
+
707
+ ### Anomaly Detection
708
+
709
+ To detect anomalies in visits and events, check out [AnomalyDetection.rb](https://github.com/ankane/AnomalyDetection.rb).
710
+
711
+ ```ruby
712
+ daily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate
713
+ AnomalyDetection.detect(daily_visits, period: 7)
714
+ ```
715
+
716
+ ### Breakout Detection
717
+
718
+ To detect breakouts in visits and events, check out [Breakout](https://github.com/ankane/breakout).
719
+
720
+ ```ruby
721
+ daily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate
722
+ Breakout.detect(daily_visits)
723
+ ```
724
+
725
+ ### Recommendations
726
+
727
+ To make recommendations based on events, check out [Disco](https://github.com/ankane/disco#ahoy).
728
+
729
+ ## Tutorials
730
+
731
+ - [Tracking Metrics with Ahoy and Blazer](https://gorails.com/episodes/internal-metrics-with-ahoy-and-blazer)
732
+
733
+ ## API Spec
734
+
735
+ ### Visits
736
+
737
+ Generate visit and visitor tokens as [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier), and include these values in the `Ahoy-Visit` and `Ahoy-Visitor` headers with all requests.
738
+
739
+ Send a `POST` request to `/ahoy/visits` with `Content-Type: application/json` and a body like:
740
+
741
+ ```json
742
+ {
743
+ "visit_token": "<visit-token>",
744
+ "visitor_token": "<visitor-token>",
745
+ "platform": "iOS",
746
+ "app_version": "1.0.0",
747
+ "os_version": "11.2.6"
748
+ }
749
+ ```
750
+
751
+ After 4 hours of inactivity, create another visit (use the same visitor token).
752
+
753
+ ### Events
754
+
755
+ Send a `POST` request to `/ahoy/events` with `Content-Type: application/json` and a body like:
756
+
757
+ ```json
758
+ {
759
+ "visit_token": "<visit-token>",
760
+ "visitor_token": "<visitor-token>",
761
+ "events": [
762
+ {
763
+ "id": "<optional-random-id>",
764
+ "name": "Viewed item",
765
+ "properties": {
766
+ "item_id": 123
767
+ },
768
+ "time": "2025-01-01T00:00:00-07:00"
769
+ }
770
+ ]
771
+ }
772
+ ```
773
+
774
+ ## History
775
+
776
+ View the [changelog](https://github.com/ankane/ahoy/blob/master/CHANGELOG.md)
777
+
778
+ ## Contributing
779
+
780
+ Everyone is encouraged to help improve this project. Here are a few ways you can help:
781
+
782
+ - [Report bugs](https://github.com/ankane/ahoy/issues)
783
+ - Fix bugs and [submit pull requests](https://github.com/ankane/ahoy/pulls)
784
+ - Write, clarify, or fix documentation
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 different adapters, use:
797
+
798
+ ```sh
799
+ ADAPTER=postgresql bundle exec rake test
800
+ ADAPTER=mysql2 bundle exec rake test
801
+ ADAPTER=mongoid bundle exec rake test
802
+ ```