Ahoy
:fire: Simple, powerful analytics for Rails
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.
Ahoy 2.0 was recently released! See how to upgrade
:postbox: To track emails, check out Ahoy Email, and for A/B testing, check out Field Test
: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
First, enable the API in config/initializers/ahoy.rb:
Ahoy.api = true
And restart your web server.
For JavaScript, add to app/assets/javascripts/application.js:
//= require ahoy
And track an event with:
ahoy.track("My second event", {language: "JavaScript"});
For Android, check out Ahoy Android. For other platforms, see the API spec.
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, search keyword
- location - country, region, and city
- technology - browser, OS, and 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.
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 three ways to track events.
JavaScript
ahoy.track("Viewed book", {title: "The World is Flat"});
or track events automatically with:
ahoy.trackAll();
See Ahoy.js for a complete list of features.
Ruby
ahoy.track "Viewed book", title: "Hot, Flat, and Crowded"
or 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
Native Apps
For Android, check out Ahoy Android. For other platforms, see the API spec.
Associated Models
Say we want to associate orders with visits. Just add visitable to the model.
class Order < ApplicationRecord visitable end
When a visitor places an order, the visit_id column is automatically set :tada:
See where orders are coming from with simple joins:
Order.joins(:visit).group("referring_domain").count Order.joins(:visit).group("city").count Order.joins(:visit).group("device_type").count
Here’s what the migration to add the visit_id column should look like:
class AddVisitIdToOrders < ActiveRecord::Migration[5.1] def change add_column :orders, :visit_id, :bigint end end
Customize the column and class name with:
visitable :sign_up_visit, class_name: "Visit"
Users
Ahoy automatically attaches the current_user to the visit. With Devise, it attaches the user even if he or she signs 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
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
Multiple Subdomains
To track visits across multiple subdomains, use:
Ahoy.cookie_domain = :all
Geocoding
Disable geocoding with:
Ahoy.geocode = false
Change the job queue with:
Ahoy.job_queue = :low_priority
Geocoding Performance
To avoid calls to a remote API, download the GeoLite2 City database and configure Geocoder to use it.
Add this line to your application’s Gemfile:
gem 'maxminddb'
And create an initializer at config/initializers/geocoder.rb with:
Geocoder.configure( ip_lookup: :geoip2, geoip2: { file: Rails.root.join("lib", "GeoLite2-City.mmdb") } )
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) }
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.4becomes8.8.4.0) - For IPv6, the last 80 bits are set to zeros (
2001:4860:4860:0:0:0:0:8844becomes2001:4860:4860::)
Ahoy.mask_ips = true
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.
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 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.
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 two 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).count
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.
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": "",
"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": "",
"visitor_token": "",
"events": [
{
"id": "",
"name": "Viewed item",
"properties": {
"item_id": 123
},
"time": "2018-01-01T00:00:00-07:00"
}
]
}
Webpacker
For Webpacker, use Yarn to install the JavaScript library:
yarn add ahoy.js
Then include it in your pack.
import ahoy from "ahoy.js";
Upgrading
2.1
Ahoy recommends Device Detector for user agent parsing and makes it the default for new installations. To switch, add to config/initializers/ahoy.rb:
Ahoy.user_agent_parser = :device_detector
Backfill existing records with:
Ahoy::Visit.find_each do |visit| client = DeviceDetector.new(visit.user_agent) device_type = case client.device_type when "smartphone" "Mobile" when "tv" "TV" else client.device_type.try(:titleize) end visit.browser = client.name visit.os = client.os_name visit.device_type = device_type visit.save(validate: false) if visit.changed? end
2.0
See the upgrade guide
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