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.
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"# 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 |
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).
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 subscriptionEvents 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).
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).
Interceptors let you automatically record events whenever a specific method is called — without modifying the original code. They are stored in the database and can be managed at runtime through the dashboard or programmatically.
# Create an interceptor that tracks every call to User#send_welcome_email
Sinaliza::Interceptor.create!(
target_class: "User",
method_name: "send_welcome_email",
method_type: "instance",
event_name: "user.welcome_email_sent"
)
# Track a class method
Sinaliza::Interceptor.create!(
target_class: "Report",
method_name: "generate_monthly",
method_type: "class",
event_name: "report.monthly_generated"
)Each interceptor can optionally capture:
| Option | Description | Default |
|---|---|---|
capture_args |
Log method arguments in metadata | false |
capture_return |
Log the return value in metadata | false |
capture_execution_time |
Log execution time (ms) in metadata | false |
# Interceptor with full instrumentation
Sinaliza::Interceptor.create!(
target_class: "PaymentGateway",
method_name: "charge",
method_type: "instance",
event_name: "payment.charged",
capture_args: true,
capture_return: true,
capture_execution_time: true
)Interceptors can be toggled on and off without removing them:
interceptor = Sinaliza::Interceptor.find_by(event_name: "user.welcome_email_sent")
interceptor.deactivate! # stops recording events
interceptor.activate! # resumes recording eventsEvents recorded by interceptors have source: "interceptor".
The dashboard includes an Interceptors section where you can create, edit, toggle, and delete interceptors through a web interface.
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_chronologicalThe 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
The gem does not include authentication. Protect access via route constraints in your host app.
With Devise:
# config/routes.rb
authenticate :user, ->(u) { u.admin? } do
mount Sinaliza::Engine => "/sinaliza"
endWithout 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# 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
endIf 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.
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).
The gem is available as open source under the terms of the MIT License.