rails_error_dashboard 0.1.36 → 0.1.37
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 +43 -2
- data/app/controllers/rails_error_dashboard/application_controller.rb +18 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +12 -53
- data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +3 -139
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +1 -84
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +1 -67
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +1 -124
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +1 -56
- data/app/models/rails_error_dashboard/application.rb +2 -27
- data/app/models/rails_error_dashboard/cascade_pattern.rb +4 -19
- data/app/models/rails_error_dashboard/error_log.rb +42 -432
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +1 -0
- data/lib/rails_error_dashboard/commands/add_error_comment.rb +28 -0
- data/lib/rails_error_dashboard/commands/assign_error.rb +28 -0
- data/lib/rails_error_dashboard/commands/calculate_cascade_probability.rb +27 -0
- data/lib/rails_error_dashboard/commands/find_or_create_application.rb +57 -0
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +76 -0
- data/lib/rails_error_dashboard/commands/increment_cascade_detection.rb +35 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +11 -177
- data/lib/rails_error_dashboard/commands/resolve_error.rb +2 -1
- data/lib/rails_error_dashboard/commands/snooze_error.rb +35 -0
- data/lib/rails_error_dashboard/commands/unassign_error.rb +26 -0
- data/lib/rails_error_dashboard/commands/unsnooze_error.rb +23 -0
- data/lib/rails_error_dashboard/commands/update_error_priority.rb +24 -0
- data/lib/rails_error_dashboard/commands/update_error_status.rb +45 -0
- data/lib/rails_error_dashboard/commands/upsert_baseline.rb +52 -0
- data/lib/rails_error_dashboard/commands/upsert_cascade_pattern.rb +47 -0
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +12 -9
- data/lib/rails_error_dashboard/queries/critical_alerts.rb +27 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +7 -21
- data/lib/rails_error_dashboard/queries/error_correlation.rb +3 -61
- data/lib/rails_error_dashboard/queries/errors_list.rb +6 -6
- data/lib/rails_error_dashboard/queries/filter_options.rb +10 -1
- data/lib/rails_error_dashboard/queries/recurring_issues.rb +2 -2
- data/lib/rails_error_dashboard/services/analytics_cache_manager.rb +31 -0
- data/lib/rails_error_dashboard/services/backtrace_processor.rb +52 -0
- data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +161 -0
- data/lib/rails_error_dashboard/services/baseline_calculator.rb +29 -58
- data/lib/rails_error_dashboard/services/cascade_detector.rb +8 -16
- data/lib/rails_error_dashboard/services/discord_payload_builder.rb +75 -0
- data/lib/rails_error_dashboard/services/error_broadcaster.rb +88 -0
- data/lib/rails_error_dashboard/services/error_hash_generator.rb +91 -0
- data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +39 -0
- data/lib/rails_error_dashboard/services/exception_filter.rb +69 -0
- data/lib/rails_error_dashboard/services/notification_helpers.rb +98 -0
- data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +52 -0
- data/lib/rails_error_dashboard/services/pattern_detector.rb +61 -95
- data/lib/rails_error_dashboard/services/pearson_correlation.rb +46 -0
- data/lib/rails_error_dashboard/services/priority_score_calculator.rb +94 -0
- data/lib/rails_error_dashboard/services/severity_classifier.rb +72 -0
- data/lib/rails_error_dashboard/services/slack_payload_builder.rb +134 -0
- data/lib/rails_error_dashboard/services/statistical_classifier.rb +64 -0
- data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +51 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +30 -0
- metadata +60 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '09a73398182f19129a70796f91eb3b4158a9c8872232dabc1443f6e9da47f98e'
|
|
4
|
+
data.tar.gz: 84fe4d09d5bb5e8c24ec5b4211858eb80b19161bf126284ed278498d9bb7ae5c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 263c8f31daa5ec9335379d343a660a5e2150c111b1ba53ec8fe58917c56251426e8e14e51b44f00bdaf2aaed147505957155cbee5780247b5d0d5583181b8ef2
|
|
7
|
+
data.tar.gz: 78ff657191d4b9eae502a1a05d051984e0a2146b49bb66a61ca99fdad7656cf3b41777c9d2c431ee6e304dd2eb84dfac101f2cd8f7a53041a51da87f1c9dc267
|
data/README.md
CHANGED
|
@@ -29,9 +29,9 @@ Experience the full dashboard with 250+ realistic Rails errors, LOTR-themed demo
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
31
|
### ⚠️ BETA SOFTWARE
|
|
32
|
-
This Rails Engine is in beta and under active development. While functional and tested (
|
|
32
|
+
This Rails Engine is in beta and under active development. While functional and tested (1,300+ tests passing, including browser-based system tests), the API may change before v1.0.0. Use in production at your own discretion.
|
|
33
33
|
|
|
34
|
-
**Supports**: Rails 7.0 - 8.
|
|
34
|
+
**Supports**: Rails 7.0 - 8.1 | Ruby 3.2 - 4.0
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
@@ -638,6 +638,47 @@ Clean, maintainable, testable architecture you can understand and modify.
|
|
|
638
638
|
|
|
639
639
|
---
|
|
640
640
|
|
|
641
|
+
## 🧪 Testing
|
|
642
|
+
|
|
643
|
+
1,300+ tests covering unit, integration, and browser-based system tests.
|
|
644
|
+
|
|
645
|
+
### Running Tests
|
|
646
|
+
|
|
647
|
+
```bash
|
|
648
|
+
# Run all tests
|
|
649
|
+
bundle exec rspec
|
|
650
|
+
|
|
651
|
+
# Run unit/integration tests only (fast)
|
|
652
|
+
bundle exec rspec --exclude-pattern "spec/system/**/*"
|
|
653
|
+
|
|
654
|
+
# Run system tests only (requires Chrome)
|
|
655
|
+
bundle exec rspec spec/system/
|
|
656
|
+
|
|
657
|
+
# Run with visible browser for debugging
|
|
658
|
+
HEADLESS=false bundle exec rspec spec/system/
|
|
659
|
+
|
|
660
|
+
# Run with Chrome DevTools inspector
|
|
661
|
+
INSPECTOR=true HEADLESS=false bundle exec rspec spec/system/
|
|
662
|
+
|
|
663
|
+
# Run with coverage report
|
|
664
|
+
COVERAGE=true bundle exec rspec
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### System Tests (Browser-Based)
|
|
668
|
+
|
|
669
|
+
System tests use **Capybara + Cuprite** (Chrome DevTools Protocol) to simulate real user interactions — opening modals, filling forms, clicking buttons, and verifying page content. No Selenium or chromedriver management needed.
|
|
670
|
+
|
|
671
|
+
**Requirements:** Chrome or Chromium installed locally.
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
# Verify Chrome is available
|
|
675
|
+
which google-chrome || which chromium-browser || which chromium
|
|
676
|
+
|
|
677
|
+
# macOS: Chrome is typically at /Applications/Google Chrome.app
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
641
682
|
## 🤝 Contributing
|
|
642
683
|
|
|
643
684
|
We welcome contributions! Here's how to get started:
|
|
@@ -17,6 +17,8 @@ module RailsErrorDashboard
|
|
|
17
17
|
|
|
18
18
|
# CRITICAL: Ensure dashboard errors never break the app
|
|
19
19
|
# Catch all exceptions and render user-friendly error page
|
|
20
|
+
# NOTE: rescue_from is checked in reverse declaration order (last = highest priority).
|
|
21
|
+
# The generic handler must be declared FIRST so specific handlers below take precedence.
|
|
20
22
|
rescue_from StandardError do |exception|
|
|
21
23
|
# Log the error for debugging
|
|
22
24
|
Rails.logger.error("[RailsErrorDashboard] Dashboard controller error: #{exception.class} - #{exception.message}")
|
|
@@ -32,5 +34,21 @@ module RailsErrorDashboard
|
|
|
32
34
|
status: :internal_server_error,
|
|
33
35
|
layout: false
|
|
34
36
|
end
|
|
37
|
+
|
|
38
|
+
# Handle record not found — return 404 instead of 500
|
|
39
|
+
rescue_from ActiveRecord::RecordNotFound do |exception|
|
|
40
|
+
Rails.logger.warn("[RailsErrorDashboard] Record not found: #{exception.message}")
|
|
41
|
+
render plain: "The requested error was not found.\n\n" \
|
|
42
|
+
"It may have been deleted or the ID is invalid.\n\n" \
|
|
43
|
+
"Error: #{exception.message}",
|
|
44
|
+
status: :not_found,
|
|
45
|
+
layout: false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Handle Pagy pagination errors — redirect to page 1
|
|
49
|
+
rescue_from Pagy::OverflowError, Pagy::VariableError do |exception|
|
|
50
|
+
Rails.logger.warn("[RailsErrorDashboard] Pagination error: #{exception.message}")
|
|
51
|
+
redirect_to request.path, status: :moved_permanently
|
|
52
|
+
end
|
|
35
53
|
end
|
|
36
54
|
end
|
|
@@ -51,14 +51,8 @@ module RailsErrorDashboard
|
|
|
51
51
|
@multi_error_users = []
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
# Get critical alerts
|
|
55
|
-
|
|
56
|
-
@critical_alerts = ErrorLog
|
|
57
|
-
.where("occurred_at >= ?", 1.hour.ago)
|
|
58
|
-
.where(resolved_at: nil)
|
|
59
|
-
.where(priority_level: [ 3, 4 ]) # 3 = high, 4 = critical (based on severity enum)
|
|
60
|
-
@critical_alerts = @critical_alerts.where(application_id: @current_application_id) if @current_application_id.present?
|
|
61
|
-
@critical_alerts = @critical_alerts.order(occurred_at: :desc).limit(10)
|
|
54
|
+
# Get critical alerts using Query
|
|
55
|
+
@critical_alerts = Queries::CriticalAlerts.call(application_id: @current_application_id)
|
|
62
56
|
end
|
|
63
57
|
|
|
64
58
|
def index
|
|
@@ -75,15 +69,7 @@ module RailsErrorDashboard
|
|
|
75
69
|
filter_options = Queries::FilterOptions.call(application_id: @current_application_id)
|
|
76
70
|
@error_types = filter_options[:error_types]
|
|
77
71
|
@platforms = filter_options[:platforms]
|
|
78
|
-
|
|
79
|
-
# Get all distinct assignees for the assignee filter dropdown
|
|
80
|
-
assignee_query = ErrorLog.where.not(assigned_to: nil)
|
|
81
|
-
# Filter by application if specified
|
|
82
|
-
assignee_query = assignee_query.where(application_id: @current_application_id) if @current_application_id.present?
|
|
83
|
-
@assignees = assignee_query.select(:assigned_to)
|
|
84
|
-
.distinct
|
|
85
|
-
.pluck(:assigned_to)
|
|
86
|
-
.sort
|
|
72
|
+
@assignees = filter_options[:assignees]
|
|
87
73
|
end
|
|
88
74
|
|
|
89
75
|
def show
|
|
@@ -109,67 +95,40 @@ module RailsErrorDashboard
|
|
|
109
95
|
redirect_to error_path(@error)
|
|
110
96
|
end
|
|
111
97
|
|
|
112
|
-
# Phase 3: Workflow Integration Actions
|
|
98
|
+
# Phase 3: Workflow Integration Actions (via Commands)
|
|
113
99
|
|
|
114
100
|
def assign
|
|
115
|
-
@error =
|
|
116
|
-
@error.assign_to!(params[:assigned_to])
|
|
117
|
-
redirect_to error_path(@error)
|
|
118
|
-
rescue => e
|
|
101
|
+
@error = Commands::AssignError.call(params[:id], assigned_to: params[:assigned_to])
|
|
119
102
|
redirect_to error_path(@error)
|
|
120
103
|
end
|
|
121
104
|
|
|
122
105
|
def unassign
|
|
123
|
-
@error =
|
|
124
|
-
@error.unassign!
|
|
125
|
-
redirect_to error_path(@error)
|
|
126
|
-
rescue => e
|
|
106
|
+
@error = Commands::UnassignError.call(params[:id])
|
|
127
107
|
redirect_to error_path(@error)
|
|
128
108
|
end
|
|
129
109
|
|
|
130
110
|
def update_priority
|
|
131
|
-
@error =
|
|
132
|
-
@error.update!(priority_level: params[:priority_level])
|
|
133
|
-
redirect_to error_path(@error)
|
|
134
|
-
rescue => e
|
|
111
|
+
@error = Commands::UpdateErrorPriority.call(params[:id], priority_level: params[:priority_level])
|
|
135
112
|
redirect_to error_path(@error)
|
|
136
113
|
end
|
|
137
114
|
|
|
138
115
|
def snooze
|
|
139
|
-
@error =
|
|
140
|
-
@error.snooze!(params[:hours].to_i, reason: params[:reason])
|
|
141
|
-
redirect_to error_path(@error)
|
|
142
|
-
rescue => e
|
|
116
|
+
@error = Commands::SnoozeError.call(params[:id], hours: params[:hours].to_i, reason: params[:reason])
|
|
143
117
|
redirect_to error_path(@error)
|
|
144
118
|
end
|
|
145
119
|
|
|
146
120
|
def unsnooze
|
|
147
|
-
@error =
|
|
148
|
-
@error.unsnooze!
|
|
149
|
-
redirect_to error_path(@error)
|
|
150
|
-
rescue => e
|
|
121
|
+
@error = Commands::UnsnoozeError.call(params[:id])
|
|
151
122
|
redirect_to error_path(@error)
|
|
152
123
|
end
|
|
153
124
|
|
|
154
125
|
def update_status
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
redirect_to error_path(@error)
|
|
158
|
-
else
|
|
159
|
-
redirect_to error_path(@error)
|
|
160
|
-
end
|
|
161
|
-
rescue => e
|
|
162
|
-
redirect_to error_path(@error)
|
|
126
|
+
result = Commands::UpdateErrorStatus.call(params[:id], status: params[:status], comment: params[:comment])
|
|
127
|
+
redirect_to error_path(result[:error])
|
|
163
128
|
end
|
|
164
129
|
|
|
165
130
|
def add_comment
|
|
166
|
-
@error =
|
|
167
|
-
@error.comments.create!(
|
|
168
|
-
author_name: params[:author_name],
|
|
169
|
-
body: params[:body]
|
|
170
|
-
)
|
|
171
|
-
redirect_to error_path(@error)
|
|
172
|
-
rescue => e
|
|
131
|
+
@error = Commands::AddErrorComment.call(params[:id], author_name: params[:author_name], body: params[:body])
|
|
173
132
|
redirect_to error_path(@error)
|
|
174
133
|
end
|
|
175
134
|
|
|
@@ -71,7 +71,7 @@ module RailsErrorDashboard
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def send_slack_notification(error_log, anomaly_data, config)
|
|
74
|
-
payload =
|
|
74
|
+
payload = Services::BaselineAlertPayloadBuilder.slack_payload(error_log, anomaly_data)
|
|
75
75
|
|
|
76
76
|
HTTParty.post(
|
|
77
77
|
config.slack_webhook_url,
|
|
@@ -83,8 +83,6 @@ module RailsErrorDashboard
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
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
86
|
Rails.logger.info(
|
|
89
87
|
"Baseline alert email would be sent for #{error_log.error_type}"
|
|
90
88
|
)
|
|
@@ -93,7 +91,7 @@ module RailsErrorDashboard
|
|
|
93
91
|
end
|
|
94
92
|
|
|
95
93
|
def send_discord_notification(error_log, anomaly_data, config)
|
|
96
|
-
payload =
|
|
94
|
+
payload = Services::BaselineAlertPayloadBuilder.discord_payload(error_log, anomaly_data)
|
|
97
95
|
|
|
98
96
|
HTTParty.post(
|
|
99
97
|
config.discord_webhook_url,
|
|
@@ -105,7 +103,7 @@ module RailsErrorDashboard
|
|
|
105
103
|
end
|
|
106
104
|
|
|
107
105
|
def send_webhook_notification(error_log, anomaly_data, config)
|
|
108
|
-
payload =
|
|
106
|
+
payload = Services::BaselineAlertPayloadBuilder.webhook_payload(error_log, anomaly_data)
|
|
109
107
|
|
|
110
108
|
config.webhook_urls.each do |url|
|
|
111
109
|
HTTParty.post(
|
|
@@ -119,145 +117,11 @@ module RailsErrorDashboard
|
|
|
119
117
|
end
|
|
120
118
|
|
|
121
119
|
def send_pagerduty_notification(error_log, _anomaly_data, _config)
|
|
122
|
-
# Use existing PagerDuty notification infrastructure if available
|
|
123
120
|
Rails.logger.info(
|
|
124
121
|
"Baseline alert PagerDuty notification for #{error_log.error_type}"
|
|
125
122
|
)
|
|
126
123
|
rescue => e
|
|
127
124
|
Rails.logger.error("Failed to send baseline alert to PagerDuty: #{e.message}")
|
|
128
125
|
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
126
|
end
|
|
263
127
|
end
|
|
@@ -13,7 +13,7 @@ module RailsErrorDashboard
|
|
|
13
13
|
|
|
14
14
|
return unless webhook_url.present?
|
|
15
15
|
|
|
16
|
-
payload =
|
|
16
|
+
payload = Services::DiscordPayloadBuilder.call(error_log)
|
|
17
17
|
|
|
18
18
|
HTTParty.post(
|
|
19
19
|
webhook_url,
|
|
@@ -25,88 +25,5 @@ module RailsErrorDashboard
|
|
|
25
25
|
Rails.logger.error("[RailsErrorDashboard] Failed to send Discord notification: #{e.message}")
|
|
26
26
|
Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
|
|
27
27
|
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def build_discord_payload(error_log)
|
|
32
|
-
{
|
|
33
|
-
embeds: [ {
|
|
34
|
-
title: "🚨 New Error: #{error_log.error_type}",
|
|
35
|
-
description: truncate_message(error_log.message),
|
|
36
|
-
color: severity_color(error_log),
|
|
37
|
-
fields: [
|
|
38
|
-
{
|
|
39
|
-
name: "Platform",
|
|
40
|
-
value: error_log.platform || "Unknown",
|
|
41
|
-
inline: true
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
name: "Occurrences",
|
|
45
|
-
value: error_log.occurrence_count.to_s,
|
|
46
|
-
inline: true
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
name: "Controller",
|
|
50
|
-
value: error_log.controller_name || "N/A",
|
|
51
|
-
inline: true
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
name: "Action",
|
|
55
|
-
value: error_log.action_name || "N/A",
|
|
56
|
-
inline: true
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: "First Seen",
|
|
60
|
-
value: format_time(error_log.first_seen_at),
|
|
61
|
-
inline: true
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: "Location",
|
|
65
|
-
value: extract_first_backtrace_line(error_log.backtrace),
|
|
66
|
-
inline: false
|
|
67
|
-
}
|
|
68
|
-
],
|
|
69
|
-
footer: {
|
|
70
|
-
text: "Rails Error Dashboard"
|
|
71
|
-
},
|
|
72
|
-
timestamp: error_log.occurred_at.iso8601
|
|
73
|
-
} ]
|
|
74
|
-
}
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def severity_color(error_log)
|
|
78
|
-
case error_log.severity
|
|
79
|
-
when :critical
|
|
80
|
-
16711680 # Red
|
|
81
|
-
when :high
|
|
82
|
-
16744192 # Orange
|
|
83
|
-
when :medium
|
|
84
|
-
16776960 # Yellow
|
|
85
|
-
else
|
|
86
|
-
8421504 # Gray
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def truncate_message(message, length = 200)
|
|
91
|
-
return "" if message.nil?
|
|
92
|
-
message.length > length ? "#{message[0...length]}..." : message
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def format_time(time)
|
|
96
|
-
return "N/A" if time.nil?
|
|
97
|
-
time.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def extract_first_backtrace_line(backtrace)
|
|
101
|
-
return "N/A" if backtrace.nil?
|
|
102
|
-
|
|
103
|
-
lines = backtrace.is_a?(String) ? backtrace.lines : backtrace
|
|
104
|
-
first_line = lines.first&.strip
|
|
105
|
-
|
|
106
|
-
return "N/A" if first_line.nil?
|
|
107
|
-
|
|
108
|
-
# Truncate if too long
|
|
109
|
-
first_line.length > 100 ? "#{first_line[0...100]}..." : first_line
|
|
110
|
-
end
|
|
111
28
|
end
|
|
112
29
|
end
|
|
@@ -19,7 +19,7 @@ module RailsErrorDashboard
|
|
|
19
19
|
routing_key = RailsErrorDashboard.configuration.pagerduty_integration_key
|
|
20
20
|
return unless routing_key.present?
|
|
21
21
|
|
|
22
|
-
payload =
|
|
22
|
+
payload = Services::PagerdutyPayloadBuilder.call(error_log, routing_key: routing_key)
|
|
23
23
|
|
|
24
24
|
response = HTTParty.post(
|
|
25
25
|
PAGERDUTY_EVENTS_API,
|
|
@@ -35,71 +35,5 @@ module RailsErrorDashboard
|
|
|
35
35
|
Rails.logger.error("[RailsErrorDashboard] Failed to send PagerDuty notification: #{e.message}")
|
|
36
36
|
Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
|
|
37
37
|
end
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
|
|
41
|
-
def build_pagerduty_payload(error_log, routing_key)
|
|
42
|
-
{
|
|
43
|
-
routing_key: routing_key,
|
|
44
|
-
event_action: "trigger",
|
|
45
|
-
payload: {
|
|
46
|
-
summary: "Critical Error: #{error_log.error_type} in #{error_log.platform}",
|
|
47
|
-
severity: "critical",
|
|
48
|
-
source: error_source(error_log),
|
|
49
|
-
component: error_log.controller_name || "Unknown",
|
|
50
|
-
group: error_log.error_type,
|
|
51
|
-
class: error_log.error_type,
|
|
52
|
-
custom_details: {
|
|
53
|
-
message: error_log.message,
|
|
54
|
-
controller: error_log.controller_name,
|
|
55
|
-
action: error_log.action_name,
|
|
56
|
-
platform: error_log.platform,
|
|
57
|
-
occurrences: error_log.occurrence_count,
|
|
58
|
-
first_seen_at: error_log.first_seen_at&.iso8601,
|
|
59
|
-
last_seen_at: error_log.last_seen_at&.iso8601,
|
|
60
|
-
request_url: error_log.request_url,
|
|
61
|
-
backtrace: extract_backtrace_summary(error_log.backtrace),
|
|
62
|
-
error_id: error_log.id
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
links: dashboard_links(error_log),
|
|
66
|
-
client: "Rails Error Dashboard",
|
|
67
|
-
client_url: dashboard_url(error_log)
|
|
68
|
-
}
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def error_source(error_log)
|
|
72
|
-
if error_log.controller_name && error_log.action_name
|
|
73
|
-
"#{error_log.controller_name}##{error_log.action_name}"
|
|
74
|
-
elsif error_log.request_url
|
|
75
|
-
error_log.request_url
|
|
76
|
-
else
|
|
77
|
-
error_log.platform || "Rails Application"
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def extract_backtrace_summary(backtrace)
|
|
82
|
-
return [] if backtrace.nil?
|
|
83
|
-
|
|
84
|
-
lines = backtrace.is_a?(String) ? backtrace.lines : backtrace
|
|
85
|
-
lines.first(10).map(&:strip)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def dashboard_links(error_log)
|
|
89
|
-
[
|
|
90
|
-
{
|
|
91
|
-
href: dashboard_url(error_log),
|
|
92
|
-
text: "View in Error Dashboard"
|
|
93
|
-
}
|
|
94
|
-
]
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def dashboard_url(error_log)
|
|
98
|
-
# This will need to be configured per deployment
|
|
99
|
-
# For now, return a placeholder
|
|
100
|
-
config = RailsErrorDashboard.configuration
|
|
101
|
-
base_url = config.dashboard_base_url || "http://localhost:3000"
|
|
102
|
-
"#{base_url}/error_dashboard/errors/#{error_log.id}"
|
|
103
|
-
end
|
|
104
38
|
end
|
|
105
39
|
end
|