rails_error_dashboard 0.1.0 → 0.1.3

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +305 -703
  3. data/app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss +107 -0
  4. data/app/assets/stylesheets/rails_error_dashboard/_components.scss +625 -0
  5. data/app/assets/stylesheets/rails_error_dashboard/_layout.scss +257 -0
  6. data/app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss +203 -0
  7. data/app/assets/stylesheets/rails_error_dashboard/application.css +926 -15
  8. data/app/assets/stylesheets/rails_error_dashboard/application.css.map +7 -0
  9. data/app/assets/stylesheets/rails_error_dashboard/application.scss +61 -0
  10. data/app/controllers/rails_error_dashboard/application_controller.rb +18 -0
  11. data/app/controllers/rails_error_dashboard/errors_controller.rb +140 -4
  12. data/app/helpers/rails_error_dashboard/application_helper.rb +55 -0
  13. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +91 -0
  14. data/app/helpers/rails_error_dashboard/overview_helper.rb +78 -0
  15. data/app/helpers/rails_error_dashboard/user_agent_helper.rb +118 -0
  16. data/app/jobs/rails_error_dashboard/application_job.rb +19 -0
  17. data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +48 -0
  18. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +263 -0
  19. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -8
  20. data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +2 -1
  21. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +5 -5
  22. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +10 -6
  23. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +5 -6
  24. data/app/mailers/rails_error_dashboard/application_mailer.rb +1 -1
  25. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +1 -1
  26. data/app/models/rails_error_dashboard/cascade_pattern.rb +74 -0
  27. data/app/models/rails_error_dashboard/error_baseline.rb +100 -0
  28. data/app/models/rails_error_dashboard/error_comment.rb +27 -0
  29. data/app/models/rails_error_dashboard/error_log.rb +471 -3
  30. data/app/models/rails_error_dashboard/error_occurrence.rb +49 -0
  31. data/app/views/layouts/rails_error_dashboard.html.erb +816 -178
  32. data/app/views/layouts/rails_error_dashboard_old_backup.html.erb +383 -0
  33. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +3 -10
  34. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +1 -2
  35. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +78 -0
  36. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +209 -0
  37. data/app/views/rails_error_dashboard/errors/_stats.html.erb +34 -0
  38. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +167 -0
  39. data/app/views/rails_error_dashboard/errors/analytics.html.erb +152 -56
  40. data/app/views/rails_error_dashboard/errors/correlation.html.erb +373 -0
  41. data/app/views/rails_error_dashboard/errors/index.html.erb +294 -138
  42. data/app/views/rails_error_dashboard/errors/overview.html.erb +253 -0
  43. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +399 -0
  44. data/app/views/rails_error_dashboard/errors/show.html.erb +781 -65
  45. data/config/routes.rb +9 -0
  46. data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +66 -0
  47. data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +26 -0
  48. data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +12 -0
  49. data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +9 -0
  50. data/db/migrate/20251225100236_create_error_occurrences.rb +31 -0
  51. data/db/migrate/20251225101920_create_cascade_patterns.rb +33 -0
  52. data/db/migrate/20251225102500_create_error_baselines.rb +38 -0
  53. data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +27 -0
  54. data/db/migrate/20251226020100_create_error_comments.rb +18 -0
  55. data/lib/generators/rails_error_dashboard/install/install_generator.rb +276 -1
  56. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +272 -37
  57. data/lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb +36 -0
  58. data/lib/generators/rails_error_dashboard/solid_queue/templates/queue.yml +55 -0
  59. data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +1 -1
  60. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -2
  61. data/lib/rails_error_dashboard/commands/log_error.rb +272 -7
  62. data/lib/rails_error_dashboard/commands/resolve_error.rb +16 -0
  63. data/lib/rails_error_dashboard/configuration.rb +90 -5
  64. data/lib/rails_error_dashboard/error_reporter.rb +15 -7
  65. data/lib/rails_error_dashboard/logger.rb +105 -0
  66. data/lib/rails_error_dashboard/middleware/error_catcher.rb +17 -10
  67. data/lib/rails_error_dashboard/plugin.rb +6 -3
  68. data/lib/rails_error_dashboard/plugin_registry.rb +2 -2
  69. data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +0 -1
  70. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +3 -4
  71. data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +1 -3
  72. data/lib/rails_error_dashboard/queries/analytics_stats.rb +44 -6
  73. data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
  74. data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +86 -0
  75. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +242 -2
  76. data/lib/rails_error_dashboard/queries/error_cascades.rb +74 -0
  77. data/lib/rails_error_dashboard/queries/error_correlation.rb +375 -0
  78. data/lib/rails_error_dashboard/queries/errors_list.rb +106 -10
  79. data/lib/rails_error_dashboard/queries/filter_options.rb +0 -1
  80. data/lib/rails_error_dashboard/queries/platform_comparison.rb +254 -0
  81. data/lib/rails_error_dashboard/queries/similar_errors.rb +93 -0
  82. data/lib/rails_error_dashboard/services/backtrace_parser.rb +113 -0
  83. data/lib/rails_error_dashboard/services/baseline_alert_throttler.rb +88 -0
  84. data/lib/rails_error_dashboard/services/baseline_calculator.rb +269 -0
  85. data/lib/rails_error_dashboard/services/cascade_detector.rb +95 -0
  86. data/lib/rails_error_dashboard/services/pattern_detector.rb +268 -0
  87. data/lib/rails_error_dashboard/services/similarity_calculator.rb +144 -0
  88. data/lib/rails_error_dashboard/value_objects/error_context.rb +27 -1
  89. data/lib/rails_error_dashboard/version.rb +1 -1
  90. data/lib/rails_error_dashboard.rb +57 -7
  91. metadata +69 -10
  92. data/app/models/rails_error_dashboard/application_record.rb +0 -5
  93. data/lib/rails_error_dashboard/queries/developer_insights.rb +0 -277
  94. data/lib/rails_error_dashboard/queries/errors_list_v2.rb +0 -149
  95. data/lib/tasks/rails_error_dashboard_tasks.rake +0 -4
@@ -6,7 +6,29 @@ module RailsErrorDashboard
6
6
  # This is a write operation that creates an ErrorLog record
7
7
  class LogError
8
8
  def self.call(exception, context = {})
9
- new(exception, context).call
9
+ # Check if async logging is enabled
10
+ if RailsErrorDashboard.configuration.async_logging
11
+ # For async logging, just enqueue the job
12
+ # All filtering happens when the job runs
13
+ call_async(exception, context)
14
+ else
15
+ # For sync logging, execute immediately
16
+ new(exception, context).call
17
+ end
18
+ end
19
+
20
+ # Queue error logging as a background job
21
+ def self.call_async(exception, context = {})
22
+ # Serialize exception data for the job
23
+ exception_data = {
24
+ class_name: exception.class.name,
25
+ message: exception.message,
26
+ backtrace: exception.backtrace
27
+ }
28
+
29
+ # Enqueue the async job using ActiveJob
30
+ # The queue adapter (:sidekiq, :solid_queue, :async) is configured separately
31
+ AsyncErrorLoggingJob.perform_later(exception_data, context)
10
32
  end
11
33
 
12
34
  def initialize(exception, context = {})
@@ -15,19 +37,26 @@ module RailsErrorDashboard
15
37
  end
16
38
 
17
39
  def call
40
+ # Check if this exception should be ignored
41
+ return nil if should_ignore_exception?(@exception)
42
+
43
+ # Check sampling rate for non-critical errors
44
+ # Critical errors are ALWAYS logged regardless of sampling
45
+ return nil if should_skip_due_to_sampling?(@exception)
46
+
18
47
  error_context = ValueObjects::ErrorContext.new(@context, @context[:source])
19
48
 
20
49
  # Build error attributes
50
+ truncated_backtrace = truncate_backtrace(@exception.backtrace)
21
51
  attributes = {
22
52
  error_type: @exception.class.name,
23
53
  message: @exception.message,
24
- backtrace: @exception.backtrace&.join("\n"),
54
+ backtrace: truncated_backtrace,
25
55
  user_id: error_context.user_id,
26
56
  request_url: error_context.request_url,
27
57
  request_params: error_context.request_params,
28
58
  user_agent: error_context.user_agent,
29
59
  ip_address: error_context.ip_address,
30
- environment: Rails.env,
31
60
  platform: error_context.platform,
32
61
  controller_name: error_context.controller_name,
33
62
  action_name: error_context.action_name,
@@ -37,31 +66,174 @@ module RailsErrorDashboard
37
66
  # Generate error hash for deduplication (including controller/action context)
38
67
  error_hash = generate_error_hash(@exception, error_context.controller_name, error_context.action_name)
39
68
 
69
+ # Calculate backtrace signature for fuzzy matching (if column exists)
70
+ if ErrorLog.column_names.include?("backtrace_signature")
71
+ attributes[:backtrace_signature] = calculate_backtrace_signature_from_backtrace(truncated_backtrace)
72
+ end
73
+
74
+ # Add git/release info if columns exist
75
+ if ErrorLog.column_names.include?("git_sha")
76
+ attributes[:git_sha] = RailsErrorDashboard.configuration.git_sha ||
77
+ ENV["GIT_SHA"] ||
78
+ ENV["HEROKU_SLUG_COMMIT"] ||
79
+ ENV["RENDER_GIT_COMMIT"] ||
80
+ detect_git_sha_from_command
81
+ end
82
+
83
+ if ErrorLog.column_names.include?("app_version")
84
+ attributes[:app_version] = RailsErrorDashboard.configuration.app_version ||
85
+ ENV["APP_VERSION"] ||
86
+ detect_version_from_file
87
+ end
88
+
40
89
  # Find existing error or create new one
41
90
  # This ensures accurate occurrence tracking
42
91
  error_log = ErrorLog.find_or_increment_by_hash(error_hash, attributes.merge(error_hash: error_hash))
43
92
 
93
+ # Track individual error occurrence for co-occurrence analysis (if table exists)
94
+ if defined?(ErrorOccurrence) && ErrorOccurrence.table_exists?
95
+ begin
96
+ ErrorOccurrence.create(
97
+ error_log: error_log,
98
+ occurred_at: attributes[:occurred_at],
99
+ user_id: attributes[:user_id],
100
+ request_id: error_context.request_id,
101
+ session_id: error_context.session_id
102
+ )
103
+ rescue => e
104
+ RailsErrorDashboard::Logger.error("Failed to create error occurrence: #{e.message}")
105
+ end
106
+ end
107
+
44
108
  # Send notifications only for new errors (not increments)
45
109
  # Check if this is first occurrence or error was just created
46
110
  if error_log.occurrence_count == 1
47
111
  send_notifications(error_log)
48
112
  # Dispatch plugin event for new error
49
113
  PluginRegistry.dispatch(:on_error_logged, error_log)
114
+ # Trigger notification callbacks
115
+ trigger_callbacks(error_log)
116
+ # Emit ActiveSupport::Notifications instrumentation events
117
+ emit_instrumentation_events(error_log)
50
118
  else
51
119
  # Dispatch plugin event for error recurrence
52
120
  PluginRegistry.dispatch(:on_error_recurred, error_log)
53
121
  end
54
122
 
123
+ # Check for baseline anomalies
124
+ check_baseline_anomaly(error_log)
125
+
55
126
  error_log
56
127
  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
128
+ # Don't let error logging cause more errors - fail silently
129
+ # CRITICAL: Log but never propagate exception
130
+ # Log to Rails logger for visibility during development
131
+ Rails.logger.error("[RailsErrorDashboard] LogError command failed: #{e.class} - #{e.message}")
132
+ Rails.logger.error("Backtrace: #{e.backtrace&.first(10)&.join("\n")}")
133
+
134
+ RailsErrorDashboard::Logger.error("[RailsErrorDashboard] LogError command failed: #{e.class} - #{e.message}")
135
+ RailsErrorDashboard::Logger.error("Original exception: #{@exception.class} - #{@exception.message}") if @exception
136
+ RailsErrorDashboard::Logger.error("Context: #{@context.inspect}") if @context
137
+ RailsErrorDashboard::Logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
138
+ nil # Explicitly return nil, never raise
61
139
  end
62
140
 
63
141
  private
64
142
 
143
+ # Trigger notification callbacks for error logging
144
+ def trigger_callbacks(error_log)
145
+ # Trigger general error_logged callbacks
146
+ RailsErrorDashboard.configuration.notification_callbacks[:error_logged].each do |callback|
147
+ callback.call(error_log)
148
+ rescue => e
149
+ RailsErrorDashboard::Logger.error("Error in error_logged callback: #{e.message}")
150
+ end
151
+
152
+ # Trigger critical_error callbacks if this is a critical error
153
+ if error_log.critical?
154
+ RailsErrorDashboard.configuration.notification_callbacks[:critical_error].each do |callback|
155
+ callback.call(error_log)
156
+ rescue => e
157
+ RailsErrorDashboard::Logger.error("Error in critical_error callback: #{e.message}")
158
+ end
159
+ end
160
+ end
161
+
162
+ # Emit ActiveSupport::Notifications instrumentation events
163
+ def emit_instrumentation_events(error_log)
164
+ # Payload for instrumentation subscribers
165
+ payload = {
166
+ error_log: error_log,
167
+ error_id: error_log.id,
168
+ error_type: error_log.error_type,
169
+ message: error_log.message,
170
+ severity: error_log.severity,
171
+ platform: error_log.platform,
172
+ occurred_at: error_log.occurred_at
173
+ }
174
+
175
+ # Emit general error_logged event
176
+ ActiveSupport::Notifications.instrument("error_logged.rails_error_dashboard", payload)
177
+
178
+ # Emit critical_error event if this is a critical error
179
+ if error_log.critical?
180
+ ActiveSupport::Notifications.instrument("critical_error.rails_error_dashboard", payload)
181
+ end
182
+ end
183
+
184
+ # Check if error should be skipped due to sampling rate
185
+ # Critical errors are ALWAYS logged, sampling only applies to non-critical errors
186
+ def should_skip_due_to_sampling?(exception)
187
+ sampling_rate = RailsErrorDashboard.configuration.sampling_rate
188
+
189
+ # If sampling is 100% (1.0) or higher, log everything
190
+ return false if sampling_rate >= 1.0
191
+
192
+ # Always log critical errors regardless of sampling rate
193
+ # Check this BEFORE checking for 0% sampling
194
+ return false if is_critical_error?(exception)
195
+
196
+ # If sampling is 0% or negative, skip all non-critical errors
197
+ return true if sampling_rate <= 0.0
198
+
199
+ # For non-critical errors, use probabilistic sampling
200
+ # rand returns 0.0 to 1.0, so if sampling_rate is 0.1 (10%),
201
+ # we skip 90% of errors (when rand > 0.1)
202
+ rand > sampling_rate
203
+ end
204
+
205
+ # Check if exception is a critical error type
206
+ def is_critical_error?(exception)
207
+ exception_class_name = exception.class.name
208
+ RailsErrorDashboard::ErrorLog::CRITICAL_ERROR_TYPES.include?(exception_class_name)
209
+ end
210
+
211
+ # Check if exception should be ignored based on configuration
212
+ # Supports both string class names and regex patterns
213
+ def should_ignore_exception?(exception)
214
+ ignored_exceptions = RailsErrorDashboard.configuration.ignored_exceptions
215
+ return false if ignored_exceptions.blank?
216
+
217
+ exception_class_name = exception.class.name
218
+
219
+ ignored_exceptions.any? do |ignored|
220
+ case ignored
221
+ when String
222
+ # Exact class name match (supports inheritance)
223
+ exception.is_a?(ignored.constantize)
224
+ when Regexp
225
+ # Regex pattern match on class name
226
+ exception_class_name.match?(ignored)
227
+ else
228
+ false
229
+ end
230
+ rescue NameError
231
+ # Handle invalid class names in configuration
232
+ RailsErrorDashboard::Logger.warn("Invalid ignored exception class: #{ignored}")
233
+ false
234
+ end
235
+ end
236
+
65
237
  # Generate consistent hash for error deduplication
66
238
  # Same hash = same error type
67
239
  # Note: This is also defined in ErrorLog model for backward compatibility
@@ -101,6 +273,28 @@ module RailsErrorDashboard
101
273
  Digest::SHA256.hexdigest(digest_input)[0..15]
102
274
  end
103
275
 
276
+ # Truncate backtrace to configured maximum lines
277
+ # This reduces database storage and improves performance
278
+ def truncate_backtrace(backtrace)
279
+ return nil if backtrace.nil?
280
+
281
+ max_lines = RailsErrorDashboard.configuration.max_backtrace_lines
282
+
283
+ # Limit backtrace to max_lines
284
+ limited_backtrace = backtrace.first(max_lines)
285
+
286
+ # Join into string
287
+ result = limited_backtrace.join("\n")
288
+
289
+ # Add truncation notice if we cut lines
290
+ if backtrace.length > max_lines
291
+ truncation_notice = "... (#{backtrace.length - max_lines} more lines truncated)"
292
+ result = result.empty? ? truncation_notice : result + "\n" + truncation_notice
293
+ end
294
+
295
+ result
296
+ end
297
+
104
298
  def send_notifications(error_log)
105
299
  config = RailsErrorDashboard.configuration
106
300
 
@@ -129,6 +323,77 @@ module RailsErrorDashboard
129
323
  WebhookErrorNotificationJob.perform_later(error_log.id)
130
324
  end
131
325
  end
326
+
327
+ # Calculate backtrace signature from backtrace string/array
328
+ # This matches the algorithm in ErrorLog#calculate_backtrace_signature
329
+ def calculate_backtrace_signature_from_backtrace(backtrace)
330
+ return nil if backtrace.blank?
331
+
332
+ lines = backtrace.is_a?(String) ? backtrace.split("\n") : backtrace
333
+ frames = lines.first(20).map do |line|
334
+ # Extract file path and method name, ignore line numbers
335
+ if line =~ %r{([^/]+\.rb):.*?in `(.+)'$}
336
+ "#{Regexp.last_match(1)}:#{Regexp.last_match(2)}"
337
+ elsif line =~ %r{([^/]+\.rb)}
338
+ Regexp.last_match(1)
339
+ end
340
+ end.compact.uniq
341
+
342
+ return nil if frames.empty?
343
+
344
+ # Create signature from sorted file paths (order-independent)
345
+ file_paths = frames.map { |frame| frame.split(":").first }.sort
346
+ Digest::SHA256.hexdigest(file_paths.join("|"))[0..15]
347
+ end
348
+
349
+ # Check if error exceeds baseline and send alert if needed
350
+ def check_baseline_anomaly(error_log)
351
+ config = RailsErrorDashboard.configuration
352
+
353
+ # Return early if baseline alerts are disabled
354
+ return unless config.enable_baseline_alerts
355
+ return unless defined?(Queries::BaselineStats)
356
+ return unless defined?(BaselineAlertJob)
357
+
358
+ # Get baseline anomaly info
359
+ anomaly = error_log.baseline_anomaly(sensitivity: config.baseline_alert_threshold_std_devs)
360
+
361
+ # Return if no anomaly detected
362
+ return unless anomaly[:anomaly]
363
+
364
+ # Check if severity level should trigger alert
365
+ return unless config.baseline_alert_severities.include?(anomaly[:level])
366
+
367
+ # Enqueue alert job (which will handle throttling)
368
+ BaselineAlertJob.perform_later(error_log.id, anomaly)
369
+
370
+ RailsErrorDashboard::Logger.info(
371
+ "Baseline alert queued for #{error_log.error_type} on #{error_log.platform}: " \
372
+ "#{anomaly[:level]} (#{anomaly[:std_devs_above]&.round(1)}σ above baseline)"
373
+ )
374
+ rescue => e
375
+ # Don't let baseline alerting cause errors
376
+ RailsErrorDashboard::Logger.error("Failed to check baseline anomaly: #{e.message}")
377
+ end
378
+
379
+ # Detect git SHA from git command (fallback)
380
+ def detect_git_sha_from_command
381
+ return nil unless File.exist?(Rails.root.join(".git"))
382
+ `git rev-parse --short HEAD 2>/dev/null`.strip.presence
383
+ rescue => e
384
+ RailsErrorDashboard::Logger.debug("Could not detect git SHA: #{e.message}")
385
+ nil
386
+ end
387
+
388
+ # Detect app version from VERSION file (fallback)
389
+ def detect_version_from_file
390
+ version_file = Rails.root.join("VERSION")
391
+ return File.read(version_file).strip if File.exist?(version_file)
392
+ nil
393
+ rescue => e
394
+ RailsErrorDashboard::Logger.debug("Could not detect version: #{e.message}")
395
+ nil
396
+ end
132
397
  end
133
398
  end
134
399
  end
@@ -28,6 +28,22 @@ module RailsErrorDashboard
28
28
  # Dispatch plugin event for resolved error
29
29
  PluginRegistry.dispatch(:on_error_resolved, error)
30
30
 
31
+ # Trigger notification callbacks
32
+ RailsErrorDashboard.configuration.notification_callbacks[:error_resolved].each do |callback|
33
+ callback.call(error)
34
+ rescue => e
35
+ RailsErrorDashboard::Logger.error("Error in error_resolved callback: #{e.message}")
36
+ end
37
+
38
+ # Emit ActiveSupport::Notifications instrumentation event
39
+ ActiveSupport::Notifications.instrument("error_resolved.rails_error_dashboard", {
40
+ error_log: error,
41
+ error_id: error.id,
42
+ error_type: error.error_type,
43
+ resolved_by: @resolution_data[:resolved_by_name],
44
+ resolved_at: error.resolved_at
45
+ })
46
+
31
47
  error
32
48
  end
33
49
  end
@@ -43,22 +43,65 @@ module RailsErrorDashboard
43
43
  # Enable/disable Rails.error subscriber
44
44
  attr_accessor :enable_error_subscriber
45
45
 
46
+ # Advanced configuration options
47
+ # Custom severity classification rules (hash of error_type => severity)
48
+ attr_accessor :custom_severity_rules
49
+
50
+ # Exceptions to ignore (array of strings, regexes, or classes)
51
+ attr_accessor :ignored_exceptions
52
+
53
+ # Sampling rate for non-critical errors (0.0 to 1.0, default 1.0 = 100%)
54
+ attr_accessor :sampling_rate
55
+
56
+ # Async logging configuration
57
+ attr_accessor :async_logging
58
+ attr_accessor :async_adapter # :sidekiq, :solid_queue, or :async
59
+
60
+ # Backtrace configuration
61
+ attr_accessor :max_backtrace_lines
62
+
63
+ # Enhanced metrics
64
+ attr_accessor :app_version
65
+ attr_accessor :git_sha
66
+ attr_accessor :total_users_for_impact # For user impact % calculation
67
+
68
+ # Advanced error analysis features
69
+ attr_accessor :enable_similar_errors # Fuzzy error matching
70
+ attr_accessor :enable_co_occurring_errors # Detect errors happening together
71
+ attr_accessor :enable_error_cascades # Parent→child error relationships
72
+ attr_accessor :enable_error_correlation # Version/user/time correlation
73
+ attr_accessor :enable_platform_comparison # iOS vs Android analytics
74
+ attr_accessor :enable_occurrence_patterns # Cyclical/burst pattern detection
75
+
76
+ # Baseline alert configuration
77
+ attr_accessor :enable_baseline_alerts
78
+ attr_accessor :baseline_alert_threshold_std_devs # Number of std devs to trigger alert (default: 2.0)
79
+ attr_accessor :baseline_alert_severities # Array of severities to alert on (default: [:critical, :high])
80
+ attr_accessor :baseline_alert_cooldown_minutes # Minutes between alerts for same error type (default: 120)
81
+
82
+ # Notification callbacks (managed via helper methods, not set directly)
83
+ attr_reader :notification_callbacks
84
+
85
+ # Internal logging configuration
86
+ attr_accessor :enable_internal_logging
87
+ attr_accessor :log_level
88
+
46
89
  def initialize
47
90
  # Default values
48
- @dashboard_username = ENV.fetch("ERROR_DASHBOARD_USER", "admin")
49
- @dashboard_password = ENV.fetch("ERROR_DASHBOARD_PASSWORD", "password")
91
+ @dashboard_username = ENV.fetch("ERROR_DASHBOARD_USER", "gandalf")
92
+ @dashboard_password = ENV.fetch("ERROR_DASHBOARD_PASSWORD", "youshallnotpass")
50
93
  @require_authentication = true
51
94
  @require_authentication_in_development = false
52
95
 
53
96
  @user_model = "User"
54
97
 
55
- # Notification settings
98
+ # Notification settings (disabled by default - enable during installation or in initializer)
56
99
  @slack_webhook_url = ENV["SLACK_WEBHOOK_URL"]
57
100
  @notification_email_recipients = ENV.fetch("ERROR_NOTIFICATION_EMAILS", "").split(",").map(&:strip)
58
101
  @notification_email_from = ENV.fetch("ERROR_NOTIFICATION_FROM", "errors@example.com")
59
102
  @dashboard_base_url = ENV["DASHBOARD_BASE_URL"]
60
- @enable_slack_notifications = true
61
- @enable_email_notifications = true
103
+ @enable_slack_notifications = false
104
+ @enable_email_notifications = false
62
105
 
63
106
  # Discord notification settings
64
107
  @discord_webhook_url = ENV["DISCORD_WEBHOOK_URL"]
@@ -78,6 +121,48 @@ module RailsErrorDashboard
78
121
 
79
122
  @enable_middleware = true
80
123
  @enable_error_subscriber = true
124
+
125
+ # Advanced configuration defaults
126
+ @custom_severity_rules = {}
127
+ @ignored_exceptions = []
128
+ @sampling_rate = 1.0 # 100% by default
129
+ @async_logging = false
130
+ @async_adapter = :sidekiq # Battle-tested default
131
+ @max_backtrace_lines = 50
132
+
133
+ # Enhanced metrics defaults
134
+ @app_version = ENV["APP_VERSION"]
135
+ @git_sha = ENV["GIT_SHA"]
136
+ @total_users_for_impact = nil # Auto-detect if not set
137
+
138
+ # Advanced error analysis features (all OFF by default - opt-in)
139
+ @enable_similar_errors = false # Fuzzy error matching
140
+ @enable_co_occurring_errors = false # Co-occurring error detection
141
+ @enable_error_cascades = false # Error cascade detection
142
+ @enable_error_correlation = false # Version/user/time correlation
143
+ @enable_platform_comparison = false # Platform health comparison
144
+ @enable_occurrence_patterns = false # Pattern detection
145
+
146
+ # Baseline alert defaults
147
+ @enable_baseline_alerts = false # OFF by default (opt-in)
148
+ @baseline_alert_threshold_std_devs = ENV.fetch("BASELINE_ALERT_THRESHOLD", "2.0").to_f
149
+ @baseline_alert_severities = [ :critical, :high ] # Alert on critical and high severity anomalies
150
+ @baseline_alert_cooldown_minutes = ENV.fetch("BASELINE_ALERT_COOLDOWN", "120").to_i
151
+
152
+ # Internal logging defaults - SILENT by default
153
+ @enable_internal_logging = false # Opt-in for debugging
154
+ @log_level = :silent # Silent by default, use :debug, :info, :warn, :error, or :silent
155
+
156
+ @notification_callbacks = {
157
+ error_logged: [],
158
+ critical_error: [],
159
+ error_resolved: []
160
+ }
161
+ end
162
+
163
+ # Reset configuration to defaults
164
+ def reset!
165
+ initialize
81
166
  end
82
167
  end
83
168
  end
@@ -22,14 +22,22 @@ module RailsErrorDashboard
22
22
  # Skip low-severity warnings
23
23
  return if handled && severity == :warning
24
24
 
25
- # Extract context information
26
- error_context = ValueObjects::ErrorContext.new(context, source)
25
+ # CRITICAL: Wrap entire process in rescue to ensure failures don't break the app
26
+ begin
27
+ # Extract context information
28
+ error_context = ValueObjects::ErrorContext.new(context, source)
27
29
 
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}")
30
+ # Log to our error dashboard using Command
31
+ Commands::LogError.call(error, error_context.to_h.merge(source: source))
32
+ rescue => e
33
+ # Don't let error logging cause more errors - fail silently
34
+ # Log failure for debugging but NEVER propagate exception
35
+ RailsErrorDashboard::Logger.error("[RailsErrorDashboard] ErrorReporter failed: #{e.class} - #{e.message}")
36
+ RailsErrorDashboard::Logger.error("Original error: #{error.class} - #{error.message}") if error
37
+ RailsErrorDashboard::Logger.error("Context: #{context.inspect}") if context
38
+ RailsErrorDashboard::Logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
39
+ nil # Explicitly return nil, never raise
40
+ end
33
41
  end
34
42
  end
35
43
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ # Internal logger wrapper for Rails Error Dashboard
5
+ #
6
+ # By default, all logging is SILENT to keep production logs clean.
7
+ # Users can opt-in to verbose logging for debugging.
8
+ #
9
+ # @example Enable logging for debugging
10
+ # RailsErrorDashboard.configure do |config|
11
+ # config.enable_internal_logging = true
12
+ # config.log_level = :debug
13
+ # end
14
+ #
15
+ # @example Production troubleshooting (errors only)
16
+ # RailsErrorDashboard.configure do |config|
17
+ # config.enable_internal_logging = true
18
+ # config.log_level = :error
19
+ # end
20
+ module Logger
21
+ LOG_LEVELS = {
22
+ debug: 0,
23
+ info: 1,
24
+ warn: 2,
25
+ error: 3,
26
+ silent: 4
27
+ }.freeze
28
+
29
+ class << self
30
+ # Log debug message (only if internal logging enabled)
31
+ #
32
+ # @param message [String] The message to log
33
+ # @example
34
+ # RailsErrorDashboard::Logger.debug("Processing error #123")
35
+ def debug(message)
36
+ return unless logging_enabled?
37
+ return unless log_level_enabled?(:debug)
38
+
39
+ Rails.logger.debug(formatted_message(message))
40
+ end
41
+
42
+ # Log info message (only if internal logging enabled)
43
+ #
44
+ # @param message [String] The message to log
45
+ # @example
46
+ # RailsErrorDashboard::Logger.info("Registered plugin: MyPlugin")
47
+ def info(message)
48
+ return unless logging_enabled?
49
+ return unless log_level_enabled?(:info)
50
+
51
+ Rails.logger.info(formatted_message(message))
52
+ end
53
+
54
+ # Log warning message (only if internal logging enabled)
55
+ #
56
+ # @param message [String] The message to log
57
+ # @example
58
+ # RailsErrorDashboard::Logger.warn("Plugin already registered")
59
+ def warn(message)
60
+ return unless logging_enabled?
61
+ return unless log_level_enabled?(:warn)
62
+
63
+ Rails.logger.warn(formatted_message(message))
64
+ end
65
+
66
+ # Log error message
67
+ # Errors are logged by default unless log_level is :silent
68
+ #
69
+ # @param message [String] The message to log
70
+ # @example
71
+ # RailsErrorDashboard::Logger.error("Failed to save error log")
72
+ def error(message)
73
+ return unless log_level_enabled?(:error)
74
+
75
+ Rails.logger.error(formatted_message(message))
76
+ end
77
+
78
+ private
79
+
80
+ # Check if internal logging is enabled
81
+ #
82
+ # @return [Boolean]
83
+ def logging_enabled?
84
+ RailsErrorDashboard.configuration.enable_internal_logging
85
+ end
86
+
87
+ # Check if the given log level is enabled
88
+ #
89
+ # @param level [Symbol] The log level to check (:debug, :info, :warn, :error)
90
+ # @return [Boolean]
91
+ def log_level_enabled?(level)
92
+ config_level = RailsErrorDashboard.configuration.log_level || :silent
93
+ LOG_LEVELS[level] >= LOG_LEVELS[config_level]
94
+ end
95
+
96
+ # Format message with gem prefix
97
+ #
98
+ # @param message [String] The message to format
99
+ # @return [String] Formatted message with prefix
100
+ def formatted_message(message)
101
+ "[RailsErrorDashboard] #{message}"
102
+ end
103
+ end
104
+ end
105
+ end
@@ -23,17 +23,24 @@ module RailsErrorDashboard
23
23
  @app.call(env)
24
24
  rescue => exception
25
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
- )
26
+ # CRITICAL: Wrap in rescue to ensure gem failures don't break the app
27
+ begin
28
+ Rails.error.report(exception,
29
+ handled: false,
30
+ severity: :error,
31
+ context: {
32
+ request: ActionDispatch::Request.new(env),
33
+ middleware: true
34
+ },
35
+ source: "rack.middleware"
36
+ )
37
+ rescue => e
38
+ # If error reporting fails, log it but DON'T break the app
39
+ RailsErrorDashboard::Logger.error("[RailsErrorDashboard] Middleware error reporting failed: #{e.class} - #{e.message}")
40
+ RailsErrorDashboard::Logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
41
+ end
35
42
 
36
- # Re-raise to let Rails handle the response
43
+ # Re-raise original exception to let Rails handle the response
37
44
  raise exception
38
45
  end
39
46
  end
@@ -85,14 +85,17 @@ module RailsErrorDashboard
85
85
  end
86
86
 
87
87
  # Helper method to safely execute plugin hooks
88
- # Prevents plugin errors from breaking the main application
88
+ # CRITICAL: Prevents plugin errors from breaking the main application
89
89
  def safe_execute(method_name, *args)
90
90
  return unless enabled?
91
91
 
92
92
  send(method_name, *args)
93
93
  rescue => e
94
- Rails.logger.error("Plugin '#{name}' failed in #{method_name}: #{e.message}")
95
- Rails.logger.error(e.backtrace.first(5).join("\n"))
94
+ # Log plugin failures but never propagate - plugins must not break the app
95
+ RailsErrorDashboard::Logger.error("[RailsErrorDashboard] Plugin '#{name}' failed in #{method_name}: #{e.class} - #{e.message}")
96
+ RailsErrorDashboard::Logger.error("Plugin version: #{version}")
97
+ RailsErrorDashboard::Logger.error(e.backtrace&.first(10)&.join("\n")) if e.backtrace
98
+ nil # Explicitly return nil, never raise
96
99
  end
97
100
  end
98
101
  end