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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +858 -0
  4. data/Rakefile +3 -0
  5. data/app/assets/stylesheets/rails_error_dashboard/application.css +15 -0
  6. data/app/controllers/rails_error_dashboard/application_controller.rb +12 -0
  7. data/app/controllers/rails_error_dashboard/errors_controller.rb +123 -0
  8. data/app/helpers/rails_error_dashboard/application_helper.rb +4 -0
  9. data/app/jobs/rails_error_dashboard/application_job.rb +4 -0
  10. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +116 -0
  11. data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +19 -0
  12. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +105 -0
  13. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +166 -0
  14. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +108 -0
  15. data/app/mailers/rails_error_dashboard/application_mailer.rb +8 -0
  16. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +27 -0
  17. data/app/models/rails_error_dashboard/application_record.rb +5 -0
  18. data/app/models/rails_error_dashboard/error_log.rb +185 -0
  19. data/app/models/rails_error_dashboard/error_logs_record.rb +34 -0
  20. data/app/views/layouts/rails_error_dashboard/application.html.erb +17 -0
  21. data/app/views/layouts/rails_error_dashboard.html.erb +351 -0
  22. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +200 -0
  23. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +32 -0
  24. data/app/views/rails_error_dashboard/errors/analytics.html.erb +237 -0
  25. data/app/views/rails_error_dashboard/errors/index.html.erb +334 -0
  26. data/app/views/rails_error_dashboard/errors/show.html.erb +294 -0
  27. data/config/routes.rb +13 -0
  28. data/db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb +40 -0
  29. data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +13 -0
  30. data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +10 -0
  31. data/lib/generators/rails_error_dashboard/install/install_generator.rb +27 -0
  32. data/lib/generators/rails_error_dashboard/install/templates/README +33 -0
  33. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +64 -0
  34. data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +40 -0
  35. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +60 -0
  36. data/lib/rails_error_dashboard/commands/log_error.rb +134 -0
  37. data/lib/rails_error_dashboard/commands/resolve_error.rb +35 -0
  38. data/lib/rails_error_dashboard/configuration.rb +83 -0
  39. data/lib/rails_error_dashboard/engine.rb +20 -0
  40. data/lib/rails_error_dashboard/error_reporter.rb +35 -0
  41. data/lib/rails_error_dashboard/middleware/error_catcher.rb +41 -0
  42. data/lib/rails_error_dashboard/plugin.rb +98 -0
  43. data/lib/rails_error_dashboard/plugin_registry.rb +88 -0
  44. data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +96 -0
  45. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +122 -0
  46. data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +78 -0
  47. data/lib/rails_error_dashboard/queries/analytics_stats.rb +108 -0
  48. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +37 -0
  49. data/lib/rails_error_dashboard/queries/developer_insights.rb +277 -0
  50. data/lib/rails_error_dashboard/queries/errors_list.rb +66 -0
  51. data/lib/rails_error_dashboard/queries/errors_list_v2.rb +149 -0
  52. data/lib/rails_error_dashboard/queries/filter_options.rb +21 -0
  53. data/lib/rails_error_dashboard/services/platform_detector.rb +41 -0
  54. data/lib/rails_error_dashboard/value_objects/error_context.rb +148 -0
  55. data/lib/rails_error_dashboard/version.rb +3 -0
  56. data/lib/rails_error_dashboard.rb +60 -0
  57. data/lib/tasks/rails_error_dashboard_tasks.rake +4 -0
  58. 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