🔥 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.
📮 To track emails, check out Ahoy Email, and for A/B testing, check out Field Test
🍊 Battle-tested at Instacart
Add this line to your application’s Gemfile:
gem 'ahoy_matey'And run:
bundle install
rails generate ahoy:install
rails db:migrateRestart your web server, open a page in your browser, and a visit will be created 🎉
Track your first event from a controller with:
ahoy.track "My first event", language: "Ruby"Enable the API in config/initializers/ahoy.rb:
Ahoy.api = trueAnd restart your web server.
For Rails 6 / Webpacker, run:
yarn add ahoy.jsAnd add to app/javascript/packs/application.js:
import ahoy from "ahoy.js";For Rails 5 / Sprockets, add to app/assets/javascripts/application.js:
//= require ahoyTrack an event with:
ahoy.track("My second event", {language: "JavaScript"});Ahoy provides a number of options to help with GDPR compliance. See the GDPR section for more info.
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_visitThis is typically useful for APIs. If your entire Rails app is an API, you can use:
Ahoy.api_only = trueYou 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_neededEach event has a name and properties. There are several ways to track events.
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
endahoy.track("Viewed book", {title: "The World is Flat"});Track events automatically with:
ahoy.trackAll();See Ahoy.js for a complete list of features.
For Android, check out Ahoy Android. For other platforms, see the API spec.
<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>Say we want to associate orders with visits. Just add visitable to the model.
class Order < ApplicationRecord
visitable :ahoy_visit
endWhen a visitor places an order, the ahoy_visit_id column is automatically set 🎉
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").countHere’s what the migration to add the ahoy_visit_id column should look like:
class AddVisitIdToOrders < ActiveRecord::Migration[6.0]
def change
add_column :orders, :ahoy_visit_id, :bigint
end
endCustomize the column with:
visitable :sign_up_visitAhoy 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"
endAnd use:
User.find(123).visitsUse a method besides current_user
Ahoy.user_method = :true_useror use a proc
Ahoy.user_method = ->(controller) { controller.true_user }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
endTo attach the user with Knock, either include Knock::Authenticablein ApplicationController:
class ApplicationController < ActionController::API
include Knock::Authenticable
endOr include it in Ahoy:
Ahoy::BaseController.include Knock::AuthenticableAnd use:
Ahoy.user_method = ->(controller) { controller.send(:authenticate_entity, "user") }Bots are excluded from tracking by default. To include them, use:
Ahoy.track_bots = trueAdd your own rules with:
Ahoy.exclude_method = lambda do |controller, request|
request.ip == "192.168.1.1"
endBy default, a new visit is created after 4 hours of inactivity. Change this with:
Ahoy.visit_duration = 30.minutesTo track visits across multiple subdomains, use:
Ahoy.cookie_domain = :allDisable geocoding with:
Ahoy.geocode = falseChange the job queue with:
Ahoy.job_queue = :low_priorityTo 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")
}
)If you use Heroku, you can use an unofficial buildpack like this one to avoid including the database in your repo.
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 }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
endExceptions 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) }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 = falseThis:
- 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});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 = trueIPs 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)
endAhoy 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 = falsePreviously set cookies are automatically deleted.
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 pageLog messages
ahoy.debug();Turn off logging
ahoy.debug(false);Debug API requests in Ruby
Ahoy.quiet = falseData 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
endThere 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
endData 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.
class Ahoy::Store < Ahoy::DatabaseStore
def track_visit(data)
data[:accept_language] = request.headers["Accept-Language"]
super(data)
end
endTwo 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
endclass Ahoy::Store < Ahoy::DatabaseStore
def visit_model
MyVisit
end
def event_model
MyEvent
end
endBlazer 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).countChartkick and Groupdate make it easy to visualize the data.
<%= line_chart Ahoy::Visit.group_by_day(:started_at).count %>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).countOr just query properties with:
Ahoy::Event.where_props(product_id: 123).countIt’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.
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).
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"
}
]
}If you installed Ahoy before 2.1 and want to keep legacy user agent parsing and bot detection, add to your Gemfile:
gem "browser", "~> 2.0"
gem "user_agent_parser"And add to config/initializers/ahoy.rb:
Ahoy.user_agent_parser = :legacyAhoy now ships with better bot detection if you use Device Detector. This should be more accurate but can significantly reduce the number of visits recorded. For existing installs, it’s opt-in to start. To use it, add to config/initializers/ahoy.rb:
Ahoy.bot_detection_version = 2Ahoy 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_detectorBackfill 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?
endSee the upgrade guide
View the changelog
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