Open-source project
ankane/ahoy avatar
ankane/ahoy

Ahoy: first-party analytics for Rails that keeps the data in your database

Simple, powerful, first-party analytics for Rails

4,482 stars385 forksRubyMIT

At a glance

What is it?
Ahoy (the ahoy_matey gem) records visits and events inside your Rails app, in your own tables, with no third-party script. It is a good fit for teams that want attribution joins against orders and users, and a poor fit for anyone who wants a hosted dashboard out of the box.
Who is it for?
Adopt Ahoy if your analytics questions are really SQL questions about your own users and orders, and you are willing to run the queries yourself. Do not adopt it if you need a hosted product with dashboards, funnels and retention charts on day one; the README's examples are joins and group counts rather than charts.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 23 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • ankane/ahoy4,482 ★ · +2
Star history
Repository2026-09-232026-11-14
ankane/ahoy4.5k4.5k

2 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

What Ahoy solves, and who ends up using it

Most Rails apps reach for a hosted analytics product, paste a script tag into the layout, and then discover they cannot join a pageview to an order. The data lives in someone else's warehouse, keyed by a cookie, and the join you actually want (which referring domain produced the customers who stayed) is either impossible or an export job. Ahoy takes the other position: analytics is application data, so it belongs in the application database, written by the application, readable with ActiveRecord.

The README describes it as "Simple, powerful, first-party analytics for Rails" and states that data is stored in your database by default, with the option to customize the store as you grow. That sentence is the whole design brief. The audience is Rails developers who already have Postgres or MySQL in production and would rather write Order.joins(:ahoy_visit).group("referring_domain").count than configure a reverse ETL pipeline. It is also for teams with a privacy or compliance reason to keep visitor data on their own infrastructure, since no request goes to a third party when a visit is recorded.

How visits and events are recorded inside a Rails request

Ahoy has two record types. A visit is created for a browser session and carries what the request itself can tell you: referrer and referring domain, landing page, country, region, city, latitude and longitude, browser, OS, device type, and the utm parameters. An event has a name and a properties hash, and belongs to a visit. Both are stored in your database by default.

On the server side, the gem installs a before_action (the README shows skip_before_action :track_ahoy_visit as the way to opt a controller out, and Ahoy.api_only = true for an app that is entirely an API). Visits are created by that filter, and ahoy.track writes events from anywhere you have the ahoy helper, including an after_action that records every controller action with its path parameters.

Two configuration switches change the data flow rather than just the data. Ahoy.server_side_visits = :when_needed makes the server create a visit only when an event needs one, and false disables server-side creation entirely, with the README noting that events arriving without a visit are discarded. The stated reason is bots that spoof their user agent and users with cookies disabled, both of which would otherwise generate a fresh visit on every request. That is a real trade-off: you move visit creation to JavaScript, and you accept that a request with no JavaScript produces no visit.

Identity is handled in two layers. Visitor tokens are random UUIDs by default and are regenerated after two years (Ahoy.visitor_duration), while a new visit starts after four hours of inactivity by default (Ahoy.visit_duration). The current_user is attached automatically, and with Devise the README says it is attached even when the user signs in after the visit began; other frameworks call ahoy.authenticate(user) at the end of the sign-in method. The visitable macro on a model adds an ahoy_visit_id reference so an Order or Signup carries the visit that produced it, which is what makes the grouping queries possible.

Installing ahoy_matey and tracking a first event

Ahoy ships as a gem named ahoy_matey. Add it to the Gemfile, then run the install generator and the migration. The generator creates the initializer and the migrations for the visits and events tables; the README gives exactly this sequence:

ruby
gem "ahoy_matey"
sh
bundle install
rails generate ahoy:install
rails db:migrate

After restarting the web server and opening a page, the README says a visit will be created. You can confirm it in the console with Ahoy::Visit.last, or by checking that the ahoy_visits table gained a row.

Tracking an event from a controller is one call. The properties argument is a plain hash and becomes the event's properties:

ruby
ahoy.track "My first event", language: "Ruby"

For events from the browser, the API has to be switched on in config/initializers/ahoy.rb, and the server restarted:

ruby
Ahoy.api = true

With Importmap, which the README calls the Rails default, pin the bundled script and import it:

ruby
# config/importmap.rb
pin "ahoy", to: "ahoy.js"
javascript
// app/javascript/application.js
import "ahoy"

If you use Bun, esbuild, rollup.js or Webpack instead, the README directs you to bun add ahoy.js or yarn add ahoy.js and then import ahoy from "ahoy.js". Sprockets users add a require directive. Once the client script is loaded, events are sent with the same shape as the Ruby call:

javascript
ahoy.track("My second event", {language: "JavaScript"});

The README also documents AMP through an amp_event helper, and points to separate Ahoy iOS and Ahoy Android projects for native apps.

Where Ahoy stops: reporting, retention and the missing dashboard

Ahoy records. It does not present. The README shows joins and group counts as the way to see where orders come from. To see the data you either write the queries, put them behind an admin page, or point a BI tool at the tables. Teams used to opening a product and clicking a funnel will find that work is still ahead of them.

Retention is the second thing to plan for. Visits and events accumulate in your primary database. On a busy site the ahoy_visits and ahoy_events tables grow with traffic, and every row sits next to your transactional data, competing for the same disk and the same backups. Ahoy's answer is the customizable store mentioned in the introduction, which is a real escape hatch but also means the simple case eventually becomes an engineering task.

The third boundary is JavaScript. With Ahoy.server_side_visits set to false, requests that do not run JavaScript produce no visit, and the README states their events are discarded. If your audience includes clients with scripting disabled, or you rely on server-rendered pages for crawlers you have chosen not to exclude, that setting will silently undercount. Bot exclusion is on by default and can be inverted with Ahoy.track_bots = true or extended with Ahoy.exclude_method, but an exclusion rule is only as good as the request attributes it inspects.

Ahoy against a hosted analytics product

The obvious alternative is a hosted product such as Plausible or Matomo, and the difference is not features, it is where the join happens. A hosted tool gives you a dashboard, traffic charts and goal tracking without writing a query, and it keeps the load off your database. What it does not give you, in the typical setup, is a foreign key from an order row to the pageview that preceded it. Ahoy's visitable macro exists precisely to create that key, which is why the README's examples are joins against orders grouped by referring domain, city or device type rather than time-series charts.

Matomo can be self-hosted, which addresses the data-residency argument, but it is a separate PHP application with its own database, not a gem inside your Rails process. Plausible is lighter and privacy-focused, and still a separate service. If your questions are "how many people visited this page" and "where did they come from", a hosted tool answers them faster and for less operational cost. If your questions are "which campaign produced customers who are still active six months later", Ahoy is the shorter path, because the answer is a join you can run in the console today.

The two are not mutually exclusive. Nothing in the README prevents running a hosted product for traffic reporting while Ahoy supplies the per-user attribution columns your application code needs.

Licence, maintenance and the cost of upgrading

Ahoy is MIT licensed, with LICENSE.txt at the repository root, so you can use, modify and redistribute it, including in closed-source applications. The practical implication of MIT here is that you are free to fork if the maintainer's direction diverges from yours, and equally free to vendor a patch while waiting for a release. This is a description of the licence terms, not legal advice; if your organisation has a policy on third-party dependencies, route it through the usual review.

The repository is not archived, and the last push was on 2026-10-22. Development is ongoing. The repository has no published GitHub releases, so the README and CHANGELOG.md are the places to look for behaviour changes between versions; the README documents configuration keys such as Ahoy.visit_duration, Ahoy.visitor_duration, Ahoy.cookie_domain and Ahoy.token_generator, and those keys are the surface you should re-check when you upgrade, since a change in default visit duration or token generation affects how your historical data groups.

Upgrade cost is mostly schema and configuration rather than code. The install generator produced migrations for the visits and events tables; if a later version adds a column, you apply a migration like any other. Because the data is yours, an upgrade cannot break your historical numbers the way a change in a vendor's attribution model can, but it also means no one else will fix a bad default for you.

Editorial conclusion

Adopt Ahoy if your analytics questions are really SQL questions about your own users and orders, and you are willing to run the queries yourself. Do not adopt it if you need a hosted product with dashboards, funnels and retention charts on day one; the README's examples are joins and group counts rather than charts. Before committing, verify three things in a scratch app: that the install generator's migration matches your database, that visit creation behaves as you expect under Ahoy.server_side_visits = :when_needed, and that your retention policy for ahoy_visits and ahoy_events is written down.

Frequently asked questions

Does Ahoy store analytics data in my own database?

Yes. The README states that data is stored in your database by default and that you can customize the store for any data store as you grow. That is the defining property of the gem: visits and events are rows in tables your application owns.

Can Ahoy track events from JavaScript as well as Ruby?

It can. Enable the API with Ahoy.api = true in config/initializers/ahoy.rb, load the client script through Importmap, Bun, esbuild, rollup.js, Webpack or Sprockets, and call ahoy.track with a name and a properties object. The README points to Ahoy.js for the complete feature list.

Does Ahoy ship with a dashboard for viewing the data?

Ahoy records data rather than presenting it. The README shows joins and group counts as the way to answer questions, for example grouping orders by referring_domain, city or device_type through the ahoy_visit association. Anything visual is something you build or point a separate tool at.

How do I stop Ahoy from creating a visit for API requests?

Add skip_before_action :track_ahoy_visit to the controller, which the README says is typically useful for APIs. If the whole application is an API, set Ahoy.api_only = true instead.

Official sources

  1. ankane/ahoy on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ankane-ahoy.svg)](https://hysenlabs.com/projects/ankane-ahoy)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/ankane-ahoy.svg)](https://hysenlabs.com/projects/ankane-ahoy)

Was this useful?