rails_error_dashboard 0.1.0 → 0.1.1

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +257 -700
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +18 -0
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +47 -4
  5. data/app/helpers/rails_error_dashboard/application_helper.rb +17 -0
  6. data/app/jobs/rails_error_dashboard/application_job.rb +19 -0
  7. data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +48 -0
  8. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +263 -0
  9. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -8
  10. data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +2 -1
  11. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +5 -5
  12. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +10 -6
  13. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +5 -6
  14. data/app/mailers/rails_error_dashboard/application_mailer.rb +1 -1
  15. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +1 -1
  16. data/app/models/rails_error_dashboard/cascade_pattern.rb +74 -0
  17. data/app/models/rails_error_dashboard/error_baseline.rb +100 -0
  18. data/app/models/rails_error_dashboard/error_log.rb +326 -3
  19. data/app/models/rails_error_dashboard/error_occurrence.rb +49 -0
  20. data/app/views/layouts/rails_error_dashboard.html.erb +150 -9
  21. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +3 -10
  22. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +1 -2
  23. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +76 -0
  24. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +209 -0
  25. data/app/views/rails_error_dashboard/errors/_stats.html.erb +34 -0
  26. data/app/views/rails_error_dashboard/errors/analytics.html.erb +19 -39
  27. data/app/views/rails_error_dashboard/errors/correlation.html.erb +373 -0
  28. data/app/views/rails_error_dashboard/errors/index.html.erb +215 -138
  29. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +388 -0
  30. data/app/views/rails_error_dashboard/errors/show.html.erb +428 -11
  31. data/config/routes.rb +2 -0
  32. data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +66 -0
  33. data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +26 -0
  34. data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +12 -0
  35. data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +9 -0
  36. data/db/migrate/20251225100236_create_error_occurrences.rb +31 -0
  37. data/db/migrate/20251225101920_create_cascade_patterns.rb +33 -0
  38. data/db/migrate/20251225102500_create_error_baselines.rb +38 -0
  39. data/lib/generators/rails_error_dashboard/install/install_generator.rb +270 -1
  40. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +251 -37
  41. data/lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb +36 -0
  42. data/lib/generators/rails_error_dashboard/solid_queue/templates/queue.yml +55 -0
  43. data/lib/rails_error_dashboard/commands/log_error.rb +234 -7
  44. data/lib/rails_error_dashboard/commands/resolve_error.rb +16 -0
  45. data/lib/rails_error_dashboard/configuration.rb +82 -5
  46. data/lib/rails_error_dashboard/error_reporter.rb +15 -7
  47. data/lib/rails_error_dashboard/middleware/error_catcher.rb +17 -10
  48. data/lib/rails_error_dashboard/plugin.rb +6 -3
  49. data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +0 -1
  50. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +2 -3
  51. data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +0 -2
  52. data/lib/rails_error_dashboard/queries/analytics_stats.rb +44 -6
  53. data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
  54. data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +86 -0
  55. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +134 -2
  56. data/lib/rails_error_dashboard/queries/error_cascades.rb +74 -0
  57. data/lib/rails_error_dashboard/queries/error_correlation.rb +375 -0
  58. data/lib/rails_error_dashboard/queries/errors_list.rb +52 -11
  59. data/lib/rails_error_dashboard/queries/filter_options.rb +0 -1
  60. data/lib/rails_error_dashboard/queries/platform_comparison.rb +254 -0
  61. data/lib/rails_error_dashboard/queries/similar_errors.rb +93 -0
  62. data/lib/rails_error_dashboard/services/baseline_alert_throttler.rb +88 -0
  63. data/lib/rails_error_dashboard/services/baseline_calculator.rb +269 -0
  64. data/lib/rails_error_dashboard/services/cascade_detector.rb +95 -0
  65. data/lib/rails_error_dashboard/services/pattern_detector.rb +268 -0
  66. data/lib/rails_error_dashboard/services/similarity_calculator.rb +144 -0
  67. data/lib/rails_error_dashboard/value_objects/error_context.rb +27 -1
  68. data/lib/rails_error_dashboard/version.rb +1 -1
  69. data/lib/rails_error_dashboard.rb +55 -7
  70. metadata +52 -9
  71. data/app/models/rails_error_dashboard/application_record.rb +0 -5
  72. data/lib/rails_error_dashboard/queries/developer_insights.rb +0 -277
  73. data/lib/rails_error_dashboard/queries/errors_list_v2.rb +0 -149
@@ -8,5 +8,23 @@ module RailsErrorDashboard
8
8
 
9
9
  # Make Pagy helpers available in views
10
10
  helper Pagy::Frontend
11
+
12
+ # CRITICAL: Ensure dashboard errors never break the app
13
+ # Catch all exceptions and render user-friendly error page
14
+ rescue_from StandardError do |exception|
15
+ # Log the error for debugging
16
+ Rails.logger.error("[RailsErrorDashboard] Dashboard controller error: #{exception.class} - #{exception.message}")
17
+ Rails.logger.error("Request: #{request.path} (#{request.method})")
18
+ Rails.logger.error("Params: #{params.inspect}")
19
+ Rails.logger.error(exception.backtrace&.first(10)&.join("\n")) if exception.backtrace
20
+
21
+ # Render user-friendly error page
22
+ render plain: "The Error Dashboard encountered an issue displaying this page.\n\n" \
23
+ "Your application is unaffected - this is only a dashboard display error.\n\n" \
24
+ "Error: #{exception.message}\n\n" \
25
+ "Check Rails logs for details: [RailsErrorDashboard]",
26
+ status: :internal_server_error,
27
+ layout: false
28
+ end
11
29
  end
12
30
  end
@@ -18,7 +18,6 @@ module RailsErrorDashboard
18
18
 
19
19
  # Get filter options using Query
20
20
  filter_options = Queries::FilterOptions.call
21
- @environments = filter_options[:environments]
22
21
  @error_types = filter_options[:error_types]
23
22
  @platforms = filter_options[:platforms]
24
23
  end
@@ -54,7 +53,6 @@ module RailsErrorDashboard
54
53
  @errors_over_time = analytics[:errors_over_time]
55
54
  @errors_by_type = analytics[:errors_by_type]
56
55
  @errors_by_platform = analytics[:errors_by_platform]
57
- @errors_by_environment = analytics[:errors_by_environment]
58
56
  @errors_by_hour = analytics[:errors_by_hour]
59
57
  @top_users = analytics[:top_users]
60
58
  @resolution_rate = analytics[:resolution_rate]
@@ -62,6 +60,30 @@ module RailsErrorDashboard
62
60
  @api_errors = analytics[:api_errors]
63
61
  end
64
62
 
63
+ def platform_comparison
64
+ # Check if feature is enabled
65
+ unless RailsErrorDashboard.configuration.enable_platform_comparison
66
+ flash[:alert] = "Platform Comparison is not enabled. Enable it in config/initializers/rails_error_dashboard.rb"
67
+ redirect_to errors_path
68
+ return
69
+ end
70
+
71
+ days = (params[:days] || 7).to_i
72
+ @days = days
73
+
74
+ # Use Query to get platform comparison data
75
+ comparison = Queries::PlatformComparison.new(days: days)
76
+
77
+ @error_rate_by_platform = comparison.error_rate_by_platform
78
+ @severity_distribution = comparison.severity_distribution_by_platform
79
+ @resolution_times = comparison.resolution_time_by_platform
80
+ @top_errors_by_platform = comparison.top_errors_by_platform
81
+ @stability_scores = comparison.platform_stability_scores
82
+ @cross_platform_errors = comparison.cross_platform_errors
83
+ @daily_trends = comparison.daily_trend_by_platform
84
+ @platform_health = comparison.platform_health_summary
85
+ end
86
+
65
87
  def batch_action
66
88
  error_ids = params[:error_ids] || []
67
89
  action_type = params[:action_type]
@@ -88,15 +110,36 @@ module RailsErrorDashboard
88
110
  redirect_to errors_path
89
111
  end
90
112
 
113
+ def correlation
114
+ # Check if feature is enabled
115
+ unless RailsErrorDashboard.configuration.enable_error_correlation
116
+ flash[:alert] = "Error Correlation is not enabled. Enable it in config/initializers/rails_error_dashboard.rb"
117
+ redirect_to errors_path
118
+ return
119
+ end
120
+
121
+ days = (params[:days] || 30).to_i
122
+ @days = days
123
+ correlation = Queries::ErrorCorrelation.new(days: days)
124
+
125
+ @errors_by_version = correlation.errors_by_version
126
+ @errors_by_git_sha = correlation.errors_by_git_sha
127
+ @problematic_releases = correlation.problematic_releases
128
+ @multi_error_users = correlation.multi_error_users(min_error_types: 2)
129
+ @time_correlated_errors = correlation.time_correlated_errors
130
+ @period_comparison = correlation.period_comparison
131
+ @platform_specific_errors = correlation.platform_specific_errors
132
+ end
133
+
91
134
  private
92
135
 
93
136
  def filter_params
94
137
  {
95
- environment: params[:environment],
96
138
  error_type: params[:error_type],
97
139
  unresolved: params[:unresolved],
98
140
  platform: params[:platform],
99
- search: params[:search]
141
+ search: params[:search],
142
+ severity: params[:severity]
100
143
  }
101
144
  end
102
145
 
@@ -1,4 +1,21 @@
1
1
  module RailsErrorDashboard
2
2
  module ApplicationHelper
3
+ # Returns Bootstrap color class for error severity
4
+ # @param severity [Symbol] The severity level (:critical, :high, :medium, :low, :info)
5
+ # @return [String] Bootstrap color class (danger, warning, info, secondary)
6
+ def severity_color(severity)
7
+ case severity&.to_sym
8
+ when :critical
9
+ "danger"
10
+ when :high
11
+ "warning"
12
+ when :medium
13
+ "info"
14
+ when :low
15
+ "secondary"
16
+ else
17
+ "secondary"
18
+ end
19
+ end
3
20
  end
4
21
  end
@@ -1,4 +1,23 @@
1
1
  module RailsErrorDashboard
2
2
  class ApplicationJob < ActiveJob::Base
3
+ # CRITICAL: Ensure job failures don't break the app or spam error logs
4
+ # Retry failed jobs with exponential backoff, but limit attempts
5
+ retry_on StandardError, wait: :exponentially_longer, attempts: 3
6
+
7
+ # Global exception handling for all dashboard jobs
8
+ rescue_from StandardError do |exception|
9
+ # Log the error for debugging but don't propagate
10
+ Rails.logger.error("[RailsErrorDashboard] Job #{self.class.name} failed: #{exception.class} - #{exception.message}")
11
+ Rails.logger.error("Job arguments: #{arguments.inspect}")
12
+ Rails.logger.error("Attempt: #{executions}/3") if respond_to?(:executions)
13
+ Rails.logger.error(exception.backtrace&.first(10)&.join("\n")) if exception.backtrace
14
+
15
+ # Re-raise to trigger retry mechanism (up to 3 attempts)
16
+ # After 3 attempts, ActiveJob will discard the job and log it
17
+ raise exception if executions < 3
18
+
19
+ # If we've exhausted retries, log and give up gracefully
20
+ Rails.logger.error("[RailsErrorDashboard] Job #{self.class.name} discarded after #{executions} attempts")
21
+ end
3
22
  end
4
23
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ # Background job for asynchronous error logging
5
+ # This prevents error logging from blocking the main request/response cycle
6
+ class AsyncErrorLoggingJob < ApplicationJob
7
+ queue_as :default
8
+
9
+ # Performs async error logging
10
+ # @param exception_data [Hash] Serialized exception data
11
+ # @param context [Hash] Error context (request, user, etc.)
12
+ def perform(exception_data, context)
13
+ # Reconstruct the exception from serialized data
14
+ exception = reconstruct_exception(exception_data)
15
+
16
+ # Log the error synchronously in the background job
17
+ # Call .new().call to bypass async check (we're already async)
18
+ Commands::LogError.new(exception, context).call
19
+ rescue => e
20
+ # Don't let async job errors break the job queue
21
+ Rails.logger.error("AsyncErrorLoggingJob failed: #{e.message}")
22
+ Rails.logger.error("Backtrace: #{e.backtrace&.first(5)&.join("\n")}")
23
+ end
24
+
25
+ private
26
+
27
+ # Reconstruct exception from serialized data
28
+ # @param data [Hash] Serialized exception data
29
+ # @return [Exception] Reconstructed exception object
30
+ def reconstruct_exception(data)
31
+ # Get or create the exception class
32
+ exception_class = begin
33
+ data[:class_name].constantize
34
+ rescue NameError
35
+ # If class doesn't exist, use StandardError
36
+ StandardError
37
+ end
38
+
39
+ # Create new exception with the original message
40
+ exception = exception_class.new(data[:message])
41
+
42
+ # Restore the backtrace
43
+ exception.set_backtrace(data[:backtrace]) if data[:backtrace]
44
+
45
+ exception
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ # Sends baseline anomaly alerts through configured notification channels
5
+ #
6
+ # This job is triggered when an error exceeds baseline thresholds.
7
+ # It respects cooldown periods to prevent alert fatigue and sends
8
+ # notifications through all enabled channels (Slack, Email, Discord, etc.)
9
+ class BaselineAlertJob < ApplicationJob
10
+ queue_as :default
11
+
12
+ # @param error_log_id [Integer] The error log that triggered the alert
13
+ # @param anomaly_data [Hash] Anomaly information from baseline check
14
+ def perform(error_log_id, anomaly_data)
15
+ error_log = ErrorLog.find_by(id: error_log_id)
16
+ return unless error_log
17
+
18
+ config = RailsErrorDashboard.configuration
19
+
20
+ # Check if we should send alert (cooldown check)
21
+ unless Services::BaselineAlertThrottler.should_alert?(
22
+ error_log.error_type,
23
+ error_log.platform,
24
+ cooldown_minutes: config.baseline_alert_cooldown_minutes
25
+ )
26
+ Rails.logger.info(
27
+ "Baseline alert throttled for #{error_log.error_type} on #{error_log.platform}"
28
+ )
29
+ return
30
+ end
31
+
32
+ # Record that we're sending an alert
33
+ Services::BaselineAlertThrottler.record_alert(
34
+ error_log.error_type,
35
+ error_log.platform
36
+ )
37
+
38
+ # Send notifications through all enabled channels
39
+ send_notifications(error_log, anomaly_data, config)
40
+ end
41
+
42
+ private
43
+
44
+ def send_notifications(error_log, anomaly_data, config)
45
+ # Slack notification
46
+ if config.enable_slack_notifications && config.slack_webhook_url.present?
47
+ send_slack_notification(error_log, anomaly_data, config)
48
+ end
49
+
50
+ # Email notification
51
+ if config.enable_email_notifications && config.notification_email_recipients.any?
52
+ send_email_notification(error_log, anomaly_data, config)
53
+ end
54
+
55
+ # Discord notification
56
+ if config.enable_discord_notifications && config.discord_webhook_url.present?
57
+ send_discord_notification(error_log, anomaly_data, config)
58
+ end
59
+
60
+ # Webhook notification
61
+ if config.enable_webhook_notifications && config.webhook_urls.any?
62
+ send_webhook_notification(error_log, anomaly_data, config)
63
+ end
64
+
65
+ # PagerDuty for critical anomalies
66
+ if config.enable_pagerduty_notifications &&
67
+ config.pagerduty_integration_key.present? &&
68
+ anomaly_data[:level] == :critical
69
+ send_pagerduty_notification(error_log, anomaly_data, config)
70
+ end
71
+ end
72
+
73
+ def send_slack_notification(error_log, anomaly_data, config)
74
+ payload = build_slack_payload(error_log, anomaly_data, config)
75
+
76
+ HTTParty.post(
77
+ config.slack_webhook_url,
78
+ body: payload.to_json,
79
+ headers: { "Content-Type" => "application/json" }
80
+ )
81
+ rescue => e
82
+ Rails.logger.error("Failed to send baseline alert to Slack: #{e.message}")
83
+ end
84
+
85
+ def send_email_notification(error_log, _anomaly_data, _config)
86
+ # Use existing email notification infrastructure if available
87
+ # For now, log that email would be sent
88
+ Rails.logger.info(
89
+ "Baseline alert email would be sent for #{error_log.error_type}"
90
+ )
91
+ rescue => e
92
+ Rails.logger.error("Failed to send baseline alert email: #{e.message}")
93
+ end
94
+
95
+ def send_discord_notification(error_log, anomaly_data, config)
96
+ payload = build_discord_payload(error_log, anomaly_data, config)
97
+
98
+ HTTParty.post(
99
+ config.discord_webhook_url,
100
+ body: payload.to_json,
101
+ headers: { "Content-Type" => "application/json" }
102
+ )
103
+ rescue => e
104
+ Rails.logger.error("Failed to send baseline alert to Discord: #{e.message}")
105
+ end
106
+
107
+ def send_webhook_notification(error_log, anomaly_data, config)
108
+ payload = build_webhook_payload(error_log, anomaly_data)
109
+
110
+ config.webhook_urls.each do |url|
111
+ HTTParty.post(
112
+ url,
113
+ body: payload.to_json,
114
+ headers: { "Content-Type" => "application/json" }
115
+ )
116
+ end
117
+ rescue => e
118
+ Rails.logger.error("Failed to send baseline alert to webhook: #{e.message}")
119
+ end
120
+
121
+ def send_pagerduty_notification(error_log, _anomaly_data, _config)
122
+ # Use existing PagerDuty notification infrastructure if available
123
+ Rails.logger.info(
124
+ "Baseline alert PagerDuty notification for #{error_log.error_type}"
125
+ )
126
+ rescue => e
127
+ Rails.logger.error("Failed to send baseline alert to PagerDuty: #{e.message}")
128
+ end
129
+
130
+ # Build Slack message payload
131
+ def build_slack_payload(error_log, anomaly_data, config)
132
+ {
133
+ text: "🚨 Baseline Anomaly Alert",
134
+ blocks: [
135
+ {
136
+ type: "header",
137
+ text: {
138
+ type: "plain_text",
139
+ text: "🚨 Baseline Anomaly Detected"
140
+ }
141
+ },
142
+ {
143
+ type: "section",
144
+ fields: [
145
+ {
146
+ type: "mrkdwn",
147
+ text: "*Error Type:*\n#{error_log.error_type}"
148
+ },
149
+ {
150
+ type: "mrkdwn",
151
+ text: "*Platform:*\n#{error_log.platform}"
152
+ },
153
+ {
154
+ type: "mrkdwn",
155
+ text: "*Severity:*\n#{anomaly_level_emoji(anomaly_data[:level])} #{anomaly_data[:level].to_s.upcase}"
156
+ },
157
+ {
158
+ type: "mrkdwn",
159
+ text: "*Standard Deviations:*\n#{anomaly_data[:std_devs_above]&.round(1)}σ above baseline"
160
+ }
161
+ ]
162
+ },
163
+ {
164
+ type: "section",
165
+ text: {
166
+ type: "mrkdwn",
167
+ text: "*Message:*\n```#{error_log.message.truncate(200)}```"
168
+ }
169
+ },
170
+ {
171
+ type: "section",
172
+ text: {
173
+ type: "mrkdwn",
174
+ text: "*Baseline Info:*\nThreshold: #{anomaly_data[:threshold]&.round(1)} errors\nBaseline Type: #{anomaly_data[:baseline_type]}"
175
+ }
176
+ },
177
+ {
178
+ type: "actions",
179
+ elements: [
180
+ {
181
+ type: "button",
182
+ text: {
183
+ type: "plain_text",
184
+ text: "View in Dashboard"
185
+ },
186
+ url: dashboard_url(error_log, config)
187
+ }
188
+ ]
189
+ }
190
+ ]
191
+ }
192
+ end
193
+
194
+ # Build Discord embed payload
195
+ def build_discord_payload(error_log, anomaly_data, config)
196
+ {
197
+ embeds: [
198
+ {
199
+ title: "🚨 Baseline Anomaly Detected",
200
+ color: anomaly_color(anomaly_data[:level]),
201
+ fields: [
202
+ { name: "Error Type", value: error_log.error_type, inline: true },
203
+ { name: "Platform", value: error_log.platform, inline: true },
204
+ { name: "Severity", value: anomaly_data[:level].to_s.upcase, inline: true },
205
+ { name: "Standard Deviations", value: "#{anomaly_data[:std_devs_above]&.round(1)}σ above baseline", inline: true },
206
+ { name: "Threshold", value: "#{anomaly_data[:threshold]&.round(1)} errors", inline: true },
207
+ { name: "Baseline Type", value: anomaly_data[:baseline_type] || "N/A", inline: true },
208
+ { name: "Message", value: "```#{error_log.message.truncate(200)}```", inline: false }
209
+ ],
210
+ url: dashboard_url(error_log, config),
211
+ timestamp: Time.current.iso8601
212
+ }
213
+ ]
214
+ }
215
+ end
216
+
217
+ # Build generic webhook payload
218
+ def build_webhook_payload(error_log, anomaly_data)
219
+ {
220
+ event: "baseline_anomaly",
221
+ timestamp: Time.current.iso8601,
222
+ error: {
223
+ id: error_log.id,
224
+ type: error_log.error_type,
225
+ message: error_log.message,
226
+ platform: error_log.platform,
227
+ severity: error_log.severity.to_s,
228
+ occurred_at: error_log.occurred_at.iso8601
229
+ },
230
+ anomaly: {
231
+ level: anomaly_data[:level].to_s,
232
+ std_devs_above: anomaly_data[:std_devs_above],
233
+ threshold: anomaly_data[:threshold],
234
+ baseline_type: anomaly_data[:baseline_type]
235
+ },
236
+ dashboard_url: dashboard_url(error_log, RailsErrorDashboard.configuration)
237
+ }
238
+ end
239
+
240
+ def anomaly_level_emoji(level)
241
+ case level
242
+ when :critical then "🔴"
243
+ when :high then "🟠"
244
+ when :elevated then "🟡"
245
+ else "⚪"
246
+ end
247
+ end
248
+
249
+ def anomaly_color(level)
250
+ case level
251
+ when :critical then 15158332 # Red
252
+ when :high then 16744192 # Orange
253
+ when :elevated then 16776960 # Yellow
254
+ else 9807270 # Gray
255
+ end
256
+ end
257
+
258
+ def dashboard_url(error_log, config)
259
+ base_url = config.dashboard_base_url || "http://localhost:3000"
260
+ "#{base_url}/error_dashboard/errors/#{error_log.id}"
261
+ end
262
+ end
263
+ end
@@ -18,11 +18,12 @@ module RailsErrorDashboard
18
18
  HTTParty.post(
19
19
  webhook_url,
20
20
  body: payload.to_json,
21
- headers: { "Content-Type" => "application/json" }
21
+ headers: { "Content-Type" => "application/json" },
22
+ timeout: 10 # CRITICAL: 10 second timeout to prevent hanging
22
23
  )
23
24
  rescue StandardError => e
24
- Rails.logger.error("Failed to send Discord notification: #{e.message}")
25
- Rails.logger.error(e.backtrace.join("\n"))
25
+ Rails.logger.error("[RailsErrorDashboard] Failed to send Discord notification: #{e.message}")
26
+ Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
26
27
  end
27
28
 
28
29
  private
@@ -39,11 +40,6 @@ module RailsErrorDashboard
39
40
  value: error_log.platform || "Unknown",
40
41
  inline: true
41
42
  },
42
- {
43
- name: "Environment",
44
- value: error_log.environment || "Unknown",
45
- inline: true
46
- },
47
43
  {
48
44
  name: "Occurrences",
49
45
  value: error_log.occurrence_count.to_s,
@@ -13,7 +13,8 @@ module RailsErrorDashboard
13
13
 
14
14
  ErrorNotificationMailer.error_alert(error_log, recipients).deliver_now
15
15
  rescue => e
16
- Rails.logger.error("Failed to send email notification: #{e.message}")
16
+ Rails.logger.error("[RailsErrorDashboard] Failed to send email notification: #{e.message}")
17
+ Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
17
18
  end
18
19
  end
19
20
  end
@@ -24,15 +24,16 @@ module RailsErrorDashboard
24
24
  response = HTTParty.post(
25
25
  PAGERDUTY_EVENTS_API,
26
26
  body: payload.to_json,
27
- headers: { "Content-Type" => "application/json" }
27
+ headers: { "Content-Type" => "application/json" },
28
+ timeout: 10 # CRITICAL: 10 second timeout to prevent hanging
28
29
  )
29
30
 
30
31
  unless response.success?
31
- Rails.logger.error("PagerDuty API error: #{response.code} - #{response.body}")
32
+ Rails.logger.error("[RailsErrorDashboard] PagerDuty API error: #{response.code} - #{response.body}")
32
33
  end
33
34
  rescue StandardError => e
34
- Rails.logger.error("Failed to send PagerDuty notification: #{e.message}")
35
- Rails.logger.error(e.backtrace.join("\n"))
35
+ Rails.logger.error("[RailsErrorDashboard] Failed to send PagerDuty notification: #{e.message}")
36
+ Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
36
37
  end
37
38
 
38
39
  private
@@ -53,7 +54,6 @@ module RailsErrorDashboard
53
54
  controller: error_log.controller_name,
54
55
  action: error_log.action_name,
55
56
  platform: error_log.platform,
56
- environment: error_log.environment,
57
57
  occurrences: error_log.occurrence_count,
58
58
  first_seen_at: error_log.first_seen_at&.iso8601,
59
59
  last_seen_at: error_log.last_seen_at&.iso8601,
@@ -26,19 +26,27 @@ module RailsErrorDashboard
26
26
  http = Net::HTTP.new(uri.host, uri.port)
27
27
  http.use_ssl = true
28
28
 
29
+ # CRITICAL: Add timeouts to prevent hanging the job queue
30
+ http.open_timeout = 5 # 5 seconds to establish connection
31
+ http.read_timeout = 10 # 10 seconds to read response
32
+
29
33
  request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" })
30
34
  request.body = slack_payload(error_log).to_json
31
35
 
32
36
  response = http.request(request)
33
37
 
34
38
  unless response.is_a?(Net::HTTPSuccess)
35
- Rails.logger.error("Slack notification failed: #{response.code} - #{response.body}")
39
+ Rails.logger.error("[RailsErrorDashboard] Slack notification failed: #{response.code} - #{response.body}")
36
40
  end
41
+ rescue Timeout::Error, Errno::ECONNREFUSED, SocketError, Net::OpenTimeout, Net::ReadTimeout => e
42
+ # Network errors - log and fail gracefully
43
+ Rails.logger.error("[RailsErrorDashboard] Slack HTTP request failed: #{e.class} - #{e.message}")
44
+ nil
37
45
  end
38
46
 
39
47
  def slack_payload(error_log)
40
48
  {
41
- text: "🚨 New Error in #{error_log.environment.titleize} Environment",
49
+ text: "🚨 New Error Alert",
42
50
  blocks: [
43
51
  {
44
52
  type: "header",
@@ -55,10 +63,6 @@ module RailsErrorDashboard
55
63
  type: "mrkdwn",
56
64
  text: "*Error Type:*\n`#{error_log.error_type}`"
57
65
  },
58
- {
59
- type: "mrkdwn",
60
- text: "*Environment:*\n#{error_log.environment.titleize}"
61
- },
62
66
  {
63
67
  type: "mrkdwn",
64
68
  text: "*Platform:*\n#{platform_emoji(error_log.platform)} #{error_log.platform || 'Unknown'}"
@@ -23,8 +23,8 @@ module RailsErrorDashboard
23
23
  send_webhook(url, payload, error_log)
24
24
  end
25
25
  rescue StandardError => e
26
- Rails.logger.error("Failed to send webhook notification: #{e.message}")
27
- Rails.logger.error(e.backtrace.join("\n"))
26
+ Rails.logger.error("[RailsErrorDashboard] Failed to send webhook notification: #{e.message}")
27
+ Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
28
28
  end
29
29
 
30
30
  private
@@ -39,14 +39,14 @@ module RailsErrorDashboard
39
39
  "X-Error-Dashboard-Event" => "error.created",
40
40
  "X-Error-Dashboard-ID" => error_log.id.to_s
41
41
  },
42
- timeout: 10 # 10 second timeout
42
+ timeout: 10 # CRITICAL: 10 second timeout to prevent hanging
43
43
  )
44
44
 
45
45
  unless response.success?
46
- Rails.logger.warn("Webhook failed for #{url}: #{response.code}")
46
+ Rails.logger.warn("[RailsErrorDashboard] Webhook failed for #{url}: #{response.code}")
47
47
  end
48
48
  rescue StandardError => e
49
- Rails.logger.error("Webhook error for #{url}: #{e.message}")
49
+ Rails.logger.error("[RailsErrorDashboard] Webhook error for #{url}: #{e.message}")
50
50
  end
51
51
 
52
52
  def build_webhook_payload(error_log)
@@ -59,7 +59,6 @@ module RailsErrorDashboard
59
59
  message: error_log.message,
60
60
  severity: error_log.severity.to_s,
61
61
  platform: error_log.platform,
62
- environment: error_log.environment,
63
62
  controller: error_log.controller_name,
64
63
  action: error_log.action_name,
65
64
  occurrence_count: error_log.occurrence_count,
@@ -3,6 +3,6 @@
3
3
  module RailsErrorDashboard
4
4
  class ApplicationMailer < ActionMailer::Base
5
5
  default from: -> { RailsErrorDashboard.configuration.notification_email_from }
6
- layout "mailer"
6
+ layout false
7
7
  end
8
8
  end
@@ -8,7 +8,7 @@ module RailsErrorDashboard
8
8
 
9
9
  mail(
10
10
  to: recipients,
11
- subject: "🚨 [#{error_log.environment.upcase}] #{error_log.error_type}: #{truncate_subject(error_log.message)}"
11
+ subject: "🚨 #{error_log.error_type}: #{truncate_subject(error_log.message)}"
12
12
  )
13
13
  end
14
14