{"id":13395032,"url":"https://github.com/ankane/ahoy","last_synced_at":"2025-05-13T16:04:57.181Z","repository":{"id":14491561,"uuid":"17204381","full_name":"ankane/ahoy","owner":"ankane","description":"Simple, powerful, first-party analytics for Rails","archived":false,"fork":false,"pushed_at":"2025-05-05T03:39:51.000Z","size":836,"stargazers_count":4313,"open_issues_count":7,"forks_count":380,"subscribers_count":56,"default_branch":"master","last_synced_at":"2025-05-05T04:31:16.956Z","etag":null,"topics":["analytics","events","first-party-analytics","rails","visits"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"coligo-io/real-time-analytics-node-socketio-vuejs","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ankane.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","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,"zenodo":null}},"created_at":"2014-02-26T08:42:22.000Z","updated_at":"2025-05-05T03:39:50.000Z","dependencies_parsed_at":"2023-11-13T05:32:27.892Z","dependency_job_id":"1b381d08-46e8-4554-8ff5-caacfa1e6a0f","html_url":"https://github.com/ankane/ahoy","commit_stats":{"total_commits":930,"total_committers":51,"mean_commits":"18.235294117647058","dds":0.4408602150537635,"last_synced_commit":"d28f6e75178a8b27f2895209f5f7aa103525e0fe"},"previous_names":[],"tags_count":69,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankane%2Fahoy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankane%2Fahoy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankane%2Fahoy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankane%2Fahoy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ankane","download_url":"https://codeload.github.com/ankane/ahoy/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252590511,"owners_count":21772934,"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","events","first-party-analytics","rails","visits"],"created_at":"2024-07-30T17:01:39.841Z","updated_at":"2025-05-13T16:04:57.168Z","avatar_url":"https://github.com/ankane.png","language":"Ruby","readme":"# Ahoy\n\n:fire: Simple, powerful, first-party analytics for Rails\n\nTrack 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.\n\n: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\n\n:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)\n\n[![Build Status](https://github.com/ankane/ahoy/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/ahoy/actions)\n\n## Installation\n\nAdd this line to your application’s Gemfile:\n\n```ruby\ngem \"ahoy_matey\"\n```\n\nAnd run:\n\n```sh\nbundle install\nrails generate ahoy:install\nrails db:migrate\n```\n\nRestart your web server, open a page in your browser, and a visit will be created :tada:\n\nTrack your first event from a controller with:\n\n```ruby\nahoy.track \"My first event\", language: \"Ruby\"\n```\n\n### JavaScript, Native Apps, \u0026 AMP\n\nEnable the API in `config/initializers/ahoy.rb`:\n\n```ruby\nAhoy.api = true\n```\n\nAnd restart your web server.\n\n### JavaScript\n\nFor Importmap (Rails 7+ default), add to `config/importmap.rb`:\n\n```ruby\npin \"ahoy\", to: \"ahoy.js\"\n```\n\nAnd add to `app/javascript/application.js`:\n\n```javascript\nimport \"ahoy\"\n```\n\nFor Bun, esbuild, rollup.js, or Webpack, run:\n\n```sh\nbun add ahoy.js\n# or\nyarn add ahoy.js\n```\n\nAnd add to `app/javascript/application.js`:\n\n```javascript\nimport ahoy from \"ahoy.js\"\n```\n\nFor Sprockets, add to `app/assets/javascripts/application.js`:\n\n```javascript\n//= require ahoy\n```\n\nTrack an event with:\n\n```javascript\nahoy.track(\"My second event\", {language: \"JavaScript\"});\n```\n\n### Native Apps\n\nCheck out [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android).\n\n### Geocoding Setup\n\nTo enable geocoding, see the [Geocoding section](#geocoding).\n\n### GDPR Compliance\n\nAhoy provides a number of options to help with GDPR compliance. See the [GDPR section](#gdpr-compliance-1) for more info.\n\n## How It Works\n\n### Visits\n\nWhen someone visits your website, Ahoy creates a visit with lots of useful information.\n\n- **traffic source** - referrer, referring domain, landing page\n- **location** - country, region, city, latitude, longitude\n- **technology** - browser, OS, device type\n- **utm parameters** - source, medium, term, content, campaign\n\nUse the `current_visit` method to access it.\n\nPrevent certain Rails actions from creating visits with:\n\n```ruby\nskip_before_action :track_ahoy_visit\n```\n\nThis is typically useful for APIs. If your entire Rails app is an API, you can use:\n\n```ruby\nAhoy.api_only = true\n```\n\nYou 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.\n\n```ruby\nAhoy.server_side_visits = :when_needed\n```\n\n### Events\n\nEach event has a `name` and `properties`. There are several ways to track events.\n\n#### Ruby\n\n```ruby\nahoy.track \"Viewed book\", title: \"Hot, Flat, and Crowded\"\n```\n\nTrack actions automatically with:\n\n```ruby\nclass ApplicationController \u003c ActionController::Base\n  after_action :track_action\n\n  protected\n\n  def track_action\n    ahoy.track \"Ran action\", request.path_parameters\n  end\nend\n```\n\n#### JavaScript\n\n```javascript\nahoy.track(\"Viewed book\", {title: \"The World is Flat\"});\n```\n\nSee [Ahoy.js](https://github.com/ankane/ahoy.js) for a complete list of features.\n\n#### Native Apps\n\nSee the docs for [Ahoy iOS](https://github.com/namolnad/ahoy-ios) and [Ahoy Android](https://github.com/instacart/ahoy-android).\n\n#### AMP\n\n```erb\n\u003chead\u003e\n  \u003cscript async custom-element=\"amp-analytics\" src=\"https://cdn.ampproject.org/v0/amp-analytics-0.1.js\"\u003e\u003c/script\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003c%= amp_event \"Viewed article\", title: \"Analytics with Rails\" %\u003e\n\u003c/body\u003e\n```\n\n### Associated Models\n\nSay we want to associate orders with visits. Just add `visitable` to the model.\n\n```ruby\nclass Order \u003c ApplicationRecord\n  visitable :ahoy_visit\nend\n```\n\nWhen a visitor places an order, the `ahoy_visit_id` column is automatically set :tada:\n\nSee where orders are coming from with simple joins:\n\n```ruby\nOrder.joins(:ahoy_visit).group(\"referring_domain\").count\nOrder.joins(:ahoy_visit).group(\"city\").count\nOrder.joins(:ahoy_visit).group(\"device_type\").count\n```\n\nHere’s what the migration to add the `ahoy_visit_id` column should look like:\n\n```ruby\nclass AddAhoyVisitToOrders \u003c ActiveRecord::Migration[8.0]\n  def change\n    add_reference :orders, :ahoy_visit\n  end\nend\n```\n\nCustomize the column with:\n\n```ruby\nvisitable :sign_up_visit\n```\n\n### Users\n\nAhoy 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.\n\nWith other authentication frameworks, add this to the end of your sign in method:\n\n```ruby\nahoy.authenticate(user)\n```\n\nTo see the visits for a given user, create an association:\n\n```ruby\nclass User \u003c ApplicationRecord\n  has_many :visits, class_name: \"Ahoy::Visit\"\nend\n```\n\nAnd use:\n\n```ruby\nUser.find(123).visits\n```\n\n#### Custom User Method\n\nUse a method besides `current_user`\n\n```ruby\nAhoy.user_method = :true_user\n```\n\nor use a proc\n\n```ruby\nAhoy.user_method = -\u003e(controller) { controller.true_user }\n```\n\n#### Doorkeeper\n\nTo attach the user with [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper), be sure you have a `current_resource_owner` method in `ApplicationController`.\n\n```ruby\nclass ApplicationController \u003c ActionController::Base\n  private\n\n  def current_resource_owner\n    User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token\n  end\nend\n```\n\n### Exclusions\n\nBots are excluded from tracking by default. To include them, use:\n\n```ruby\nAhoy.track_bots = true\n```\n\nAdd your own rules with:\n\n```ruby\nAhoy.exclude_method = lambda do |controller, request|\n  request.ip == \"192.168.1.1\"\nend\n```\n\n### Visit Duration\n\nBy default, a new visit is created after 4 hours of inactivity. Change this with:\n\n```ruby\nAhoy.visit_duration = 30.minutes\n```\n\n### Visitor Duration\n\nBy default, a new `visitor_token` is generated after 2 years. Change this with:\n\n```ruby\nAhoy.visitor_duration = 30.days\n```\n\n### Cookies\n\nTo track visits across multiple subdomains, use:\n\n```ruby\nAhoy.cookie_domain = :all\n```\n\nSet other [cookie options](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html) with:\n\n```ruby\nAhoy.cookie_options = {same_site: :lax}\n```\n\nYou can also [disable cookies](#anonymity-sets--cookies)\n\n### Token Generation\n\nAhoy uses random UUIDs for visit and visitor tokens by default, but you can use your own generator like [ULID](https://github.com/rafaelsales/ulid).\n\n```ruby\nAhoy.token_generator = -\u003e { ULID.generate }\n```\n\n### Throttling\n\nYou can use [Rack::Attack](https://github.com/rack/rack-attack) to throttle requests to the API.\n\n```ruby\nclass Rack::Attack\n  throttle(\"ahoy/ip\", limit: 20, period: 1.minute) do |req|\n    if req.path.start_with?(\"/ahoy/\")\n      req.ip\n    end\n  end\nend\n```\n\n### Exceptions\n\nExceptions 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:\n\n```ruby\nSafely.report_exception_method = -\u003e(e) { Rollbar.error(e) }\n```\n\n## Geocoding\n\nAhoy 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).\n\nTo enable geocoding, add this line to your application’s Gemfile:\n\n```ruby\ngem \"geocoder\"\n```\n\nAnd update `config/initializers/ahoy.rb`:\n\n```ruby\nAhoy.geocode = true\n```\n\nGeocoding is performed in a background job so it doesn’t slow down web requests. The default job queue is `:ahoy`. Change this with:\n\n```ruby\nAhoy.job_queue = :low_priority\n```\n\n### Local Geocoding\n\nFor privacy and performance, we recommend geocoding locally.\n\nFor city-level geocoding, download the [GeoLite2 City database](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data).\n\nAdd this line to your application’s Gemfile:\n\n```ruby\ngem \"maxminddb\"\n```\n\nAnd create `config/initializers/geocoder.rb` with:\n\n```ruby\nGeocoder.configure(\n  ip_lookup: :geoip2,\n  geoip2: {\n    file: \"path/to/GeoLite2-City.mmdb\"\n  }\n)\n```\n\nFor country-level geocoding, install the `geoip-database` package. It’s preinstalled on Heroku. For Ubuntu, use:\n\n```sh\nsudo apt-get install geoip-database\n```\n\nAdd this line to your application’s Gemfile:\n\n```ruby\ngem \"geoip\"\n```\n\nAnd create `config/initializers/geocoder.rb` with:\n\n```ruby\nGeocoder.configure(\n  ip_lookup: :maxmind_local,\n  maxmind_local: {\n    file: \"/usr/share/GeoIP/GeoIP.dat\",\n    package: :country\n  }\n)\n```\n\n### Load Balancer Geocoding\n\nSome load balancers can add geocoding information to request headers.\n\n- [nginx](https://nginx.org/en/docs/http/ngx_http_geoip_module.html)\n- [Google Cloud](https://cloud.google.com/load-balancing/docs/custom-headers)\n- [Cloudflare](https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-Cloudflare-IP-Geolocation)\n\nUpdate `config/initializers/ahoy.rb` with:\n\n```ruby\nAhoy.geocode = false\n\nclass Ahoy::Store \u003c Ahoy::DatabaseStore\n  def track_visit(data)\n    data[:country] = request.headers[\"\u003ccountry-header\u003e\"]\n    data[:region] = request.headers[\"\u003cregion-header\u003e\"]\n    data[:city] = request.headers[\"\u003ccity-header\u003e\"]\n    super(data)\n  end\nend\n```\n\n## GDPR Compliance\n\nAhoy provides a number of options to help with [GDPR compliance](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation).\n\nUpdate `config/initializers/ahoy.rb` with:\n\n```ruby\nclass Ahoy::Store \u003c Ahoy::DatabaseStore\n  def authenticate(data)\n    # disables automatic linking of visits and users\n  end\nend\n\nAhoy.mask_ips = true\nAhoy.cookies = :none\n```\n\nThis:\n\n- Masks IP addresses\n- Switches from cookies to anonymity sets\n- Disables automatic linking of visits and users\n\nIf you use JavaScript tracking, also set:\n\n```javascript\nahoy.configure({cookies: false});\n```\n\n### IP Masking\n\nAhoy can mask IPs with the same approach [Google Analytics uses for IP anonymization](https://support.google.com/analytics/answer/2763052). This means:\n\n- For IPv4, the last octet is set to 0 (`8.8.4.4` becomes `8.8.4.0`)\n- For IPv6, the last 80 bits are set to zeros (`2001:4860:4860:0:0:0:0:8844` becomes `2001:4860:4860::`)\n\n```ruby\nAhoy.mask_ips = true\n```\n\nIPs are masked before geolocation is performed.\n\nTo mask previously collected IPs, use:\n\n```ruby\nAhoy::Visit.find_each do |visit|\n  visit.update_column :ip, Ahoy.mask_ip(visit.ip)\nend\n```\n\n### Anonymity Sets \u0026 Cookies\n\nAhoy can switch from cookies to [anonymity sets](https://privacypatterns.org/patterns/Anonymity-set). Instead of cookies, visitors with the same IP mask and user agent are grouped together in an anonymity set.\n\n```ruby\nAhoy.cookies = :none\n```\n\nNote: If Ahoy was installed before v5, [add an index](#50) before making this change.\n\nPreviously set cookies are automatically deleted. If you use JavaScript tracking, also set:\n\n```javascript\nahoy.configure({cookies: false});\n```\n\n## Data Retention\n\nData should only be retained for as long as it’s needed. Delete older data with:\n\n```ruby\nAhoy::Visit.where(\"started_at \u003c ?\", 2.years.ago).find_in_batches do |visits|\n  visit_ids = visits.map(\u0026:id)\n  Ahoy::Event.where(visit_id: visit_ids).delete_all\n  Ahoy::Visit.where(id: visit_ids).delete_all\nend\n```\n\nYou can use [Rollup](https://github.com/ankane/rollup) to aggregate important data before you do.\n\n```ruby\nAhoy::Visit.rollup(\"Visits\", interval: \"hour\")\n```\n\nDelete data for a specific user with:\n\n```ruby\nuser_id = 123\nvisit_ids = Ahoy::Visit.where(user_id: user_id).pluck(:id)\nAhoy::Event.where(visit_id: visit_ids).delete_all\nAhoy::Visit.where(id: visit_ids).delete_all\nAhoy::Event.where(user_id: user_id).delete_all\n```\n\n## Development\n\nAhoy is built with developers in mind. You can run the following code in your browser’s console.\n\nForce a new visit\n\n```javascript\nahoy.reset(); // then reload the page\n```\n\nLog messages\n\n```javascript\nahoy.debug();\n```\n\nTurn off logging\n\n```javascript\nahoy.debug(false);\n```\n\nDebug API requests in Ruby\n\n```ruby\nAhoy.quiet = false\n```\n\n## Data Stores\n\nData 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`:\n\n```ruby\nclass Ahoy::Store \u003c Ahoy::DatabaseStore\nend\n```\n\nThere are four events data stores can subscribe to:\n\n```ruby\nclass Ahoy::Store \u003c Ahoy::BaseStore\n  def track_visit(data)\n    # new visit\n  end\n\n  def track_event(data)\n    # new event\n  end\n\n  def geocode(data)\n    # visit geocoded\n  end\n\n  def authenticate(data)\n    # user authenticates\n  end\nend\n```\n\nData stores are designed to be highly customizable so you can scale as you grow. Check out [examples](docs/Data-Store-Examples.md) for Kafka, RabbitMQ, Fluentd, NATS, NSQ, and Amazon Kinesis Firehose.\n\n### Track Additional Data\n\n```ruby\nclass Ahoy::Store \u003c Ahoy::DatabaseStore\n  def track_visit(data)\n    data[:accept_language] = request.headers[\"Accept-Language\"]\n    super(data)\n  end\nend\n```\n\nTwo useful methods you can use are `request` and `controller`.\n\nYou can pass additional visit data from JavaScript with:\n\n```javascript\nahoy.configure({visitParams: {referral_code: 123}});\n```\n\nAnd use:\n\n```ruby\nclass Ahoy::Store \u003c Ahoy::DatabaseStore\n  def track_visit(data)\n    data[:referral_code] = request.parameters[:referral_code]\n    super(data)\n  end\nend\n```\n\n### Use Different Models\n\n```ruby\nclass Ahoy::Store \u003c Ahoy::DatabaseStore\n  def visit_model\n    MyVisit\n  end\n\n  def event_model\n    MyEvent\n  end\nend\n```\n\n## Explore the Data\n\n[Blazer](https://github.com/ankane/blazer) is a great tool for exploring your data.\n\nWith Active Record, you can do:\n\n```ruby\nAhoy::Visit.group(:search_keyword).count\nAhoy::Visit.group(:country).count\nAhoy::Visit.group(:referring_domain).count\n```\n\n[Chartkick](https://www.chartkick.com/) and [Groupdate](https://github.com/ankane/groupdate) make it easy to visualize the data.\n\n```erb\n\u003c%= line_chart Ahoy::Visit.group_by_day(:started_at).count %\u003e\n```\n\n### Querying Events\n\nAhoy provides a few methods on the event model to make querying easier.\n\nTo query on both name and properties, you can use:\n\n```ruby\nAhoy::Event.where_event(\"Viewed product\", product_id: 123).count\n```\n\nOr just query properties with:\n\n```ruby\nAhoy::Event.where_props(product_id: 123, category: \"Books\").count\n```\n\nGroup by properties with:\n\n```ruby\nAhoy::Event.group_prop(:product_id, :category).count\n```\n\nNote: MySQL and MariaDB always return string keys (including `\"null\"` for `nil`) for `group_prop`.\n\n### Funnels\n\nIt’s easy to create funnels.\n\n```ruby\nviewed_store_ids = Ahoy::Event.where(name: \"Viewed store\").distinct.pluck(:user_id)\nadded_item_ids = Ahoy::Event.where(user_id: viewed_store_ids, name: \"Added item to cart\").distinct.pluck(:user_id)\nviewed_checkout_ids = Ahoy::Event.where(user_id: added_item_ids, name: \"Viewed checkout\").distinct.pluck(:user_id)\n```\n\nThe same approach also works with visitor tokens.\n\n### Rollups\n\nImprove query performance by pre-aggregating data with [Rollup](https://github.com/ankane/rollup).\n\n```ruby\nAhoy::Event.where(name: \"Viewed store\").rollup(\"Store views\")\n```\n\nThis is only needed if you have a lot of data.\n\n### Forecasting\n\nTo forecast future visits and events, check out [Prophet](https://github.com/ankane/prophet).\n\n```ruby\ndaily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate\nProphet.forecast(daily_visits)\n```\n\n### Anomaly Detection\n\nTo detect anomalies in visits and events, check out [AnomalyDetection.rb](https://github.com/ankane/AnomalyDetection.rb).\n\n```ruby\ndaily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate\nAnomalyDetection.detect(daily_visits, period: 7)\n```\n\n### Breakout Detection\n\nTo detect breakouts in visits and events, check out [Breakout](https://github.com/ankane/breakout).\n\n```ruby\ndaily_visits = Ahoy::Visit.group_by_day(:started_at).count # uses Groupdate\nBreakout.detect(daily_visits)\n```\n\n### Recommendations\n\nTo make recommendations based on events, check out [Disco](https://github.com/ankane/disco#ahoy).\n\n## Tutorials\n\n- [Tracking Metrics with Ahoy and Blazer](https://gorails.com/episodes/internal-metrics-with-ahoy-and-blazer)\n\n## API Spec\n\n### Visits\n\nGenerate visit and visitor tokens as [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier), and include these values in the `Ahoy-Visit` and `Ahoy-Visitor` headers with all requests.\n\nSend a `POST` request to `/ahoy/visits` with `Content-Type: application/json` and a body like:\n\n```json\n{\n  \"visit_token\": \"\u003cvisit-token\u003e\",\n  \"visitor_token\": \"\u003cvisitor-token\u003e\",\n  \"platform\": \"iOS\",\n  \"app_version\": \"1.0.0\",\n  \"os_version\": \"11.2.6\"\n}\n```\n\nAfter 4 hours of inactivity, create another visit (use the same visitor token).\n\n### Events\n\nSend a `POST` request to `/ahoy/events` with `Content-Type: application/json` and a body like:\n\n```json\n{\n  \"visit_token\": \"\u003cvisit-token\u003e\",\n  \"visitor_token\": \"\u003cvisitor-token\u003e\",\n  \"events\": [\n    {\n      \"id\": \"\u003coptional-random-id\u003e\",\n      \"name\": \"Viewed item\",\n      \"properties\": {\n        \"item_id\": 123\n      },\n      \"time\": \"2025-01-01T00:00:00-07:00\"\n    }\n  ]\n}\n```\n\n## History\n\nView the [changelog](https://github.com/ankane/ahoy/blob/master/CHANGELOG.md)\n\n## Contributing\n\nEveryone is encouraged to help improve this project. Here are a few ways you can help:\n\n- [Report bugs](https://github.com/ankane/ahoy/issues)\n- Fix bugs and [submit pull requests](https://github.com/ankane/ahoy/pulls)\n- Write, clarify, or fix documentation\n- Suggest or add new features\n\nTo get started with development:\n\n```sh\ngit clone https://github.com/ankane/ahoy.git\ncd ahoy\nbundle install\nbundle exec rake test\n```\n\nTo test different adapters, use:\n\n```sh\nADAPTER=postgresql bundle exec rake test\nADAPTER=mysql2 bundle exec rake test\nADAPTER=mongoid bundle exec rake test\n```\n","funding_links":[],"categories":["Ruby","Logging","Analytics","Uncategorized","rails","日志"],"sub_categories":["Omniauth","Uncategorized"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fankane%2Fahoy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fankane%2Fahoy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fankane%2Fahoy/lists"}