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.
- checksums.yaml +4 -4
- data/README.md +257 -700
- data/app/controllers/rails_error_dashboard/application_controller.rb +18 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +47 -4
- data/app/helpers/rails_error_dashboard/application_helper.rb +17 -0
- data/app/jobs/rails_error_dashboard/application_job.rb +19 -0
- data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +48 -0
- data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +263 -0
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -8
- data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +2 -1
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +5 -5
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +10 -6
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +5 -6
- data/app/mailers/rails_error_dashboard/application_mailer.rb +1 -1
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +1 -1
- data/app/models/rails_error_dashboard/cascade_pattern.rb +74 -0
- data/app/models/rails_error_dashboard/error_baseline.rb +100 -0
- data/app/models/rails_error_dashboard/error_log.rb +326 -3
- data/app/models/rails_error_dashboard/error_occurrence.rb +49 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +150 -9
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +3 -10
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +1 -2
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +76 -0
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +209 -0
- data/app/views/rails_error_dashboard/errors/_stats.html.erb +34 -0
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +19 -39
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +373 -0
- data/app/views/rails_error_dashboard/errors/index.html.erb +215 -138
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +388 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +428 -11
- data/config/routes.rb +2 -0
- data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +66 -0
- data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +26 -0
- data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +12 -0
- data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +9 -0
- data/db/migrate/20251225100236_create_error_occurrences.rb +31 -0
- data/db/migrate/20251225101920_create_cascade_patterns.rb +33 -0
- data/db/migrate/20251225102500_create_error_baselines.rb +38 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +270 -1
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +251 -37
- data/lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb +36 -0
- data/lib/generators/rails_error_dashboard/solid_queue/templates/queue.yml +55 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +234 -7
- data/lib/rails_error_dashboard/commands/resolve_error.rb +16 -0
- data/lib/rails_error_dashboard/configuration.rb +82 -5
- data/lib/rails_error_dashboard/error_reporter.rb +15 -7
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +17 -10
- data/lib/rails_error_dashboard/plugin.rb +6 -3
- data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +0 -1
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +2 -3
- data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +0 -2
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +44 -6
- data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
- data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +86 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +134 -2
- data/lib/rails_error_dashboard/queries/error_cascades.rb +74 -0
- data/lib/rails_error_dashboard/queries/error_correlation.rb +375 -0
- data/lib/rails_error_dashboard/queries/errors_list.rb +52 -11
- data/lib/rails_error_dashboard/queries/filter_options.rb +0 -1
- data/lib/rails_error_dashboard/queries/platform_comparison.rb +254 -0
- data/lib/rails_error_dashboard/queries/similar_errors.rb +93 -0
- data/lib/rails_error_dashboard/services/baseline_alert_throttler.rb +88 -0
- data/lib/rails_error_dashboard/services/baseline_calculator.rb +269 -0
- data/lib/rails_error_dashboard/services/cascade_detector.rb +95 -0
- data/lib/rails_error_dashboard/services/pattern_detector.rb +268 -0
- data/lib/rails_error_dashboard/services/similarity_calculator.rb +144 -0
- data/lib/rails_error_dashboard/value_objects/error_context.rb +27 -1
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +55 -7
- metadata +52 -9
- data/app/models/rails_error_dashboard/application_record.rb +0 -5
- data/lib/rails_error_dashboard/queries/developer_insights.rb +0 -277
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
@@ -8,7 +8,7 @@ module RailsErrorDashboard
|
|
|
8
8
|
|
|
9
9
|
mail(
|
|
10
10
|
to: recipients,
|
|
11
|
-
subject: "🚨
|
|
11
|
+
subject: "🚨 #{error_log.error_type}: #{truncate_subject(error_log.message)}"
|
|
12
12
|
)
|
|
13
13
|
end
|
|
14
14
|
|