# 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](https://github.com/ankane/ahoy_email) for emails and [Field Test](https://github.com/ankane/field_test) for A/B testing :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource) [](https://github.com/ankane/ahoy/actions) ## Installation Add this line to your application’s Gemfile: ```ruby gem "ahoy_matey" ``` And run: ```sh 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: ```ruby ahoy.track "My first event", language: "Ruby" ``` ### JavaScript, Native Apps, & AMP Enable the API in `config/initializers/ahoy.rb`: ```ruby Ahoy.api = true ``` And restart your web server. ### JavaScript For Importmap (Rails default), add to `config/importmap.rb`: ```ruby pin "ahoy", to: "ahoy.js" ``` And add to `app/javascript/application.js`: ```javascript import "ahoy" ``` For Bun, esbuild, rollup.js, or Webpack, run: ```sh bun add ahoy.js # or yarn add ahoy.js ``` And add to `app/javascript/application.js`: ```javascript import ahoy from "ahoy.js" ``` For Sprockets, add to `app/assets/javascripts/application.js`: ```javascript //= require ahoy ``` Track an event with: ```javascript ahoy.track("My second event", {language: "JavaScript"}); ``` ### Native Apps Check out [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android). ### Geocoding Setup To enable geocoding, see the [Geocoding section](#geocoding). ### GDPR Compliance Ahoy provides a number of options to help with GDPR compliance. See the [GDPR section](#gdpr-compliance-1) 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: ```ruby skip_before_action :track_ahoy_visit ``` This is typically useful for APIs. If your entire Rails app is an API, you can use: ```ruby 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. ```ruby Ahoy.server_side_visits = :when_needed ``` ### Events Each event has a `name` and `properties`. There are several ways to track events. #### Ruby ```ruby ahoy.track "Viewed book", title: "Hot, Flat, and Crowded" ``` Track actions automatically with: ```ruby class ApplicationController < ActionController::Base after_action :track_action protected def track_action ahoy.track "Ran action", request.path_parameters end end ``` #### JavaScript ```javascript ahoy.track("Viewed book", {title: "The World is Flat"}); ``` See [Ahoy.js](https://github.com/ankane/ahoy.js) for a complete list of features. #### Native Apps See the docs for [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android). #### AMP ```erb
<%= amp_event "Viewed article", title: "Analytics with Rails" %> ``` ### Associated Models Say we want to associate orders with visits. Just add `visitable` to the model. ```ruby 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: ```ruby 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: ```ruby class AddAhoyVisitToOrders < ActiveRecord::Migration[8.1] def change add_reference :orders, :ahoy_visit end end ``` Customize the column with: ```ruby visitable :sign_up_visit ``` ### Users 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. With other authentication frameworks, add this to the end of your sign in method: ```ruby ahoy.authenticate(user) ``` To see the visits for a given user, create an association: ```ruby class User < ApplicationRecord has_many :visits, class_name: "Ahoy::Visit" end ``` And use: ```ruby User.find(123).visits ``` #### Custom User Method Use a method besides `current_user` ```ruby Ahoy.user_method = :true_user ``` or use a proc ```ruby Ahoy.user_method = ->(controller) { controller.true_user } ``` #### Doorkeeper To attach the user with [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper), be sure you have a `current_resource_owner` method in `ApplicationController`. ```ruby 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: ```ruby Ahoy.track_bots = true ``` Add your own rules with: ```ruby 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: ```ruby Ahoy.visit_duration = 30.minutes ``` ### Visitor Duration By default, a new `visitor_token` is generated after 2 years. Change this with: ```ruby Ahoy.visitor_duration = 30.days ``` ### Cookies To track visits across multiple subdomains, use: ```ruby Ahoy.cookie_domain = :all ``` Set other [cookie options](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html) with: ```ruby Ahoy.cookie_options = {same_site: :lax} ``` You can also [disable cookies](#anonymity-sets--cookies) ### Token Generation 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). ```ruby Ahoy.token_generator = -> { ULID.generate } ``` ### Throttling You can use [Rack::Attack](https://github.com/rack/rack-attack) to throttle requests to the API. ```ruby 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](https://github.com/ankane/safely) to try to report them to a service by default. To customize this, use: ```ruby Safely.report_exception_method = ->(e) { Rollbar.error(e) } ``` ## Geocoding 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). To enable geocoding, add this line to your application’s Gemfile: ```ruby gem "geocoder" ``` And update `config/initializers/ahoy.rb`: ```ruby 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: ```ruby Ahoy.job_queue = :low_priority ``` ### Local Geocoding For privacy and performance, we recommend geocoding locally. For city-level geocoding, download the [GeoLite2 City database](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data). Add this line to your application’s Gemfile: ```ruby gem "maxminddb" ``` And create `config/initializers/geocoder.rb` with: ```ruby 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: ```sh sudo apt-get install geoip-database ``` Add this line to your application’s Gemfile: ```ruby gem "geoip" ``` And create `config/initializers/geocoder.rb` with: ```ruby 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. - [nginx](https://nginx.org/en/docs/http/ngx_http_geoip_module.html) - [Google Cloud](https://cloud.google.com/load-balancing/docs/custom-headers) - [Cloudflare](https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-Cloudflare-IP-Geolocation) Update `config/initializers/ahoy.rb` with: ```ruby Ahoy.geocode = false class Ahoy::Store < Ahoy::DatabaseStore def track_visit(data) data[:country] = request.headers["