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,236 @@
1
+ <!-- Breadcrumbs (Request Activity Trail) -->
2
+ <% if RailsErrorDashboard.configuration.enable_breadcrumbs && error.respond_to?(:breadcrumbs) && error.breadcrumbs.present? %>
3
+ <% breadcrumbs = JSON.parse(error.breadcrumbs) rescue [] %>
4
+ <% if breadcrumbs.any? %>
5
+ <% deprecation_crumbs = breadcrumbs.select { |c| c["c"] == "deprecation" } %>
6
+ <% if deprecation_crumbs.any? %>
7
+ <div class="card mb-4" id="section-deprecation-warnings" style="border-color: var(--bs-danger);">
8
+ <div class="card-header bg-white">
9
+ <h5 class="mb-0">
10
+ <i class="bi bi-exclamation-triangle text-danger"></i> Deprecation Warnings
11
+ <span class="badge bg-danger"><%= deprecation_crumbs.size %></span>
12
+ </h5>
13
+ <small class="text-muted">Rails deprecation warnings detected during this request</small>
14
+ </div>
15
+ <div class="card-body p-0">
16
+ <div class="table-responsive">
17
+ <table class="table table-sm table-hover mb-0">
18
+ <thead class="table-light">
19
+ <tr>
20
+ <th>Warning</th>
21
+ <th width="300">Source</th>
22
+ </tr>
23
+ </thead>
24
+ <tbody>
25
+ <% deprecation_crumbs.each do |crumb| %>
26
+ <tr>
27
+ <td><code style="font-size: 0.85em;"><%= crumb["m"] %></code></td>
28
+ <td><small class="text-muted"><%= crumb.dig("meta", "caller") || "N/A" %></small></td>
29
+ </tr>
30
+ <% end %>
31
+ </tbody>
32
+ </table>
33
+ </div>
34
+ </div>
35
+ <div class="card-footer bg-white border-top">
36
+ <small class="text-muted">
37
+ <i class="bi bi-lightbulb text-warning"></i> Fix deprecations before upgrading Rails to avoid breaking changes.
38
+ </small>
39
+ <small class="float-end">
40
+ <a href="https://guides.rubyonrails.org/upgrading_ruby_on_rails.html" target="_blank" rel="noopener" class="text-decoration-none">
41
+ <i class="bi bi-book"></i> Rails Upgrade Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
42
+ </a>
43
+ </small>
44
+ </div>
45
+ </div>
46
+ <% end %>
47
+
48
+ <% if RailsErrorDashboard.configuration.respond_to?(:enable_n_plus_one_detection) && RailsErrorDashboard.configuration.enable_n_plus_one_detection %>
49
+ <% n_plus_one_patterns = RailsErrorDashboard::Services::NplusOneDetector.call(breadcrumbs) %>
50
+ <% if n_plus_one_patterns.any? %>
51
+ <div class="card mb-4" id="section-n-plus-one" style="border-color: var(--bs-warning);">
52
+ <div class="card-header bg-white">
53
+ <h5 class="mb-0">
54
+ <i class="bi bi-arrow-repeat text-warning"></i> N+1 Query Detection
55
+ <span class="badge bg-warning text-dark"><%= n_plus_one_patterns.size %> pattern<%= n_plus_one_patterns.size == 1 ? "" : "s" %></span>
56
+ </h5>
57
+ <small class="text-muted">Repeated query patterns detected in this request's SQL breadcrumbs</small>
58
+ </div>
59
+ <div class="card-body p-0">
60
+ <div class="table-responsive">
61
+ <table class="table table-sm table-hover mb-0">
62
+ <thead class="table-light">
63
+ <tr>
64
+ <th width="80">Repeats</th>
65
+ <th>Query Pattern</th>
66
+ <th width="100">Total Time</th>
67
+ </tr>
68
+ </thead>
69
+ <tbody>
70
+ <% n_plus_one_patterns.each do |pattern| %>
71
+ <tr>
72
+ <td><span class="badge bg-warning text-dark"><%= pattern[:count] %>x</span></td>
73
+ <td>
74
+ <code style="word-break: break-all; font-size: 0.85em;"><%= truncate(pattern[:sample_query], length: 200) %></code>
75
+ <br><small class="text-muted">Fingerprint: <%= truncate(pattern[:fingerprint], length: 120) %></small>
76
+ <% table_name = extract_table_from_sql(pattern[:sample_query]) %>
77
+ <br><small class="text-muted">
78
+ <i class="bi bi-lightbulb text-warning"></i>
79
+ <% if table_name %>
80
+ Tip: Preload <strong><%= table_name %></strong> with <code>.includes(:association)</code> or <code>.preload(:association)</code>
81
+ <% else %>
82
+ Tip: Use <code>.includes</code> or <code>.preload</code> to eager load associations
83
+ <% end %>
84
+ </small>
85
+ </td>
86
+ <td>
87
+ <span class="<%= pattern[:total_duration_ms] > 100 ? 'text-danger fw-bold' : 'text-muted' %>">
88
+ <%= "%.1f" % pattern[:total_duration_ms] %>ms
89
+ </span>
90
+ </td>
91
+ </tr>
92
+ <% end %>
93
+ </tbody>
94
+ </table>
95
+ </div>
96
+ </div>
97
+ <div class="card-footer bg-white border-top">
98
+ <small class="float-end">
99
+ <a href="https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations" target="_blank" rel="noopener" class="text-decoration-none">
100
+ <i class="bi bi-book"></i> Rails Eager Loading Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
101
+ </a>
102
+ </small>
103
+ </div>
104
+ </div>
105
+ <% end %>
106
+ <% end %>
107
+
108
+ <% cache_analysis = RailsErrorDashboard::Services::CacheAnalyzer.call(breadcrumbs) %>
109
+ <% if cache_analysis %>
110
+ <div class="card mb-4" id="section-cache-health" style="border-color: var(--bs-info);">
111
+ <div class="card-header bg-white">
112
+ <h5 class="mb-0">
113
+ <i class="bi bi-lightning-charge text-info"></i> Cache Health
114
+ <span class="badge bg-info text-dark"><%= cache_analysis[:reads] + cache_analysis[:writes] %> operations</span>
115
+ </h5>
116
+ <small class="text-muted">Cache activity analysis from this request's breadcrumbs</small>
117
+ </div>
118
+ <div class="card-body">
119
+ <div class="row text-center mb-3">
120
+ <div class="col-4">
121
+ <div class="fs-4 fw-bold"><%= cache_analysis[:reads] %></div>
122
+ <small class="text-muted">Reads</small>
123
+ </div>
124
+ <div class="col-4">
125
+ <div class="fs-4 fw-bold"><%= cache_analysis[:writes] %></div>
126
+ <small class="text-muted">Writes</small>
127
+ </div>
128
+ <div class="col-4">
129
+ <% if cache_analysis[:hit_rate] %>
130
+ <div class="fs-4 fw-bold text-<%= cache_analysis[:hit_rate] >= 80 ? 'success' : cache_analysis[:hit_rate] >= 50 ? 'warning' : 'danger' %>">
131
+ <%= cache_analysis[:hit_rate] %>%
132
+ </div>
133
+ <small class="text-muted">Hit Rate</small>
134
+ <% else %>
135
+ <div class="fs-4 fw-bold text-muted">--</div>
136
+ <small class="text-muted">Hit Rate</small>
137
+ <% end %>
138
+ </div>
139
+ </div>
140
+
141
+ <% if cache_analysis[:hits] > 0 || cache_analysis[:misses] > 0 %>
142
+ <div class="mb-3">
143
+ <div class="d-flex justify-content-between mb-1">
144
+ <small><span class="badge bg-success"><%= cache_analysis[:hits] %> hits</span></small>
145
+ <small><span class="badge bg-danger"><%= cache_analysis[:misses] %> misses</span></small>
146
+ </div>
147
+ <% hit_pct = cache_analysis[:hit_rate] || 0 %>
148
+ <div class="progress" style="height: 6px;">
149
+ <div class="progress-bar bg-success" style="width: <%= hit_pct %>%"></div>
150
+ <div class="progress-bar bg-danger" style="width: <%= 100 - hit_pct %>%"></div>
151
+ </div>
152
+ </div>
153
+ <% end %>
154
+
155
+ <% if cache_analysis[:unknown] > 0 %>
156
+ <small class="text-muted d-block mb-2">
157
+ <i class="bi bi-info-circle"></i> <%= cache_analysis[:unknown] %> read<%= cache_analysis[:unknown] == 1 ? "" : "s" %> with unknown hit status (older breadcrumbs)
158
+ </small>
159
+ <% end %>
160
+
161
+ <div class="d-flex justify-content-between">
162
+ <small class="text-muted">Total cache time: <strong><%= "%.1f" % cache_analysis[:total_duration_ms] %>ms</strong></small>
163
+ <% if cache_analysis[:slowest] && cache_analysis[:slowest][:duration_ms] > 0 %>
164
+ <small class="text-muted">Slowest: <strong><%= "%.1f" % cache_analysis[:slowest][:duration_ms] %>ms</strong></small>
165
+ <% end %>
166
+ </div>
167
+
168
+ <% if cache_analysis[:hit_rate] && cache_analysis[:hit_rate] < 80 %>
169
+ <div class="alert alert-<%= cache_analysis[:hit_rate] < 50 ? 'warning' : 'info' %> py-1 px-2 mb-0 mt-2">
170
+ <small>
171
+ <i class="bi bi-lightbulb text-warning"></i>
172
+ <% if cache_analysis[:hit_rate] < 50 %>
173
+ Low hit rate — review cache key design and TTL settings.
174
+ <% else %>
175
+ Moderate hit rate — consider warming caches or adjusting expiration.
176
+ <% end %>
177
+ </small>
178
+ </div>
179
+ <% end %>
180
+ </div>
181
+ <div class="card-footer bg-white border-top">
182
+ <small class="float-end">
183
+ <a href="https://guides.rubyonrails.org/caching_with_rails.html" target="_blank" rel="noopener" class="text-decoration-none">
184
+ <i class="bi bi-book"></i> Rails Caching Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
185
+ </a>
186
+ </small>
187
+ </div>
188
+ </div>
189
+ <% end %>
190
+
191
+ <div class="card mb-4" id="section-breadcrumbs">
192
+ <div class="card-header bg-white">
193
+ <h5 class="mb-0">
194
+ <i class="bi bi-signpost-2"></i> Breadcrumbs
195
+ <span class="badge bg-secondary"><%= breadcrumbs.size %> events</span>
196
+ </h5>
197
+ <small class="text-muted">Activity trail leading up to this error</small>
198
+ </div>
199
+ <div class="card-body p-0">
200
+ <div class="table-responsive">
201
+ <table class="table table-sm table-hover mb-0">
202
+ <thead class="table-light">
203
+ <tr>
204
+ <th width="60">#</th>
205
+ <th width="100">Category</th>
206
+ <th>Message</th>
207
+ <th width="100">Duration</th>
208
+ </tr>
209
+ </thead>
210
+ <tbody>
211
+ <% breadcrumbs.each_with_index do |crumb, i| %>
212
+ <tr>
213
+ <td class="text-muted"><%= i + 1 %></td>
214
+ <td><span class="badge bg-<%= breadcrumb_badge_color(crumb["c"]) %>"><%= crumb["c"] %></span></td>
215
+ <td>
216
+ <code style="word-break: break-all; font-size: 0.85em;"><%= truncate(crumb["m"].to_s, length: 200) %></code>
217
+ <% if crumb["meta"].present? %>
218
+ <br><small class="text-muted"><%= crumb["meta"].inspect %></small>
219
+ <% end %>
220
+ </td>
221
+ <td>
222
+ <% if crumb["d"] %>
223
+ <span class="<%= crumb["d"].to_f > 100 ? 'text-danger fw-bold' : 'text-muted' %>">
224
+ <%= "%.1f" % crumb["d"] %>ms
225
+ </span>
226
+ <% end %>
227
+ </td>
228
+ </tr>
229
+ <% end %>
230
+ </tbody>
231
+ </table>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ <% end %>
236
+ <% end %>
@@ -0,0 +1,70 @@
1
+ <!-- Co-occurring Errors -->
2
+ <% if RailsErrorDashboard.configuration.enable_co_occurring_errors && error.respond_to?(:co_occurring_errors) %>
3
+ <% co_occurring = error.co_occurring_errors(window_minutes: 5, min_frequency: 2, limit: 5) %>
4
+ <% if co_occurring.any? %>
5
+ <div class="card mb-4" id="section-co-occurring">
6
+ <div class="card-header bg-white">
7
+ <h5 class="mb-0">
8
+ <i class="bi bi-clock-history"></i> Co-occurring Errors
9
+ <span class="badge bg-info text-dark">Time Patterns</span>
10
+ </h5>
11
+ <small class="text-muted">Errors that occur within 5 minutes of this error</small>
12
+ </div>
13
+ <div class="card-body p-0">
14
+ <div class="table-responsive">
15
+ <table class="table table-hover mb-0">
16
+ <thead class="table-light">
17
+ <tr>
18
+ <th>Error Type</th>
19
+ <th>Message</th>
20
+ <th>Platform</th>
21
+ <th>Frequency</th>
22
+ <th>Avg Delay</th>
23
+ <th></th>
24
+ </tr>
25
+ </thead>
26
+ <tbody>
27
+ <% co_occurring.each do |result| %>
28
+ <% error_record = result[:error] %>
29
+ <% frequency = result[:frequency] %>
30
+ <% avg_delay = result[:avg_delay_seconds] %>
31
+ <tr>
32
+ <td>
33
+ <span class="badge bg-danger"><%= error_record.error_type %></span>
34
+ </td>
35
+ <td>
36
+ <span data-bs-toggle="tooltip" title="<%= error_record.message %>">
37
+ <%= truncate(error_record.message, length: 60) %>
38
+ </span>
39
+ </td>
40
+ <td>
41
+ <% if error_record.platform.present? %>
42
+ <span class="badge bg-secondary"><%= error_record.platform %></span>
43
+ <% end %>
44
+ </td>
45
+ <td>
46
+ <span class="badge bg-info text-dark">
47
+ <%= frequency %> time<%= frequency > 1 ? 's' : '' %>
48
+ </span>
49
+ </td>
50
+ <td>
51
+ <% if avg_delay < 0 %>
52
+ <span class="text-warning"><%= number_to_human(avg_delay.abs, units: {unit: "s", thousand: "s"}, precision: 1) %> before</span>
53
+ <% elsif avg_delay > 0 %>
54
+ <span class="text-success"><%= number_to_human(avg_delay, units: {unit: "s", thousand: "s"}, precision: 1) %> after</span>
55
+ <% else %>
56
+ <span class="text-muted">simultaneous</span>
57
+ <% end %>
58
+ </td>
59
+ <td class="text-end">
60
+ <%= link_to "View", error_path(error_record), class: "btn btn-sm btn-outline-primary" %>
61
+ </td>
62
+ </tr>
63
+ <% end %>
64
+ </tbody>
65
+ </table>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ <% end %>
70
+ <% end %>
@@ -0,0 +1,107 @@
1
+ <!-- Phase 3: Comment Threads -->
2
+ <% if error.respond_to?(:comments) %>
3
+ <div class="card mb-4" id="section-discussion">
4
+ <div class="card-header bg-white">
5
+ <h5 class="mb-0">
6
+ <i class="bi bi-chat-dots"></i> Discussion
7
+ <span class="badge bg-secondary ms-2"><%= error.comments.count %></span>
8
+ </h5>
9
+ </div>
10
+ <div class="card-body">
11
+ <!-- Existing Comments -->
12
+ <% if error.comments.recent_first.any? %>
13
+ <div class="mb-4">
14
+ <% error.comments.recent_first.each_with_index do |comment, index| %>
15
+ <div class="<%= index < error.comments.count - 1 ? 'border-bottom' : '' %> pb-3 mb-3">
16
+ <div class="d-flex justify-content-between align-items-start mb-2">
17
+ <div>
18
+ <strong class="text-primary">
19
+ <i class="bi bi-person-circle"></i> <%= comment.author_name %>
20
+ </strong>
21
+ <% if comment.recent? %>
22
+ <span class="badge bg-success ms-2">New</span>
23
+ <% end %>
24
+ </div>
25
+ <small class="text-muted">
26
+ <%= local_time(comment.created_at, format: :datetime) %>
27
+ <span class="ms-1 text-muted">(<%= local_time_ago(comment.created_at) %>)</span>
28
+ </small>
29
+ </div>
30
+ <div class="text-break">
31
+ <%= auto_link_urls(comment.body, error: error).html_safe %>
32
+ </div>
33
+ </div>
34
+ <% end %>
35
+ </div>
36
+ <% else %>
37
+ <p class="text-muted mb-4">
38
+ <i class="bi bi-info-circle"></i> No comments yet. Start the discussion below.
39
+ </p>
40
+ <% end %>
41
+
42
+ <!-- Add Comment Form -->
43
+ <div class="<%= error.comments.any? ? '' : 'border-top' %> pt-3">
44
+ <h6 class="mb-3">
45
+ <i class="bi bi-plus-circle"></i> Add Comment
46
+ </h6>
47
+ <%= form_with url: add_comment_error_path(error), method: :post do |f| %>
48
+ <div class="mb-3">
49
+ <label for="author_name" class="form-label">Your Name <span class="text-danger">*</span></label>
50
+ <%= text_field_tag :author_name, error.assigned_to, class: "form-control", placeholder: "e.g., John Doe", required: true %>
51
+ </div>
52
+ <div class="mb-3">
53
+ <label for="body" class="form-label">Comment <span class="text-danger">*</span></label>
54
+
55
+ <!-- Quick Templates -->
56
+ <div class="mb-2">
57
+ <small class="metadata-label d-block mb-1">
58
+ <i class="bi bi-lightning-fill"></i> Quick templates:
59
+ </small>
60
+ <div class="d-flex flex-wrap gap-1">
61
+ <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('investigating')">
62
+ <i class="bi bi-search"></i> Investigating
63
+ </button>
64
+ <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('found_fix')">
65
+ <i class="bi bi-wrench"></i> Found Fix
66
+ </button>
67
+ <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('need_info')">
68
+ <i class="bi bi-question-circle"></i> Need Info
69
+ </button>
70
+ <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('duplicate')">
71
+ <i class="bi bi-files"></i> Duplicate
72
+ </button>
73
+ <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('cannot_reproduce')">
74
+ <i class="bi bi-x-circle"></i> Cannot Reproduce
75
+ </button>
76
+ </div>
77
+ </div>
78
+
79
+ <%= text_area_tag :body, nil, class: "form-control", rows: 4, placeholder: "Share your thoughts, findings, or updates...", required: true, id: "comment_body" %>
80
+ </div>
81
+ <%= submit_tag "Post Comment", class: "btn btn-primary" %>
82
+ <% end %>
83
+ </div>
84
+
85
+ <script>
86
+ function insertTemplate(templateType) {
87
+ const textarea = document.getElementById('comment_body');
88
+ const templates = {
89
+ investigating: "🔍 Investigating this issue now. Will update with findings.",
90
+ found_fix: "✅ Found the fix!\n\nRoot cause: \nSolution: \nPR: ",
91
+ need_info: "ℹ️ Need more information:\n\n- \n- \n\nPlease provide details to help debug this issue.",
92
+ duplicate: "📋 This appears to be a duplicate of error #\n\nClosing as duplicate.",
93
+ cannot_reproduce: "❌ Cannot reproduce this issue.\n\nAttempted:\n- \n- \n\nNeed more details or steps to reproduce."
94
+ };
95
+
96
+ const template = templates[templateType];
97
+ if (template) {
98
+ textarea.value = template;
99
+ textarea.focus();
100
+ // Move cursor to end
101
+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
102
+ }
103
+ }
104
+ </script>
105
+ </div>
106
+ </div>
107
+ <% end %>
@@ -0,0 +1,138 @@
1
+ <!-- Error Cascades -->
2
+ <% if RailsErrorDashboard.configuration.enable_error_cascades %>
3
+ <% cascades = error.error_cascades %>
4
+ <% if cascades[:parents].any? || cascades[:children].any? %>
5
+ <div class="card" id="section-error-cascades">
6
+ <div class="card-header bg-white">
7
+ <h5 class="mb-0">
8
+ <i class="bi bi-diagram-3"></i> Error Cascades
9
+ <small class="text-muted ms-2">Causal relationships between errors</small>
10
+ </h5>
11
+ </div>
12
+ <div class="card-body">
13
+ <!-- Parent Errors (What causes this error) -->
14
+ <% if cascades[:parents].any? %>
15
+ <div class="mb-4">
16
+ <h6 class="text-danger">
17
+ <i class="bi bi-arrow-down-circle"></i> Triggered By
18
+ <small class="text-muted">(errors that cause this one)</small>
19
+ </h6>
20
+ <div class="table-responsive">
21
+ <table class="table table-sm table-hover mb-0">
22
+ <thead class="table-light">
23
+ <tr>
24
+ <th>Error Type</th>
25
+ <th>Message</th>
26
+ <th>Probability</th>
27
+ <th>Frequency</th>
28
+ <th>Avg Delay</th>
29
+ <th></th>
30
+ </tr>
31
+ </thead>
32
+ <tbody>
33
+ <% cascades[:parents].each do |cascade| %>
34
+ <% parent = cascade[:error] %>
35
+ <tr>
36
+ <td>
37
+ <span class="badge bg-<%= severity_color(parent.severity) %>">
38
+ <%= parent.error_type %>
39
+ </span>
40
+ </td>
41
+ <td>
42
+ <div class="text-truncate" style="max-width: 250px;">
43
+ <%= parent.message %>
44
+ </div>
45
+ </td>
46
+ <td>
47
+ <% probability_pct = ((cascade[:probability] || 0) * 100).round(1) %>
48
+ <span class="badge <%= probability_pct >= 70 ? 'bg-danger' : 'bg-warning' %>">
49
+ <%= probability_pct %>%
50
+ </span>
51
+ </td>
52
+ <td>
53
+ <span class="badge bg-secondary"><%= cascade[:frequency] %>x</span>
54
+ </td>
55
+ <td>
56
+ <small class="text-muted">
57
+ <%= (cascade[:avg_delay_seconds] || 0).round(1) %>s
58
+ </small>
59
+ </td>
60
+ <td>
61
+ <%= link_to "View", error_path(parent), class: "btn btn-sm btn-outline-primary" %>
62
+ </td>
63
+ </tr>
64
+ <% end %>
65
+ </tbody>
66
+ </table>
67
+ </div>
68
+ </div>
69
+ <% end %>
70
+
71
+ <!-- Child Errors (What this error causes) -->
72
+ <% if cascades[:children].any? %>
73
+ <div>
74
+ <h6 class="text-warning">
75
+ <i class="bi bi-arrow-up-circle"></i> Triggers
76
+ <small class="text-muted">(errors caused by this one)</small>
77
+ </h6>
78
+ <div class="table-responsive">
79
+ <table class="table table-sm table-hover mb-0">
80
+ <thead class="table-light">
81
+ <tr>
82
+ <th>Error Type</th>
83
+ <th>Message</th>
84
+ <th>Probability</th>
85
+ <th>Frequency</th>
86
+ <th>Avg Delay</th>
87
+ <th></th>
88
+ </tr>
89
+ </thead>
90
+ <tbody>
91
+ <% cascades[:children].each do |cascade| %>
92
+ <% child = cascade[:error] %>
93
+ <tr>
94
+ <td>
95
+ <span class="badge bg-<%= severity_color(child.severity) %>">
96
+ <%= child.error_type %>
97
+ </span>
98
+ </td>
99
+ <td>
100
+ <div class="text-truncate" style="max-width: 250px;">
101
+ <%= child.message %>
102
+ </div>
103
+ </td>
104
+ <td>
105
+ <% probability_pct = ((cascade[:probability] || 0) * 100).round(1) %>
106
+ <span class="badge <%= probability_pct >= 70 ? 'bg-danger' : 'bg-warning' %>">
107
+ <%= probability_pct %>%
108
+ </span>
109
+ </td>
110
+ <td>
111
+ <span class="badge bg-secondary"><%= cascade[:frequency] %>x</span>
112
+ </td>
113
+ <td>
114
+ <small class="text-muted">
115
+ +<%= (cascade[:avg_delay_seconds] || 0).round(1) %>s
116
+ </small>
117
+ </td>
118
+ <td>
119
+ <%= link_to "View", error_path(child), class: "btn btn-sm btn-outline-primary" %>
120
+ </td>
121
+ </tr>
122
+ <% end %>
123
+ </tbody>
124
+ </table>
125
+ </div>
126
+ </div>
127
+ <% end %>
128
+
129
+ <div class="mt-3">
130
+ <small class="text-muted">
131
+ <i class="bi bi-info-circle"></i>
132
+ Cascade patterns show causal relationships. High probability (≥70%) and frequency (≥3) indicate strong cascades.
133
+ </small>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ <% end %>
138
+ <% end %>