rails_error_dashboard 0.1.36 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -2
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +19 -4
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +13 -56
  5. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +3 -139
  6. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +1 -84
  7. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +1 -67
  8. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +1 -124
  9. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +1 -56
  10. data/app/models/rails_error_dashboard/application.rb +2 -27
  11. data/app/models/rails_error_dashboard/cascade_pattern.rb +4 -19
  12. data/app/models/rails_error_dashboard/error_log.rb +42 -432
  13. data/app/views/rails_error_dashboard/errors/index.html.erb +2 -2
  14. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +1 -0
  15. data/lib/rails_error_dashboard/commands/add_error_comment.rb +28 -0
  16. data/lib/rails_error_dashboard/commands/assign_error.rb +28 -0
  17. data/lib/rails_error_dashboard/commands/calculate_cascade_probability.rb +27 -0
  18. data/lib/rails_error_dashboard/commands/find_or_create_application.rb +57 -0
  19. data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +76 -0
  20. data/lib/rails_error_dashboard/commands/increment_cascade_detection.rb +35 -0
  21. data/lib/rails_error_dashboard/commands/log_error.rb +11 -177
  22. data/lib/rails_error_dashboard/commands/resolve_error.rb +2 -1
  23. data/lib/rails_error_dashboard/commands/snooze_error.rb +35 -0
  24. data/lib/rails_error_dashboard/commands/unassign_error.rb +26 -0
  25. data/lib/rails_error_dashboard/commands/unsnooze_error.rb +23 -0
  26. data/lib/rails_error_dashboard/commands/update_error_priority.rb +24 -0
  27. data/lib/rails_error_dashboard/commands/update_error_status.rb +45 -0
  28. data/lib/rails_error_dashboard/commands/upsert_baseline.rb +52 -0
  29. data/lib/rails_error_dashboard/commands/upsert_cascade_pattern.rb +47 -0
  30. data/lib/rails_error_dashboard/queries/analytics_stats.rb +12 -9
  31. data/lib/rails_error_dashboard/queries/critical_alerts.rb +27 -0
  32. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +7 -21
  33. data/lib/rails_error_dashboard/queries/error_correlation.rb +3 -61
  34. data/lib/rails_error_dashboard/queries/errors_list.rb +6 -6
  35. data/lib/rails_error_dashboard/queries/filter_options.rb +10 -1
  36. data/lib/rails_error_dashboard/queries/recurring_issues.rb +2 -2
  37. data/lib/rails_error_dashboard/services/analytics_cache_manager.rb +31 -0
  38. data/lib/rails_error_dashboard/services/backtrace_processor.rb +52 -0
  39. data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +161 -0
  40. data/lib/rails_error_dashboard/services/baseline_calculator.rb +29 -58
  41. data/lib/rails_error_dashboard/services/cascade_detector.rb +8 -16
  42. data/lib/rails_error_dashboard/services/discord_payload_builder.rb +75 -0
  43. data/lib/rails_error_dashboard/services/error_broadcaster.rb +88 -0
  44. data/lib/rails_error_dashboard/services/error_hash_generator.rb +91 -0
  45. data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +39 -0
  46. data/lib/rails_error_dashboard/services/exception_filter.rb +69 -0
  47. data/lib/rails_error_dashboard/services/notification_helpers.rb +98 -0
  48. data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +52 -0
  49. data/lib/rails_error_dashboard/services/pattern_detector.rb +61 -95
  50. data/lib/rails_error_dashboard/services/pearson_correlation.rb +46 -0
  51. data/lib/rails_error_dashboard/services/priority_score_calculator.rb +94 -0
  52. data/lib/rails_error_dashboard/services/severity_classifier.rb +72 -0
  53. data/lib/rails_error_dashboard/services/slack_payload_builder.rb +134 -0
  54. data/lib/rails_error_dashboard/services/statistical_classifier.rb +64 -0
  55. data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +51 -0
  56. data/lib/rails_error_dashboard/version.rb +1 -1
  57. data/lib/rails_error_dashboard.rb +30 -1
  58. metadata +62 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aae0da8a196598bd4a8ab5955e07684c4cb8dd1b959e821c6d35c514abcb3fea
4
- data.tar.gz: 9479ae765eaaa528dd62d8c6cfe3e4bab8dbf66af056c8ef4210f432d9fc3f71
3
+ metadata.gz: 2e6cc2792ac8e61248b57b513ac64f02426ee651bb9cf07c3d4557760f8d7d31
4
+ data.tar.gz: 9450f4745f01d22c8eef4e5988c745d89f7951d318a7f625a8839b1eebd371f5
5
5
  SHA512:
6
- metadata.gz: 25640c5213a9b56ca9392945cd74319d216a8dae4e81966d2f64920202e7ab9e92ce214b2c710f6d1e0561753bc6dc5ec7c5e840ba701365099a3b9cd0803deb
7
- data.tar.gz: 651df5b45d9547541482185aea74dd357140f74fd879f15f4d5c90073b1e3e50f98e29a64e3d00ebd1c439878d959e03bbaf5e31add1566e2f4808ef424b4fc2
6
+ metadata.gz: 49a0c0f2988e1dfe03a40599693411d4a55690be71b7f7921d3dc6e305bc9f2bb54e2792bea7ad8a956127a7d87abf565b81e935536e8b37adbfe844fd017025
7
+ data.tar.gz: c040c6f3ea54b21d6c23addd72c9a787075e17cc4413c5a88c911c7e06c6376e3d5969bea85f106c53bb8b226a155b5ad01646626372488496120f580475cec7
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 (935+ tests passing), the API may change before v1.0.0. Use in production at your own discretion.
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.0 (+ edge 8.1) | Ruby 3.2 - 3.4
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:
@@ -1,6 +1,6 @@
1
1
  module RailsErrorDashboard
2
2
  class ApplicationController < ActionController::Base
3
- include Pagy::Backend
3
+ include Pagy::Method
4
4
 
5
5
  # Enable features that are disabled in API-only mode
6
6
  # These are ONLY enabled for Error Dashboard routes, not the entire app
@@ -12,11 +12,10 @@ module RailsErrorDashboard
12
12
 
13
13
  protect_from_forgery with: :exception
14
14
 
15
- # Make Pagy helpers available in views
16
- helper Pagy::Frontend
17
-
18
15
  # CRITICAL: Ensure dashboard errors never break the app
19
16
  # Catch all exceptions and render user-friendly error page
17
+ # NOTE: rescue_from is checked in reverse declaration order (last = highest priority).
18
+ # The generic handler must be declared FIRST so specific handlers below take precedence.
20
19
  rescue_from StandardError do |exception|
21
20
  # Log the error for debugging
22
21
  Rails.logger.error("[RailsErrorDashboard] Dashboard controller error: #{exception.class} - #{exception.message}")
@@ -32,5 +31,21 @@ module RailsErrorDashboard
32
31
  status: :internal_server_error,
33
32
  layout: false
34
33
  end
34
+
35
+ # Handle record not found — return 404 instead of 500
36
+ rescue_from ActiveRecord::RecordNotFound do |exception|
37
+ Rails.logger.warn("[RailsErrorDashboard] Record not found: #{exception.message}")
38
+ render plain: "The requested error was not found.\n\n" \
39
+ "It may have been deleted or the ID is invalid.\n\n" \
40
+ "Error: #{exception.message}",
41
+ status: :not_found,
42
+ layout: false
43
+ end
44
+
45
+ # Handle Pagy pagination errors — redirect to page 1
46
+ rescue_from Pagy::RangeError, Pagy::OptionError do |exception|
47
+ Rails.logger.warn("[RailsErrorDashboard] Pagination error: #{exception.message}")
48
+ redirect_to request.path, status: :moved_permanently
49
+ end
35
50
  end
36
51
  end
@@ -2,8 +2,6 @@
2
2
 
3
3
  module RailsErrorDashboard
4
4
  class ErrorsController < ApplicationController
5
- include Pagy::Backend
6
-
7
5
  before_action :authenticate_dashboard_user!
8
6
  before_action :set_application_context
9
7
 
@@ -51,14 +49,8 @@ module RailsErrorDashboard
51
49
  @multi_error_users = []
52
50
  end
53
51
 
54
- # Get critical alerts (critical/high severity errors from last hour)
55
- # Filter by priority_level in database instead of loading all records into memory
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)
52
+ # Get critical alerts using Query
53
+ @critical_alerts = Queries::CriticalAlerts.call(application_id: @current_application_id)
62
54
  end
63
55
 
64
56
  def index
@@ -66,7 +58,7 @@ module RailsErrorDashboard
66
58
  errors_query = Queries::ErrorsList.call(filter_params)
67
59
 
68
60
  # Paginate with Pagy
69
- @pagy, @errors = pagy(errors_query, items: params[:per_page] || 25)
61
+ @pagy, @errors = pagy(:offset, errors_query, limit: params[:per_page] || 25)
70
62
 
71
63
  # Get dashboard stats using Query (pass application filter)
72
64
  @stats = Queries::DashboardStats.call(application_id: @current_application_id)
@@ -75,15 +67,7 @@ module RailsErrorDashboard
75
67
  filter_options = Queries::FilterOptions.call(application_id: @current_application_id)
76
68
  @error_types = filter_options[:error_types]
77
69
  @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
70
+ @assignees = filter_options[:assignees]
87
71
  end
88
72
 
89
73
  def show
@@ -109,67 +93,40 @@ module RailsErrorDashboard
109
93
  redirect_to error_path(@error)
110
94
  end
111
95
 
112
- # Phase 3: Workflow Integration Actions
96
+ # Phase 3: Workflow Integration Actions (via Commands)
113
97
 
114
98
  def assign
115
- @error = ErrorLog.find(params[:id])
116
- @error.assign_to!(params[:assigned_to])
117
- redirect_to error_path(@error)
118
- rescue => e
99
+ @error = Commands::AssignError.call(params[:id], assigned_to: params[:assigned_to])
119
100
  redirect_to error_path(@error)
120
101
  end
121
102
 
122
103
  def unassign
123
- @error = ErrorLog.find(params[:id])
124
- @error.unassign!
125
- redirect_to error_path(@error)
126
- rescue => e
104
+ @error = Commands::UnassignError.call(params[:id])
127
105
  redirect_to error_path(@error)
128
106
  end
129
107
 
130
108
  def update_priority
131
- @error = ErrorLog.find(params[:id])
132
- @error.update!(priority_level: params[:priority_level])
133
- redirect_to error_path(@error)
134
- rescue => e
109
+ @error = Commands::UpdateErrorPriority.call(params[:id], priority_level: params[:priority_level])
135
110
  redirect_to error_path(@error)
136
111
  end
137
112
 
138
113
  def snooze
139
- @error = ErrorLog.find(params[:id])
140
- @error.snooze!(params[:hours].to_i, reason: params[:reason])
141
- redirect_to error_path(@error)
142
- rescue => e
114
+ @error = Commands::SnoozeError.call(params[:id], hours: params[:hours].to_i, reason: params[:reason])
143
115
  redirect_to error_path(@error)
144
116
  end
145
117
 
146
118
  def unsnooze
147
- @error = ErrorLog.find(params[:id])
148
- @error.unsnooze!
149
- redirect_to error_path(@error)
150
- rescue => e
119
+ @error = Commands::UnsnoozeError.call(params[:id])
151
120
  redirect_to error_path(@error)
152
121
  end
153
122
 
154
123
  def update_status
155
- @error = ErrorLog.find(params[:id])
156
- if @error.update_status!(params[:status], comment: params[:comment])
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)
124
+ result = Commands::UpdateErrorStatus.call(params[:id], status: params[:status], comment: params[:comment])
125
+ redirect_to error_path(result[:error])
163
126
  end
164
127
 
165
128
  def add_comment
166
- @error = ErrorLog.find(params[:id])
167
- @error.comments.create!(
168
- author_name: params[:author_name],
169
- body: params[:body]
170
- )
171
- redirect_to error_path(@error)
172
- rescue => e
129
+ @error = Commands::AddErrorComment.call(params[:id], author_name: params[:author_name], body: params[:body])
173
130
  redirect_to error_path(@error)
174
131
  end
175
132
 
@@ -71,7 +71,7 @@ module RailsErrorDashboard
71
71
  end
72
72
 
73
73
  def send_slack_notification(error_log, anomaly_data, config)
74
- payload = build_slack_payload(error_log, anomaly_data, config)
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 = build_discord_payload(error_log, anomaly_data, config)
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 = build_webhook_payload(error_log, anomaly_data)
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 = build_discord_payload(error_log)
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 = build_pagerduty_payload(error_log, routing_key)
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