rails_error_dashboard 0.2.4 → 0.3.0

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -14
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +88 -1
  4. data/app/helpers/rails_error_dashboard/application_helper.rb +25 -0
  5. data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +18 -6
  6. data/app/views/layouts/rails_error_dashboard.html.erb +145 -1
  7. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +236 -0
  8. data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +70 -0
  9. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +107 -0
  10. data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +138 -0
  11. data/app/views/rails_error_dashboard/errors/_error_info.html.erb +190 -0
  12. data/app/views/rails_error_dashboard/errors/_modals.html.erb +139 -0
  13. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +1 -1
  14. data/app/views/rails_error_dashboard/errors/_request_context.html.erb +97 -0
  15. data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +156 -0
  16. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +352 -0
  17. data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +75 -0
  18. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +1 -1
  19. data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +143 -0
  20. data/app/views/rails_error_dashboard/errors/deprecations.html.erb +129 -0
  21. data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +134 -0
  22. data/app/views/rails_error_dashboard/errors/settings.html.erb +17 -0
  23. data/app/views/rails_error_dashboard/errors/show.html.erb +20 -1132
  24. data/config/routes.rb +3 -0
  25. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +6 -0
  26. data/db/migrate/20260303000001_add_breadcrumbs_to_error_logs.rb +9 -0
  27. data/db/migrate/20260304000001_add_system_health_to_error_logs.rb +12 -0
  28. data/lib/generators/rails_error_dashboard/install/install_generator.rb +31 -3
  29. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +67 -5
  30. data/lib/rails_error_dashboard/commands/log_error.rb +33 -0
  31. data/lib/rails_error_dashboard/configuration.rb +45 -3
  32. data/lib/rails_error_dashboard/engine.rb +6 -1
  33. data/lib/rails_error_dashboard/middleware/error_catcher.rb +8 -0
  34. data/lib/rails_error_dashboard/queries/cache_health_summary.rb +72 -0
  35. data/lib/rails_error_dashboard/queries/deprecation_warnings.rb +80 -0
  36. data/lib/rails_error_dashboard/queries/n_plus_one_summary.rb +83 -0
  37. data/lib/rails_error_dashboard/services/breadcrumb_collector.rb +182 -0
  38. data/lib/rails_error_dashboard/services/cache_analyzer.rb +76 -0
  39. data/lib/rails_error_dashboard/services/curl_generator.rb +80 -0
  40. data/lib/rails_error_dashboard/services/n_plus_one_detector.rb +74 -0
  41. data/lib/rails_error_dashboard/services/system_health_snapshot.rb +145 -0
  42. data/lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb +210 -0
  43. data/lib/rails_error_dashboard/version.rb +1 -1
  44. data/lib/rails_error_dashboard.rb +20 -0
  45. data/lib/tasks/error_dashboard.rake +68 -2
  46. metadata +27 -2
@@ -0,0 +1,352 @@
1
+ <!-- Metadata Card -->
2
+ <div class="card mb-4">
3
+ <div class="card-header bg-white">
4
+ <h5 class="mb-0"><i class="bi bi-info-circle"></i> Metadata</h5>
5
+ </div>
6
+ <div class="card-body">
7
+ <div class="mb-3">
8
+ <small class="metadata-label d-block mb-1">Occurrence Count</small>
9
+ <span class="badge bg-primary fs-5"><%= error.occurrence_count %>x</span>
10
+ <% if error.occurrence_count > 1 %>
11
+ <br><small class="text-muted">This error has occurred <%= error.occurrence_count %> times</small>
12
+ <% end %>
13
+ </div>
14
+
15
+ <div class="mb-3">
16
+ <small class="metadata-label d-block mb-1">First Seen</small>
17
+ <% if related_errors.any? %>
18
+ <%= link_to "#section-timeline", class: "text-decoration-none", data: { bs_toggle: "tooltip" }, title: "Jump to timeline" do %>
19
+ <strong><%= local_time(error.first_seen_at, format: :date_only) %></strong><br>
20
+ <small><%= local_time(error.first_seen_at, format: :time_only) %></small>
21
+ <i class="bi bi-arrow-down-circle ms-1"></i>
22
+ <% end %>
23
+ <% else %>
24
+ <strong><%= local_time(error.first_seen_at, format: :date_only) %></strong><br>
25
+ <small><%= local_time(error.first_seen_at, format: :time_only) %></small>
26
+ <% end %>
27
+ </div>
28
+
29
+ <div class="mb-3">
30
+ <small class="metadata-label d-block mb-1">Last Seen</small>
31
+ <strong><%= local_time(error.last_seen_at, format: :date_only) %></strong><br>
32
+ <small><%= local_time(error.last_seen_at, format: :time_only) %></small>
33
+ </div>
34
+
35
+ <div class="mb-3">
36
+ <small class="metadata-label d-block mb-1">Severity Level</small>
37
+ <% severity = error.severity %>
38
+ <% if severity == :critical %>
39
+ <span class="badge bg-danger fs-6">CRITICAL</span>
40
+ <% elsif severity == :high %>
41
+ <span class="badge bg-warning text-dark fs-6">HIGH</span>
42
+ <% elsif severity == :medium %>
43
+ <span class="badge bg-info text-dark fs-6">MEDIUM</span>
44
+ <% else %>
45
+ <span class="badge bg-secondary fs-6">LOW</span>
46
+ <% end %>
47
+ </div>
48
+
49
+ <div class="mb-3">
50
+ <small class="metadata-label d-block mb-1">Platform</small>
51
+ <% if error.platform == 'iOS' %>
52
+ <span class="badge badge-ios"><i class="bi bi-apple"></i> iOS</span>
53
+ <% elsif error.platform == 'Android' %>
54
+ <span class="badge badge-android"><i class="bi bi-android2"></i> Android</span>
55
+ <% elsif error.platform == 'Web' %>
56
+ <span class="badge badge-web"><i class="bi bi-globe"></i> Web</span>
57
+ <% else %>
58
+ <span class="badge badge-api"><i class="bi bi-server"></i> <%= error.platform || 'API' %></span>
59
+ <% end %>
60
+ </div>
61
+
62
+ <div class="mb-3">
63
+ <small class="metadata-label d-block mb-1">User</small>
64
+ <% if error.respond_to?(:user) && error.user %>
65
+ <strong><%= error.user.email %></strong><br>
66
+ <small class="text-muted">ID: <%= error.user_id %></small>
67
+ <% elsif error.user_id %>
68
+ <span class="text-muted">User ID: <%= error.user_id %></span>
69
+ <% else %>
70
+ <span class="text-muted">Guest / Unauthenticated</span>
71
+ <% end %>
72
+ </div>
73
+
74
+ <!-- Phase 3: Workflow Status -->
75
+ <div class="mb-3">
76
+ <small class="metadata-label d-block mb-1">Workflow Status</small>
77
+ <% if error.respond_to?(:status) %>
78
+ <% badge_color = error.status_badge_color %>
79
+ <% text_dark = %w[info warning light].include?(badge_color) ? 'text-dark' : '' %>
80
+ <span class="badge bg-<%= badge_color %> <%= text_dark %> fs-6">
81
+ <%= error.status.titleize %>
82
+ </span>
83
+ <% elsif error.resolved? %>
84
+ <span class="badge bg-success">
85
+ <i class="bi bi-check-circle"></i> Resolved
86
+ </span>
87
+ <% if error.resolved_at.present? %>
88
+ <br>
89
+ <small class="text-muted mt-1 d-block">
90
+ <%= local_time(error.resolved_at, format: :full) %>
91
+ </small>
92
+ <% end %>
93
+ <% else %>
94
+ <span class="badge bg-danger">
95
+ <i class="bi bi-exclamation-circle"></i> Unresolved
96
+ </span>
97
+ <% end %>
98
+ </div>
99
+
100
+ <!-- Reopened indicator -->
101
+ <% if error.reopened? %>
102
+ <div class="mb-3">
103
+ <small class="metadata-label d-block mb-1">Reopened</small>
104
+ <span class="badge bg-warning text-dark">
105
+ <i class="bi bi-arrow-counterclockwise"></i> Reopened
106
+ </span>
107
+ <br>
108
+ <small class="text-muted mt-1 d-block">
109
+ <%= local_time(error.reopened_at, format: :full) %>
110
+ </small>
111
+ </div>
112
+ <% end %>
113
+
114
+ <!-- Phase 3: Assignment -->
115
+ <% if error.respond_to?(:assigned_to) %>
116
+ <div class="mb-3">
117
+ <small class="metadata-label d-block mb-1">Assigned To</small>
118
+ <% if error.assigned? %>
119
+ <div class="d-flex align-items-center justify-content-between">
120
+ <div>
121
+ <i class="bi bi-person-fill text-primary"></i>
122
+ <strong><%= error.assigned_to %></strong>
123
+ <% if error.assigned_at.present? %>
124
+ <br>
125
+ <small class="text-muted">
126
+ <%= local_time_ago(error.assigned_at) %>
127
+ </small>
128
+ <% end %>
129
+ </div>
130
+ <%= button_to unassign_error_path(error), method: :post, class: "btn btn-sm btn-outline-secondary",
131
+ data: { turbo_confirm: "Remove assignment?" } do %>
132
+ <i class="bi bi-x"></i>
133
+ <% end %>
134
+ </div>
135
+ <% else %>
136
+ <button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#assignModal">
137
+ <i class="bi bi-person-plus"></i> Assign
138
+ </button>
139
+ <% end %>
140
+ </div>
141
+
142
+ <!-- Phase 3: Priority -->
143
+ <div class="mb-3">
144
+ <small class="metadata-label d-block mb-1">Priority</small>
145
+ <% if error.respond_to?(:priority_level) %>
146
+ <span class="badge bg-<%= error.priority_color %> fs-6">
147
+ <%= error.priority_label %>
148
+ </span>
149
+ <button type="button" class="btn btn-sm btn-outline-secondary ms-2" data-bs-toggle="modal" data-bs-target="#priorityModal">
150
+ <i class="bi bi-pencil"></i>
151
+ </button>
152
+ <% end %>
153
+ </div>
154
+
155
+ <!-- Phase 3: Snooze -->
156
+ <div class="mb-3">
157
+ <small class="metadata-label d-block mb-1">Snooze</small>
158
+ <% if error.respond_to?(:snoozed?) && error.snoozed? %>
159
+ <div class="alert alert-warning py-2 mb-2">
160
+ <i class="bi bi-alarm"></i>
161
+ <strong>Snoozed</strong><br>
162
+ <small>Until <%= local_time(error.snoozed_until, format: :datetime) %></small>
163
+ </div>
164
+ <%= button_to unsnooze_error_path(error), method: :post, class: "btn btn-sm btn-outline-warning" do %>
165
+ <i class="bi bi-alarm-fill"></i> Unsnooze
166
+ <% end %>
167
+ <% else %>
168
+ <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#snoozeModal">
169
+ <i class="bi bi-alarm"></i> Snooze
170
+ </button>
171
+ <% end %>
172
+ </div>
173
+ <% end %>
174
+
175
+ <% if error.resolved? && error.resolved_by_name.present? %>
176
+ <div class="mb-3">
177
+ <small class="metadata-label d-block mb-1">Resolved By</small>
178
+ <strong><%= error.resolved_by_name %></strong>
179
+ </div>
180
+ <% end %>
181
+
182
+ <% if error.resolved? && error.resolution_reference.present? %>
183
+ <div class="mb-3">
184
+ <small class="metadata-label d-block mb-1">Reference</small>
185
+ <code><%= error.resolution_reference %></code>
186
+ </div>
187
+ <% end %>
188
+
189
+ <% if error.resolved? && error.resolution_comment.present? %>
190
+ <div class="mb-3">
191
+ <small class="metadata-label d-block mb-1">Resolution Notes</small>
192
+ <div class="alert alert-success mb-0">
193
+ <%= auto_link_urls(error.resolution_comment, error: error).html_safe %>
194
+ </div>
195
+ </div>
196
+ <% end %>
197
+
198
+ <!-- Environment Information -->
199
+ <div class="sidebar-section sidebar-section-blue">
200
+ <div class="sidebar-section-title"><i class="bi bi-gear"></i> Environment</div>
201
+ <small class="sidebar-section-hint">Runtime context at error time</small>
202
+ <div class="sidebar-section-body">
203
+ <% if error.app_version.present? %>
204
+ <div class="mb-1">
205
+ <small class="text-muted">App Version:</small>
206
+ <code class="ms-1"><%= error.app_version %></code>
207
+ </div>
208
+ <% end %>
209
+
210
+ <% if error.git_sha.present? %>
211
+ <div class="mb-1">
212
+ <small class="text-muted">Git SHA:</small>
213
+ <span class="ms-1"><%= git_commit_link(error.git_sha) %></span>
214
+ </div>
215
+ <% end %>
216
+
217
+ <% env_info = error.respond_to?(:environment_info) && error.environment_info.present? ?
218
+ JSON.parse(error.environment_info, symbolize_names: true) : nil rescue nil %>
219
+
220
+ <div class="mb-1">
221
+ <small class="text-muted">Rails:</small>
222
+ <code class="ms-1"><%= env_info&.dig(:rails_version) || Rails.version %></code>
223
+ </div>
224
+
225
+ <div class="mb-1">
226
+ <small class="text-muted">Ruby:</small>
227
+ <code class="ms-1"><%= env_info&.dig(:ruby_version) || RUBY_VERSION %></code>
228
+ </div>
229
+
230
+ <div class="mb-1">
231
+ <small class="text-muted">Environment:</small>
232
+ <% env_name = env_info&.dig(:rails_env) || Rails.env.to_s %>
233
+ <span class="badge bg-<%= env_name == 'production' ? 'danger' : env_name == 'development' ? 'success' : 'warning' %> ms-1">
234
+ <%= env_name.titleize %>
235
+ </span>
236
+ </div>
237
+
238
+ <% if env_info&.dig(:server).present? && env_info[:server] != "unknown" %>
239
+ <div class="mb-1">
240
+ <small class="text-muted">Server:</small>
241
+ <code class="ms-1"><%= env_info[:server].capitalize %></code>
242
+ </div>
243
+ <% end %>
244
+
245
+ <% if env_info&.dig(:database_adapter).present? && env_info[:database_adapter] != "unknown" %>
246
+ <div class="mb-1">
247
+ <small class="text-muted">Database:</small>
248
+ <code class="ms-1"><%= env_info[:database_adapter] %></code>
249
+ </div>
250
+ <% end %>
251
+
252
+ <% if env_info&.dig(:gem_versions)&.any? %>
253
+ <div class="mb-1">
254
+ <small class="text-muted d-block">Key Gems:</small>
255
+ <div class="ps-2">
256
+ <% env_info[:gem_versions].each do |name, version| %>
257
+ <small><code><%= name %> <%= version %></code></small><br>
258
+ <% end %>
259
+ </div>
260
+ </div>
261
+ <% end %>
262
+ </div>
263
+ </div>
264
+
265
+ <% if RailsErrorDashboard.configuration.respond_to?(:enable_system_health) && RailsErrorDashboard.configuration.enable_system_health &&
266
+ error.respond_to?(:system_health) && error.system_health.present? %>
267
+ <% health = begin; JSON.parse(error.system_health, symbolize_names: true); rescue; nil; end %>
268
+ <% if health %>
269
+ <div class="sidebar-section sidebar-section-red">
270
+ <div class="sidebar-section-title"><i class="bi bi-heart-pulse"></i> System Health</div>
271
+ <small class="sidebar-section-hint">Runtime state when error fired</small>
272
+ <div class="sidebar-section-body">
273
+ <% if health[:process_memory_mb] %>
274
+ <div class="mb-1">
275
+ <small class="text-muted">Memory (RSS):</small>
276
+ <% mem = health[:process_memory_mb].to_f %>
277
+ <code class="ms-1 text-<%= mem > 1024 ? 'danger' : mem > 512 ? 'warning' : 'success' %>">
278
+ <%= mem %> MB
279
+ </code>
280
+ </div>
281
+ <% end %>
282
+
283
+ <% if health[:thread_count] %>
284
+ <div class="mb-1">
285
+ <small class="text-muted">Threads:</small>
286
+ <code class="ms-1"><%= health[:thread_count] %></code>
287
+ </div>
288
+ <% end %>
289
+
290
+ <% if health[:gc] %>
291
+ <div class="mb-1">
292
+ <small class="text-muted">GC Live Objects:</small>
293
+ <code class="ms-1"><%= begin; number_with_delimiter(health[:gc][:heap_live_slots]); rescue; health[:gc][:heap_live_slots]; end %></code>
294
+ </div>
295
+ <div class="mb-1">
296
+ <small class="text-muted">Major GC Runs:</small>
297
+ <code class="ms-1"><%= health[:gc][:major_gc_count] %></code>
298
+ </div>
299
+ <% end %>
300
+
301
+ <% if health[:connection_pool] %>
302
+ <% cp = health[:connection_pool] %>
303
+ <div class="mb-1">
304
+ <small class="text-muted">Connection Pool:</small>
305
+ <code class="ms-1">
306
+ <%= cp[:busy] %>/<%= cp[:size] %> busy, <%= cp[:idle] %> idle<% if cp[:dead].to_i > 0 %>, <span class="text-danger"><%= cp[:dead] %> dead</span><% end %><% if cp[:waiting].to_i > 0 %>, <span class="text-warning"><%= cp[:waiting] %> waiting</span><% end %>
307
+ </code>
308
+ </div>
309
+ <% end %>
310
+
311
+ <% if health[:puma] %>
312
+ <% ps = health[:puma] %>
313
+ <div class="mb-1">
314
+ <small class="text-muted">Puma Threads:</small>
315
+ <code class="ms-1">
316
+ <%= ps[:running] %>/<%= ps[:max_threads] %> running, <%= ps[:pool_capacity] %> capacity<% if ps[:backlog].to_i > 0 %>, <span class="text-warning"><%= ps[:backlog] %> backlog</span><% end %>
317
+ </code>
318
+ </div>
319
+ <% end %>
320
+
321
+ <% if health[:job_queue] %>
322
+ <% jq = health[:job_queue] %>
323
+ <div class="mb-1">
324
+ <small class="text-muted">Job Queue (<%= jq[:adapter] %>):</small>
325
+ <code class="ms-1">
326
+ <% case jq[:adapter] %>
327
+ <% when "sidekiq" %>
328
+ <%= jq[:enqueued] %> enqueued, <%= jq[:workers] %> workers<% if jq[:dead].to_i > 0 %>, <span class="text-danger"><%= jq[:dead] %> dead</span><% end %><% if jq[:retry].to_i > 0 %>, <span class="text-warning"><%= jq[:retry] %> retry</span><% end %>
329
+ <% when "solid_queue" %>
330
+ <%= jq[:ready] %> ready, <%= jq[:claimed] %> claimed, <%= jq[:scheduled] %> scheduled<% if jq[:failed].to_i > 0 %>, <span class="text-danger"><%= jq[:failed] %> failed</span><% end %><% if jq[:blocked].to_i > 0 %>, <span class="text-warning"><%= jq[:blocked] %> blocked</span><% end %>
331
+ <% when "good_job" %>
332
+ <%= jq[:queued] %> queued, <%= jq[:finished] %> finished<% if jq[:errored].to_i > 0 %>, <span class="text-danger"><%= jq[:errored] %> errored</span><% end %>
333
+ <% end %>
334
+ </code>
335
+ </div>
336
+ <% end %>
337
+ </div>
338
+ </div>
339
+ <% end %>
340
+ <% end %>
341
+
342
+ <div>
343
+ <small class="metadata-label d-block mb-1">Error ID</small>
344
+ <div class="d-flex align-items-center gap-2">
345
+ <code><%= error.id %></code>
346
+ <button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard('<%= error.id %>', this)" title="Copy error ID">
347
+ <i class="bi bi-clipboard"></i>
348
+ </button>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </div>
@@ -0,0 +1,75 @@
1
+ <!-- Similar Errors (Fuzzy Matching) -->
2
+ <% if RailsErrorDashboard.configuration.enable_similar_errors && error.respond_to?(:similar_errors) %>
3
+ <% similar = error.similar_errors(threshold: 0.6, limit: 5) %>
4
+ <% if similar.any? %>
5
+ <% cache [error, 'similar_errors_v1', similar.map { |s| s[:error]&.updated_at }.compact.max] do %>
6
+ <div class="card mb-4" id="section-similar-errors">
7
+ <div class="card-header bg-white">
8
+ <h5 class="mb-0">
9
+ <i class="bi bi-diagram-3"></i> Similar Errors
10
+ <span class="badge bg-info text-dark">Fuzzy Matching</span>
11
+ </h5>
12
+ <small class="text-muted">Errors with similar backtraces and messages (60%+ similarity)</small>
13
+ </div>
14
+ <div class="card-body p-0">
15
+ <div class="table-responsive">
16
+ <table class="table table-hover mb-0">
17
+ <thead class="table-light">
18
+ <tr>
19
+ <th>Similarity</th>
20
+ <th>Error Type</th>
21
+ <th>Message</th>
22
+ <th>Platform</th>
23
+ <th>Occurrences</th>
24
+ <th></th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ <% similar.each do |item| %>
29
+ <% similar_error = item[:error] %>
30
+ <% similarity_pct = (item[:similarity] * 100).round %>
31
+ <tr>
32
+ <td>
33
+ <div class="progress" style="width: 60px; height: 20px;">
34
+ <div class="progress-bar bg-success" role="progressbar"
35
+ style="width: <%= similarity_pct %>%"
36
+ aria-valuenow="<%= similarity_pct %>"
37
+ aria-valuemin="0"
38
+ aria-valuemax="100">
39
+ <%= similarity_pct %>%
40
+ </div>
41
+ </div>
42
+ </td>
43
+ <td><code><%= similar_error.error_type %></code></td>
44
+ <td>
45
+ <div class="text-truncate" style="max-width: 300px;"
46
+ data-bs-toggle="tooltip"
47
+ title="<%= similar_error.message %>">
48
+ <%= similar_error.message %>
49
+ </div>
50
+ </td>
51
+ <td>
52
+ <% if similar_error.platform == 'iOS' %>
53
+ <span class="badge badge-ios"><i class="bi bi-apple"></i> iOS</span>
54
+ <% elsif similar_error.platform == 'Android' %>
55
+ <span class="badge badge-android"><i class="bi bi-android2"></i> Android</span>
56
+ <% elsif similar_error.platform == 'Web' %>
57
+ <span class="badge badge-web"><i class="bi bi-globe"></i> Web</span>
58
+ <% else %>
59
+ <span class="badge badge-api"><i class="bi bi-server"></i> <%= similar_error.platform || 'API' %></span>
60
+ <% end %>
61
+ </td>
62
+ <td><span class="badge bg-primary"><%= similar_error.occurrence_count %>x</span></td>
63
+ <td>
64
+ <%= link_to "View", error_path(similar_error), class: "btn btn-sm btn-outline-primary" %>
65
+ </td>
66
+ </tr>
67
+ <% end %>
68
+ </tbody>
69
+ </table>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ <% end %>
74
+ <% end %>
75
+ <% end %>
@@ -1,6 +1,6 @@
1
1
  <%# Timeline view for related errors leading up to this error %>
2
2
  <% if @related_errors.any? %>
3
- <div class="card" id="timeline">
3
+ <div class="card" id="section-timeline">
4
4
  <div class="card-header bg-white">
5
5
  <h5 class="mb-0">
6
6
  <i class="bi bi-clock-history"></i> Timeline
@@ -0,0 +1,143 @@
1
+ <% content_for :page_title, "Cache Health" %>
2
+
3
+ <div class="container-fluid py-4">
4
+ <div class="d-flex justify-content-between align-items-center mb-4">
5
+ <h1 class="h3 mb-0">
6
+ <i class="bi bi-lightning-charge me-2"></i>
7
+ Cache Health
8
+ </h1>
9
+
10
+ <div class="btn-group" role="group">
11
+ <%= link_to cache_health_summary_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
12
+ 7 Days
13
+ <% end %>
14
+ <%= link_to cache_health_summary_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
15
+ 30 Days
16
+ <% end %>
17
+ <%= link_to cache_health_summary_errors_path(days: 90), class: "btn btn-sm #{@days == 90 ? 'btn-primary' : 'btn-outline-primary'}" do %>
18
+ 90 Days
19
+ <% end %>
20
+ </div>
21
+ </div>
22
+
23
+ <% if @errors_with_cache == 0 %>
24
+ <div class="text-center py-5">
25
+ <i class="bi bi-check-circle display-1 text-success mb-3"></i>
26
+ <h4 class="text-muted">No Cache Activity Found</h4>
27
+ <p class="text-muted">
28
+ No cache operations were detected in error breadcrumbs over the last <%= @days %> days.
29
+ </p>
30
+ <div class="card mx-auto" style="max-width: 500px;">
31
+ <div class="card-body text-start">
32
+ <h6>How cache tracking works:</h6>
33
+ <ul class="mb-0">
34
+ <li>Breadcrumbs must be enabled (<code>enable_breadcrumbs = true</code>)</li>
35
+ <li>Cache reads and writes are captured as breadcrumbs during requests that produce errors</li>
36
+ <li>Hit/miss status is tracked for read operations</li>
37
+ <li>This page shows cache performance per-error, sorted worst-first</li>
38
+ </ul>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <% else %>
43
+ <div class="row mb-4">
44
+ <div class="col-md-4">
45
+ <div class="card text-center">
46
+ <div class="card-body">
47
+ <div class="display-6 text-info"><%= @errors_with_cache %></div>
48
+ <small class="text-muted">Errors with Cache</small>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ <div class="col-md-4">
53
+ <div class="card text-center">
54
+ <div class="card-body">
55
+ <% if @avg_hit_rate.nil? %>
56
+ <div class="display-6 text-secondary">N/A</div>
57
+ <% else %>
58
+ <% hit_color = @avg_hit_rate >= 80 ? "success" : (@avg_hit_rate >= 50 ? "warning" : "danger") %>
59
+ <div class="display-6 text-<%= hit_color %>"><%= @avg_hit_rate %>%</div>
60
+ <% end %>
61
+ <small class="text-muted">Avg Hit Rate</small>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ <div class="col-md-4">
66
+ <div class="card text-center">
67
+ <div class="card-body">
68
+ <div class="display-6 text-secondary"><%= @total_cache_ops %></div>
69
+ <small class="text-muted">Total Cache Ops</small>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </div>
74
+
75
+ <div class="card mb-4">
76
+ <div class="card-header bg-white d-flex justify-content-between align-items-center">
77
+ <h5 class="mb-0">
78
+ <i class="bi bi-lightning-charge text-info me-2"></i>
79
+ Cache Performance by Error
80
+ <span class="badge bg-info"><%= @errors_with_cache %></span>
81
+ </h5>
82
+ <small class="text-muted"><%== @pagy.info_tag %></small>
83
+ </div>
84
+ <div class="card-body p-0">
85
+ <div class="table-responsive">
86
+ <table class="table table-hover mb-0">
87
+ <thead class="table-light">
88
+ <tr>
89
+ <th width="100">Error</th>
90
+ <th width="80">Reads</th>
91
+ <th width="80">Writes</th>
92
+ <th width="120">Hit Rate</th>
93
+ <th width="120">Total Time</th>
94
+ <th>Slowest Op</th>
95
+ <th width="140">Last Seen</th>
96
+ </tr>
97
+ </thead>
98
+ <tbody>
99
+ <% @entries.each do |entry| %>
100
+ <tr>
101
+ <td><%= link_to "##{entry[:error_id]}", error_path(entry[:error_id]), class: "text-decoration-none" %></td>
102
+ <td><%= entry[:reads] %></td>
103
+ <td><%= entry[:writes] %></td>
104
+ <td>
105
+ <% if entry[:hit_rate].nil? %>
106
+ <span class="badge bg-secondary">N/A</span>
107
+ <% else %>
108
+ <% rate_color = entry[:hit_rate] >= 80 ? "success" : (entry[:hit_rate] >= 50 ? "warning" : "danger") %>
109
+ <span class="badge bg-<%= rate_color %>"><%= entry[:hit_rate] %>%</span>
110
+ <% end %>
111
+ </td>
112
+ <td><%= entry[:total_duration_ms] %> ms</td>
113
+ <td>
114
+ <% if entry[:slowest_message] %>
115
+ <small class="text-muted"><%= truncate(entry[:slowest_message], length: 60) %></small>
116
+ <small class="text-danger">(<%= entry[:slowest_duration_ms] %> ms)</small>
117
+ <% end %>
118
+ </td>
119
+ <td><%= local_time_ago(entry[:occurred_at]) %></td>
120
+ </tr>
121
+ <% end %>
122
+ </tbody>
123
+ </table>
124
+ </div>
125
+ </div>
126
+ <div class="card-footer bg-white border-top d-flex justify-content-between align-items-center">
127
+ <div>
128
+ <small class="text-muted">
129
+ <i class="bi bi-lightbulb text-warning"></i> Low hit rates may indicate cache keys that expire too quickly or aren't being set properly.
130
+ </small>
131
+ <small class="ms-3">
132
+ <a href="https://guides.rubyonrails.org/caching_with_rails.html" target="_blank" rel="noopener" class="text-decoration-none">
133
+ <i class="bi bi-book"></i> Rails Caching Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
134
+ </a>
135
+ </small>
136
+ </div>
137
+ <div>
138
+ <%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ <% end %>
143
+ </div>