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:
- π Track custom metrics (StatsD, Datadog, Prometheus)
- π Log audit trails for compliance
- π« Create tickets in project management tools (Jira, Linear, GitHub Issues)
- π’ Send custom notifications beyond built-in backends
- π Analyze error patterns with ML/AI services
- πΎ Store errors in external databases or data warehouses
- π Trigger custom workflows based on error events
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:
error_log(ErrorLog) - The newly created error record
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:
error_log(ErrorLog) - The updated error record with incremented occurrence_count
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:
error_log(ErrorLog) - The resolved error record
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:
error_logs(Array) - Array of resolved error records
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:
error_ids(Array) - Array of deleted error IDs
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:
error_log(ErrorLog) - The viewed error record
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
- Check plugin is registered:
RailsErrorDashboard::PluginRegistry.names - Check
enabled?returns true:plugin = RailsErrorDashboard::PluginRegistry.find("My Plugin") plugin.enabled? - 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:
- Move slow operations to background jobs
- Use
enabled?to conditionally run plugins - Cache expensive operations
- Profile plugin code
Related Documentation
- Main README - Overall gem documentation
- Notifications - Built-in notification backends
- Batch Operations - Batch operations
Plugin system is fully functional! π