rails_error_dashboard 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +257 -700
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +18 -0
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +47 -4
  5. data/app/helpers/rails_error_dashboard/application_helper.rb +17 -0
  6. data/app/jobs/rails_error_dashboard/application_job.rb +19 -0
  7. data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +48 -0
  8. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +263 -0
  9. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -8
  10. data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +2 -1
  11. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +5 -5
  12. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +10 -6
  13. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +5 -6
  14. data/app/mailers/rails_error_dashboard/application_mailer.rb +1 -1
  15. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +1 -1
  16. data/app/models/rails_error_dashboard/cascade_pattern.rb +74 -0
  17. data/app/models/rails_error_dashboard/error_baseline.rb +100 -0
  18. data/app/models/rails_error_dashboard/error_log.rb +326 -3
  19. data/app/models/rails_error_dashboard/error_occurrence.rb +49 -0
  20. data/app/views/layouts/rails_error_dashboard.html.erb +150 -9
  21. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +3 -10
  22. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +1 -2
  23. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +76 -0
  24. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +209 -0
  25. data/app/views/rails_error_dashboard/errors/_stats.html.erb +34 -0
  26. data/app/views/rails_error_dashboard/errors/analytics.html.erb +19 -39
  27. data/app/views/rails_error_dashboard/errors/correlation.html.erb +373 -0
  28. data/app/views/rails_error_dashboard/errors/index.html.erb +215 -138
  29. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +388 -0
  30. data/app/views/rails_error_dashboard/errors/show.html.erb +428 -11
  31. data/config/routes.rb +2 -0
  32. data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +66 -0
  33. data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +26 -0
  34. data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +12 -0
  35. data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +9 -0
  36. data/db/migrate/20251225100236_create_error_occurrences.rb +31 -0
  37. data/db/migrate/20251225101920_create_cascade_patterns.rb +33 -0
  38. data/db/migrate/20251225102500_create_error_baselines.rb +38 -0
  39. data/lib/generators/rails_error_dashboard/install/install_generator.rb +270 -1
  40. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +251 -37
  41. data/lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb +36 -0
  42. data/lib/generators/rails_error_dashboard/solid_queue/templates/queue.yml +55 -0
  43. data/lib/rails_error_dashboard/commands/log_error.rb +234 -7
  44. data/lib/rails_error_dashboard/commands/resolve_error.rb +16 -0
  45. data/lib/rails_error_dashboard/configuration.rb +82 -5
  46. data/lib/rails_error_dashboard/error_reporter.rb +15 -7
  47. data/lib/rails_error_dashboard/middleware/error_catcher.rb +17 -10
  48. data/lib/rails_error_dashboard/plugin.rb +6 -3
  49. data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +0 -1
  50. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +2 -3
  51. data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +0 -2
  52. data/lib/rails_error_dashboard/queries/analytics_stats.rb +44 -6
  53. data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
  54. data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +86 -0
  55. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +134 -2
  56. data/lib/rails_error_dashboard/queries/error_cascades.rb +74 -0
  57. data/lib/rails_error_dashboard/queries/error_correlation.rb +375 -0
  58. data/lib/rails_error_dashboard/queries/errors_list.rb +52 -11
  59. data/lib/rails_error_dashboard/queries/filter_options.rb +0 -1
  60. data/lib/rails_error_dashboard/queries/platform_comparison.rb +254 -0
  61. data/lib/rails_error_dashboard/queries/similar_errors.rb +93 -0
  62. data/lib/rails_error_dashboard/services/baseline_alert_throttler.rb +88 -0
  63. data/lib/rails_error_dashboard/services/baseline_calculator.rb +269 -0
  64. data/lib/rails_error_dashboard/services/cascade_detector.rb +95 -0
  65. data/lib/rails_error_dashboard/services/pattern_detector.rb +268 -0
  66. data/lib/rails_error_dashboard/services/similarity_calculator.rb +144 -0
  67. data/lib/rails_error_dashboard/value_objects/error_context.rb +27 -1
  68. data/lib/rails_error_dashboard/version.rb +1 -1
  69. data/lib/rails_error_dashboard.rb +55 -7
  70. metadata +52 -9
  71. data/app/models/rails_error_dashboard/application_record.rb +0 -5
  72. data/lib/rails_error_dashboard/queries/developer_insights.rb +0 -277
  73. data/lib/rails_error_dashboard/queries/errors_list_v2.rb +0 -149
@@ -4,7 +4,19 @@
4
4
  <%= link_to errors_path, class: "btn btn-outline-secondary btn-sm mb-2" do %>
5
5
  <i class="bi bi-arrow-left"></i> Back to Errors
6
6
  <% end %>
7
- <h2 class="mb-0">Error Details</h2>
7
+ <h2 class="mb-0">
8
+ Error Details
9
+ <% severity = @error.severity %>
10
+ <% if severity == :critical %>
11
+ <span class="badge bg-danger fs-5">CRITICAL</span>
12
+ <% elsif severity == :high %>
13
+ <span class="badge bg-warning text-dark fs-5">HIGH</span>
14
+ <% elsif severity == :medium %>
15
+ <span class="badge bg-info text-dark fs-5">MEDIUM</span>
16
+ <% else %>
17
+ <span class="badge bg-secondary fs-5">LOW</span>
18
+ <% end %>
19
+ </h2>
8
20
  </div>
9
21
  <div>
10
22
  <% if @error.resolved? %>
@@ -76,6 +88,149 @@
76
88
  </div>
77
89
  </div>
78
90
 
91
+ <!-- Similar Errors (Fuzzy Matching) -->
92
+ <% if RailsErrorDashboard.configuration.enable_similar_errors && @error.respond_to?(:similar_errors) %>
93
+ <% similar = @error.similar_errors(threshold: 0.6, limit: 5) %>
94
+ <% if similar.any? %>
95
+ <div class="card mb-4">
96
+ <div class="card-header bg-white">
97
+ <h5 class="mb-0">
98
+ <i class="bi bi-diagram-3"></i> Similar Errors
99
+ <span class="badge bg-info text-dark">Fuzzy Matching</span>
100
+ </h5>
101
+ <small class="text-muted">Errors with similar backtraces and messages (60%+ similarity)</small>
102
+ </div>
103
+ <div class="card-body p-0">
104
+ <div class="table-responsive">
105
+ <table class="table table-hover mb-0">
106
+ <thead class="table-light">
107
+ <tr>
108
+ <th>Similarity</th>
109
+ <th>Error Type</th>
110
+ <th>Message</th>
111
+ <th>Platform</th>
112
+ <th>Occurrences</th>
113
+ <th></th>
114
+ </tr>
115
+ </thead>
116
+ <tbody>
117
+ <% similar.each do |item| %>
118
+ <% similar_error = item[:error] %>
119
+ <% similarity_pct = (item[:similarity] * 100).round %>
120
+ <tr>
121
+ <td>
122
+ <div class="progress" style="width: 60px; height: 20px;">
123
+ <div class="progress-bar bg-success" role="progressbar"
124
+ style="width: <%= similarity_pct %>%"
125
+ aria-valuenow="<%= similarity_pct %>"
126
+ aria-valuemin="0"
127
+ aria-valuemax="100">
128
+ <%= similarity_pct %>%
129
+ </div>
130
+ </div>
131
+ </td>
132
+ <td><code><%= similar_error.error_type %></code></td>
133
+ <td>
134
+ <div class="text-truncate" style="max-width: 300px;"
135
+ data-bs-toggle="tooltip"
136
+ title="<%= similar_error.message %>">
137
+ <%= similar_error.message %>
138
+ </div>
139
+ </td>
140
+ <td>
141
+ <% if similar_error.platform == 'iOS' %>
142
+ <span class="badge badge-ios">iOS</span>
143
+ <% elsif similar_error.platform == 'Android' %>
144
+ <span class="badge badge-android">Android</span>
145
+ <% else %>
146
+ <span class="badge badge-api"><%= similar_error.platform || 'API' %></span>
147
+ <% end %>
148
+ </td>
149
+ <td><span class="badge bg-primary"><%= similar_error.occurrence_count %>x</span></td>
150
+ <td>
151
+ <%= link_to "View", error_path(similar_error), class: "btn btn-sm btn-outline-primary" %>
152
+ </td>
153
+ </tr>
154
+ <% end %>
155
+ </tbody>
156
+ </table>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ <% end %>
161
+ <% end %>
162
+
163
+ <!-- Co-occurring Errors -->
164
+ <% if RailsErrorDashboard.configuration.enable_co_occurring_errors && @error.respond_to?(:co_occurring_errors) %>
165
+ <% co_occurring = @error.co_occurring_errors(window_minutes: 5, min_frequency: 2, limit: 5) %>
166
+ <% if co_occurring.any? %>
167
+ <div class="card mb-4">
168
+ <div class="card-header bg-white">
169
+ <h5 class="mb-0">
170
+ <i class="bi bi-clock-history"></i> Co-occurring Errors
171
+ <span class="badge bg-info text-dark">Time Patterns</span>
172
+ </h5>
173
+ <small class="text-muted">Errors that occur within 5 minutes of this error</small>
174
+ </div>
175
+ <div class="card-body p-0">
176
+ <div class="table-responsive">
177
+ <table class="table table-hover mb-0">
178
+ <thead class="table-light">
179
+ <tr>
180
+ <th>Error Type</th>
181
+ <th>Message</th>
182
+ <th>Platform</th>
183
+ <th>Frequency</th>
184
+ <th>Avg Delay</th>
185
+ <th></th>
186
+ </tr>
187
+ </thead>
188
+ <tbody>
189
+ <% co_occurring.each do |result| %>
190
+ <% error = result[:error] %>
191
+ <% frequency = result[:frequency] %>
192
+ <% avg_delay = result[:avg_delay_seconds] %>
193
+ <tr>
194
+ <td>
195
+ <span class="badge bg-danger"><%= error.error_type %></span>
196
+ </td>
197
+ <td>
198
+ <span data-bs-toggle="tooltip" title="<%= error.message %>">
199
+ <%= truncate(error.message, length: 60) %>
200
+ </span>
201
+ </td>
202
+ <td>
203
+ <% if error.platform.present? %>
204
+ <span class="badge bg-secondary"><%= error.platform %></span>
205
+ <% end %>
206
+ </td>
207
+ <td>
208
+ <span class="badge bg-info text-dark">
209
+ <%= frequency %> time<%= frequency > 1 ? 's' : '' %>
210
+ </span>
211
+ </td>
212
+ <td>
213
+ <% if avg_delay < 0 %>
214
+ <span class="text-warning"><%= number_to_human(avg_delay.abs, units: {unit: "s", thousand: "s"}, precision: 1) %> before</span>
215
+ <% elsif avg_delay > 0 %>
216
+ <span class="text-success"><%= number_to_human(avg_delay, units: {unit: "s", thousand: "s"}, precision: 1) %> after</span>
217
+ <% else %>
218
+ <span class="text-muted">simultaneous</span>
219
+ <% end %>
220
+ </td>
221
+ <td class="text-end">
222
+ <%= link_to "View", error_path(error), class: "btn btn-sm btn-outline-primary" %>
223
+ </td>
224
+ </tr>
225
+ <% end %>
226
+ </tbody>
227
+ </table>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ <% end %>
232
+ <% end %>
233
+
79
234
  <!-- Related Errors -->
80
235
  <% if @related_errors.any? %>
81
236
  <div class="card">
@@ -130,6 +285,148 @@
130
285
  </div>
131
286
  </div>
132
287
  <% end %>
288
+
289
+ <!-- Error Cascades -->
290
+ <% if RailsErrorDashboard.configuration.enable_error_cascades %>
291
+ <% cascades = @error.error_cascades %>
292
+ <% if cascades[:parents].any? || cascades[:children].any? %>
293
+ <div class="card">
294
+ <div class="card-header bg-white">
295
+ <h5 class="mb-0">
296
+ <i class="bi bi-diagram-3"></i> Error Cascades
297
+ <small class="text-muted ms-2">Causal relationships between errors</small>
298
+ </h5>
299
+ </div>
300
+ <div class="card-body">
301
+ <!-- Parent Errors (What causes this error) -->
302
+ <% if cascades[:parents].any? %>
303
+ <div class="mb-4">
304
+ <h6 class="text-danger">
305
+ <i class="bi bi-arrow-down-circle"></i> Triggered By
306
+ <small class="text-muted">(errors that cause this one)</small>
307
+ </h6>
308
+ <div class="table-responsive">
309
+ <table class="table table-sm table-hover mb-0">
310
+ <thead class="table-light">
311
+ <tr>
312
+ <th>Error Type</th>
313
+ <th>Message</th>
314
+ <th>Probability</th>
315
+ <th>Frequency</th>
316
+ <th>Avg Delay</th>
317
+ <th></th>
318
+ </tr>
319
+ </thead>
320
+ <tbody>
321
+ <% cascades[:parents].each do |cascade| %>
322
+ <% parent = cascade[:error] %>
323
+ <tr>
324
+ <td>
325
+ <span class="badge bg-<%= severity_color(parent.severity) %>">
326
+ <%= parent.error_type %>
327
+ </span>
328
+ </td>
329
+ <td>
330
+ <div class="text-truncate" style="max-width: 250px;">
331
+ <%= parent.message %>
332
+ </div>
333
+ </td>
334
+ <td>
335
+ <% probability_pct = (cascade[:probability] * 100).round(1) %>
336
+ <span class="badge <%= probability_pct >= 70 ? 'bg-danger' : 'bg-warning' %>">
337
+ <%= probability_pct %>%
338
+ </span>
339
+ </td>
340
+ <td>
341
+ <span class="badge bg-secondary"><%= cascade[:frequency] %>x</span>
342
+ </td>
343
+ <td>
344
+ <small class="text-muted">
345
+ <%= cascade[:avg_delay_seconds].round(1) %>s
346
+ </small>
347
+ </td>
348
+ <td>
349
+ <%= link_to "View", error_path(parent), class: "btn btn-sm btn-outline-primary" %>
350
+ </td>
351
+ </tr>
352
+ <% end %>
353
+ </tbody>
354
+ </table>
355
+ </div>
356
+ </div>
357
+ <% end %>
358
+
359
+ <!-- Child Errors (What this error causes) -->
360
+ <% if cascades[:children].any? %>
361
+ <div>
362
+ <h6 class="text-warning">
363
+ <i class="bi bi-arrow-up-circle"></i> Triggers
364
+ <small class="text-muted">(errors caused by this one)</small>
365
+ </h6>
366
+ <div class="table-responsive">
367
+ <table class="table table-sm table-hover mb-0">
368
+ <thead class="table-light">
369
+ <tr>
370
+ <th>Error Type</th>
371
+ <th>Message</th>
372
+ <th>Probability</th>
373
+ <th>Frequency</th>
374
+ <th>Avg Delay</th>
375
+ <th></th>
376
+ </tr>
377
+ </thead>
378
+ <tbody>
379
+ <% cascades[:children].each do |cascade| %>
380
+ <% child = cascade[:error] %>
381
+ <tr>
382
+ <td>
383
+ <span class="badge bg-<%= severity_color(child.severity) %>">
384
+ <%= child.error_type %>
385
+ </span>
386
+ </td>
387
+ <td>
388
+ <div class="text-truncate" style="max-width: 250px;">
389
+ <%= child.message %>
390
+ </div>
391
+ </td>
392
+ <td>
393
+ <% probability_pct = (cascade[:probability] * 100).round(1) %>
394
+ <span class="badge <%= probability_pct >= 70 ? 'bg-danger' : 'bg-warning' %>">
395
+ <%= probability_pct %>%
396
+ </span>
397
+ </td>
398
+ <td>
399
+ <span class="badge bg-secondary"><%= cascade[:frequency] %>x</span>
400
+ </td>
401
+ <td>
402
+ <small class="text-muted">
403
+ +<%= cascade[:avg_delay_seconds].round(1) %>s
404
+ </small>
405
+ </td>
406
+ <td>
407
+ <%= link_to "View", error_path(child), class: "btn btn-sm btn-outline-primary" %>
408
+ </td>
409
+ </tr>
410
+ <% end %>
411
+ </tbody>
412
+ </table>
413
+ </div>
414
+ </div>
415
+ <% end %>
416
+
417
+ <div class="mt-3">
418
+ <small class="text-muted">
419
+ <i class="bi bi-info-circle"></i>
420
+ Cascade patterns show causal relationships. High probability (≥70%) and frequency (≥3) indicate strong cascades.
421
+ </small>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ <% end %>
426
+ <% end %>
427
+
428
+ <!-- Occurrence Patterns and Bursts -->
429
+ <%= render "pattern_insights" %>
133
430
  </div>
134
431
 
135
432
  <!-- Sidebar -->
@@ -141,16 +438,37 @@
141
438
  </div>
142
439
  <div class="card-body">
143
440
  <div class="mb-3">
144
- <small class="text-muted d-block mb-1">Occurred At</small>
145
- <strong><%= @error.occurred_at.strftime("%B %d, %Y") %></strong><br>
146
- <small><%= @error.occurred_at.strftime("%I:%M:%S %p %Z") %></small>
441
+ <small class="text-muted d-block mb-1">Occurrence Count</small>
442
+ <span class="badge bg-primary fs-5"><%= @error.occurrence_count %>x</span>
443
+ <% if @error.occurrence_count > 1 %>
444
+ <br><small class="text-muted">This error has occurred <%= @error.occurrence_count %> times</small>
445
+ <% end %>
147
446
  </div>
148
447
 
149
448
  <div class="mb-3">
150
- <small class="text-muted d-block mb-1">Environment</small>
151
- <span class="badge <%= @error.environment == 'production' ? 'bg-danger' : 'bg-info' %>">
152
- <%= @error.environment.titleize %>
153
- </span>
449
+ <small class="text-muted d-block mb-1">First Seen</small>
450
+ <strong><%= @error.first_seen_at&.strftime("%B %d, %Y") || 'N/A' %></strong><br>
451
+ <small><%= @error.first_seen_at&.strftime("%I:%M:%S %p %Z") || 'N/A' %></small>
452
+ </div>
453
+
454
+ <div class="mb-3">
455
+ <small class="text-muted d-block mb-1">Last Seen</small>
456
+ <strong><%= @error.last_seen_at&.strftime("%B %d, %Y") || 'N/A' %></strong><br>
457
+ <small><%= @error.last_seen_at&.strftime("%I:%M:%S %p %Z") || 'N/A' %></small>
458
+ </div>
459
+
460
+ <div class="mb-3">
461
+ <small class="text-muted d-block mb-1">Severity Level</small>
462
+ <% severity = @error.severity %>
463
+ <% if severity == :critical %>
464
+ <span class="badge bg-danger fs-6">CRITICAL</span>
465
+ <% elsif severity == :high %>
466
+ <span class="badge bg-warning text-dark fs-6">HIGH</span>
467
+ <% elsif severity == :medium %>
468
+ <span class="badge bg-info text-dark fs-6">MEDIUM</span>
469
+ <% else %>
470
+ <span class="badge bg-secondary fs-6">LOW</span>
471
+ <% end %>
154
472
  </div>
155
473
 
156
474
  <div class="mb-3">
@@ -166,9 +484,11 @@
166
484
 
167
485
  <div class="mb-3">
168
486
  <small class="text-muted d-block mb-1">User</small>
169
- <% if @error.user %>
487
+ <% if @error.respond_to?(:user) && @error.user %>
170
488
  <strong><%= @error.user.email %></strong><br>
171
489
  <small class="text-muted">ID: <%= @error.user_id %></small>
490
+ <% elsif @error.user_id %>
491
+ <span class="text-muted">User ID: <%= @error.user_id %></span>
172
492
  <% else %>
173
493
  <span class="text-muted">Guest / Unauthenticated</span>
174
494
  <% end %>
@@ -223,6 +543,103 @@
223
543
  </div>
224
544
  </div>
225
545
 
546
+ <!-- Baseline Statistics -->
547
+ <% if RailsErrorDashboard.configuration.enable_baseline_alerts %>
548
+ <% baselines = @error.baselines %>
549
+ <% if baselines[:hourly].present? || baselines[:daily].present? || baselines[:weekly].present? %>
550
+ <div class="card mb-4">
551
+ <div class="card-header bg-white">
552
+ <h5 class="mb-0">
553
+ <i class="bi bi-graph-up"></i> Baseline Statistics
554
+ <small class="text-muted ms-2">Historical patterns</small>
555
+ </h5>
556
+ </div>
557
+ <div class="card-body">
558
+ <% anomaly = @error.baseline_anomaly %>
559
+ <% if anomaly[:anomaly] %>
560
+ <div class="alert alert-<%= anomaly[:level] == :critical ? 'danger' : anomaly[:level] == :high ? 'warning' : 'info' %> mb-3">
561
+ <i class="bi bi-exclamation-triangle-fill"></i>
562
+ <strong>Anomaly Detected!</strong><br>
563
+ <small>
564
+ This error is <%= anomaly[:std_devs_above]&.round(1) %>σ above baseline
565
+ (<%= anomaly[:level].to_s.upcase %>)
566
+ </small>
567
+ </div>
568
+ <% end %>
569
+
570
+ <% [:hourly, :daily, :weekly].each do |type| %>
571
+ <% baseline = baselines[type] %>
572
+ <% next unless baseline %>
573
+
574
+ <div class="mb-3">
575
+ <small class="text-muted d-block mb-1 text-capitalize">
576
+ <i class="bi bi-<%= type == :hourly ? 'clock' : type == :daily ? 'calendar-day' : 'calendar-week' %>"></i>
577
+ <%= type.to_s.capitalize %> Baseline
578
+ </small>
579
+
580
+ <div class="row g-2">
581
+ <div class="col-6">
582
+ <div class="text-center p-2 bg-light rounded">
583
+ <small class="text-muted d-block">Mean</small>
584
+ <strong><%= baseline.mean&.round(1) || 'N/A' %></strong>
585
+ </div>
586
+ </div>
587
+ <div class="col-6">
588
+ <div class="text-center p-2 bg-light rounded">
589
+ <small class="text-muted d-block">Std Dev</small>
590
+ <strong><%= baseline.std_dev&.round(1) || 'N/A' %></strong>
591
+ </div>
592
+ </div>
593
+ </div>
594
+
595
+ <div class="row g-2 mt-2">
596
+ <div class="col-6">
597
+ <div class="text-center p-2 bg-light rounded">
598
+ <small class="text-muted d-block">95th %</small>
599
+ <strong><%= baseline.percentile_95&.round(1) || 'N/A' %></strong>
600
+ </div>
601
+ </div>
602
+ <div class="col-6">
603
+ <div class="text-center p-2 bg-light rounded">
604
+ <small class="text-muted d-block">99th %</small>
605
+ <strong><%= baseline.percentile_99&.round(1) || 'N/A' %></strong>
606
+ </div>
607
+ </div>
608
+ </div>
609
+
610
+ <% if baseline.mean && baseline.std_dev %>
611
+ <div class="mt-2">
612
+ <small class="text-muted">
613
+ Threshold (2σ): <strong><%= baseline.threshold(sensitivity: 2)&.round(1) %></strong>
614
+ </small>
615
+ </div>
616
+ <% end %>
617
+
618
+ <div class="mt-2">
619
+ <small class="text-muted">
620
+ Sample: <%= baseline.sample_size %> periods
621
+ <span class="text-muted">|</span>
622
+ Updated: <%= baseline.updated_at&.strftime("%m/%d %I:%M%p") %>
623
+ </small>
624
+ </div>
625
+ </div>
626
+
627
+ <% unless baseline == baselines.values.compact.last %>
628
+ <hr>
629
+ <% end %>
630
+ <% end %>
631
+
632
+ <div class="mt-3">
633
+ <small class="text-muted">
634
+ <i class="bi bi-info-circle"></i>
635
+ Baselines establish "normal" error rates. Anomalies are detected when current rates exceed baseline + 2σ.
636
+ </small>
637
+ </div>
638
+ </div>
639
+ </div>
640
+ <% end %>
641
+ <% end %>
642
+
226
643
  <!-- Quick Actions -->
227
644
  <div class="card">
228
645
  <div class="card-header bg-white">
@@ -234,7 +651,7 @@
234
651
  <i class="bi bi-filter"></i> View Similar Errors
235
652
  <% end %>
236
653
 
237
- <% if @error.user %>
654
+ <% if @error.user_id %>
238
655
  <%= link_to errors_path(user_id: @error.user_id), class: "btn btn-outline-primary btn-sm" do %>
239
656
  <i class="bi bi-person"></i> View User's Errors
240
657
  <% end %>
@@ -258,7 +675,7 @@
258
675
  <div class="modal fade" id="resolveModal" tabindex="-1" aria-labelledby="resolveModalLabel" aria-hidden="true">
259
676
  <div class="modal-dialog">
260
677
  <div class="modal-content">
261
- <%= form_with url: resolve_error_path(@error), method: :patch, class: "modal-content" do |f| %>
678
+ <%= form_with url: resolve_error_path(@error), method: :post, class: "modal-content" do |f| %>
262
679
  <div class="modal-header">
263
680
  <h5 class="modal-title" id="resolveModalLabel">
264
681
  <i class="bi bi-check-circle"></i> Mark Error as Resolved
data/config/routes.rb CHANGED
@@ -7,6 +7,8 @@ RailsErrorDashboard::Engine.routes.draw do
7
7
  end
8
8
  collection do
9
9
  get :analytics
10
+ get :platform_comparison
11
+ get :correlation
10
12
  post :batch_action
11
13
  end
12
14
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddOptimizedIndexesToErrorLogs < ActiveRecord::Migration[8.1]
4
+ def change
5
+ # Composite indexes for common query patterns
6
+ # These improve performance when filtering and sorting together
7
+
8
+ # Dashboard stats: Count unresolved errors from recent time periods
9
+ # Query: WHERE resolved = false AND occurred_at >= ?
10
+ add_index :rails_error_dashboard_error_logs, [ :resolved, :occurred_at ],
11
+ name: 'index_error_logs_on_resolved_and_occurred_at'
12
+
13
+ # Error type filtering with time ordering
14
+ # Query: WHERE error_type = ? ORDER BY occurred_at DESC
15
+ add_index :rails_error_dashboard_error_logs, [ :error_type, :occurred_at ],
16
+ name: 'index_error_logs_on_error_type_and_occurred_at'
17
+
18
+ # Platform filtering with time ordering
19
+ # Query: WHERE platform = ? ORDER BY occurred_at DESC
20
+ add_index :rails_error_dashboard_error_logs, [ :platform, :occurred_at ],
21
+ name: 'index_error_logs_on_platform_and_occurred_at'
22
+
23
+ # Deduplication lookup: Find existing unresolved errors by hash within 24 hours
24
+ # Query: WHERE error_hash = ? AND resolved = false AND occurred_at >= ?
25
+ # This is the hot path for error logging - happens on EVERY error
26
+ add_index :rails_error_dashboard_error_logs, [ :error_hash, :resolved, :occurred_at ],
27
+ name: 'index_error_logs_on_hash_resolved_occurred'
28
+
29
+ # Partial index for unresolved errors (most queries filter by resolved=false)
30
+ # Only indexes unresolved errors, making it smaller and faster
31
+ # PostgreSQL-specific but gracefully ignored on other databases
32
+ if postgresql?
33
+ add_index :rails_error_dashboard_error_logs, :occurred_at,
34
+ where: "resolved = false",
35
+ name: 'index_error_logs_on_occurred_at_unresolved'
36
+
37
+ # Full-text search index for message field (PostgreSQL only)
38
+ # Dramatically improves search performance
39
+ execute <<-SQL
40
+ CREATE INDEX index_error_logs_on_message_gin
41
+ ON rails_error_dashboard_error_logs
42
+ USING gin(to_tsvector('english', message))
43
+ SQL
44
+ end
45
+ end
46
+
47
+ def down
48
+ # Remove composite indexes
49
+ remove_index :rails_error_dashboard_error_logs, name: 'index_error_logs_on_resolved_and_occurred_at'
50
+ remove_index :rails_error_dashboard_error_logs, name: 'index_error_logs_on_error_type_and_occurred_at'
51
+ remove_index :rails_error_dashboard_error_logs, name: 'index_error_logs_on_platform_and_occurred_at'
52
+ remove_index :rails_error_dashboard_error_logs, name: 'index_error_logs_on_hash_resolved_occurred'
53
+
54
+ # Remove partial/GIN indexes if PostgreSQL
55
+ if postgresql?
56
+ remove_index :rails_error_dashboard_error_logs, name: 'index_error_logs_on_occurred_at_unresolved'
57
+ execute "DROP INDEX IF EXISTS index_error_logs_on_message_gin"
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def postgresql?
64
+ ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql'
65
+ end
66
+ end
@@ -0,0 +1,26 @@
1
+ class RemoveEnvironmentFromErrorLogs < ActiveRecord::Migration[8.1]
2
+ def up
3
+ # Remove composite index first
4
+ remove_index :rails_error_dashboard_error_logs,
5
+ name: 'index_error_logs_on_environment_and_occurred_at',
6
+ if_exists: true
7
+
8
+ # Remove single column index
9
+ remove_index :rails_error_dashboard_error_logs,
10
+ column: :environment,
11
+ if_exists: true
12
+
13
+ # Remove the column
14
+ remove_column :rails_error_dashboard_error_logs, :environment, :string
15
+ end
16
+
17
+ def down
18
+ # Add column back
19
+ add_column :rails_error_dashboard_error_logs, :environment, :string, null: false, default: 'production'
20
+
21
+ # Recreate indexes
22
+ add_index :rails_error_dashboard_error_logs, :environment
23
+ add_index :rails_error_dashboard_error_logs, [ :environment, :occurred_at ],
24
+ name: 'index_error_logs_on_environment_and_occurred_at'
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ class AddEnhancedMetricsToErrorLogs < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :rails_error_dashboard_error_logs, :app_version, :string
4
+ add_column :rails_error_dashboard_error_logs, :git_sha, :string
5
+ add_column :rails_error_dashboard_error_logs, :priority_score, :integer
6
+
7
+ # Indexes for enhanced metrics
8
+ add_index :rails_error_dashboard_error_logs, :app_version
9
+ add_index :rails_error_dashboard_error_logs, :git_sha
10
+ add_index :rails_error_dashboard_error_logs, :priority_score
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ class AddSimilarityTrackingToErrorLogs < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :rails_error_dashboard_error_logs, :similarity_score, :float
4
+ add_column :rails_error_dashboard_error_logs, :backtrace_signature, :string
5
+
6
+ add_index :rails_error_dashboard_error_logs, :similarity_score
7
+ add_index :rails_error_dashboard_error_logs, :backtrace_signature
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateErrorOccurrences < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :rails_error_dashboard_error_occurrences do |t|
6
+ t.references :error_log, null: false, foreign_key: { to_table: :rails_error_dashboard_error_logs }
7
+ t.datetime :occurred_at, null: false
8
+ t.integer :user_id
9
+ t.string :request_id
10
+ t.string :session_id
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ # Index for finding co-occurring errors by time window
16
+ add_index :rails_error_dashboard_error_occurrences, [ :occurred_at, :error_log_id ],
17
+ name: 'index_error_occurrences_on_time_and_error'
18
+
19
+ # Index for finding all occurrences of a specific error
20
+ add_index :rails_error_dashboard_error_occurrences, :error_log_id,
21
+ name: 'index_error_occurrences_on_error_log'
22
+
23
+ # Index for finding errors by user
24
+ add_index :rails_error_dashboard_error_occurrences, :user_id,
25
+ name: 'index_error_occurrences_on_user'
26
+
27
+ # Index for finding errors by request
28
+ add_index :rails_error_dashboard_error_occurrences, :request_id,
29
+ name: 'index_error_occurrences_on_request'
30
+ end
31
+ end