Skip to content

marcelonmoraes/sinaliza

Repository files navigation

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:migrate

Mount 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
end

This 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 subscription

Parent & 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.roots

When 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
end

Events 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

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 events

Events 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.

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 first

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. 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.

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|
  # 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
end

Purging old events

If purge_after is configured, run the rake task to delete old events:

bin/rails sinaliza:purge

Schedule 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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors