rails_error_dashboard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +858 -0
- data/Rakefile +3 -0
- data/app/assets/stylesheets/rails_error_dashboard/application.css +15 -0
- data/app/controllers/rails_error_dashboard/application_controller.rb +12 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +123 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +4 -0
- data/app/jobs/rails_error_dashboard/application_job.rb +4 -0
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +116 -0
- data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +19 -0
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +105 -0
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +166 -0
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +108 -0
- data/app/mailers/rails_error_dashboard/application_mailer.rb +8 -0
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +27 -0
- data/app/models/rails_error_dashboard/application_record.rb +5 -0
- data/app/models/rails_error_dashboard/error_log.rb +185 -0
- data/app/models/rails_error_dashboard/error_logs_record.rb +34 -0
- data/app/views/layouts/rails_error_dashboard/application.html.erb +17 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +351 -0
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +200 -0
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +32 -0
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +237 -0
- data/app/views/rails_error_dashboard/errors/index.html.erb +334 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +294 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb +40 -0
- data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +13 -0
- data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +10 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +27 -0
- data/lib/generators/rails_error_dashboard/install/templates/README +33 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +64 -0
- data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +40 -0
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +60 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +134 -0
- data/lib/rails_error_dashboard/commands/resolve_error.rb +35 -0
- data/lib/rails_error_dashboard/configuration.rb +83 -0
- data/lib/rails_error_dashboard/engine.rb +20 -0
- data/lib/rails_error_dashboard/error_reporter.rb +35 -0
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +41 -0
- data/lib/rails_error_dashboard/plugin.rb +98 -0
- data/lib/rails_error_dashboard/plugin_registry.rb +88 -0
- data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +96 -0
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +122 -0
- data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +78 -0
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +108 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +37 -0
- data/lib/rails_error_dashboard/queries/developer_insights.rb +277 -0
- data/lib/rails_error_dashboard/queries/errors_list.rb +66 -0
- data/lib/rails_error_dashboard/queries/errors_list_v2.rb +149 -0
- data/lib/rails_error_dashboard/queries/filter_options.rb +21 -0
- data/lib/rails_error_dashboard/services/platform_detector.rb +41 -0
- data/lib/rails_error_dashboard/value_objects/error_context.rb +148 -0
- data/lib/rails_error_dashboard/version.rb +3 -0
- data/lib/rails_error_dashboard.rb +60 -0
- data/lib/tasks/rails_error_dashboard_tasks.rake +4 -0
- metadata +318 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRailsErrorDashboardErrorLogs < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :rails_error_dashboard_error_logs do |t|
|
|
6
|
+
# Error details
|
|
7
|
+
t.string :error_type, null: false
|
|
8
|
+
t.text :message, null: false
|
|
9
|
+
t.text :backtrace
|
|
10
|
+
|
|
11
|
+
# Context
|
|
12
|
+
t.integer :user_id
|
|
13
|
+
t.text :request_url
|
|
14
|
+
t.text :request_params
|
|
15
|
+
t.text :user_agent
|
|
16
|
+
t.string :ip_address
|
|
17
|
+
t.string :environment, null: false
|
|
18
|
+
t.string :platform
|
|
19
|
+
|
|
20
|
+
# Resolution tracking
|
|
21
|
+
t.boolean :resolved, default: false, null: false
|
|
22
|
+
t.text :resolution_comment
|
|
23
|
+
t.string :resolution_reference
|
|
24
|
+
t.string :resolved_by_name
|
|
25
|
+
t.datetime :resolved_at
|
|
26
|
+
|
|
27
|
+
# Timestamps
|
|
28
|
+
t.datetime :occurred_at, null: false
|
|
29
|
+
t.timestamps
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Indexes for performance
|
|
33
|
+
add_index :rails_error_dashboard_error_logs, :user_id
|
|
34
|
+
add_index :rails_error_dashboard_error_logs, :error_type
|
|
35
|
+
add_index :rails_error_dashboard_error_logs, :environment
|
|
36
|
+
add_index :rails_error_dashboard_error_logs, :resolved
|
|
37
|
+
add_index :rails_error_dashboard_error_logs, :occurred_at
|
|
38
|
+
add_index :rails_error_dashboard_error_logs, :platform
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class AddBetterTrackingToErrorLogs < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
add_column :rails_error_dashboard_error_logs, :error_hash, :string
|
|
4
|
+
add_column :rails_error_dashboard_error_logs, :first_seen_at, :datetime
|
|
5
|
+
add_column :rails_error_dashboard_error_logs, :last_seen_at, :datetime
|
|
6
|
+
add_column :rails_error_dashboard_error_logs, :occurrence_count, :integer, default: 1, null: false
|
|
7
|
+
|
|
8
|
+
add_index :rails_error_dashboard_error_logs, :error_hash
|
|
9
|
+
add_index :rails_error_dashboard_error_logs, :first_seen_at
|
|
10
|
+
add_index :rails_error_dashboard_error_logs, :last_seen_at
|
|
11
|
+
add_index :rails_error_dashboard_error_logs, :occurrence_count
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class AddControllerActionToErrorLogs < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
add_column :rails_error_dashboard_error_logs, :controller_name, :string
|
|
4
|
+
add_column :rails_error_dashboard_error_logs, :action_name, :string
|
|
5
|
+
|
|
6
|
+
# Add composite index for efficient querying by controller/action
|
|
7
|
+
add_index :rails_error_dashboard_error_logs, [ :controller_name, :action_name, :error_hash ],
|
|
8
|
+
name: 'index_error_logs_on_controller_action_hash'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
desc "Installs Rails Error Dashboard and generates the necessary files"
|
|
9
|
+
|
|
10
|
+
def create_initializer_file
|
|
11
|
+
template "initializer.rb", "config/initializers/rails_error_dashboard.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def copy_migrations
|
|
15
|
+
rake "rails_error_dashboard:install:migrations"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def add_route
|
|
19
|
+
route "mount RailsErrorDashboard::Engine => '/error_dashboard'"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def show_readme
|
|
23
|
+
readme "README" if behavior == :invoke
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
Rails Error Dashboard has been installed!
|
|
4
|
+
|
|
5
|
+
Next steps:
|
|
6
|
+
|
|
7
|
+
1. Run migrations:
|
|
8
|
+
rails db:migrate
|
|
9
|
+
|
|
10
|
+
2. (Optional) If you want to use a separate database for error logs:
|
|
11
|
+
- Set USE_SEPARATE_ERROR_DB=true in your .env file
|
|
12
|
+
- Configure error_logs database in config/database.yml
|
|
13
|
+
- Run: rails db:create:error_logs
|
|
14
|
+
- Run: rails db:migrate:error_logs
|
|
15
|
+
|
|
16
|
+
3. Start your Rails server:
|
|
17
|
+
rails server
|
|
18
|
+
|
|
19
|
+
4. Visit the error dashboard:
|
|
20
|
+
http://localhost:3000/error_dashboard
|
|
21
|
+
|
|
22
|
+
5. Default credentials (change in config/initializers/rails_error_dashboard.rb):
|
|
23
|
+
Username: admin
|
|
24
|
+
Password: password
|
|
25
|
+
|
|
26
|
+
6. (Optional) Set up Slack notifications:
|
|
27
|
+
- Set SLACK_WEBHOOK_URL in your .env file
|
|
28
|
+
- Errors will be sent to your Slack channel automatically
|
|
29
|
+
|
|
30
|
+
For more information, visit:
|
|
31
|
+
https://github.com/anjanjagirdar/rails_error_dashboard
|
|
32
|
+
|
|
33
|
+
===============================================================================
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RailsErrorDashboard.configure do |config|
|
|
4
|
+
# Dashboard authentication credentials
|
|
5
|
+
# Change these in production or use environment variables
|
|
6
|
+
config.dashboard_username = ENV.fetch("ERROR_DASHBOARD_USER", "admin")
|
|
7
|
+
config.dashboard_password = ENV.fetch("ERROR_DASHBOARD_PASSWORD", "password")
|
|
8
|
+
|
|
9
|
+
# Require authentication for dashboard access
|
|
10
|
+
# Set to false to disable authentication (not recommended in production)
|
|
11
|
+
config.require_authentication = true
|
|
12
|
+
|
|
13
|
+
# Require authentication even in development mode
|
|
14
|
+
# Set to true if you want to test authentication in development
|
|
15
|
+
config.require_authentication_in_development = false
|
|
16
|
+
|
|
17
|
+
# User model for associations (defaults to 'User')
|
|
18
|
+
# Change this if your user model has a different name
|
|
19
|
+
config.user_model = "User"
|
|
20
|
+
|
|
21
|
+
# === NOTIFICATION SETTINGS ===
|
|
22
|
+
#
|
|
23
|
+
# Notifications are sent asynchronously via the :error_notifications queue
|
|
24
|
+
# Works with: Solid Queue (Rails 8.1+), Sidekiq, Delayed Job, Resque, etc.
|
|
25
|
+
#
|
|
26
|
+
# For Sidekiq, add to config/sidekiq.yml:
|
|
27
|
+
# :queues:
|
|
28
|
+
# - error_notifications
|
|
29
|
+
# - default
|
|
30
|
+
#
|
|
31
|
+
# For Solid Queue (Rails 8.1+), add to config/queue.yml:
|
|
32
|
+
# workers:
|
|
33
|
+
# - queues: error_notifications
|
|
34
|
+
# threads: 3
|
|
35
|
+
|
|
36
|
+
# Slack notifications
|
|
37
|
+
config.enable_slack_notifications = true
|
|
38
|
+
config.slack_webhook_url = ENV["SLACK_WEBHOOK_URL"]
|
|
39
|
+
|
|
40
|
+
# Email notifications
|
|
41
|
+
config.enable_email_notifications = true
|
|
42
|
+
config.notification_email_recipients = ENV.fetch("ERROR_NOTIFICATION_EMAILS", "").split(",").map(&:strip)
|
|
43
|
+
config.notification_email_from = ENV.fetch("ERROR_NOTIFICATION_FROM", "errors@example.com")
|
|
44
|
+
|
|
45
|
+
# Dashboard base URL (used in notification links)
|
|
46
|
+
# Example: 'https://myapp.com' or 'http://localhost:3000'
|
|
47
|
+
config.dashboard_base_url = ENV["DASHBOARD_BASE_URL"]
|
|
48
|
+
|
|
49
|
+
# Use a separate database for error logs (optional)
|
|
50
|
+
# See documentation for setup instructions: docs/SEPARATE_ERROR_DATABASE.md
|
|
51
|
+
config.use_separate_database = ENV.fetch("USE_SEPARATE_ERROR_DB", "false") == "true"
|
|
52
|
+
|
|
53
|
+
# Retention policy - number of days to keep error logs
|
|
54
|
+
# Old errors will be automatically deleted after this many days
|
|
55
|
+
config.retention_days = 90
|
|
56
|
+
|
|
57
|
+
# Enable/disable error catching middleware
|
|
58
|
+
# Set to false if you want to handle errors differently
|
|
59
|
+
config.enable_middleware = true
|
|
60
|
+
|
|
61
|
+
# Enable/disable Rails.error subscriber
|
|
62
|
+
# Set to false if you don't want to use Rails error reporting
|
|
63
|
+
config.enable_error_subscriber = true
|
|
64
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Commands
|
|
5
|
+
# Command: Delete multiple errors at once
|
|
6
|
+
# This is a write operation that destroys multiple ErrorLog records
|
|
7
|
+
class BatchDeleteErrors
|
|
8
|
+
def self.call(error_ids)
|
|
9
|
+
new(error_ids).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(error_ids)
|
|
13
|
+
@error_ids = Array(error_ids).compact
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
return { success: false, count: 0, errors: [ "No error IDs provided" ] } if @error_ids.empty?
|
|
18
|
+
|
|
19
|
+
errors = ErrorLog.where(id: @error_ids)
|
|
20
|
+
count = errors.count
|
|
21
|
+
error_ids_to_delete = errors.pluck(:id)
|
|
22
|
+
|
|
23
|
+
errors.destroy_all
|
|
24
|
+
|
|
25
|
+
# Dispatch plugin event for batch deleted errors
|
|
26
|
+
PluginRegistry.dispatch(:on_errors_batch_deleted, error_ids_to_delete) if error_ids_to_delete.any?
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
success: true,
|
|
30
|
+
count: count,
|
|
31
|
+
total: @error_ids.size,
|
|
32
|
+
errors: []
|
|
33
|
+
}
|
|
34
|
+
rescue => e
|
|
35
|
+
Rails.logger.error("Batch delete failed: #{e.message}")
|
|
36
|
+
{ success: false, count: 0, total: @error_ids.size, errors: [ e.message ] }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Commands
|
|
5
|
+
# Command: Resolve multiple errors at once
|
|
6
|
+
# This is a write operation that updates multiple ErrorLog records
|
|
7
|
+
class BatchResolveErrors
|
|
8
|
+
def self.call(error_ids, resolved_by_name: nil, resolution_comment: nil)
|
|
9
|
+
new(error_ids, resolved_by_name, resolution_comment).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(error_ids, resolved_by_name = nil, resolution_comment = nil)
|
|
13
|
+
@error_ids = Array(error_ids).compact
|
|
14
|
+
@resolved_by_name = resolved_by_name
|
|
15
|
+
@resolution_comment = resolution_comment
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
return { success: false, count: 0, errors: [ "No error IDs provided" ] } if @error_ids.empty?
|
|
20
|
+
|
|
21
|
+
errors = ErrorLog.where(id: @error_ids)
|
|
22
|
+
|
|
23
|
+
resolved_count = 0
|
|
24
|
+
failed_ids = []
|
|
25
|
+
|
|
26
|
+
resolved_errors = []
|
|
27
|
+
|
|
28
|
+
errors.each do |error|
|
|
29
|
+
begin
|
|
30
|
+
error.update!(
|
|
31
|
+
resolved: true,
|
|
32
|
+
resolved_at: Time.current,
|
|
33
|
+
resolved_by_name: @resolved_by_name,
|
|
34
|
+
resolution_comment: @resolution_comment
|
|
35
|
+
)
|
|
36
|
+
resolved_count += 1
|
|
37
|
+
resolved_errors << error
|
|
38
|
+
rescue => e
|
|
39
|
+
failed_ids << error.id
|
|
40
|
+
Rails.logger.error("Failed to resolve error #{error.id}: #{e.message}")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Dispatch plugin event for batch resolved errors
|
|
45
|
+
PluginRegistry.dispatch(:on_errors_batch_resolved, resolved_errors) if resolved_errors.any?
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
success: failed_ids.empty?,
|
|
49
|
+
count: resolved_count,
|
|
50
|
+
total: @error_ids.size,
|
|
51
|
+
failed_ids: failed_ids,
|
|
52
|
+
errors: failed_ids.empty? ? [] : [ "Failed to resolve #{failed_ids.size} error(s)" ]
|
|
53
|
+
}
|
|
54
|
+
rescue => e
|
|
55
|
+
Rails.logger.error("Batch resolve failed: #{e.message}")
|
|
56
|
+
{ success: false, count: 0, total: @error_ids.size, errors: [ e.message ] }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Commands
|
|
5
|
+
# Command: Log an error to the database
|
|
6
|
+
# This is a write operation that creates an ErrorLog record
|
|
7
|
+
class LogError
|
|
8
|
+
def self.call(exception, context = {})
|
|
9
|
+
new(exception, context).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(exception, context = {})
|
|
13
|
+
@exception = exception
|
|
14
|
+
@context = context
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
error_context = ValueObjects::ErrorContext.new(@context, @context[:source])
|
|
19
|
+
|
|
20
|
+
# Build error attributes
|
|
21
|
+
attributes = {
|
|
22
|
+
error_type: @exception.class.name,
|
|
23
|
+
message: @exception.message,
|
|
24
|
+
backtrace: @exception.backtrace&.join("\n"),
|
|
25
|
+
user_id: error_context.user_id,
|
|
26
|
+
request_url: error_context.request_url,
|
|
27
|
+
request_params: error_context.request_params,
|
|
28
|
+
user_agent: error_context.user_agent,
|
|
29
|
+
ip_address: error_context.ip_address,
|
|
30
|
+
environment: Rails.env,
|
|
31
|
+
platform: error_context.platform,
|
|
32
|
+
controller_name: error_context.controller_name,
|
|
33
|
+
action_name: error_context.action_name,
|
|
34
|
+
occurred_at: Time.current
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Generate error hash for deduplication (including controller/action context)
|
|
38
|
+
error_hash = generate_error_hash(@exception, error_context.controller_name, error_context.action_name)
|
|
39
|
+
|
|
40
|
+
# Find existing error or create new one
|
|
41
|
+
# This ensures accurate occurrence tracking
|
|
42
|
+
error_log = ErrorLog.find_or_increment_by_hash(error_hash, attributes.merge(error_hash: error_hash))
|
|
43
|
+
|
|
44
|
+
# Send notifications only for new errors (not increments)
|
|
45
|
+
# Check if this is first occurrence or error was just created
|
|
46
|
+
if error_log.occurrence_count == 1
|
|
47
|
+
send_notifications(error_log)
|
|
48
|
+
# Dispatch plugin event for new error
|
|
49
|
+
PluginRegistry.dispatch(:on_error_logged, error_log)
|
|
50
|
+
else
|
|
51
|
+
# Dispatch plugin event for error recurrence
|
|
52
|
+
PluginRegistry.dispatch(:on_error_recurred, error_log)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
error_log
|
|
56
|
+
rescue => e
|
|
57
|
+
# Don't let error logging cause more errors
|
|
58
|
+
Rails.logger.error("Failed to log error: #{e.message}")
|
|
59
|
+
Rails.logger.error("Backtrace: #{e.backtrace&.first(5)&.join("\n")}")
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Generate consistent hash for error deduplication
|
|
66
|
+
# Same hash = same error type
|
|
67
|
+
# Note: This is also defined in ErrorLog model for backward compatibility
|
|
68
|
+
def generate_error_hash(exception, controller_name = nil, action_name = nil)
|
|
69
|
+
# Hash components:
|
|
70
|
+
# 1. Error class (NoMethodError, ArgumentError, etc.)
|
|
71
|
+
# 2. Normalized message (replace numbers, quoted strings)
|
|
72
|
+
# 3. First stack frame file (ignore line numbers)
|
|
73
|
+
# 4. Controller name (for context-aware grouping)
|
|
74
|
+
# 5. Action name (for context-aware grouping)
|
|
75
|
+
|
|
76
|
+
normalized_message = exception.message
|
|
77
|
+
&.gsub(/\d+/, "N") # Replace numbers: "User 123" -> "User N"
|
|
78
|
+
&.gsub(/"[^"]*"/, '""') # Replace quoted strings: "foo" -> ""
|
|
79
|
+
&.gsub(/'[^']*'/, "''") # Replace single quoted strings
|
|
80
|
+
&.gsub(/0x[0-9a-f]+/i, "0xHEX") # Replace hex addresses
|
|
81
|
+
&.gsub(/#<[^>]+>/, "#<OBJ>") # Replace object inspections
|
|
82
|
+
|
|
83
|
+
# Get first meaningful stack frame (skip gems, focus on app code)
|
|
84
|
+
first_app_frame = exception.backtrace&.find { |frame|
|
|
85
|
+
# Look for app code, not gems
|
|
86
|
+
frame.include?("/app/") || frame.include?("/lib/") || !frame.include?("/gems/")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Extract just the file path, not line number
|
|
90
|
+
file_path = first_app_frame&.split(":")&.first
|
|
91
|
+
|
|
92
|
+
# Generate hash including controller/action for better grouping
|
|
93
|
+
digest_input = [
|
|
94
|
+
exception.class.name,
|
|
95
|
+
normalized_message,
|
|
96
|
+
file_path,
|
|
97
|
+
controller_name, # Context: which controller
|
|
98
|
+
action_name # Context: which action
|
|
99
|
+
].compact.join("|")
|
|
100
|
+
|
|
101
|
+
Digest::SHA256.hexdigest(digest_input)[0..15]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def send_notifications(error_log)
|
|
105
|
+
config = RailsErrorDashboard.configuration
|
|
106
|
+
|
|
107
|
+
# Send Slack notification
|
|
108
|
+
if config.enable_slack_notifications && config.slack_webhook_url.present?
|
|
109
|
+
SlackErrorNotificationJob.perform_later(error_log.id)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Send email notification
|
|
113
|
+
if config.enable_email_notifications && config.notification_email_recipients.present?
|
|
114
|
+
EmailErrorNotificationJob.perform_later(error_log.id)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Send Discord notification
|
|
118
|
+
if config.enable_discord_notifications && config.discord_webhook_url.present?
|
|
119
|
+
DiscordErrorNotificationJob.perform_later(error_log.id)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Send PagerDuty notification (critical errors only)
|
|
123
|
+
if config.enable_pagerduty_notifications && config.pagerduty_integration_key.present?
|
|
124
|
+
PagerdutyErrorNotificationJob.perform_later(error_log.id)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Send webhook notifications
|
|
128
|
+
if config.enable_webhook_notifications && config.webhook_urls.present?
|
|
129
|
+
WebhookErrorNotificationJob.perform_later(error_log.id)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Commands
|
|
5
|
+
# Command: Mark an error as resolved
|
|
6
|
+
# This is a write operation that updates an ErrorLog record
|
|
7
|
+
class ResolveError
|
|
8
|
+
def self.call(error_id, resolution_data = {})
|
|
9
|
+
new(error_id, resolution_data).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(error_id, resolution_data = {})
|
|
13
|
+
@error_id = error_id
|
|
14
|
+
@resolution_data = resolution_data
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
error = ErrorLog.find(@error_id)
|
|
19
|
+
|
|
20
|
+
error.update!(
|
|
21
|
+
resolved: true,
|
|
22
|
+
resolved_at: Time.current,
|
|
23
|
+
resolved_by_name: @resolution_data[:resolved_by_name],
|
|
24
|
+
resolution_comment: @resolution_data[:resolution_comment],
|
|
25
|
+
resolution_reference: @resolution_data[:resolution_reference]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Dispatch plugin event for resolved error
|
|
29
|
+
PluginRegistry.dispatch(:on_error_resolved, error)
|
|
30
|
+
|
|
31
|
+
error
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
class Configuration
|
|
5
|
+
# Dashboard authentication
|
|
6
|
+
attr_accessor :dashboard_username
|
|
7
|
+
attr_accessor :dashboard_password
|
|
8
|
+
attr_accessor :require_authentication
|
|
9
|
+
attr_accessor :require_authentication_in_development
|
|
10
|
+
|
|
11
|
+
# User model (for associations)
|
|
12
|
+
attr_accessor :user_model
|
|
13
|
+
|
|
14
|
+
# Notifications
|
|
15
|
+
attr_accessor :slack_webhook_url
|
|
16
|
+
attr_accessor :notification_email_recipients
|
|
17
|
+
attr_accessor :notification_email_from
|
|
18
|
+
attr_accessor :dashboard_base_url
|
|
19
|
+
attr_accessor :enable_slack_notifications
|
|
20
|
+
attr_accessor :enable_email_notifications
|
|
21
|
+
|
|
22
|
+
# Discord notifications
|
|
23
|
+
attr_accessor :discord_webhook_url
|
|
24
|
+
attr_accessor :enable_discord_notifications
|
|
25
|
+
|
|
26
|
+
# PagerDuty notifications (critical errors only)
|
|
27
|
+
attr_accessor :pagerduty_integration_key
|
|
28
|
+
attr_accessor :enable_pagerduty_notifications
|
|
29
|
+
|
|
30
|
+
# Generic webhook notifications
|
|
31
|
+
attr_accessor :webhook_urls
|
|
32
|
+
attr_accessor :enable_webhook_notifications
|
|
33
|
+
|
|
34
|
+
# Separate database configuration
|
|
35
|
+
attr_accessor :use_separate_database
|
|
36
|
+
|
|
37
|
+
# Retention policy (days to keep errors)
|
|
38
|
+
attr_accessor :retention_days
|
|
39
|
+
|
|
40
|
+
# Enable/disable error catching middleware
|
|
41
|
+
attr_accessor :enable_middleware
|
|
42
|
+
|
|
43
|
+
# Enable/disable Rails.error subscriber
|
|
44
|
+
attr_accessor :enable_error_subscriber
|
|
45
|
+
|
|
46
|
+
def initialize
|
|
47
|
+
# Default values
|
|
48
|
+
@dashboard_username = ENV.fetch("ERROR_DASHBOARD_USER", "admin")
|
|
49
|
+
@dashboard_password = ENV.fetch("ERROR_DASHBOARD_PASSWORD", "password")
|
|
50
|
+
@require_authentication = true
|
|
51
|
+
@require_authentication_in_development = false
|
|
52
|
+
|
|
53
|
+
@user_model = "User"
|
|
54
|
+
|
|
55
|
+
# Notification settings
|
|
56
|
+
@slack_webhook_url = ENV["SLACK_WEBHOOK_URL"]
|
|
57
|
+
@notification_email_recipients = ENV.fetch("ERROR_NOTIFICATION_EMAILS", "").split(",").map(&:strip)
|
|
58
|
+
@notification_email_from = ENV.fetch("ERROR_NOTIFICATION_FROM", "errors@example.com")
|
|
59
|
+
@dashboard_base_url = ENV["DASHBOARD_BASE_URL"]
|
|
60
|
+
@enable_slack_notifications = true
|
|
61
|
+
@enable_email_notifications = true
|
|
62
|
+
|
|
63
|
+
# Discord notification settings
|
|
64
|
+
@discord_webhook_url = ENV["DISCORD_WEBHOOK_URL"]
|
|
65
|
+
@enable_discord_notifications = false
|
|
66
|
+
|
|
67
|
+
# PagerDuty notification settings (critical errors only)
|
|
68
|
+
@pagerduty_integration_key = ENV["PAGERDUTY_INTEGRATION_KEY"]
|
|
69
|
+
@enable_pagerduty_notifications = false
|
|
70
|
+
|
|
71
|
+
# Generic webhook settings (array of URLs)
|
|
72
|
+
@webhook_urls = ENV.fetch("WEBHOOK_URLS", "").split(",").map(&:strip).reject(&:empty?)
|
|
73
|
+
@enable_webhook_notifications = false
|
|
74
|
+
|
|
75
|
+
@use_separate_database = ENV.fetch("USE_SEPARATE_ERROR_DB", "false") == "true"
|
|
76
|
+
|
|
77
|
+
@retention_days = 90
|
|
78
|
+
|
|
79
|
+
@enable_middleware = true
|
|
80
|
+
@enable_error_subscriber = true
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RailsErrorDashboard
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace RailsErrorDashboard
|
|
4
|
+
|
|
5
|
+
# Initialize the engine
|
|
6
|
+
initializer "rails_error_dashboard.middleware" do |app|
|
|
7
|
+
# Add error catching middleware if enabled
|
|
8
|
+
if RailsErrorDashboard.configuration.enable_middleware
|
|
9
|
+
app.config.middleware.insert_before 0, RailsErrorDashboard::Middleware::ErrorCatcher
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Subscribe to Rails error reporter
|
|
14
|
+
config.after_initialize do
|
|
15
|
+
if RailsErrorDashboard.configuration.enable_error_subscriber
|
|
16
|
+
Rails.error.subscribe(RailsErrorDashboard::ErrorReporter.new)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Centralized Error Reporting for ALL errors in the application
|
|
4
|
+
# Uses Rails 7+ built-in error reporter - single source of truth
|
|
5
|
+
#
|
|
6
|
+
# This catches errors from:
|
|
7
|
+
# - Controllers (via ErrorHandler concern)
|
|
8
|
+
# - Background Jobs (via ActiveJob integration)
|
|
9
|
+
# - Sidekiq (via ActiveJob)
|
|
10
|
+
# - Services (if they use Rails.error.handle)
|
|
11
|
+
# - Model callbacks
|
|
12
|
+
# - Rake tasks
|
|
13
|
+
# - Console
|
|
14
|
+
# - Anywhere in the app
|
|
15
|
+
#
|
|
16
|
+
# Based on Rails 7+ Error Reporting Guide:
|
|
17
|
+
# https://guides.rubyonrails.org/error_reporting.html
|
|
18
|
+
|
|
19
|
+
module RailsErrorDashboard
|
|
20
|
+
class ErrorReporter
|
|
21
|
+
def report(error, handled:, severity:, context:, source: nil)
|
|
22
|
+
# Skip low-severity warnings
|
|
23
|
+
return if handled && severity == :warning
|
|
24
|
+
|
|
25
|
+
# Extract context information
|
|
26
|
+
error_context = ValueObjects::ErrorContext.new(context, source)
|
|
27
|
+
|
|
28
|
+
# Log to our error dashboard using Command
|
|
29
|
+
Commands::LogError.call(error, error_context.to_h.merge(source: source))
|
|
30
|
+
rescue => e
|
|
31
|
+
# Don't let error logging cause more errors
|
|
32
|
+
Rails.logger.error("ErrorReporter failed: #{e.message}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rack Middleware: Final safety net for uncaught errors
|
|
4
|
+
# This catches errors that somehow escape controller error handling
|
|
5
|
+
# Positioned at the Rack layer (outermost layer of Rails)
|
|
6
|
+
#
|
|
7
|
+
# Middleware stack order (outer to inner):
|
|
8
|
+
# 1. ErrorCatcher (this file) ← Catches everything
|
|
9
|
+
# 2. ActionDispatch middleware
|
|
10
|
+
# 3. Rails routing
|
|
11
|
+
# 4. Controllers (with ErrorHandler concern)
|
|
12
|
+
#
|
|
13
|
+
# This ensures NO error goes unreported
|
|
14
|
+
|
|
15
|
+
module RailsErrorDashboard
|
|
16
|
+
module Middleware
|
|
17
|
+
class ErrorCatcher
|
|
18
|
+
def initialize(app)
|
|
19
|
+
@app = app
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(env)
|
|
23
|
+
@app.call(env)
|
|
24
|
+
rescue => exception
|
|
25
|
+
# Report to Rails.error (will be logged by our ErrorReporter)
|
|
26
|
+
Rails.error.report(exception,
|
|
27
|
+
handled: false,
|
|
28
|
+
severity: :error,
|
|
29
|
+
context: {
|
|
30
|
+
request: ActionDispatch::Request.new(env),
|
|
31
|
+
middleware: true
|
|
32
|
+
},
|
|
33
|
+
source: "rack.middleware"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Re-raise to let Rails handle the response
|
|
37
|
+
raise exception
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|