rails-persona 🎭
Model-level behavioral analytics for Rails — own your data, zero external services.
rails-persona is a lightweight Rails gem that adds first-class behavioral tracking directly to your ActiveRecord models. Unlike ahoy, which is focused on HTTP visit and page-view tracking, rails-persona is built for model-level action tracking — understanding what your users actually do in your app, not just what pages they visit.
Why rails-persona over ahoy?
| ahoy | rails-persona | |
|---|---|---|
| Focus | HTTP visits + page views | Model actions + user behavior |
| Setup | Controllers + JS snippet | Pure Ruby — one concern |
| Async | Manual Sidekiq setup | Built-in (async: true) |
| Bulk tracking | ❌ | ✅ bulk_track! with insert_all! |
| Class-level analytics | ❌ | ✅ Leaderboards, class summaries |
| Open tracking mode | ❌ | ✅ No whitelist required |
| Streak / pattern queries | ❌ | ✅ daily_activity, peak_hour |
| Cookies / sessions | Required | Never needed |
| Works on non-User models | Awkward | First-class |
Installation
gem "rails-persona"
bundle install
rails db:migrate
Quick start
1. Include in any model
class User < ApplicationRecord
include Persona::Trackable
persona do
track :login
track :export_report
track :view_dashboard
track :upgrade_plan
end
end
2. Track actions in your app
# In a controller, service, or job:
current_user.track!(:login)
current_user.track!(:upgrade_plan, metadata: { plan: "pro", amount: 49 })
3. Query behavior
user.action_count(:login) # => 42
user.most_frequent_action # => :login
user.least_frequent_action # => :upgrade_plan
user.top_actions(3) # => { login: 42, view_dashboard: 18, export_report: 5 }
user.last_action # => :export_report
user.last_active_at # => 2024-05-30 14:22 UTC
user.first_action # => :login
user.first_active_at # => 2023-01-10 08:00 UTC
user.inactive_since? # => false (default threshold: 30 days)
user.inactive_since?(7) # => false (custom: 7 days)
user.days_since_last_activity # => 2
user.ever_did?(:export_report) # => true
user.never_did?(:upgrade_plan) # => false
user.action_share(:login) # => 64.6 (% of all events)
user.total_events # => 65
user.persona_summary
# => { login: 42, view_dashboard: 18, export_report: 5, upgrade_plan: 1 }
user.actions_between(1.week.ago, Time.current)
# => { login: 7, view_dashboard: 3 }
user.activity_log(5)
# => [
# { action: :export_report, at: 2024-05-30 14:22:00, metadata: {} },
# { action: :login, at: 2024-05-30 09:01:00, metadata: {} },
# ]
user.daily_activity(30)
# => { "2024-05-28" => 4, "2024-05-29" => 7, "2024-05-30" => 2 }
user.peak_hour
# => 14 (2pm is when this user is most active)
Class-level analytics
# Top 10 most active users
User.persona_leaderboard(limit: 10)
# => [
# { record: #<User id=4>, total_events: 128 },
# { record: #<User id=9>, total_events: 97 },
# ]
# App-wide breakdown of all user actions
User.persona_class_summary
# => { login: 8420, view_dashboard: 5210, export_report: 820 }
Bulk tracking (high-performance)
Uses insert_all! — no N+1, no per-row callbacks:
user.bulk_track!([:login, :view_dashboard, :export_report])
user.bulk_track!([:login, :login, :login]) # track repeated actions
Async tracking (Sidekiq)
# config/initializers/persona.rb
Persona.configure do |config|
config.async = true # fires a Sidekiq job instead of writing inline
end
Requires the sidekiq gem. Falls back to synchronous if Sidekiq is not available.
Open tracking (no whitelist)
If you want to track arbitrary actions without declaring them:
class Post < ApplicationRecord
include Persona::Trackable
persona do
open_tracking! # any string is valid — no UntrackedActionError raised
end
end
post.track!("custom_#{SecureRandom.hex(4)}") # works fine
Works on any model
class Post < ApplicationRecord
include Persona::Trackable
persona do
track :viewed
track :shared
track :bookmarked
end
end
post.track!(:viewed)
post.action_count(:viewed) # => 128
post.most_frequent_action # => :viewed
Post.persona_class_summary # => { viewed: 50_420, shared: 890, bookmarked: 210 }
Configuration
# config/initializers/persona.rb
Persona.configure do |config|
config.inactivity_threshold_days = 14 # default: 30
config.max_events_per_record = 500 # default: nil (unlimited)
config.async = true # default: false
config.auto_prune_after_days = 90 # default: nil (no auto-prune)
end
Manual pruning
# Delete events older than 60 days for all records
Persona::Pruner.prune_older_than(60)
Add to a scheduled job (e.g. whenever or Sidekiq-Cron):
# lib/tasks/persona.rake
namespace :persona do
desc "Prune old persona events"
task prune: :environment do
Persona::Pruner.prune_older_than(Persona.configuration.auto_prune_after_days || 90)
puts "Pruned old persona events"
end
end
Comparison with other gems
| Gem | Tracks | rails-persona advantage |
|---|---|---|
| ahoy | HTTP visits, JS events | Model actions, no JS needed, async built-in |
| paper_trail | Model attribute changes | Behavioral patterns, not diffs |
| audited | CRUD audit logs | Who acted, not what changed |
| mixpanel-ruby | Remote SaaS events | Your DB, no 3rd party, no cost |
API reference
| Method | Description |
|---|---|
track!(action, metadata: {}) |
Record an action (sync or async) |
bulk_track!(actions) |
Record multiple actions via insert_all! |
reset_persona! |
Delete all events for this record |
action_count(action) |
Count of a specific action |
total_events |
Total event count |
most_frequent_action |
Most-performed action |
least_frequent_action |
Least-performed action |
top_actions(n) |
Top N actions by count |
last_action |
Most recent action symbol |
last_active_at |
Timestamp of last action |
first_action |
Earliest action symbol |
first_active_at |
Timestamp of first action |
inactive_since?(days) |
True if no action in N days |
days_since_last_activity |
Integer days since last event |
ever_did?(action) |
True if action occurred |
never_did?(action) |
True if action never occurred |
action_share(action) |
% of all events this action represents |
persona_summary |
Full action → count hash |
actions_between(from, to) |
Actions in a time window |
activity_log(limit) |
Recent events as array of hashes |
daily_activity(days) |
Events grouped by day |
peak_hour |
Hour (0-23) with most activity |
User.persona_leaderboard(limit:) |
Top N most active records |
User.persona_class_summary |
App-wide action breakdown |
Contributing
Bug reports and pull requests welcome at https://github.com/sghani001/rails-persona.
License
MIT — © Syed M. Ghani