Description
Ahoy provides a solid foundation to track visits and events in Ruby, JavaScript, and native apps. Works with any data store so you can easily scale.
Ahoy alternatives and similar gems
Based on the "Analytics" category.
Alternatively, view Ahoy alternatives based on common mentions on social networks and blogs.
-
Rack::Tracker
Tracking made easy: Don’t fool around with adding tracking and analytics partials to your app and concentrate on the things that matter. -
Staccato
Ruby library to perform server-side tracking into the official Google Analytics Measurement Protocol -
The Chartable Ruby gem
A lightweight and database-level Ruby library to transform any Active Record query into analytics hash ready for use with any chart library. -
RailsUrlShortener
RailsUrlShortener is a lightweight Rails engine that enables easy creation and management of short URLs within your project. Similar to bitly.com, it condenses long links into short, user-friendly addresses. Enhance your app's functionality with this simple yet powerful URL shortening solution. -
RequestResponseStats
A Ruby gem which captures request response statistics such as cycle time, memory allocation, etc. for each request response cycle grouped in configurable granularity level. As this library makes use of TCP protocol, using DataDog or NewRelic RPM would be way faster because of UDP protocol.
CodeRabbit: AI Code Reviews for Developers
* Code Quality Rankings and insights are calculated and provided by Lumnify.
They vary from L1 to L5 with "L5" being the highest.
Do you think we are missing an alternative of Ahoy or a related project?
README
Ahoy
:fire: Simple, powerful, first-party analytics for Rails
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.
:postbox: Check out Ahoy Email for emails and Field Test for A/B testing
:tangerine: Battle-tested at Instacart
Installation
Add this line to your application’s Gemfile:
gem "ahoy_matey"
And run:
bundle install
rails generate ahoy:install
rails db:migrate
Restart your web server, open a page in your browser, and a visit will be created :tada:
Track your first event from a controller with:
ahoy.track "My first event", language: "Ruby"
JavaScript, Native Apps, & AMP
Enable the API in config/initializers/ahoy.rb
:
Ahoy.api = true
And restart your web server.
JavaScript
For Rails 7 / Importmap, add to config/importmap.rb
:
pin "ahoy", to: "ahoy.js"
And add to app/javascript/application.js
:
import "ahoy"
For Rails 6 / Webpacker, run:
yarn add ahoy.js
And add to app/javascript/packs/application.js
:
import ahoy from "ahoy.js"
For Rails 5 / Sprockets, add to app/assets/javascripts/application.js
:
//= require ahoy
Track an event with:
ahoy.track("My second event", {language: "JavaScript"});
Native Apps
Check out Ahoy iOS and Ahoy Android.
Geocoding Setup
To enable geocoding, see the Geocoding section.
GDPR Compliance
Ahoy provides a number of options to help with GDPR compliance. See the GDPR section for more info.
How It Works
Visits
When someone visits your website, Ahoy creates a visit with lots of useful information.
- traffic source - referrer, referring domain, landing page
- location - country, region, city, latitude, longitude
- technology - browser, OS, device type
- utm parameters - source, medium, term, content, campaign
Use the current_visit
method to access it.
Prevent certain Rails actions from creating visits with:
skip_before_action :track_ahoy_visit
This is typically useful for APIs. If your entire Rails app is an API, you can use:
Ahoy.api_only = true
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.
Ahoy.server_side_visits = :when_needed
Events
Each event has a name
and properties
. There are several ways to track events.
Ruby
ahoy.track "Viewed book", title: "Hot, Flat, and Crowded"
Track actions automatically with:
class ApplicationController < ActionController::Base
after_action :track_action
protected
def track_action
ahoy.track "Ran action", request.path_parameters
end
end
JavaScript
ahoy.track("Viewed book", {title: "The World is Flat"});
See Ahoy.js for a complete list of features.
Native Apps
See the docs for Ahoy iOS and Ahoy Android.
AMP
<head>
<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
</head>
<body>
<%= amp_event "Viewed article", title: "Analytics with Rails" %>
</body>
Associated Models
Say we want to associate orders with visits. Just add visitable
to the model.
class Order < ApplicationRecord
visitable :ahoy_visit
end
When a visitor places an order, the ahoy_visit_id
column is automatically set :tada:
See where orders are coming from with simple joins:
Order.joins(:ahoy_visit).group("referring_domain").count
Order.joins(:ahoy_visit).group("city").count
Order.joins(:ahoy_visit).group("device_type").count
Here’s what the migration to add the ahoy_visit_id
column should look like:
class AddAhoyVisitToOrders < ActiveRecord::Migration[7.0]
def change
add_reference :orders, :ahoy_visit
end
end
Customize the column with:
visitable :sign_up_visit
Users
Ahoy automatically attaches the current_user
to the visit. With Devise, it attaches the user even if they sign in after the visit starts.
With other authentication frameworks, add this to the end of your sign in method:
ahoy.authenticate(user)
To see the visits for a given user, create an association:
class User < ApplicationRecord
has_many :visits, class_name: "Ahoy::Visit"
end
And use:
User.find(123).visits
Custom User Method
Use a method besides current_user
Ahoy.user_method = :true_user
or use a proc
Ahoy.user_method = ->(controller) { controller.true_user }
Doorkeeper
To attach the user with Doorkeeper, be sure you have a current_resource_owner
method in ApplicationController
.
class ApplicationController < ActionController::Base
private
def current_resource_owner
User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
end
end
Knock
To attach the user with Knock, either include Knock::Authenticable
in ApplicationController
:
class ApplicationController < ActionController::API
include Knock::Authenticable
end
Or include it in Ahoy:
Ahoy::BaseController.include Knock::Authenticable
And use:
Ahoy.user_method = ->(controller) { controller.send(:authenticate_entity, "user") }
Exclusions
Bots are excluded from tracking by default. To include them, use:
Ahoy.track_bots = true
Add your own rules with:
Ahoy.exclude_method = lambda do |controller, request|
request.ip == "192.168.1.1"
end
Visit Duration
By default, a new visit is created after 4 hours of inactivity. Change this with:
Ahoy.visit_duration = 30.minutes
Cookies
To track visits across multiple subdomains, use:
Ahoy.cookie_domain = :all
Set other cookie options with:
Ahoy.cookie_options = {same_site: :lax}
You can also disable cookies
Token Generation
Ahoy uses random UUIDs for visit and visitor tokens by default, but you can use your own generator like Druuid.
Ahoy.token_generator = -> { Druuid.gen }
Throttling
You can use Rack::Attack to throttle requests to the API.
class Rack::Attack
throttle("ahoy/ip", limit: 20, period: 1.minute) do |req|
if req.path.start_with?("/ahoy/")
req.ip
end
end
end
Exceptions
Exceptions are rescued so analytics do not break your app. Ahoy uses Safely to try to report them to a service by default. To customize this, use:
Safely.report_exception_method = ->(e) { Rollbar.error(e) }
Geocoding
Ahoy uses Geocoder for geocoding. We recommend configuring local geocoding or 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, the masked IP is used (this can reduce accuracy but is better for privacy).
To enable geocoding, add this line to your application’s Gemfile:
gem "geocoder"
And update config/initializers/ahoy.rb
:
Ahoy.geocode = true
Geocoding is performed in a background job so it doesn’t slow down web requests. The default job queue is :ahoy
. Change this with:
Ahoy.job_queue = :low_priority
Local Geocoding
For privacy and performance, we recommend geocoding locally. Add this line to your application’s Gemfile:
gem "maxminddb"
For city-level geocoding, download the GeoLite2 City database and create config/initializers/geocoder.rb
with:
Geocoder.configure(
ip_lookup: :geoip2,
geoip2: {
file: "path/to/GeoLite2-City.mmdb"
}
)
For country-level geocoding, install the geoip-database
package. It’s preinstalled on Heroku. For Ubuntu, use:
sudo apt-get install geoip-database
And create config/initializers/geocoder.rb
with:
Geocoder.configure(
ip_lookup: :maxmind_local,
maxmind_local: {
file: "/usr/share/GeoIP/GeoIP.dat",
package: :country
}
)
Load Balancer Geocoding
Some load balancers can add geocoding information to request headers.
Update config/initializers/ahoy.rb
with:
Ahoy.geocode = false
class Ahoy::Store < Ahoy::DatabaseStore
def track_visit(data)
data[:country] = request.headers["<country-header>"]
data[:region] = request.headers["<region-header>"]
data[:city] = request.headers["<city-header>"]
super(data)
end
end
GDPR Compliance
Ahoy provides a number of options to help with GDPR compliance.
Update config/initializers/ahoy.rb
with:
class Ahoy::Store < Ahoy::DatabaseStore
def authenticate(data)
# disables automatic linking of visits and users
end
end
Ahoy.mask_ips = true
Ahoy.cookies = false
This:
- Masks IP addresses
- Switches from cookies to anonymity sets
- Disables automatic linking of visits and users
If you use JavaScript tracking, also set:
ahoy.configure({cookies: false});
IP Masking
Ahoy can mask IPs with the same approach Google Analytics uses for IP anonymization. This means:
- For IPv4, the last octet is set to 0 (
8.8.4.4
becomes8.8.4.0
) - For IPv6, the last 80 bits are set to zeros (
2001:4860:4860:0:0:0:0:8844
becomes2001:4860:4860::
)
Ahoy.mask_ips = true
IPs are masked before geolocation is performed.
To mask previously collected IPs, use:
Ahoy::Visit.find_each do |visit|
visit.update_column :ip, Ahoy.mask_ip(visit.ip)
end
Anonymity Sets & Cookies
Ahoy can switch from cookies to anonymity sets. Instead of cookies, visitors with the same IP mask and user agent are grouped together in an anonymity set.
Ahoy.cookies = false
Previously set cookies are automatically deleted. If you use JavaScript tracking, also set:
ahoy.configure({cookies: false});
Note: With anonymity sets, visits no longer expire after 4 hours of inactivity. A new visit is only created when the IP mask or user agent changes (for instance, when a user updates their browser). There are plans to address this in the next major version.
Data Retention
Data should only be retained for as long as it’s needed. Delete older data with:
Ahoy::Visit.where("started_at < ?", 2.years.ago).find_in_batches do |visits|
visit_ids = visits.map(&:id)
Ahoy::Event.where(visit_id: visit_ids).delete_all
Ahoy::Visit.where(id: visit_ids).delete_all
end
You can use Rollup to aggregate important data before you do.
Ahoy::Visit.rollup("Visits", interval: "hour")
Delete data for a specific user with:
user_id = 123
visit_ids = Ahoy::Visit.where(user_id: user_id).pluck(:id)
Ahoy::Event.where(visit_id: visit_ids).delete_all
Ahoy::Visit.where(id: visit_ids).delete_all
Ahoy::Event.where(user_id: user_id).delete_all
Development
Ahoy is built with developers in mind. You can run the following code in your browser’s console.
Force a new visit
ahoy.reset(); // then reload the page
Log messages
ahoy.debug();
Turn off logging
ahoy.debug(false);
Debug API requests in Ruby
Ahoy.quiet = false
Data Stores
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
:
class Ahoy::Store < Ahoy::DatabaseStore
end
There are four events data stores can subscribe to:
class Ahoy::Store < Ahoy::BaseStore
def track_visit(data)
# new visit
end
def track_event(data)
# new event
end
def geocode(data)
# visit geocoded
end
def authenticate(data)
# user authenticates
end
end
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.
Track Additional Data
class Ahoy::Store < Ahoy::DatabaseStore
def track_visit(data)
data[:accept_language] = request.headers["Accept-Language"]
super(data)
end
end
Two useful methods you can use are request
and controller
.
You can pass additional visit data from JavaScript with:
ahoy.configure({visitParams: {referral_code: 123}});
And use:
class Ahoy::Store < Ahoy::DatabaseStore
def track_visit(data)
data[:referral_code] = request.parameters[:referral_code]
super(data)
end
end
Use Different Models
class Ahoy::Store < Ahoy::DatabaseStore
def visit_model
MyVisit
end
def event_model
MyEvent
end
end
Explore the Data
Blazer is a great tool for exploring your data.
With ActiveRecord, you can do:
Ahoy::Visit.group(:search_keyword).count
Ahoy::Visit.group(:country).count
Ahoy::Visit.group(:referring_domain).count
Chartkick and Groupdate make it easy to visualize the data.
<%= line_chart Ahoy::Visit.group_by_day(:started_at).count %>
Querying Events
Ahoy provides a few methods on the event model to make querying easier.
To query on both name and properties, you can use:
Ahoy::Event.where_event("Viewed product", product_id: 123).count
Or just query properties with:
Ahoy::Event.where_props(product_id: 123, category: "Books").count
Group by properties with:
Ahoy::Event.group_prop(:product_id, :category).count
Note: MySQL and MariaDB always return string keys (including "null"
for nil
) for group_prop
.
Funnels
It’s easy to create funnels.
viewed_store_ids = Ahoy::Event.where(name: "Viewed store").distinct.pluck(:user_id)
added_item_ids = Ahoy::Event.where(user_id: viewed_store_ids, name: "Added item to cart").distinct.pluck(:user_id)
viewed_checkout_ids = Ahoy::Event.where(user_id: added_item_ids, name: "Viewed checkout").distinct.pluck(:user_id)
The same approach also works with visitor tokens.
Rollups
Improve query performance by pre-aggregating data with Rollup.
Ahoy::Event.where(name: "Viewed store").rollup("Store views")
This is only needed if you have a lot of data.
Forecasting
To forecast future visits and events, check out Prophet.
daily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate
Prophet.forecast(daily_visits)
Anomaly Detection
To detect anomalies in visits and events, check out AnomalyDetection.rb.
daily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate
AnomalyDetection.detect(daily_visits, period: 7)
Breakout Detection
To detect breakouts in visits and events, check out Breakout.
daily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate
Breakout.detect(daily_visits)
Recommendations
To make recommendations based on events, check out Disco.
Tutorials
API Spec
Visits
Generate visit and visitor tokens as UUIDs, and include these values in the Ahoy-Visit
and Ahoy-Visitor
headers with all requests.
Send a POST
request to /ahoy/visits
with Content-Type: application/json
and a body like:
{
"visit_token": "<visit-token>",
"visitor_token": "<visitor-token>",
"platform": "iOS",
"app_version": "1.0.0",
"os_version": "11.2.6"
}
After 4 hours of inactivity, create another visit (use the same visitor token).
Events
Send a POST
request to /ahoy/events
with Content-Type: application/json
and a body like:
{
"visit_token": "<visit-token>",
"visitor_token": "<visitor-token>",
"events": [
{
"id": "<optional-random-id>",
"name": "Viewed item",
"properties": {
"item_id": 123
},
"time": "2018-01-01T00:00:00-07:00"
}
]
}
Upgrading
4.0
There are two notable changes to geocoding:
Geocoding is now disabled by default (this was already the case for new installations with 3.2.0+). Check out the instructions for how to enable it.
The
geocoder
gem is now an optional dependency. To use geocoding, add it to your Gemfile:
gem "geocoder"
Also, check out the upgrade notes for Ahoy.js.
History
View the changelog
Contributing
Everyone is encouraged to help improve this project. Here are a few ways you can help:
- Report bugs
- Fix bugs and submit pull requests
- Write, clarify, or fix documentation
- Suggest or add new features
To get started with development:
git clone https://github.com/ankane/ahoy.git
cd ahoy
bundle install
bundle exec rake test
To test Mongoid, use:
ADAPTER=mongoid bundle exec rake test
To test query methods, use:
# Postgres
createdb ahoy_test
bundle exec rake test:query_methods:postgresql
# SQLite
bundle exec rake test:query_methods:sqlite
# MySQL and MariaDB
mysqladmin create ahoy_test
bundle exec rake test:query_methods:mysql
# MongoDB
bundle exec rake test:query_methods:mongoid