rails_error_dashboard 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +257 -700
- data/app/controllers/rails_error_dashboard/application_controller.rb +18 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +47 -4
- data/app/helpers/rails_error_dashboard/application_helper.rb +17 -0
- data/app/jobs/rails_error_dashboard/application_job.rb +19 -0
- data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +48 -0
- data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +263 -0
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -8
- data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +2 -1
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +5 -5
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +10 -6
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +5 -6
- data/app/mailers/rails_error_dashboard/application_mailer.rb +1 -1
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +1 -1
- data/app/models/rails_error_dashboard/cascade_pattern.rb +74 -0
- data/app/models/rails_error_dashboard/error_baseline.rb +100 -0
- data/app/models/rails_error_dashboard/error_log.rb +326 -3
- data/app/models/rails_error_dashboard/error_occurrence.rb +49 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +150 -9
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +3 -10
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +1 -2
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +76 -0
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +209 -0
- data/app/views/rails_error_dashboard/errors/_stats.html.erb +34 -0
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +19 -39
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +373 -0
- data/app/views/rails_error_dashboard/errors/index.html.erb +215 -138
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +388 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +428 -11
- data/config/routes.rb +2 -0
- data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +66 -0
- data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +26 -0
- data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +12 -0
- data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +9 -0
- data/db/migrate/20251225100236_create_error_occurrences.rb +31 -0
- data/db/migrate/20251225101920_create_cascade_patterns.rb +33 -0
- data/db/migrate/20251225102500_create_error_baselines.rb +38 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +270 -1
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +251 -37
- data/lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb +36 -0
- data/lib/generators/rails_error_dashboard/solid_queue/templates/queue.yml +55 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +234 -7
- data/lib/rails_error_dashboard/commands/resolve_error.rb +16 -0
- data/lib/rails_error_dashboard/configuration.rb +82 -5
- data/lib/rails_error_dashboard/error_reporter.rb +15 -7
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +17 -10
- data/lib/rails_error_dashboard/plugin.rb +6 -3
- data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +0 -1
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +2 -3
- data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +0 -2
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +44 -6
- data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
- data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +86 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +134 -2
- data/lib/rails_error_dashboard/queries/error_cascades.rb +74 -0
- data/lib/rails_error_dashboard/queries/error_correlation.rb +375 -0
- data/lib/rails_error_dashboard/queries/errors_list.rb +52 -11
- data/lib/rails_error_dashboard/queries/filter_options.rb +0 -1
- data/lib/rails_error_dashboard/queries/platform_comparison.rb +254 -0
- data/lib/rails_error_dashboard/queries/similar_errors.rb +93 -0
- data/lib/rails_error_dashboard/services/baseline_alert_throttler.rb +88 -0
- data/lib/rails_error_dashboard/services/baseline_calculator.rb +269 -0
- data/lib/rails_error_dashboard/services/cascade_detector.rb +95 -0
- data/lib/rails_error_dashboard/services/pattern_detector.rb +268 -0
- data/lib/rails_error_dashboard/services/similarity_calculator.rb +144 -0
- data/lib/rails_error_dashboard/value_objects/error_context.rb +27 -1
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +55 -7
- metadata +52 -9
- data/app/models/rails_error_dashboard/application_record.rb +0 -5
- data/lib/rails_error_dashboard/queries/developer_insights.rb +0 -277
- data/lib/rails_error_dashboard/queries/errors_list_v2.rb +0 -149
|
@@ -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">
|
|
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">
|
|
145
|
-
<
|
|
146
|
-
|
|
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">
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
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.
|
|
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: :
|
|
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
|
@@ -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
|