Rails Error Dashboard

Self-hosted error tracking and exception monitoring for Ruby on Rails. Open source, free forever. A Sentry alternative with cause chains, analytics, workflow management, and multi-channel notifications.

Download as .zip Download as .tar.gz View on GitHub

Plugin System Guide

Rails Error Dashboard includes a powerful plugin system that allows you to extend functionality and integrate with external services.

Overview

The plugin system provides event hooks throughout the error lifecycle, allowing you to:

Quick Start

1. Create a Plugin

# config/initializers/error_dashboard_plugins.rb

class MyCustomPlugin < RailsErrorDashboard::Plugin
  def name
    "My Custom Plugin"
  end

  def description
    "Does something awesome with errors"
  end

  def on_error_logged(error_log)
    # Called when a new error occurs
    puts "New error: #{error_log.error_type}"
  end
end

2. Register the Plugin

# config/initializers/error_dashboard_plugins.rb

RailsErrorDashboard.register_plugin(MyCustomPlugin.new)

That’s it! Your plugin will now receive events whenever errors are logged.


Available Event Hooks

The plugin system provides six event hooks:

1. on_error_logged(error_log)

When: A new error occurs (first occurrence)

Parameters:

Example:

def on_error_logged(error_log)
  # Send to metrics service
  Metrics.increment("errors.new")

  # Create Jira ticket for critical errors
  if error_log.critical?
    JiraService.create_ticket(error_log)
  end
end

2. on_error_recurred(error_log)

When: An existing error occurs again (subsequent occurrences)

Parameters:

Example:

def on_error_recurred(error_log)
  # Alert if error occurs frequently
  if error_log.occurrence_count > 10
    AlertService.send_alert("Error #{error_log.id} has occurred #{error_log.occurrence_count} times!")
  end
end

3. on_error_resolved(error_log)

When: An error is marked as resolved (single error)

Parameters:

Example:

def on_error_resolved(error_log)
  # Update Jira ticket status
  JiraService.resolve_ticket(error_log)

  # Track resolution metrics
  Metrics.increment("errors.resolved")
  Metrics.timing("errors.time_to_resolve", error_log.resolved_at - error_log.first_seen_at)
end

4. on_errors_batch_resolved(error_logs)

When: Multiple errors are resolved via batch operation

Parameters:

Example:

def on_errors_batch_resolved(error_logs)
  # Log batch resolution for audit trail
  AuditLog.create(
    action: "batch_resolve",
    count: error_logs.size,
    error_ids: error_logs.map(&:id)
  )
end

5. on_errors_batch_deleted(error_ids)

When: Multiple errors are deleted via batch operation

Parameters:

Example:

def on_errors_batch_deleted(error_ids)
  # Archive deleted errors to external storage
  ArchiveService.archive_errors(error_ids)

  # Log for compliance
  AuditLog.create(
    action: "batch_delete",
    count: error_ids.size
  )
end

6. on_error_viewed(error_log)

When: An error is viewed in the dashboard

Parameters:

Example:

def on_error_viewed(error_log)
  # Track error views for analytics
  Analytics.track("error_viewed", {
    error_id: error_log.id,
    error_type: error_log.error_type
  })
end

Plugin API Reference

Base Plugin Class

class RailsErrorDashboard::Plugin
  # Required: Plugin name (must be unique)
  def name
    raise NotImplementedError
  end

  # Optional: Plugin description
  def description
    "No description provided"
  end

  # Optional: Plugin version
  def version
    "1.0.0"
  end

  # Optional: Called when plugin is registered
  def on_register
    # Initialization logic
  end

  # Optional: Check if plugin should run
  def enabled?
    true
  end

  # Event hooks (all optional, implement as needed)
  def on_error_logged(error_log); end
  def on_error_recurred(error_log); end
  def on_error_resolved(error_log); end
  def on_errors_batch_resolved(error_logs); end
  def on_errors_batch_deleted(error_ids); end
  def on_error_viewed(error_log); end
end

Registration Methods

# Register a plugin
RailsErrorDashboard.register_plugin(plugin_instance)
# => true (success) or false (already registered)

# Unregister a plugin by name
RailsErrorDashboard.unregister_plugin("My Plugin Name")

# Get all registered plugins
RailsErrorDashboard.plugins
# => [plugin1, plugin2, ...]

# Access plugin registry directly
RailsErrorDashboard::PluginRegistry.count
# => 3

RailsErrorDashboard::PluginRegistry.names
# => ["Plugin 1", "Plugin 2", "Plugin 3"]

RailsErrorDashboard::PluginRegistry.info
# => [{ name: "...", version: "...", description: "...", enabled: true }, ...]

Example Plugins

Example 1: Metrics Tracking (StatsD/Datadog)

class MetricsPlugin < RailsErrorDashboard::Plugin
  def name
    "Metrics Tracker"
  end

  def on_error_logged(error_log)
    StatsD.increment("errors.new")
    StatsD.increment("errors.by_type.#{sanitize(error_log.error_type)}")
    StatsD.increment("errors.by_platform.#{error_log.platform}")
  end

  def on_error_resolved(error_log)
    StatsD.increment("errors.resolved")

    # Track time to resolution
    resolution_time = error_log.resolved_at - error_log.first_seen_at
    StatsD.timing("errors.time_to_resolve", resolution_time)
  end

  private

  def sanitize(name)
    name.gsub('::', '.').downcase
  end
end

# Register
RailsErrorDashboard.register_plugin(MetricsPlugin.new)

Example 2: Audit Logging

class AuditLogPlugin < RailsErrorDashboard::Plugin
  def initialize(logger: Rails.logger)
    @logger = logger
  end

  def name
    "Audit Logger"
  end

  def on_error_logged(error_log)
    log_event("error_logged", error_log)
  end

  def on_error_resolved(error_log)
    log_event("error_resolved", error_log, {
      resolved_by: error_log.resolved_by_name,
      resolution_comment: error_log.resolution_comment
    })
  end

  def on_errors_batch_deleted(error_ids)
    @logger.info("[Audit] Batch deleted #{error_ids.size} errors: #{error_ids.join(', ')}")
  end

  private

  def log_event(event, error_log, extra = {})
    @logger.info("[Audit] #{event}: #{error_log.id} (#{error_log.error_type}) #{extra.to_json}")
  end
end

# Register
RailsErrorDashboard.register_plugin(AuditLogPlugin.new)

Example 3: Jira Integration

class JiraIntegrationPlugin < RailsErrorDashboard::Plugin
  def initialize(jira_client:, project_key:, only_critical: true)
    @jira = jira_client
    @project_key = project_key
    @only_critical = only_critical
  end

  def name
    "Jira Integration"
  end

  def enabled?
    @jira.present?
  end

  def on_error_logged(error_log)
    return if @only_critical && !error_log.critical?

    create_jira_issue(error_log)
  end

  def on_error_resolved(error_log)
    # Find related Jira ticket and resolve it
    resolve_jira_issue(error_log)
  end

  private

  def create_jira_issue(error_log)
    issue = @jira.Issue.build
    issue.save({
      "fields" => {
        "project" => { "key" => @project_key },
        "summary" => "[#{error_log.environment}] #{error_log.error_type}",
        "description" => build_description(error_log),
        "issuetype" => { "name" => "Bug" },
        "priority" => { "name" => jira_priority(error_log) }
      }
    })

    # Store Jira ticket ID in error metadata
    error_log.update(metadata: error_log.metadata.merge(jira_ticket: issue.key))
  end

  def build_description(error_log)
    <<~DESC
      Error Type: #{error_log.error_type}
      Message: #{error_log.message}
      Platform: #{error_log.platform}
      Environment: #{error_log.environment}

      View in Dashboard: #{dashboard_url(error_log)}
    DESC
  end

  def jira_priority(error_log)
    case error_log.severity.to_s
    when "critical" then "Highest"
    when "high" then "High"
    when "medium" then "Medium"
    else "Low"
    end
  end

  def dashboard_url(error_log)
    "#{RailsErrorDashboard.configuration.dashboard_base_url}/error_dashboard/errors/#{error_log.id}"
  end

  def resolve_jira_issue(error_log)
    ticket_key = error_log.metadata&.dig("jira_ticket")
    return unless ticket_key

    issue = @jira.Issue.find(ticket_key)
    issue.transition("Done")
  end
end

# Register with Jira client
jira_client = JIRA::Client.new(
  username: ENV['JIRA_USERNAME'],
  password: ENV['JIRA_API_TOKEN'],
  site: ENV['JIRA_URL'],
  context_path: '',
  auth_type: :basic
)

RailsErrorDashboard.register_plugin(
  JiraIntegrationPlugin.new(
    jira_client: jira_client,
    project_key: "MYPROJECT",
    only_critical: true
  )
)

Example 4: Conditional Plugin (Production Only)

class ProductionOnlyPlugin < RailsErrorDashboard::Plugin
  def name
    "Production Alert Plugin"
  end

  def enabled?
    Rails.env.production?
  end

  def on_error_logged(error_log)
    # Only runs in production
    ProductionAlertService.send_alert(error_log)
  end
end

RailsErrorDashboard.register_plugin(ProductionOnlyPlugin.new)

Example 5: ML Error Classification

class ErrorClassificationPlugin < RailsErrorDashboard::Plugin
  def name
    "ML Error Classifier"
  end

  def on_error_logged(error_log)
    # Use ML to classify error severity/category
    classification = MLService.classify_error(
      error_type: error_log.error_type,
      message: error_log.message,
      backtrace: error_log.backtrace
    )

    # Store ML insights in metadata
    error_log.update(
      metadata: error_log.metadata.merge(
        ml_category: classification[:category],
        ml_confidence: classification[:confidence],
        ml_similar_errors: classification[:similar_ids]
      )
    )
  end
end

RailsErrorDashboard.register_plugin(ErrorClassificationPlugin.new)

Built-in Example Plugins

Rails Error Dashboard includes three example plugins you can use as templates:

1. MetricsPlugin

Location: lib/rails_error_dashboard/plugins/metrics_plugin.rb

Purpose: Track error metrics and send to monitoring services

Usage:

require 'rails_error_dashboard/plugins/metrics_plugin'

RailsErrorDashboard.register_plugin(
  RailsErrorDashboard::Plugins::MetricsPlugin.new
)

2. AuditLogPlugin

Location: lib/rails_error_dashboard/plugins/audit_log_plugin.rb

Purpose: Log all error dashboard activities for compliance

Usage:

require 'rails_error_dashboard/plugins/audit_log_plugin'

RailsErrorDashboard.register_plugin(
  RailsErrorDashboard::Plugins::AuditLogPlugin.new(logger: Rails.logger)
)

3. JiraIntegrationPlugin

Location: lib/rails_error_dashboard/plugins/jira_integration_plugin.rb

Purpose: Automatically create Jira tickets for critical errors

Usage:

require 'rails_error_dashboard/plugins/jira_integration_plugin'

RailsErrorDashboard.register_plugin(
  RailsErrorDashboard::Plugins::JiraIntegrationPlugin.new(
    jira_url: ENV['JIRA_URL'],
    jira_username: ENV['JIRA_USERNAME'],
    jira_api_token: ENV['JIRA_API_TOKEN'],
    jira_project_key: ENV['JIRA_PROJECT_KEY'],
    only_critical: true
  )
)

Best Practices

1. Error Handling

Always handle errors gracefully in plugins to prevent breaking the main application:

def on_error_logged(error_log)
  send_to_external_service(error_log)
rescue => e
  # Plugin errors are automatically logged by safe_execute
  # But you can add custom handling
  Rails.logger.error("My plugin failed: #{e.message}")
end

Note: The base Plugin class includes safe_execute that wraps all event hooks with error handling.

2. Conditional Execution

Use enabled? to control when plugins run:

def enabled?
  # Only run if configuration present
  ENV['EXTERNAL_SERVICE_API_KEY'].present? &&
  # Only run in production
  Rails.env.production? &&
  # Only run during business hours
  Time.current.hour.between?(9, 17)
end

3. Async Processing

For slow operations, use background jobs:

def on_error_logged(error_log)
  # Don't block error logging with slow API calls
  ExternalServiceJob.perform_later(error_log.id)
end

4. Initialization

Use on_register for one-time setup:

def on_register
  @client = ExternalService::Client.new(api_key: ENV['API_KEY'])
  @cache = Rails.cache

  Rails.logger.info("#{name} initialized successfully")
end

5. Plugin Dependencies

Check for required gems/services:

def enabled?
  return false unless defined?(Datadog)

  ENV['DATADOG_API_KEY'].present?
end

Configuration Examples

Multi-Plugin Setup

# config/initializers/error_dashboard_plugins.rb

Rails.application.configure do
  # Metrics tracking
  RailsErrorDashboard.register_plugin(
    RailsErrorDashboard::Plugins::MetricsPlugin.new
  )

  # Audit logging
  RailsErrorDashboard.register_plugin(
    RailsErrorDashboard::Plugins::AuditLogPlugin.new(
      logger: Logger.new(Rails.root.join('log', 'error_audit.log'))
    )
  )

  # Jira integration (production only)
  if Rails.env.production?
    RailsErrorDashboard.register_plugin(
      RailsErrorDashboard::Plugins::JiraIntegrationPlugin.new(
        jira_url: ENV['JIRA_URL'],
        jira_username: ENV['JIRA_USERNAME'],
        jira_api_token: ENV['JIRA_API_TOKEN'],
        jira_project_key: 'PROD',
        only_critical: true
      )
    )
  end
end

Environment-Specific Plugins

# config/initializers/error_dashboard_plugins.rb

Rails.application.configure do
  case Rails.env
  when 'production'
    # Production: Full monitoring stack
    RailsErrorDashboard.register_plugin(DatadogPlugin.new)
    RailsErrorDashboard.register_plugin(PagerDutyPlugin.new)
    RailsErrorDashboard.register_plugin(JiraPlugin.new)

  when 'staging'
    # Staging: Metrics only
    RailsErrorDashboard.register_plugin(MetricsPlugin.new)

  when 'development'
    # Development: Console logging only
    RailsErrorDashboard.register_plugin(ConsoleLoggerPlugin.new)
  end
end

Debugging Plugins

Check Registered Plugins

# Rails console

# List all plugins
RailsErrorDashboard.plugins
# => [#<MetricsPlugin>, #<AuditLogPlugin>]

# Get plugin names
RailsErrorDashboard::PluginRegistry.names
# => ["Metrics Tracker", "Audit Logger"]

# Get plugin info
RailsErrorDashboard::PluginRegistry.info
# => [
#   { name: "Metrics Tracker", version: "1.0.0", description: "...", enabled: true },
#   { name: "Audit Logger", version: "1.0.0", description: "...", enabled: true }
# ]

# Find specific plugin
RailsErrorDashboard::PluginRegistry.find("Metrics Tracker")
# => #<MetricsPlugin>

Test Plugin Events

# Rails console

# Create test error
error = begin
  raise StandardError, "Test error"
rescue => e
  e
end

error_log = RailsErrorDashboard::Commands::LogError.call(error, {
  controller_name: "TestController",
  action_name: "test"
})

# Manually trigger plugin events
RailsErrorDashboard::PluginRegistry.dispatch(:on_error_logged, error_log)

# Check plugin is enabled
plugin = RailsErrorDashboard::PluginRegistry.find("My Plugin")
plugin.enabled?
# => true/false

Plugin Logs

Plugin errors are automatically logged:

# log/production.log
Plugin 'My Plugin' failed in on_error_logged: Connection refused
/path/to/plugin.rb:45:in `send_to_service'
/path/to/plugin.rb:12:in `on_error_logged'

Performance Considerations

1. Async Operations

Plugins run synchronously during error logging. Keep operations fast:

# Bad: Slow synchronous API call
def on_error_logged(error_log)
  SlowExternalAPI.send_error(error_log) # Blocks error logging
end

# Good: Async job
def on_error_logged(error_log)
  SendErrorJob.perform_later(error_log.id) # Non-blocking
end

2. Bulk Operations

Use batch hooks efficiently:

# Good: Single API call for batch
def on_errors_batch_resolved(error_logs)
  ExternalAPI.bulk_update(error_logs.map(&:id))
end

# Bad: N API calls
def on_errors_batch_resolved(error_logs)
  error_logs.each do |error_log|
    ExternalAPI.update(error_log.id) # N+1 API calls
  end
end

3. Caching

Cache expensive operations:

def on_error_logged(error_log)
  client = Rails.cache.fetch("external_api_client", expires_in: 1.hour) do
    ExternalAPI::Client.new(api_key: ENV['API_KEY'])
  end

  client.send_error(error_log)
end

Security Considerations

1. Sensitive Data

Be careful with error messages and backtraces:

def on_error_logged(error_log)
  # Filter sensitive data before sending externally
  sanitized_message = sanitize_sensitive_data(error_log.message)

  ExternalService.send(
    error_type: error_log.error_type,
    message: sanitized_message
    # Don't send: passwords, tokens, API keys, PII
  )
end

private

def sanitize_sensitive_data(message)
  message
    .gsub(/password[=:]\s*\S+/i, 'password=REDACTED')
    .gsub(/token[=:]\s*\S+/i, 'token=REDACTED')
    .gsub(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, 'EMAIL_REDACTED')
end

2. API Keys

Store credentials securely:

# Good: Environment variables
def initialize
  @api_key = ENV['EXTERNAL_SERVICE_API_KEY']
end

# Bad: Hardcoded
def initialize
  @api_key = "secret_key_123" # Never do this
end

3. Rate Limiting

Implement rate limiting to prevent abuse:

def on_error_logged(error_log)
  # Only send first 100 errors per hour to external service
  count = Rails.cache.increment("plugin_events:#{Time.current.hour}", 1, expires_in: 1.hour)

  return if count > 100

  ExternalService.send(error_log)
end

Testing Plugins

RSpec Example

# spec/plugins/my_plugin_spec.rb

RSpec.describe MyPlugin do
  let(:plugin) { described_class.new }
  let(:error_log) { create(:error_log, error_type: "StandardError") }

  describe "#name" do
    it "returns plugin name" do
      expect(plugin.name).to eq("My Plugin")
    end
  end

  describe "#enabled?" do
    it "is enabled when API key is present" do
      allow(ENV).to receive(:[]).with('API_KEY').and_return('key123')
      expect(plugin.enabled?).to be true
    end

    it "is disabled when API key is missing" do
      allow(ENV).to receive(:[]).with('API_KEY').and_return(nil)
      expect(plugin.enabled?).to be false
    end
  end

  describe "#on_error_logged" do
    it "sends error to external service" do
      expect(ExternalService).to receive(:send).with(error_log)
      plugin.on_error_logged(error_log)
    end

    it "handles errors gracefully" do
      allow(ExternalService).to receive(:send).and_raise(StandardError, "API error")

      expect {
        plugin.on_error_logged(error_log)
      }.not_to raise_error
    end
  end
end

Integration Testing

# spec/integration/plugin_system_spec.rb

RSpec.describe "Plugin System" do
  before do
    RailsErrorDashboard::PluginRegistry.clear
  end

  it "dispatches events to registered plugins" do
    plugin = MyPlugin.new
    RailsErrorDashboard.register_plugin(plugin)

    expect(plugin).to receive(:on_error_logged)

    error = begin
      raise StandardError, "Test"
    rescue => e
      e
    end

    RailsErrorDashboard::Commands::LogError.call(error, {})
  end
end

FAQ

Q: Can plugins modify error_log records?

A: Yes, plugins can call error_log.update(...) to add custom data:

def on_error_logged(error_log)
  error_log.update(
    metadata: error_log.metadata.merge(
      external_ticket_id: create_ticket(error_log)
    )
  )
end

Q: What happens if a plugin crashes?

A: Plugins are wrapped in safe_execute which catches errors and logs them without breaking the main application:

Plugin 'My Plugin' failed in on_error_logged: Connection refused

Q: Can I use background jobs in plugins?

A: Yes, recommended for slow operations:

def on_error_logged(error_log)
  MyPluginJob.perform_later(error_log.id)
end

Q: How do I unregister a plugin?

A:

RailsErrorDashboard.unregister_plugin("Plugin Name")

Q: Can plugins depend on each other?

A: Not directly. Keep plugins independent. If you need shared logic, extract it to a service class.

Q: How many plugins can I register?

A: No hard limit, but be mindful of performance. Each event dispatches to all enabled plugins.


Troubleshooting

Plugin Not Receiving Events

  1. Check plugin is registered:
    RailsErrorDashboard::PluginRegistry.names
    
  2. Check enabled? returns true:
    plugin = RailsErrorDashboard::PluginRegistry.find("My Plugin")
    plugin.enabled?
    
  3. Check for errors in logs:
    tail -f log/production.log | grep "Plugin"
    

Plugin Registered Multiple Times

Plugins are only registered once. Subsequent registrations with the same name are ignored:

RailsErrorDashboard.register_plugin(MyPlugin.new) # Registered
RailsErrorDashboard.register_plugin(MyPlugin.new) # Skipped (logs warning)

Performance Issues

If plugins slow down error logging:

  1. Move slow operations to background jobs
  2. Use enabled? to conditionally run plugins
  3. Cache expensive operations
  4. Profile plugin code


Plugin system is fully functional! πŸŽ‰