{"id":18103172,"url":"https://github.com/rameerez/footprinted","last_synced_at":"2026-02-09T01:05:04.087Z","repository":{"id":260385453,"uuid":"863468310","full_name":"rameerez/footprinted","owner":"rameerez","description":"👣 Ruby gem to track geolocated user activity in Rails","archived":false,"fork":false,"pushed_at":"2024-10-30T02:52:08.000Z","size":33,"stargazers_count":21,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-21T07:46:49.255Z","etag":null,"topics":["analytics","data-analysis","events","gem","geolocation","ip","ip-geolocation","monitoring","rails","ruby","ruby-on-rails","user","user-management","user-tracking"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rameerez.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-09-26T10:51:21.000Z","updated_at":"2024-11-08T21:32:00.000Z","dependencies_parsed_at":"2024-10-31T03:06:25.404Z","dependency_job_id":null,"html_url":"https://github.com/rameerez/footprinted","commit_stats":null,"previous_names":["rameerez/footprinted"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Ffootprinted","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Ffootprinted/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Ffootprinted/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Ffootprinted/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rameerez","download_url":"https://codeload.github.com/rameerez/footprinted/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248766734,"owners_count":21158301,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["analytics","data-analysis","events","gem","geolocation","ip","ip-geolocation","monitoring","rails","ruby","ruby-on-rails","user","user-management","user-tracking"],"created_at":"2024-10-31T22:10:46.351Z","updated_at":"2026-02-09T01:05:04.080Z","avatar_url":"https://github.com/rameerez.png","language":"Ruby","readme":"# 👣 `footprinted` - Simple event tracking for Rails apps\n\n[![Gem Version](https://badge.fury.io/rb/footprinted.svg)](https://badge.fury.io/rb/footprinted) [![Build Status](https://github.com/rameerez/footprinted/workflows/Tests/badge.svg)](https://github.com/rameerez/footprinted/actions)\n\n\u003e [!TIP]\n\u003e **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=footprinted)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks. Go [check it out](https://railsfast.com/?ref=footprinted)!\n\n`footprinted` makes it trivial to add event tracking to any Rails model.\n\nThink of a file transfer app like WeTransfer. You may want to track where every file download came from:\n\n```ruby\n# Add to your model\nhas_trackable :downloads\n\n# Track events in the controller\n@file.track_download(ip: request.remote_ip, metadata: { version: \"2.1.0\" })\n\n# Query the data\n@file.downloads.by_country(\"US\").last_days(30).count\n# =\u003e 42\n```\n\nIn the example above, `footprinted` adds `footprints` to your `File` model, allowing you to easily record event data; and provides you with methods to build dashboards and analytics / business intelligence systems.\n\nMore use cases:\n- Track login attempts\n- Track profile views in a social app (think: LinkedIn)\n- Track document open events in a file-signing app (think: DocuSign)\n- Track any business-critical operation for enterprise-compliant audit logs\n- Track any interaction where knowing the *where* (IP, geolocation) or *what* (OS, app version, device ID...) matters\n\nEvery event (footprint) in `footprinted` records the IP address, full geolocation data (country, city, region, coordinates, timezone), arbitrary JSONB metadata, and who triggered it — all resolved automatically via [`trackdown`](https://github.com/rameerez/trackdown). `footprinted` allows you to trivially build analytics dashboards and audit logs for all your app events.\n\n\u003e [!NOTE]\n\u003e By adding `has_trackable :downloads` to your model, `footprinted` automatically creates a `downloads` scoped association and a `track_download` method. No extra models or associations to define. Just track and query.\n\n## Installation\n\nAdd this to your Gemfile:\n\n```ruby\ngem \"footprinted\"\n```\n\nThen run:\n\n```bash\nbundle install\nrails generate footprinted:install\nrails db:migrate\n```\n\nThis creates the `footprints` table with columns for IP, geolocation fields, event type, JSONB metadata, polymorphic trackable/performer references, and all the necessary indexes.\n\n\u003e [!IMPORTANT]\n\u003e This gem depends on [`trackdown`](https://github.com/rameerez/trackdown) for IP geolocation. `trackdown` works out of the box with Cloudflare (zero config) and also supports MaxMind. See the [trackdown README](https://github.com/rameerez/trackdown) for setup instructions.\n\n## Quick Start\n\nInclude the concern in any model you want to track activity on:\n\n```ruby\nclass Product \u003c ApplicationRecord\n  include Footprinted::Model\n\n  has_trackable :activations\n  has_trackable :downloads\nend\n```\n\nTrack events in your controller:\n\n```ruby\nclass DownloadsController \u003c ApplicationController\n  def create\n    @product = Product.find(params[:product_id])\n    @product.track_download(ip: request.remote_ip, metadata: { version: params[:version] })\n  end\nend\n```\n\nQuery the data:\n\n```ruby\n@product.downloads.count                    # =\u003e 847\n@product.downloads.by_country(\"US\").count   # =\u003e 529\n@product.downloads.last_days(7)             # recent downloads\n@product.downloads.countries                # =\u003e [\"US\", \"UK\", \"CA\", ...]\n```\n\n## `has_trackable` DSL\n\nDeclare one or more trackable event types on any model:\n\n```ruby\nclass Resource \u003c ApplicationRecord\n  include Footprinted::Model\n\n  has_trackable :downloads\n  has_trackable :previews\n  has_trackable :shares\nend\n```\n\nEach `has_trackable` call gives you:\n\n- A **scoped association** (e.g., `resource.downloads`) that only returns footprints of that event type\n- A **track method** (e.g., `resource.track_download(ip:)`) that creates footprints with the correct event type\n\nThe association name is pluralized, and the track method is singularized:\n\n| Declaration | Association | Track method |\n|---|---|---|\n| `has_trackable :profile_views` | `.profile_views` | `.track_profile_view(ip:)` |\n| `has_trackable :downloads` | `.downloads` | `.track_download(ip:)` |\n| `has_trackable :login_attempts` | `.login_attempts` | `.track_login_attempt(ip:)` |\n\n### Track method parameters\n\n```ruby\n@resource.track_download(\n  ip: request.remote_ip,          # Required: the IP address\n  request: request,               # Optional: passed to Trackdown for better geolocation\n  performer: current_user,        # Optional: who triggered this activity\n  metadata: { browser: \"Chrome\" },# Optional: arbitrary JSONB metadata\n  occurred_at: 2.hours.ago        # Optional: defaults to Time.current\n)\n```\n\n## Generic `track()` method\n\nFor ad-hoc event types that don't need a dedicated association, use the generic `track` method:\n\n```ruby\n@user.track(:signup, ip: request.remote_ip)\n@user.track(:password_reset, ip: request.remote_ip, performer: admin)\n@user.track(\"api_call\", ip: request.remote_ip, metadata: { endpoint: \"/users\" })\n```\n\nIt accepts the same parameters as `track_\u003cevent_type\u003e` and works with both symbols and strings. All events tracked this way are accessible through the `footprints` association:\n\n```ruby\n@user.footprints.by_event(\"signup\").count\n```\n\n## Scopes\n\n`footprinted` provides several useful scopes out of the box:\n\n```ruby\n# Filter by event type\n@user.footprints.by_event(\"download\")\n\n# Filter by country code\n@user.footprints.by_country(\"US\")\n\n# Order by most recent\n@user.footprints.recent\n\n# Time-based filtering\n@user.footprints.last_days(30)\n@user.footprints.between(1.week.ago, Time.current)\n\n# Filter by performer\n@user.footprints.performed_by(some_user)\n```\n\nScopes are chainable:\n\n```ruby\n@user.profile_views\n  .by_country(\"US\")\n  .last_days(7)\n  .performed_by(current_user)\n  .recent\n```\n\n### Class methods\n\n```ruby\n# Get all distinct event types\nFootprinted::Footprint.event_types\n# =\u003e [\"view\", \"download\", \"login\"]\n\n# Get all distinct country codes (excludes nil)\nFootprinted::Footprint.countries\n# =\u003e [\"US\", \"UK\", \"CA\", \"DE\"]\n```\n\n## JSONB metadata\n\nEvery footprint can store arbitrary metadata as JSONB. This is great for device info, SDK versions, or any context you want to associate with the event:\n\n```ruby\n@product.track(:activation, ip: request.remote_ip, metadata: {\n  device_id: \"A1B2C3\",\n  app_version: \"2.1.0\",\n  platform: \"macOS\",\n  os_version: \"15.2\",\n  sdk_version: \"0.4.0\",\n  locale: \"en_US\"\n})\n```\n\nQuery metadata using your database's JSON operators:\n\n```ruby\n# Find activations from macOS\n@product.footprints\n  .by_event(\"activation\")\n  .where(\"metadata-\u003e\u003e'platform' = ?\", \"macOS\")\n\n# Group by app version\n@product.footprints\n  .by_event(\"activation\")\n  .group(\"metadata-\u003e\u003e'app_version'\")\n  .count\n# =\u003e { \"2.0.0\" =\u003e 12, \"2.1.0\" =\u003e 45 }\n```\n\n### Performance at scale\n\nThe default migration creates a [GIN index](https://www.postgresql.org/docs/current/gin-intro.html) on the `metadata` column. GIN indexes are excellent for **containment queries** (`@\u003e`, `?`, `?|`) but do **not** speed up key extraction queries like `GROUP BY metadata-\u003e\u003e'field'` or `COUNT(DISTINCT metadata-\u003e\u003e'field')`.\n\nFor small-to-medium tables (up to hundreds of thousands of rows), JSONB queries work just fine. At larger scale (millions of rows), if you're frequently grouping or counting distinct values on specific metadata keys, you have two options:\n\n**Option 1: Expression indexes** — add B-tree indexes on the specific JSONB keys you query most. No schema change needed:\n\n```ruby\n# In a migration in your host app\nadd_index :footprints, \"(metadata-\u003e\u003e'device_id')\", name: \"idx_footprints_device_id\"\nadd_index :footprints, \"(metadata-\u003e\u003e'app_version')\", name: \"idx_footprints_app_version\"\n```\n\n**Option 2: Promote to columns** — for your hottest query paths (e.g., `device_id` for DAU/MAU), add dedicated columns to the `footprints` table in your host app. This gives you proper B-tree indexes and fast `DISTINCT` counts:\n\n```ruby\n# In a migration in your host app\nadd_column :footprints, :device_id,    :string\nadd_column :footprints, :app_version,  :string\nadd_column :footprints, :platform,     :string\n\nadd_index :footprints, :device_id\nadd_index :footprints, :app_version\n```\n\nThen write to both the column and the metadata hash in your tracking code. The gem stays generic; your app adds the columns it needs.\n\n\u003e [!TIP]\n\u003e Which metadata keys to promote depends on your use case. A licensing SaaS might promote `device_id` + `app_version`. An e-commerce app might promote `product_id` + `session_id`. A CMS might promote `page_url` + `referrer`. Keep the JSONB for everything else.\n\n### Database compatibility\n\n| Feature | PostgreSQL | MySQL 5.7+ | SQLite |\n|---|---|---|---|\n| JSONB `metadata` column | `jsonb` (native, fast) | `json` (native) | `text` (stored as string) |\n| GIN index on `metadata` | Supported | Not supported | Not supported |\n| JSON queries (`-\u003e\u003e`, `@\u003e`) | Full support | `JSON_EXTRACT()` syntax | `json_extract()` via extension |\n| Expression indexes | Supported | Supported (generated columns) | Not supported |\n\nPostgreSQL is the recommended database for `footprinted`. It has the best JSONB support, GIN indexes for containment queries, and expression indexes for key extraction. MySQL works but uses different JSON syntax. SQLite works for development and testing but stores metadata as text and has limited JSON query support.\n\n## Async mode with ActiveJob\n\nFor high-traffic endpoints, you can enqueue footprint creation in the background:\n\n```ruby\n# config/initializers/footprinted.rb\nFootprinted.configure do |config|\n  config.async = true\nend\n```\n\nWhen async is enabled, `track` and `track_\u003cevent_type\u003e` calls enqueue a `Footprinted::TrackJob` instead of writing to the database immediately. The job serializes all attributes (including metadata as a hash and occurred_at as ISO 8601) and processes them in the background.\n\nYou need a working ActiveJob backend (Sidekiq, Solid Queue, etc.) for this to work.\n\n## Geolocation via Trackdown\n\n`footprinted` automatically resolves geolocation data from IP addresses using the [`trackdown`](https://github.com/rameerez/trackdown) gem. For every footprint, the following fields are populated:\n\n| Field | Example |\n|---|---|\n| `country_code` | `\"US\"` |\n| `country_name` | `\"United States\"` |\n| `city` | `\"San Francisco\"` |\n| `region` | `\"California\"` |\n| `continent` | `\"NA\"` |\n| `timezone` | `\"America/Los_Angeles\"` |\n| `latitude` | `37.7749` |\n| `longitude` | `-122.4194` |\n\nIf geolocation fails (network error, invalid IP, etc.), the footprint is still saved — just without geolocation data. Errors are logged via `Rails.logger`.\n\nIf you already know the `country_code`, you can set it directly and geolocation will be skipped:\n\n```ruby\n@user.track_profile_view(ip: \"1.2.3.4\", country_code: \"DE\")\n```\n\n## Configuration\n\nCreate an initializer (the generator does this for you):\n\n```ruby\n# config/initializers/footprinted.rb\nFootprinted.configure do |config|\n  # Enqueue footprint creation via ActiveJob (default: false)\n  config.async = false\nend\n```\n\n## Generator\n\nThe install generator creates two things:\n\n1. A migration for the `footprints` table with all geolocation columns, polymorphic references, JSONB metadata, indexes, and a composite index on `[trackable_type, trackable_id, event_type, occurred_at]`\n2. An initializer at `config/initializers/footprinted.rb`\n\n```bash\nrails generate footprinted:install\nrails db:migrate\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`.\n\n### Releasing a new version\n\nWhen bumping the version, update all three of these together:\n\n1. `lib/footprinted/version.rb` — the version constant\n2. `gemfiles/*.gemfile.lock` — run `bundle exec appraisal install` to regenerate\n3. `test/footprinted/version_test.rb` — the hardcoded version assertion\n\nCI will fail if any of these are out of sync.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/rameerez/footprinted. Our code of conduct is: just be nice and make your mom proud of what you do and post online.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frameerez%2Ffootprinted","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frameerez%2Ffootprinted","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frameerez%2Ffootprinted/lists"}