Sinaliza
A Rails engine for recording and browsing application events. Track user actions, system events, and anything worth logging — from models, controllers, or anywhere in your code.
Events are stored in the database and viewable through a mountable monitor dashboard.
Quick Start
Add to your Gemfile:
gem "sinaliza"
Then run:
bundle install
bin/rails sinaliza:install:migrations
bin/rails db:migrate
Mount the engine in your config/routes.rb:
mount Sinaliza::Engine => "/sinaliza"
Recording Events
From anywhere in your code
# Synchronous
Sinaliza.record(name: "user.signed_up", actor: user, metadata: { plan: "pro" })
# Asynchronous (via ActiveJob)
Sinaliza.record_later(name: "report.generated", target: report)
Both methods accept:
| Parameter | Description | Default |
|---|---|---|
name |
Event name (required) | — |
actor |
Who performed the action (any model) | nil |
target |
What was acted upon (any model) | nil |
metadata |
Arbitrary hash of extra data | {} |
source |
Origin label | "manual" |
ip_address |
IP address | nil |
user_agent |
User agent string | nil |
request_id |
Request ID | nil |
context |
Business context for grouping (any model) | nil |
parent |
Parent event (for hierarchies) | nil |
From models — Sinaliza::Trackable
Include in any model to get event associations and helper methods:
class User < ApplicationRecord
include Sinaliza::Trackable
end
user.events_as_actor # events where user is the actor
user.events_as_target # events where user is the target
user.events_as_context # events where user is the context
user.track_event("profile.updated", metadata: { field: "email" })
user.track_event("post.published", target: post, context: subscription)
post.track_event_as_target("post.featured", actor: admin)
subscription.track_event_as_context("plan.upgraded", actor: user)
From controllers — Sinaliza::Traceable
Track actions declaratively or manually:
class OrdersController < ApplicationController
include Sinaliza::Traceable
# Declarative — runs as an after_action callback
track_event "orders.listed", only: :index
track_event "order.viewed", only: :show, metadata: -> { { order_id: params[:id] } }
# Conditional tracking
track_event "order.created", only: :create, if: -> { response.successful? }
def refund
order = Order.find(params[:id])
order.refund!
# Manual — call anywhere in an action
record_event("order.refunded", target: order, metadata: { reason: params[:reason] })
redirect_to order
end
end
Event Context
The context parameter is a polymorphic association that lets you group events under a business object — such as a subscription, an order, or a project.
subscription = user.subscriptions.current
Sinaliza.record(name: "plan.upgraded", actor: user, context: subscription,
metadata: { from: "basic", to: "pro" })
Sinaliza.record(name: "payment.processed", actor: user, context: subscription)
Sinaliza.record(name: "invoice.sent", target: user, context: subscription)
# Query events by context
subscription.events_as_context
Sinaliza::Event.by_context(subscription)
Sinaliza::Event.by_context_type("Subscription")
Parent & Children Events
Events support a parent/children hierarchy for representing causal chains or grouping sub-steps under a main event.
signup = Sinaliza.record(name: "user.signed_up", actor: user)
Sinaliza.record(name: "welcome_email.sent", actor: user, parent: signup)
Sinaliza.record(name: "default_settings.created", actor: user, parent: signup)
signup.children
signup.root? # => true
signup.children.first.child? # => true
signup.children.first.parent # => the signup event
Sinaliza::Event.roots # only top-level events
Query Scopes
Sinaliza::Event.by_name("user.login")
Sinaliza::Event.by_source("controller")
Sinaliza::Event.by_actor_type("User")
Sinaliza::Event.by_context(subscription)
Sinaliza::Event.by_context_type("Subscription")
Sinaliza::Event.roots
Sinaliza::Event.since(1.week.ago)
Sinaliza::Event.before(Date.yesterday)
Sinaliza::Event.between(1.week.ago, 1.day.ago)
Sinaliza::Event.search("login")
Sinaliza::Event.chronological
Sinaliza::Event.reverse_chronological
Scopes are chainable:
Sinaliza::Event.by_name("order.created").by_actor_type("User").since(1.day.ago)
Sinaliza::Event.by_context(subscription).roots.reverse_chronological
Dashboard
The engine mounts a monitor dashboard at your chosen path with:
- Paginated event list (cursor-based, 50 per page)
- Filtering by name, source, actor type, date range, and free-text search
- Detail view for each event with formatted JSON metadata
Protecting the dashboard
With Devise:
# config/routes.rb
authenticate :user, ->(u) { u.admin? } do
mount Sinaliza::Engine => "/sinaliza"
end
Without Devise:
# app/constraints/admin_constraint.rb
class AdminConstraint
def matches?(request)
user_id = request.session[:user_id]
return false unless user_id
User.find_by(id: user_id)&.admin?
end
end
# config/routes.rb
mount Sinaliza::Engine => "/sinaliza", constraints: AdminConstraint.new
Configuration
# config/initializers/sinaliza.rb
Sinaliza.configure do |config|
config.actor_method = :current_user
config.default_source = "manual"
config.record_request_info = true
config.purge_after = 90.days
end
Purging Old Events
If purge_after is configured:
bin/rails sinaliza:purge
Schedule it with cron, Heroku Scheduler, or whatever you prefer.
Requirements
- Ruby >= 3.1
- Rails >= 7.1, < 9
License
Available as open source under the MIT License.