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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -2
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +18 -0
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +12 -53
  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/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +1 -0
  14. data/lib/rails_error_dashboard/commands/add_error_comment.rb +28 -0
  15. data/lib/rails_error_dashboard/commands/assign_error.rb +28 -0
  16. data/lib/rails_error_dashboard/commands/calculate_cascade_probability.rb +27 -0
  17. data/lib/rails_error_dashboard/commands/find_or_create_application.rb +57 -0
  18. data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +76 -0
  19. data/lib/rails_error_dashboard/commands/increment_cascade_detection.rb +35 -0
  20. data/lib/rails_error_dashboard/commands/log_error.rb +11 -177
  21. data/lib/rails_error_dashboard/commands/resolve_error.rb +2 -1
  22. data/lib/rails_error_dashboard/commands/snooze_error.rb +35 -0
  23. data/lib/rails_error_dashboard/commands/unassign_error.rb +26 -0
  24. data/lib/rails_error_dashboard/commands/unsnooze_error.rb +23 -0
  25. data/lib/rails_error_dashboard/commands/update_error_priority.rb +24 -0
  26. data/lib/rails_error_dashboard/commands/update_error_status.rb +45 -0
  27. data/lib/rails_error_dashboard/commands/upsert_baseline.rb +52 -0
  28. data/lib/rails_error_dashboard/commands/upsert_cascade_pattern.rb +47 -0
  29. data/lib/rails_error_dashboard/queries/analytics_stats.rb +12 -9
  30. data/lib/rails_error_dashboard/queries/critical_alerts.rb +27 -0
  31. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +7 -21
  32. data/lib/rails_error_dashboard/queries/error_correlation.rb +3 -61
  33. data/lib/rails_error_dashboard/queries/errors_list.rb +6 -6
  34. data/lib/rails_error_dashboard/queries/filter_options.rb +10 -1
  35. data/lib/rails_error_dashboard/queries/recurring_issues.rb +2 -2
  36. data/lib/rails_error_dashboard/services/analytics_cache_manager.rb +31 -0
  37. data/lib/rails_error_dashboard/services/backtrace_processor.rb +52 -0
  38. data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +161 -0
  39. data/lib/rails_error_dashboard/services/baseline_calculator.rb +29 -58
  40. data/lib/rails_error_dashboard/services/cascade_detector.rb +8 -16
  41. data/lib/rails_error_dashboard/services/discord_payload_builder.rb +75 -0
  42. data/lib/rails_error_dashboard/services/error_broadcaster.rb +88 -0
  43. data/lib/rails_error_dashboard/services/error_hash_generator.rb +91 -0
  44. data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +39 -0
  45. data/lib/rails_error_dashboard/services/exception_filter.rb +69 -0
  46. data/lib/rails_error_dashboard/services/notification_helpers.rb +98 -0
  47. data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +52 -0
  48. data/lib/rails_error_dashboard/services/pattern_detector.rb +61 -95
  49. data/lib/rails_error_dashboard/services/pearson_correlation.rb +46 -0
  50. data/lib/rails_error_dashboard/services/priority_score_calculator.rb +94 -0
  51. data/lib/rails_error_dashboard/services/severity_classifier.rb +72 -0
  52. data/lib/rails_error_dashboard/services/slack_payload_builder.rb +134 -0
  53. data/lib/rails_error_dashboard/services/statistical_classifier.rb +64 -0
  54. data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +51 -0
  55. data/lib/rails_error_dashboard/version.rb +1 -1
  56. data/lib/rails_error_dashboard.rb +30 -0
  57. metadata +60 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aae0da8a196598bd4a8ab5955e07684c4cb8dd1b959e821c6d35c514abcb3fea
4
- data.tar.gz: 9479ae765eaaa528dd62d8c6cfe3e4bab8dbf66af056c8ef4210f432d9fc3f71
3
+ metadata.gz: '09a73398182f19129a70796f91eb3b4158a9c8872232dabc1443f6e9da47f98e'
4
+ data.tar.gz: 84fe4d09d5bb5e8c24ec5b4211858eb80b19161bf126284ed278498d9bb7ae5c
5
5
  SHA512:
6
- metadata.gz: 25640c5213a9b56ca9392945cd74319d216a8dae4e81966d2f64920202e7ab9e92ce214b2c710f6d1e0561753bc6dc5ec7c5e840ba701365099a3b9cd0803deb
7
- data.tar.gz: 651df5b45d9547541482185aea74dd357140f74fd879f15f4d5c90073b1e3e50f98e29a64e3d00ebd1c439878d959e03bbaf5e31add1566e2f4808ef424b4fc2
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 (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:
@@ -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 (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)
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 = ErrorLog.find(params[:id])
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 = ErrorLog.find(params[:id])
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 = ErrorLog.find(params[:id])
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 = ErrorLog.find(params[:id])
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 = ErrorLog.find(params[:id])
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
- @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)
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 = 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
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 = 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