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
@@ -1,97 +1,122 @@
1
+ <!-- Subscribe to Turbo Stream updates -->
2
+ <%= turbo_stream_from "error_list" %>
3
+
1
4
  <div class="py-4">
2
5
  <div class="d-flex justify-content-between align-items-center mb-4">
3
6
  <h2 class="mb-0"><i class="bi bi-bug-fill text-primary"></i> Error Overview</h2>
4
7
  <div class="text-muted">
5
- <small>Last updated: <%= Time.current.strftime("%B %d, %Y %I:%M %p") %></small>
8
+ <small>
9
+ Last updated: <%= Time.current.strftime("%B %d, %Y %I:%M %p") %>
10
+ <span class="badge bg-success ms-2" id="live-indicator">
11
+ <i class="bi bi-broadcast"></i> Live
12
+ </span>
13
+ </small>
6
14
  </div>
7
15
  </div>
8
16
 
9
17
  <!-- Stats Cards -->
10
- <div class="row g-4 mb-4">
11
- <div class="col-md-3">
12
- <div class="card stat-card">
13
- <div class="card-body">
14
- <div class="stat-label mb-2">Today</div>
15
- <div class="stat-value text-info"><%= @stats[:total_today] %></div>
16
- </div>
17
- </div>
18
- </div>
19
- <div class="col-md-3">
20
- <div class="card stat-card">
21
- <div class="card-body">
22
- <div class="stat-label mb-2">This Week</div>
23
- <div class="stat-value text-primary"><%= @stats[:total_week] %></div>
24
- </div>
25
- </div>
26
- </div>
27
- <div class="col-md-3">
28
- <div class="card stat-card">
29
- <div class="card-body">
30
- <div class="stat-label mb-2">Unresolved</div>
31
- <div class="stat-value text-danger"><%= @stats[:unresolved] %></div>
32
- </div>
33
- </div>
34
- </div>
35
- <div class="col-md-3">
36
- <div class="card stat-card">
37
- <div class="card-body">
38
- <div class="stat-label mb-2">Resolved</div>
39
- <div class="stat-value text-success"><%= @stats[:resolved] %></div>
18
+ <div id="dashboard_stats" class="mb-4">
19
+ <%= render "stats", stats: @stats %>
20
+ </div>
21
+
22
+ <!-- Top Error Types (Only show if there are errors) -->
23
+ <% if @stats[:top_errors].any? %>
24
+ <div class="row g-4 mb-4">
25
+ <div class="col-md-12">
26
+ <div class="card">
27
+ <div class="card-header bg-white">
28
+ <h5 class="mb-0">Top Error Types</h5>
29
+ </div>
30
+ <div class="card-body">
31
+ <div class="row">
32
+ <% @stats[:top_errors].first(5).each do |error_type, count| %>
33
+ <div class="col-md-2">
34
+ <div class="text-center p-3 border rounded">
35
+ <div class="fw-bold text-danger" style="font-size: 1.5rem;"><%= count %></div>
36
+ <small class="text-muted text-truncate d-block" title="<%= error_type %>"><%= error_type.split('::').last %></small>
37
+ </div>
38
+ </div>
39
+ <% end %>
40
+ </div>
41
+ </div>
40
42
  </div>
41
43
  </div>
42
44
  </div>
43
- </div>
45
+ <% end %>
44
46
 
45
- <!-- Platform & Environment Breakdown -->
46
- <div class="row g-4 mb-4">
47
- <div class="col-md-6">
48
- <div class="card">
49
- <div class="card-header bg-white">
50
- <h5 class="mb-0">Platform Breakdown</h5>
51
- </div>
52
- <div class="card-body">
53
- <% if @stats[:by_platform].any? %>
54
- <% @stats[:by_platform].each do |platform, count| %>
55
- <div class="d-flex justify-content-between align-items-center mb-2">
56
- <span>
57
- <% if platform == 'iOS' %>
58
- <span class="badge badge-ios">iOS</span>
59
- <% elsif platform == 'Android' %>
60
- <span class="badge badge-android">Android</span>
61
- <% else %>
62
- <span class="badge badge-api">API</span>
63
- <% end %>
64
- </span>
65
- <span class="fw-bold"><%= count %></span>
66
- </div>
47
+ <!-- Spike Detection Alert -->
48
+ <% if @stats[:spike_detected] %>
49
+ <div class="alert alert-warning mb-4" role="alert">
50
+ <div class="d-flex align-items-center">
51
+ <i class="bi bi-exclamation-triangle-fill fs-3 me-3"></i>
52
+ <div>
53
+ <h5 class="alert-heading mb-1">
54
+ <% case @stats[:spike_info][:severity] %>
55
+ <% when :critical %>
56
+ 🚨 Critical Error Spike Detected!
57
+ <% when :high %>
58
+ ⚠️ High Error Spike Detected
59
+ <% when :elevated %>
60
+ 📈 Elevated Error Activity
67
61
  <% end %>
68
- <% else %>
69
- <p class="text-muted mb-0">No errors recorded yet</p>
70
- <% end %>
62
+ </h5>
63
+ <p class="mb-0">
64
+ Today: <strong><%= @stats[:spike_info][:today_count] %> errors</strong>
65
+ (7-day avg: <%= @stats[:spike_info][:avg_count] %>) —
66
+ <strong><%= @stats[:spike_info][:multiplier] %>x normal levels</strong>
67
+ </p>
71
68
  </div>
72
69
  </div>
73
70
  </div>
71
+ <% end %>
74
72
 
75
- <div class="col-md-6">
76
- <div class="card">
77
- <div class="card-header bg-white">
78
- <h5 class="mb-0">Top Error Types</h5>
79
- </div>
80
- <div class="card-body">
81
- <% if @stats[:top_errors].any? %>
82
- <% @stats[:top_errors].first(5).each do |error_type, count| %>
83
- <div class="d-flex justify-content-between align-items-center mb-2">
84
- <small class="text-truncate" style="max-width: 70%;"><%= error_type %></small>
85
- <span class="badge bg-secondary"><%= count %></span>
86
- </div>
73
+ <!-- 7-Day Error Trend -->
74
+ <% if @stats[:errors_trend_7d]&.any? %>
75
+ <div class="row g-4 mb-4">
76
+ <div class="col-md-8">
77
+ <div class="card">
78
+ <div class="card-header bg-white d-flex justify-content-between align-items-center">
79
+ <h5 class="mb-0"><i class="bi bi-graph-up"></i> 7-Day Error Trend</h5>
80
+ <%= link_to analytics_errors_path, class: "btn btn-sm btn-outline-primary" do %>
81
+ <i class="bi bi-bar-chart"></i> Full Analytics
87
82
  <% end %>
88
- <% else %>
89
- <p class="text-muted mb-0">No errors recorded yet</p>
90
- <% end %>
83
+ </div>
84
+ <div class="card-body">
85
+ <%= line_chart @stats[:errors_trend_7d],
86
+ color: "#8B5CF6",
87
+ curve: false,
88
+ points: true,
89
+ height: "250px",
90
+ library: {
91
+ plugins: {
92
+ legend: { display: false }
93
+ },
94
+ scales: {
95
+ y: {
96
+ beginAtZero: true,
97
+ ticks: { precision: 0 }
98
+ }
99
+ }
100
+ } %>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ <div class="col-md-4">
105
+ <div class="card">
106
+ <div class="card-header bg-white">
107
+ <h5 class="mb-0"><i class="bi bi-pie-chart"></i> By Severity (7d)</h5>
108
+ </div>
109
+ <div class="card-body">
110
+ <%= pie_chart @stats[:errors_by_severity_7d],
111
+ colors: ["#EF4444", "#F59E0B", "#3B82F6", "#6B7280"],
112
+ height: "250px",
113
+ legend: "bottom",
114
+ donut: true %>
115
+ </div>
91
116
  </div>
92
117
  </div>
93
118
  </div>
94
- </div>
119
+ <% end %>
95
120
 
96
121
  <!-- Filters -->
97
122
  <div class="card mb-4">
@@ -100,24 +125,38 @@
100
125
  </div>
101
126
  <div class="card-body">
102
127
  <%= form_with url: errors_path, method: :get, class: "row g-3" do %>
103
- <div class="col-md-3">
128
+ <div class="col-md-4">
104
129
  <%= text_field_tag :search, params[:search], placeholder: "Search errors...", class: "form-control" %>
105
130
  </div>
131
+
132
+ <% if @platforms.size > 1 %>
133
+ <div class="col-md-2">
134
+ <%= select_tag :platform, options_for_select([['All Platforms', '']] + @platforms.map { |p| [p, p] }, params[:platform]), class: "form-select" %>
135
+ </div>
136
+ <% end %>
137
+
106
138
  <div class="col-md-2">
107
- <%= select_tag :environment, options_for_select([['All Environments', '']] + @environments.map { |e| [e.titleize, e] }, params[:environment]), class: "form-select" %>
108
- </div>
109
- <div class="col-md-2">
110
- <%= select_tag :platform, options_for_select([['All Platforms', '']] + @platforms.map { |p| [p, p] }, params[:platform]), class: "form-select" %>
111
- </div>
112
- <div class="col-md-3">
113
139
  <%= select_tag :error_type, options_for_select([['All Types', '']] + @error_types.map { |t| [t, t] }, params[:error_type]), class: "form-select" %>
114
140
  </div>
141
+
115
142
  <div class="col-md-2">
116
- <div class="form-check">
117
- <%= check_box_tag :unresolved, true, params[:unresolved] == 'true', class: "form-check-input" %>
143
+ <%= select_tag :severity, options_for_select([
144
+ ['All Severities', ''],
145
+ ['🔴 Critical', 'critical'],
146
+ ['🟠 High', 'high'],
147
+ ['🟡 Medium', 'medium'],
148
+ ['⚪ Low', 'low']
149
+ ], params[:severity]), class: "form-select" %>
150
+ </div>
151
+
152
+ <div class="col-auto">
153
+ <div class="form-check mt-2">
154
+ <%= check_box_tag :unresolved, "true", params[:unresolved] != "false" && params[:unresolved] != "0", class: "form-check-input", data: { uncheck_value: "false" } %>
118
155
  <%= label_tag :unresolved, "Unresolved only", class: "form-check-label" %>
156
+ <%= hidden_field_tag :unresolved, "false", id: nil %>
119
157
  </div>
120
158
  </div>
159
+
121
160
  <div class="col-12">
122
161
  <%= submit_tag "Apply Filters", class: "btn btn-primary" %>
123
162
  <%= link_to "Clear", errors_path, class: "btn btn-outline-secondary" %>
@@ -168,67 +207,21 @@
168
207
  <th style="width: 40px;">
169
208
  <input type="checkbox" id="select-all" class="form-check-input">
170
209
  </th>
171
- <th>Time</th>
210
+ <th>Severity</th>
172
211
  <th>Error Type</th>
173
212
  <th>Message</th>
174
- <th>Platform</th>
175
- <th>Environment</th>
176
- <th>User</th>
213
+ <th>Occurrences</th>
214
+ <th>First / Last Seen</th>
215
+ <% if @platforms.size > 1 %>
216
+ <th>Platform</th>
217
+ <% end %>
177
218
  <th>Status</th>
178
219
  <th></th>
179
220
  </tr>
180
221
  </thead>
181
- <tbody>
222
+ <tbody id="error_list">
182
223
  <% @errors.each do |error| %>
183
- <tr>
184
- <td onclick="event.stopPropagation();">
185
- <input type="checkbox" class="error-checkbox form-check-input" value="<%= error.id %>" data-error-id="<%= error.id %>">
186
- </td>
187
- <td onclick="window.location='<%= error_path(error) %>';">
188
- <small><%= error.occurred_at.strftime("%m/%d %I:%M%p") %></small>
189
- </td>
190
- <td onclick="window.location='<%= error_path(error) %>';">
191
- <code class="text-danger"><%= error.error_type.split('::').last %></code>
192
- </td>
193
- <td onclick="window.location='<%= error_path(error) %>';">
194
- <div class="text-truncate" style="max-width: 300px;" title="<%= error.message %>">
195
- <%= error.message %>
196
- </div>
197
- </td>
198
- <td onclick="window.location='<%= error_path(error) %>';">
199
- <% if error.platform == 'iOS' %>
200
- <span class="badge badge-ios">iOS</span>
201
- <% elsif error.platform == 'Android' %>
202
- <span class="badge badge-android">Android</span>
203
- <% else %>
204
- <span class="badge badge-api"><%= error.platform || 'API' %></span>
205
- <% end %>
206
- </td>
207
- <td onclick="window.location='<%= error_path(error) %>';">
208
- <span class="badge <%= error.environment == 'production' ? 'bg-danger' : 'bg-info' %>">
209
- <%= error.environment %>
210
- </span>
211
- </td>
212
- <td onclick="window.location='<%= error_path(error) %>';">
213
- <% if error.user %>
214
- <small><%= error.user.email %></small>
215
- <% else %>
216
- <small class="text-muted">Guest</small>
217
- <% end %>
218
- </td>
219
- <td onclick="window.location='<%= error_path(error) %>';">
220
- <% if error.resolved? %>
221
- <i class="bi bi-check-circle-fill text-success" title="Resolved"></i>
222
- <% else %>
223
- <i class="bi bi-exclamation-circle-fill text-danger" title="Unresolved"></i>
224
- <% end %>
225
- </td>
226
- <td onclick="event.stopPropagation();">
227
- <%= link_to error_path(error), class: "btn btn-sm btn-outline-primary" do %>
228
- <i class="bi bi-eye"></i>
229
- <% end %>
230
- </td>
231
- </tr>
224
+ <%= render "error_row", error: error, show_platform: @platforms.size > 1 %>
232
225
  <% end %>
233
226
  </tbody>
234
227
  </table>
@@ -240,8 +233,20 @@
240
233
  </div>
241
234
  <% else %>
242
235
  <div class="text-center py-5">
243
- <i class="bi bi-inbox display-1 text-muted"></i>
244
- <p class="text-muted mt-3">No errors found</p>
236
+ <i class="bi bi-check-circle display-1 text-success mb-3"></i>
237
+ <h4 class="text-muted">All Clear!</h4>
238
+ <p class="text-muted">
239
+ <% if params[:search].present? || params[:error_type].present? || params[:platform].present? || params[:severity].present? %>
240
+ No errors match your current filters. Try adjusting your search criteria.
241
+ <% else %>
242
+ No errors have been logged yet. Your application is running smoothly!
243
+ <% end %>
244
+ </p>
245
+ <% unless params.values.compact.any? %>
246
+ <small class="text-muted d-block mt-3">
247
+ <i class="bi bi-lightbulb"></i> Errors will appear here automatically when they occur.
248
+ </small>
249
+ <% end %>
245
250
  </div>
246
251
  <% end %>
247
252
  </div>
@@ -331,4 +336,76 @@
331
336
  });
332
337
  }
333
338
  });
339
+
340
+ // Turbo Stream animation for new errors
341
+ document.addEventListener('turbo:before-stream-render', (event) => {
342
+ const { target, action } = event.detail.newStream;
343
+
344
+ // Highlight new errors when prepended
345
+ if (action === 'prepend' && target === 'error_list') {
346
+ setTimeout(() => {
347
+ const firstRow = document.querySelector('#error_list tr:first-child');
348
+ if (firstRow) {
349
+ firstRow.classList.add('new-error');
350
+
351
+ // Remove class after animation completes
352
+ setTimeout(() => {
353
+ firstRow.classList.remove('new-error');
354
+ }, 3000);
355
+ }
356
+ }, 10);
357
+ }
358
+
359
+ // Pulse stats cards when updated
360
+ if (action === 'replace' && target === 'dashboard_stats') {
361
+ setTimeout(() => {
362
+ const statCards = document.querySelectorAll('.stat-card');
363
+ statCards.forEach(card => {
364
+ card.classList.add('updated');
365
+ setTimeout(() => card.classList.remove('updated'), 500);
366
+ });
367
+ }, 10);
368
+ }
369
+ });
370
+
371
+ // Initialize Bootstrap tooltips
372
+ document.addEventListener('DOMContentLoaded', function() {
373
+ const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
374
+ tooltipTriggerList.map(function (tooltipTriggerEl) {
375
+ return new bootstrap.Tooltip(tooltipTriggerEl);
376
+ });
377
+ });
378
+
379
+ // Keyboard shortcuts
380
+ document.addEventListener('keydown', function(e) {
381
+ // Ignore if typing in input/textarea
382
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
383
+
384
+ // 'r' - Refresh page
385
+ if (e.key === 'r') {
386
+ location.reload();
387
+ }
388
+
389
+ // '/' - Focus search
390
+ if (e.key === '/') {
391
+ e.preventDefault();
392
+ const searchInput = document.querySelector('input[name="search"]');
393
+ if (searchInput) searchInput.focus();
394
+ }
395
+
396
+ // 'a' - Go to analytics
397
+ if (e.key === 'a') {
398
+ window.location.href = '<%= analytics_errors_path %>';
399
+ }
400
+
401
+ // '?' - Show keyboard shortcuts help
402
+ if (e.key === '?') {
403
+ e.preventDefault();
404
+ alert('Keyboard Shortcuts:\n\n' +
405
+ 'r - Refresh page\n' +
406
+ '/ - Focus search\n' +
407
+ 'a - Analytics page\n' +
408
+ '? - Show this help');
409
+ }
410
+ });
334
411
  </script>