RepoPilotOpen in app →

ankane/ahoy

Simple, powerful, first-party analytics for Rails

Healthy

Healthy across all four use cases

Use as dependencyHealthy

Permissive license, no critical CVEs, actively maintained — safe to depend on.

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture — runnable as-is.

  • Last commit 4w ago
  • MIT licensed
  • CI configured
Show 2 more →
  • Tests present
  • Solo or near-solo (1 contributor active in recent commits)

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Healthy" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Healthy
[![RepoPilot: Healthy](https://repopilot.app/api/badge/ankane/ahoy)](https://repopilot.app/r/ankane/ahoy)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/ankane/ahoy on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: ankane/ahoy

Generated by RepoPilot · 2026-05-10 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/ankane/ahoy shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

GO — Healthy across all four use cases

  • Last commit 4w ago
  • MIT licensed
  • CI configured
  • Tests present
  • ⚠ Solo or near-solo (1 contributor active in recent commits)

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live ankane/ahoy repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/ankane/ahoy.

What it runs against: a local clone of ankane/ahoy — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in ankane/ahoy | Confirms the artifact applies here, not a fork | | 2 | License is still MIT | Catches relicense before you depend on it | | 3 | Default branch master exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 61 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>ankane/ahoy</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of ankane/ahoy. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/ankane/ahoy.git
#   cd ahoy
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of ankane/ahoy and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "ankane/ahoy(\\.git)?\\b" \\
  && ok "origin remote is ankane/ahoy" \\
  || miss "origin remote is not ankane/ahoy (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
  && ok "license is MIT" \\
  || miss "license drift — was MIT at generation time"

# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
  && ok "default branch master exists" \\
  || miss "default branch master no longer exists"

# 4. Critical files exist
test -f "lib/ahoy.rb" \\
  && ok "lib/ahoy.rb" \\
  || miss "missing critical file: lib/ahoy.rb"
test -f "lib/ahoy/tracker.rb" \\
  && ok "lib/ahoy/tracker.rb" \\
  || miss "missing critical file: lib/ahoy/tracker.rb"
test -f "lib/ahoy/database_store.rb" \\
  && ok "lib/ahoy/database_store.rb" \\
  || miss "missing critical file: lib/ahoy/database_store.rb"
test -f "lib/ahoy/controller.rb" \\
  && ok "lib/ahoy/controller.rb" \\
  || miss "missing critical file: lib/ahoy/controller.rb"
test -f "app/controllers/ahoy/events_controller.rb" \\
  && ok "app/controllers/ahoy/events_controller.rb" \\
  || miss "missing critical file: app/controllers/ahoy/events_controller.rb"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 61 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~31d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/ankane/ahoy"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

Ahoy is a Rails gem that provides first-party analytics tracking by recording visits (session metadata like referrer, location, browser, utm parameters) and custom events into your database. It includes both server-side Rails controllers/middleware and a JavaScript client (ahoy.js) for tracking events from browsers, native apps, and APIs — all stored in your own PostgreSQL/MongoDB without third-party services. Modular Rails plugin architecture: lib/ahoy/engine.rb boots the gem, lib/ahoy/tracker.rb handles event/visit recording via configurable stores (database_store.rb, base_store.rb for custom implementations). Controllers in app/controllers/ahoy/ expose REST API endpoints. Generators (lib/generators/ahoy/) scaffold Visit/Event models for ActiveRecord (active_record_visit_model.rb.tt) or Mongoid. Core middleware in lib/ahoy/controller.rb and lib/ahoy/model.rb inject tracking into Rails request/response cycle.

👥Who it's for

Rails developers building SaaS or web applications who want GDPR-compliant, self-hosted analytics without relying on Google Analytics or Mixpanel. They need to track user behavior (visits, funnels, custom events) while maintaining full data ownership and privacy control.

🌱Maturity & risk

Production-ready and actively maintained. The project is battle-tested at Instacart (per README), has comprehensive CI via GitHub Actions (build.yml), generators for both ActiveRecord and Mongoid, and a mature CHANGELOG. Latest updates suggest active development for Rails 7.2+ and 8.0 support (gemfiles/ directory).

Low risk for single-maintainer projects; ankane (Andrew Kane) maintains this alongside other major gems. No obvious breaking changes visible in structure. Primary dependency risk is on Rails version compatibility — the gem actively tracks Rails 7.2 and 8.0 in gemfiles/, but old Rails apps may face compatibility friction. Minimal external service dependencies (geocoding is optional via job queue).

Active areas of work

Active Rails version compatibility work: recent gemfiles reference Rails 7.2 and 8.0. The gem is stable in its core analytics features; ongoing work appears focused on maintaining database adapter support and geocoding (lib/ahoy/geocode_v2_job.rb suggests background job integration). CONTRIBUTING.md exists, indicating community contribution is encouraged.

🚀Get running

git clone https://github.com/ankane/ahoy.git
cd ahoy
bundle install
# Then in a Rails app:
bundle add ahoy_matey
rails generate ahoy:install
rails db:migrate

Daily commands:

bundle exec rake  # runs test suite via Rakefile
# In a Rails test app after running generators:
rails server       # starts dev server with ahoy tracking active

For development: Ahoy is a gem; run tests with bundle exec rake. No standalone server needed; it works embedded in your Rails app.

🗺️Map of the codebase

  • lib/ahoy.rb — Main entry point and gem configuration—establishes core settings, API mode toggle, and initialization hooks
  • lib/ahoy/tracker.rb — Core tracking logic for events and visits—handles the instrumentation that powers all analytics capture
  • lib/ahoy/database_store.rb — Default data persistence layer—implements the abstraction for storing visits and events in the database
  • lib/ahoy/controller.rb — Controller integration mixin—provides ahoy.track and ahoy context methods used throughout Rails apps
  • app/controllers/ahoy/events_controller.rb — API endpoint for client-side event submission—enables JavaScript and native apps to send analytics
  • lib/generators/ahoy/install_generator.rb — Installation scaffolding—sets up models, migrations, and initializer configuration for first-time users

🧩Components & responsibilities

  • Tracker — Orchestrates visit

🛠️How to make changes

Add a new event property to track

  1. In your controller, call ahoy.track with custom properties (lib/ahoy/controller.rb)
  2. Add a migration to your Visit or Event model to include the properties column (JSON/JSONB) (lib/generators/ahoy/templates/active_record_migration.rb.tt)
  3. Query the property using polymorphic query methods in lib/ahoy/query_methods.rb (lib/ahoy/query_methods.rb)

Implement a custom data store backend

  1. Create a new class inheriting from BaseStore (lib/ahoy/base_store.rb)
  2. Implement track_visit and track_event methods (lib/ahoy/database_store.rb)
  3. Set Ahoy.store in config/initializers/ahoy.rb to your custom store instance (lib/generators/ahoy/templates/database_store_initializer.rb.tt)

Add geocoding enrichment to visits

  1. Enable Ahoy.geocode in config/initializers/ahoy.rb and set api_key for geocoding service (lib/generators/ahoy/templates/database_store_initializer.rb.tt)
  2. The tracker will queue geocoding jobs when a visit is created (lib/ahoy/tracker.rb)
  3. Geocode job updates visit with latitude, longitude, and city/country (lib/ahoy/geocode_v2_job.rb)

Exclude certain requests from being tracked

  1. Set Ahoy.exclude_paths or Ahoy.exclude_user_agents in initializer (lib/ahoy.rb)
  2. Tracker checks these filters before recording visits (lib/ahoy/tracker.rb)
  3. Controller checks filters in before_action hook (lib/ahoy/controller.rb)

🔧Why these technologies

  • Rails Engine — Provides isolated routing, controllers, and asset pipeline for the analytics gem while integrating seamlessly into host Rails apps
  • ActiveRecord ORM (primary) + Mongoid (alternative) — Enables flexible data storage in relational or NoSQL databases; users can swap stores or extend with custom implementations
  • First-party analytics (same-domain requests) — Avoids third-party tracking blockers and privacy concerns; data stays in user's own database
  • Async geocoding jobs (Active Job) — Enriches visits with location data without blocking request handling; user can choose queue adapter (Sidekiq, Resque, etc.)

⚖️Trade-offs already made

  • Event properties stored as JSON/JSONB, not relational columns

    • Why: Allows arbitrary custom properties without schema migration for every event type
    • Consequence: Query performance depends on database JSON indexing support; text-based fallback for older databases is slower
  • Pluggable store abstraction (BaseStore) rather than hardcoded database logic

    • Why: Users can implement custom stores for data lakes, analytics warehouses, or real-time streams
    • Consequence: Added complexity in tracker; database_store.rb is the reference implementation but not the only path
  • Polymorphic user association (user_type + user_id instead of user_id FK)

    • Why: Supports tracking visits for multiple user models or non-authenticated sessions
    • Consequence: Requires custom query logic; cannot use standard database constraints or joins
  • Client-side JavaScript tracking via POST /ahoy/events API

    • Why: Enables real-time event capture from browsers without page reloads; independent of server-side rendering
    • Consequence: Requires CORS handling and API authentication; network failures may drop events if not retried client-side

🚫Non-goals (don't propose these)

  • Real-time dashboarding (stores data; visualization is user's responsibility)
  • Third-party ad network integration (intentionally first-party only)
  • User consent/GDPR enforcement (integrates with hosts' compliance tools, does not dictate policy)
  • Multi-tenant isolation (single Rails app = single analytics dataset; tenants must deploy separate instances)
  • Mobile SDK shipping (JavaScript only in-gem; native apps must implement HTTP client themselves)

🪤Traps & gotchas

Geocoding dependency: Enabling Ahoy.geocoding requires Geocoder gem + external service (MaxMind); defaults to false. Visit deduplication: Ahoy defers visit creation to JavaScript by default (prevent bots/cookie-disabled users); set Ahoy.track_visits_immediately = true if needed. Custom initializer required: After running generators, config/initializers/ahoy.rb must be present and loaded before app starts. Warden integration for auth: lib/ahoy/warden.rb integrates with Devise/Warden; user tracking depends on correct middleware ordering. Database-specific migrations: ActiveRecord migrations generated by install_generator use Rails' syntax; Mongoid has separate templates — verify you're using the right generator (ahoy:activerecord_generator vs ahoy:mongoid_generator).

🏗️Architecture

💡Concepts to learn

  • First-party analytics (vs third-party) — Ahoy's core value prop; you own the data and don't send user info to external SaaS, which is critical for GDPR and privacy-forward products. Understanding this distinction clarifies why the architecture uses your own database instead of beaconing to Google/Mixpanel.
  • Visit vs Event distinction — Ahoy's data model splits session-level data (Visit: referrer, browser, location, utm_source) from action-level data (Event: custom_name, properties). Understanding this shapes how you instrument your app — you don't track visit data repeatedly, only event data.
  • User-Agent parsing — lib/ahoy/visit_properties.rb extracts browser/OS/device from User-Agent string; this is how Ahoy enriches visit data without external calls. Knowing that User-Agent parsing is inherently fragile helps when debugging why certain mobile/bot detection is inaccurate.
  • Geocoding via background jobs — lib/ahoy/geocode_v2_job.rb defers IP → lat/long conversion to async job queue (not blocking requests); this is how Ahoy scales location tracking without blocking web requests. Critical for understanding where to add Geocoder gem and why it's optional.
  • Store abstraction pattern (Strategy pattern) — base_store.rb defines the interface; database_store.rb implements it; you can swap in custom stores (Redis, DynamoDB, etc.) without touching tracker.rb. This is essential for growing from SQLite to distributed systems.
  • Rails before_action middleware for request lifecycle injection — lib/ahoy/controller.rb uses before_action :track_ahoy_visit to inject tracking into every controller without modifying application code. Understanding Rails action hooks is key to extending or disabling tracking on specific routes (skip_before_action).
  • UTM parameter tracking — Ahoy extracts utm_source, utm_medium, utm_campaign, utm_term, utm_content from query strings and stores them per visit. This is the standard for attribution analytics; knowing Ahoy does this out-of-box saves you from writing ad-hoc tracking code.
  • ankane/ahoy_email — Companion gem by same author for tracking email opens and clicks; often used alongside ahoy for full funnel analytics
  • ankane/field_test — A/B testing gem by same author; designed to work with Ahoy for experiment tracking and variant assignment
  • plausible/analytics — Privacy-first analytics alternative; solves the same self-hosted first-party analytics problem but as a standalone service rather than a Rails gem
  • rubycdp/ferret — Event tracking gem for Rails with a different architecture; competes in the same 'in-database Rails analytics' space
  • Mixpanel/mixpanel-rails — Third-party SaaS analytics wrapper; Ahoy's primary alternative if you prefer outsourced analytics vs self-hosted

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add comprehensive tests for lib/ahoy/visit_properties.rb

visit_properties.rb is a core module that extracts and processes visit metadata (user agent, IP, referrer, etc.), but there are no dedicated test files for it. Given its critical role in data collection accuracy and the absence of visit_properties_test.rb in the test directory, adding thorough unit tests would catch regressions in property extraction logic and improve maintainability.

  • [ ] Create test/visit_properties_test.rb with tests for user agent parsing
  • [ ] Add tests for IP address extraction and validation
  • [ ] Add tests for referrer parsing and edge cases (empty, invalid, cross-domain)
  • [ ] Add tests for timezone detection and handling
  • [ ] Test integration with Rails request objects

Add tests for lib/ahoy/warden.rb authentication integration

warden.rb provides Devise integration for user identification but has no dedicated test coverage. Given that authentication is critical for proper user tracking and the gem supports multiple auth strategies, testing this integration ensures it works correctly with Devise and other authentication systems.

  • [ ] Create test/warden_test.rb
  • [ ] Add tests for authenticated user identification
  • [ ] Add tests for user change detection within a visit
  • [ ] Add tests for Devise strategy integration
  • [ ] Test fallback behavior when Warden is not available

Add integration tests for lib/ahoy/geocode_v2_job.rb background job processing

geocode_v2_job.rb handles asynchronous IP geolocation but there's a geocode_test.rb that doesn't comprehensively cover the job queue behavior, retry logic, and error handling. Adding dedicated job tests would ensure reliability of async geocoding and catch issues with different job backends.

  • [ ] Create test/geocode_v2_job_test.rb (or expand geocode_test.rb)
  • [ ] Add tests for successful IP to geocode mapping
  • [ ] Add tests for job retry logic on API failures
  • [ ] Add tests for handling invalid or private IPs
  • [ ] Test integration with different Active Job backends (Sidekiq, Delayed Job)

🌿Good first issues

  • Add integration tests for the REST API endpoints (app/controllers/ahoy/events_controller.rb, visits_controller.rb); currently test/ directory shows activerecord_generator_test.rb but no request/controller specs for the actual tracking endpoints.: The API is the primary integration point for JavaScript/mobile; test coverage there would catch regressions.
  • Document the store abstraction pattern with a step-by-step example in docs/Data-Store-Examples.md showing how to implement a custom Redis-backed store by subclassing lib/ahoy/base_store.rb.: The README mentions 'customize for any data store as you grow' but no concrete example exists; a Redis store example would clarify the extension point.
  • Add missing tests or improve coverage for lib/ahoy/visit_properties.rb (extracts browser, location, utm params); verify all User-Agent parsing edge cases are tested across desktop, mobile, and bot scenarios.: Visit enrichment is a core feature; incomplete User-Agent parsing could silently drop analytics data for real users.

Top contributors

Click to expand

📝Recent commits

Click to expand
  • 0934ed9 — Version bump to 5.5.0 [skip ci] (ankane)
  • 1881c47 — Updated changelog [skip ci] (ankane)
  • bb10d30 — Fixed error with Ahoy::Tracker outside of request when cookies disabled - resolves #554 (ankane)
  • 3b4e45f — Dropped support for Ruby < 3.3 and Rails < 7.2 (ankane)
  • 41d7834 — Updated readme [skip ci] (ankane)
  • 2186f38 — Updated license year [skip ci] (ankane)
  • eed4fda — Version bump to 5.4.2 [skip ci] (ankane)
  • f73c04e — Updated changelog [skip ci] (ankane)
  • 7c50688 — Fixed cookie deletion with path option (ankane)
  • 5a801f4 — Fixed cookie deletion with cookie_domain option - fixes #581 (ankane)

🔒Security observations

  • High · Potential SQL Injection in Database Store — lib/ahoy/database_store.rb. The file lib/ahoy/database_store.rb likely constructs database queries for storing analytics data. Without examining the actual implementation, there's a risk of SQL injection if user input (visit properties, event data) is not properly parameterized when building queries. Fix: Ensure all database queries use parameterized statements or ActiveRecord's query interface. Audit database_store.rb to confirm proper use of prepared statements and sanitization of all user-supplied input.
  • High · Potential XSS Vulnerability in Event/Visit Data Storage — lib/ahoy/tracker.rb, lib/ahoy/model.rb. The application tracks and stores user-provided event names and properties (e.g., 'ahoy.track "My first event", language: "Ruby"'). If this data is displayed in admin panels or reports without proper escaping, it could enable XSS attacks. Fix: Implement proper output encoding/escaping for all tracked data when displayed. Use Rails' automatic escaping in templates, and explicitly escape data in API responses. Sanitize event names and properties.
  • High · Missing CSRF Protection on API Endpoints — app/controllers/ahoy/events_controller.rb, app/controllers/ahoy/visits_controller.rb. The app/controllers/ahoy/events_controller.rb and visits_controller.rb expose API endpoints for tracking. These endpoints may be vulnerable to CSRF attacks if they accept state-changing requests without proper CSRF token validation. Fix: Implement CSRF protection via Rails' 'protect_from_forgery' or configure 'skip_before_action :verify_authenticity_token' only if using alternative token validation (e.g., API tokens). Document API security requirements.
  • Medium · Inadequate Input Validation on Event Properties — lib/ahoy/tracker.rb, lib/ahoy/controller.rb. User-supplied event properties and visit metadata may not have sufficient validation constraints (type checking, length limits, allowed characters). This could lead to storage of malicious or excessively large data. Fix: Implement strict input validation for all tracked data: validate types, enforce reasonable size limits, and reject unexpected input formats. Sanitize strings before storage.
  • Medium · Potential Sensitive Data Exposure in Visit Properties — lib/ahoy/visit_properties.rb, test/internal/config/initializers/ahoy.rb. The visit_properties.rb and related tracking code capture browser/device information and custom properties. If not carefully configured, this could inadvertently capture sensitive user data (e.g., PII in custom events). Fix: Document what data should/shouldn't be tracked. Implement filtering to prevent capture of sensitive fields. Provide configuration options to exclude specific data types. Add warnings in documentation about PII risks.
  • Medium · Insecure Geocoding Data Handling — lib/ahoy/geocode_v2_job.rb. The lib/ahoy/geocode_v2_job.rb performs geocoding operations, likely calling external APIs. If API keys or sensitive geocoding data are logged or exposed, this could compromise privacy and security. Fix: Ensure API credentials are stored in environment variables, not hardcoded. Avoid logging sensitive geocoding requests/responses. Implement rate limiting and validate geocoding API responses. Consider privacy implications of storing location data.
  • Medium · Missing Content Security Policy Headers — lib/ahoy/engine.rb, app/controllers/ahoy/base_controller.rb. The application serves tracking data and may not enforce CSP headers, increasing XSS attack surface. No evidence of CSP configuration in the provided file structure. Fix: Configure Content Security Policy headers in the Rails application to restrict script sources and prevent inline script execution. Implement in base_controller.rb for all Ahoy endpoints.
  • Low · Dependency Version Management — Gemfile, ahoy_matey.gemspec, gemfiles/. The gemfile entries (rails72.gemfile, rails80.gemfile) indicate the gem supports multiple Rails versions. Without seeing explicit version constraints in the Gemfile, there's a risk of using vulnerable dependency versions. Fix: Explicitly pin all dependencies to secure, tested versions

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.