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.
Installation
Add to your Gemfile:
gem "sinaliza"Then run:
bundle install
bin/rails sinaliza:install:migrations
bin/rails db:migrateMount the engine in your config/routes.rb:
mount Sinaliza::Engine => "/sinaliza"Usage
Direct recording
# 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 |
Model concern — Sinaliza::Trackable
Include in any model to get event associations and helper methods:
class User < ApplicationRecord
include Sinaliza::Trackable
endThis gives you:
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)
user.track_event("invoice.paid", target: invoice, context: subscription, parent: signup_event)
post.track_event_as_target("post.featured", actor: admin)
subscription.track_event_as_context("plan.upgraded", actor: user)Events are recorded with source: "model". When an actor, target, or context is destroyed, associated events are preserved with nullified references (dependent: :nullify).
Event context
The context parameter is a polymorphic association that lets you group events under a business object. This is useful when multiple events belong to the same logical context — such as a subscription, an order, or a project.
subscription = user.subscriptions.current
# Record events within a subscription context
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 # all events for this subscription
Sinaliza::Event.by_context(subscription) # same, via scope
Sinaliza::Event.by_context_type("Subscription") # all events for any subscriptionParent & children events
Events support a parent/children hierarchy. Use this to represent causal chains or group sub-steps under a main event.
# Create a parent event
signup = Sinaliza.record(name: "user.signed_up", actor: user)
# Create child events
Sinaliza.record(name: "welcome_email.sent", actor: user, parent: signup)
Sinaliza.record(name: "default_settings.created", actor: user, parent: signup)
# Navigate the hierarchy
signup.children # child events
signup.root? # => true
signup.children.first.child? # => true
signup.children.first.parent # => the signup event
# Query only top-level events
Sinaliza::Event.rootsWhen a parent event is destroyed, its children are also destroyed (dependent: :destroy).
Controller concern — Sinaliza::Traceable
Include in any controller to 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
endEvents are recorded with source: "controller". Request context (IP address, user agent, request ID) is captured automatically based on configuration.
The actor is resolved by calling the method defined in Sinaliza.configuration.actor_method (default: current_user).
Query scopes
Sinaliza::Event.by_name("user.login")
Sinaliza::Event.by_source("controller")
Sinaliza::Event.by_actor_type("User")
Sinaliza::Event.by_context(subscription) # events for a specific context record
Sinaliza::Event.by_context_type("Subscription") # events for any record of this type
Sinaliza::Event.roots # only top-level events (no parent)
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 # oldest first
Sinaliza::Event.reverse_chronological # newest firstScopes are chainable:
Sinaliza::Event.by_name("order.created").by_actor_type("User").since(1.day.ago)
Sinaliza::Event.by_context(subscription).roots.reverse_chronologicalDashboard
The engine mounts a monitor dashboard at your chosen path. It provides:
- 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
The gem does not include authentication. Protect access via route constraints in your host app:
# config/routes.rb
authenticate :user, ->(u) { u.admin? } do
mount Sinaliza::Engine => "/sinaliza"
end
# or with a simple constraint
mount Sinaliza::Engine => "/sinaliza", constraints: AdminConstraint.newConfiguration
# config/initializers/sinaliza.rb
Sinaliza.configure do |config|
# Controller method used to resolve the actor (default: :current_user)
config.actor_method = :current_user
# Default source label for Sinaliza.record calls (default: "manual")
config.default_source = "manual"
# Capture IP, user agent, and request ID in controller events (default: true)
config.record_request_info = true
# Auto-purge events older than this duration (default: nil — no purging)
config.purge_after = 90.days
endPurging old events
If purge_after is configured, run the rake task to delete old events:
bin/rails sinaliza:purgeSchedule it with cron, Heroku Scheduler, or whatever you prefer.
Database schema
Events are stored in a single sinaliza_events table with polymorphic actor, target, and context columns, plus a parent_id foreign key for hierarchies. The metadata column uses json type for cross-database compatibility (SQLite, PostgreSQL, MySQL).
License
The gem is available as open source under the terms of the MIT License.