rails_error_dashboard 0.2.3 → 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.
- checksums.yaml +4 -4
- data/README.md +82 -14
- data/app/controllers/rails_error_dashboard/errors_controller.rb +88 -1
- data/app/helpers/rails_error_dashboard/application_helper.rb +25 -0
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +18 -6
- data/app/views/layouts/rails_error_dashboard.html.erb +145 -1
- data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +236 -0
- data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +70 -0
- data/app/views/rails_error_dashboard/errors/_discussion.html.erb +107 -0
- data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +138 -0
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +190 -0
- data/app/views/rails_error_dashboard/errors/_modals.html.erb +139 -0
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +97 -0
- data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +156 -0
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +352 -0
- data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +75 -0
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +143 -0
- data/app/views/rails_error_dashboard/errors/deprecations.html.erb +129 -0
- data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +134 -0
- data/app/views/rails_error_dashboard/errors/settings.html.erb +17 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +20 -1132
- data/config/routes.rb +3 -0
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +11 -5
- data/db/migrate/20260303000001_add_breadcrumbs_to_error_logs.rb +9 -0
- data/db/migrate/20260304000001_add_system_health_to_error_logs.rb +12 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +82 -6
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +67 -5
- data/lib/rails_error_dashboard/commands/log_error.rb +33 -0
- data/lib/rails_error_dashboard/configuration.rb +45 -3
- data/lib/rails_error_dashboard/engine.rb +14 -0
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +8 -0
- data/lib/rails_error_dashboard/queries/cache_health_summary.rb +72 -0
- data/lib/rails_error_dashboard/queries/deprecation_warnings.rb +80 -0
- data/lib/rails_error_dashboard/queries/n_plus_one_summary.rb +83 -0
- data/lib/rails_error_dashboard/services/breadcrumb_collector.rb +182 -0
- data/lib/rails_error_dashboard/services/cache_analyzer.rb +76 -0
- data/lib/rails_error_dashboard/services/curl_generator.rb +80 -0
- data/lib/rails_error_dashboard/services/n_plus_one_detector.rb +74 -0
- data/lib/rails_error_dashboard/services/system_health_snapshot.rb +145 -0
- data/lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb +210 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +20 -0
- data/lib/tasks/error_dashboard.rake +68 -2
- metadata +37 -11
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<% cache [error, 'error_details_v1'] do %>
|
|
2
|
+
<div class="card mb-4" id="section-error-info">
|
|
3
|
+
<div class="card-header bg-danger text-white">
|
|
4
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
5
|
+
<h5 class="mb-0">
|
|
6
|
+
<i class="bi bi-bug-fill"></i> <%= error.error_type %>
|
|
7
|
+
<% if error.recent? %>
|
|
8
|
+
<span class="badge bg-success ms-2" data-bs-toggle="tooltip" title="Error occurred within the last hour">NEW</span>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% if error.reopened? %>
|
|
11
|
+
<span class="badge bg-warning text-dark ms-2" data-bs-toggle="tooltip" title="Previously resolved, recurred <%= error.reopened_at&.strftime('%b %d, %Y %H:%M') %>">
|
|
12
|
+
<i class="bi bi-arrow-counterclockwise"></i> Reopened
|
|
13
|
+
</span>
|
|
14
|
+
<% end %>
|
|
15
|
+
</h5>
|
|
16
|
+
<button class="btn btn-sm btn-outline-light" onclick="copyToClipboard('<%= j error.error_type %>', this)" title="Copy error type">
|
|
17
|
+
<i class="bi bi-clipboard"></i>
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="card-body">
|
|
22
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
23
|
+
<h6 class="text-muted mb-0">Error Message:</h6>
|
|
24
|
+
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard('<%= j error.message %>', this)" title="Copy error message">
|
|
25
|
+
<i class="bi bi-clipboard"></i> Copy
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="alert alert-danger">
|
|
29
|
+
<%= error.message %>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<% if error.respond_to?(:exception_cause) && error.exception_cause.present? %>
|
|
33
|
+
<% begin %>
|
|
34
|
+
<% cause_chain = JSON.parse(error.exception_cause) %>
|
|
35
|
+
<% if cause_chain.is_a?(Array) && cause_chain.any? %>
|
|
36
|
+
<div class="mt-4 mb-3">
|
|
37
|
+
<h6 class="text-muted mb-2">
|
|
38
|
+
<i class="bi bi-link-45deg"></i> Exception Cause Chain
|
|
39
|
+
<span class="badge bg-secondary ms-1"><%= cause_chain.length %></span>
|
|
40
|
+
</h6>
|
|
41
|
+
<div class="cause-chain">
|
|
42
|
+
<% cause_chain.each_with_index do |cause, index| %>
|
|
43
|
+
<% cause_bt = cause["backtrace"] %>
|
|
44
|
+
<% cause_bt = cause_bt.is_a?(String) ? cause_bt.split("\n") : Array(cause_bt) %>
|
|
45
|
+
<div class="card mb-2 border-warning">
|
|
46
|
+
<div class="card-header bg-warning bg-opacity-10 py-2">
|
|
47
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
48
|
+
<div>
|
|
49
|
+
<small class="text-muted">Caused by<%= " (##{index + 1})" if cause_chain.length > 1 %>:</small>
|
|
50
|
+
<strong class="ms-1"><code><%= cause["class_name"] %></code></strong>
|
|
51
|
+
</div>
|
|
52
|
+
<% if cause_bt.any? %>
|
|
53
|
+
<button class="btn btn-sm btn-outline-secondary py-0" type="button"
|
|
54
|
+
data-bs-toggle="collapse"
|
|
55
|
+
data-bs-target="#cause-backtrace-<%= index %>"
|
|
56
|
+
aria-expanded="false">
|
|
57
|
+
<small><i class="bi bi-code-slash"></i> Backtrace</small>
|
|
58
|
+
</button>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="card-body py-2">
|
|
63
|
+
<small><%= cause["message"] %></small>
|
|
64
|
+
</div>
|
|
65
|
+
<% if cause_bt.any? %>
|
|
66
|
+
<div class="collapse" id="cause-backtrace-<%= index %>">
|
|
67
|
+
<div class="card-body p-0 border-top">
|
|
68
|
+
<div class="code-block p-2" style="max-height: 200px; overflow-y: auto; overflow-x: auto; font-size: 0.8rem;">
|
|
69
|
+
<pre class="mb-0"><code><% cause_bt.each do |line| %><%= line %>
|
|
70
|
+
<% end %></code></pre>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<% end %>
|
|
75
|
+
</div>
|
|
76
|
+
<% end %>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<% end %>
|
|
80
|
+
<% rescue JSON::ParserError %>
|
|
81
|
+
<%# Silently skip invalid JSON — safety first %>
|
|
82
|
+
<% end %>
|
|
83
|
+
<% end %>
|
|
84
|
+
|
|
85
|
+
<div class="d-flex justify-content-between align-items-center mb-2 mt-4">
|
|
86
|
+
<h6 class="text-muted mb-0">
|
|
87
|
+
Backtrace:
|
|
88
|
+
<% if error.backtrace.present? %>
|
|
89
|
+
<% frames = parse_backtrace(error.backtrace) %>
|
|
90
|
+
<% app_frames = filter_app_code(frames) %>
|
|
91
|
+
<% framework_frames = filter_framework_code(frames) %>
|
|
92
|
+
<small class="text-muted">
|
|
93
|
+
(<%= app_frames.count %> your code, <%= framework_frames.count %> framework/gems)
|
|
94
|
+
</small>
|
|
95
|
+
<% end %>
|
|
96
|
+
</h6>
|
|
97
|
+
<% if error.backtrace.present? %>
|
|
98
|
+
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard(`<%= j error.backtrace %>`, this)" title="Copy full backtrace">
|
|
99
|
+
<i class="bi bi-clipboard"></i> Copy Full Backtrace
|
|
100
|
+
</button>
|
|
101
|
+
<% end %>
|
|
102
|
+
</div>
|
|
103
|
+
<% if error.backtrace.present? %>
|
|
104
|
+
<% frames = parse_backtrace(error.backtrace) %>
|
|
105
|
+
<% app_frames = filter_app_code(frames) %>
|
|
106
|
+
<% framework_frames = filter_framework_code(frames) %>
|
|
107
|
+
|
|
108
|
+
<% if app_frames.any? %>
|
|
109
|
+
<!-- Your Code Section (Expanded by default) -->
|
|
110
|
+
<div class="card mb-3">
|
|
111
|
+
<div class="card-header bg-success bg-opacity-10 border-success">
|
|
112
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
113
|
+
<strong class="text-success">
|
|
114
|
+
<i class="bi bi-code-square"></i> Your Code
|
|
115
|
+
</strong>
|
|
116
|
+
<span class="badge bg-success"><%= app_frames.count %> frames</span>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="card-body p-0">
|
|
120
|
+
<div class="code-block p-3" style="max-height: none; overflow-y: visible; overflow-x: auto;">
|
|
121
|
+
<% app_frames.each_with_index do |frame, index| %>
|
|
122
|
+
<div class="backtrace-frame-wrapper mb-2">
|
|
123
|
+
<!-- Frame header (always visible) -->
|
|
124
|
+
<div class="<%= frame_bg_class(frame[:category]) %> d-flex justify-content-between align-items-center px-2 py-1">
|
|
125
|
+
<div>
|
|
126
|
+
<span class="backtrace-frame-number"><%= index + 1 %></span>
|
|
127
|
+
<span class="<%= frame_color_class(frame[:category]) %>">
|
|
128
|
+
<%= frame_icon(frame[:category]) %> <%= frame[:short_path] %>:<%= frame[:line_number] %>
|
|
129
|
+
</span> in <span class="backtrace-method-name">`<%= frame[:method_name] %>'</span>
|
|
130
|
+
</div>
|
|
131
|
+
<% if RailsErrorDashboard.configuration.enable_source_code_integration && frame[:category] == :app %>
|
|
132
|
+
<button class="btn btn-sm btn-outline-secondary" type="button"
|
|
133
|
+
data-bs-toggle="collapse"
|
|
134
|
+
data-bs-target="#source-code-<%= index %>"
|
|
135
|
+
aria-expanded="false"
|
|
136
|
+
aria-controls="source-code-<%= index %>">
|
|
137
|
+
<i class="bi bi-code-slash"></i> <span class="d-none d-md-inline">View Source</span>
|
|
138
|
+
</button>
|
|
139
|
+
<% end %>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Collapsible source code (only for app frames) -->
|
|
143
|
+
<% if RailsErrorDashboard.configuration.enable_source_code_integration && frame[:category] == :app %>
|
|
144
|
+
<div class="collapse" id="source-code-<%= index %>">
|
|
145
|
+
<%= render "source_code", frame: frame, error: error, index: index %>
|
|
146
|
+
</div>
|
|
147
|
+
<% end %>
|
|
148
|
+
</div>
|
|
149
|
+
<% end %>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<% end %>
|
|
154
|
+
|
|
155
|
+
<% if framework_frames.any? %>
|
|
156
|
+
<!-- Framework/Gem Code Section (Collapsed by default) -->
|
|
157
|
+
<div class="accordion" id="frameworkBacktraceAccordion">
|
|
158
|
+
<div class="accordion-item">
|
|
159
|
+
<h2 class="accordion-header">
|
|
160
|
+
<button class="accordion-button collapsed bg-light" type="button" data-bs-toggle="collapse" data-bs-target="#frameworkBacktrace" aria-expanded="false" aria-controls="frameworkBacktrace">
|
|
161
|
+
<i class="bi bi-gear me-2 text-warning"></i>
|
|
162
|
+
<strong>Framework & Gem Code</strong>
|
|
163
|
+
<span class="badge bg-secondary ms-2"><%= framework_frames.count %> frames</span>
|
|
164
|
+
<small class="text-muted ms-2">(click to expand)</small>
|
|
165
|
+
</button>
|
|
166
|
+
</h2>
|
|
167
|
+
<div id="frameworkBacktrace" class="accordion-collapse collapse" data-bs-parent="#frameworkBacktraceAccordion">
|
|
168
|
+
<div class="accordion-body p-0">
|
|
169
|
+
<div class="code-block p-3" style="max-height: 400px; overflow-y: auto; overflow-x: auto;">
|
|
170
|
+
<pre class="mb-0"><code><% framework_frames.each_with_index do |frame, index| %><span class="<%= frame_bg_class(frame[:category]) %> d-block px-2 py-1"><span class="backtrace-frame-number"><%= index + 1 %></span><span class="<%= frame_color_class(frame[:category]) %>"><%= frame_icon(frame[:category]) %> <%= frame[:short_path] %>:<%= frame[:line_number] %></span> in <span class="backtrace-method-name">`<%= frame[:method_name] %>'</span></span>
|
|
171
|
+
<% end %></code></pre>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
<% end %>
|
|
178
|
+
|
|
179
|
+
<% if app_frames.empty? && framework_frames.empty? %>
|
|
180
|
+
<!-- Fallback: Show raw backtrace if parsing failed -->
|
|
181
|
+
<div class="code-block p-3 rounded" style="max-height: 400px; overflow-y: auto; overflow-x: auto;">
|
|
182
|
+
<pre class="mb-0"><code><%= error.backtrace %></code></pre>
|
|
183
|
+
</div>
|
|
184
|
+
<% end %>
|
|
185
|
+
<% else %>
|
|
186
|
+
<p class="text-muted">No backtrace available</p>
|
|
187
|
+
<% end %>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<% end %>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<!-- Resolve Error Modal -->
|
|
2
|
+
<div class="modal fade" id="resolveModal" tabindex="-1" aria-labelledby="resolveModalLabel" aria-hidden="true">
|
|
3
|
+
<div class="modal-dialog">
|
|
4
|
+
<div class="modal-content">
|
|
5
|
+
<%= form_with url: resolve_error_path(error), method: :post, class: "modal-content" do |f| %>
|
|
6
|
+
<div class="modal-header">
|
|
7
|
+
<h5 class="modal-title" id="resolveModalLabel">
|
|
8
|
+
<i class="bi bi-check-circle"></i> Mark Error as Resolved
|
|
9
|
+
</h5>
|
|
10
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="modal-body">
|
|
13
|
+
<div class="mb-3">
|
|
14
|
+
<label for="resolved_by_name" class="form-label">Your Name <span class="text-danger">*</span></label>
|
|
15
|
+
<%= text_field_tag :resolved_by_name, nil, class: "form-control", placeholder: "e.g., John Doe", required: true %>
|
|
16
|
+
<small class="text-muted">Who is resolving this error?</small>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="mb-3">
|
|
20
|
+
<label for="resolution_reference" class="form-label">Reference (Optional)</label>
|
|
21
|
+
<%= text_field_tag :resolution_reference, nil, class: "form-control", placeholder: "e.g., JIRA-123, PR #456, GitHub Issue #789" %>
|
|
22
|
+
<small class="text-muted">Link to ticket, PR, or issue</small>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="mb-3">
|
|
26
|
+
<label for="resolution_comment" class="form-label">Resolution Notes (Optional)</label>
|
|
27
|
+
<%= text_area_tag :resolution_comment, nil, class: "form-control", rows: 4, placeholder: "Describe what was done to fix this error..." %>
|
|
28
|
+
<small class="text-muted">What was done to resolve this error?</small>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="modal-footer">
|
|
32
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
33
|
+
<%= submit_tag "Mark as Resolved", class: "btn btn-success", data: { action: "click->loading#click" } %>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- Phase 3: Assignment Modal -->
|
|
41
|
+
<div class="modal fade" id="assignModal" tabindex="-1" aria-labelledby="assignModalLabel" aria-hidden="true">
|
|
42
|
+
<div class="modal-dialog">
|
|
43
|
+
<div class="modal-content">
|
|
44
|
+
<%= form_with url: assign_error_path(error), method: :post do |f| %>
|
|
45
|
+
<div class="modal-header">
|
|
46
|
+
<h5 class="modal-title" id="assignModalLabel">
|
|
47
|
+
<i class="bi bi-person-plus"></i> Assign Error
|
|
48
|
+
</h5>
|
|
49
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="modal-body">
|
|
52
|
+
<div class="mb-3">
|
|
53
|
+
<label for="assigned_to" class="form-label">Assign To <span class="text-danger">*</span></label>
|
|
54
|
+
<%= text_field_tag :assigned_to, nil, class: "form-control", placeholder: "e.g., John Doe", required: true, autofocus: true %>
|
|
55
|
+
<small class="text-muted">Who should investigate this error?</small>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="modal-footer">
|
|
59
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
60
|
+
<%= submit_tag "Assign", class: "btn btn-primary", data: { action: "click->loading#click" } %>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Phase 3: Priority Modal -->
|
|
68
|
+
<div class="modal fade" id="priorityModal" tabindex="-1" aria-labelledby="priorityModalLabel" aria-hidden="true">
|
|
69
|
+
<div class="modal-dialog">
|
|
70
|
+
<div class="modal-content">
|
|
71
|
+
<%= form_with url: update_priority_error_path(error), method: :post do |f| %>
|
|
72
|
+
<div class="modal-header">
|
|
73
|
+
<h5 class="modal-title" id="priorityModalLabel">
|
|
74
|
+
<i class="bi bi-flag"></i> Update Priority
|
|
75
|
+
</h5>
|
|
76
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="modal-body">
|
|
79
|
+
<div class="mb-3">
|
|
80
|
+
<label for="priority_level" class="form-label">Priority Level <span class="text-danger">*</span></label>
|
|
81
|
+
<%= select_tag :priority_level,
|
|
82
|
+
options_for_select(
|
|
83
|
+
RailsErrorDashboard::ErrorLog.priority_options,
|
|
84
|
+
error.priority_level
|
|
85
|
+
),
|
|
86
|
+
class: "form-select", required: true %>
|
|
87
|
+
<small class="text-muted">Current: <%= error.priority_label %></small>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="modal-footer">
|
|
91
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
92
|
+
<%= submit_tag "Update Priority", class: "btn btn-warning", data: { action: "click->loading#click" } %>
|
|
93
|
+
</div>
|
|
94
|
+
<% end %>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Phase 3: Snooze Modal -->
|
|
100
|
+
<div class="modal fade" id="snoozeModal" tabindex="-1" aria-labelledby="snoozeModalLabel" aria-hidden="true">
|
|
101
|
+
<div class="modal-dialog">
|
|
102
|
+
<div class="modal-content">
|
|
103
|
+
<%= form_with url: snooze_error_path(error), method: :post do |f| %>
|
|
104
|
+
<div class="modal-header">
|
|
105
|
+
<h5 class="modal-title" id="snoozeModalLabel">
|
|
106
|
+
<i class="bi bi-alarm"></i> Snooze Error
|
|
107
|
+
</h5>
|
|
108
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="modal-body">
|
|
111
|
+
<div class="mb-3">
|
|
112
|
+
<label for="hours" class="form-label">Snooze Duration <span class="text-danger">*</span></label>
|
|
113
|
+
<%= select_tag :hours,
|
|
114
|
+
options_for_select([
|
|
115
|
+
['1 hour', 1],
|
|
116
|
+
['4 hours', 4],
|
|
117
|
+
['8 hours', 8],
|
|
118
|
+
['24 hours (1 day)', 24],
|
|
119
|
+
['48 hours (2 days)', 48],
|
|
120
|
+
['168 hours (1 week)', 168]
|
|
121
|
+
], 24),
|
|
122
|
+
class: "form-select", required: true %>
|
|
123
|
+
<small class="text-muted">Hide this error from active views</small>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="mb-3">
|
|
127
|
+
<label for="reason" class="form-label">Reason (Optional)</label>
|
|
128
|
+
<%= text_area_tag :reason, nil, class: "form-control", rows: 3, placeholder: "Why are you snoozing this error?" %>
|
|
129
|
+
<small class="text-muted">Reason will be added as a comment</small>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="modal-footer">
|
|
133
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
134
|
+
<%= submit_tag "Snooze", class: "btn btn-warning", data: { action: "click->loading#click" } %>
|
|
135
|
+
</div>
|
|
136
|
+
<% end %>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
%>
|
|
7
7
|
|
|
8
8
|
<% if pattern_data.present? && pattern_data[:total_errors] > 0 %>
|
|
9
|
-
<div class="card mb-4">
|
|
9
|
+
<div class="card mb-4" id="section-occurrence-patterns">
|
|
10
10
|
<div class="card-header bg-white">
|
|
11
11
|
<h5 class="mb-0">
|
|
12
12
|
<i class="bi bi-graph-up"></i> Occurrence Patterns
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<!-- Request Context -->
|
|
2
|
+
<% cache [error, 'request_context_v3'] do %>
|
|
3
|
+
<div class="card mb-4" id="section-request-context">
|
|
4
|
+
<div class="card-header bg-white">
|
|
5
|
+
<h5 class="mb-0"><i class="bi bi-globe"></i> Request Context</h5>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="card-body">
|
|
8
|
+
<table class="table table-sm">
|
|
9
|
+
<tr>
|
|
10
|
+
<th width="200">Request URL:</th>
|
|
11
|
+
<td>
|
|
12
|
+
<% if error.respond_to?(:http_method) && error.http_method.present? %>
|
|
13
|
+
<span class="badge bg-primary me-1"><%= error.http_method %></span>
|
|
14
|
+
<% end %>
|
|
15
|
+
<code><%= error.request_url || 'N/A' %></code>
|
|
16
|
+
</td>
|
|
17
|
+
</tr>
|
|
18
|
+
<% if error.respond_to?(:hostname) && error.hostname.present? %>
|
|
19
|
+
<tr>
|
|
20
|
+
<th>Hostname:</th>
|
|
21
|
+
<td><code><%= error.hostname %></code></td>
|
|
22
|
+
</tr>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% if error.respond_to?(:content_type) && error.content_type.present? %>
|
|
25
|
+
<tr>
|
|
26
|
+
<th>Content Type:</th>
|
|
27
|
+
<td><code><%= error.content_type %></code></td>
|
|
28
|
+
</tr>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% if error.respond_to?(:request_duration_ms) && error.request_duration_ms.present? %>
|
|
31
|
+
<tr>
|
|
32
|
+
<th>Request Duration:</th>
|
|
33
|
+
<td>
|
|
34
|
+
<% duration = error.request_duration_ms %>
|
|
35
|
+
<span class="badge bg-<%= duration > 5000 ? 'danger' : duration > 1000 ? 'warning' : 'success' %>">
|
|
36
|
+
<%= duration > 1000 ? "#{(duration / 1000.0).round(1)}s" : "#{duration}ms" %>
|
|
37
|
+
</span>
|
|
38
|
+
</td>
|
|
39
|
+
</tr>
|
|
40
|
+
<% end %>
|
|
41
|
+
<tr>
|
|
42
|
+
<th>Request Params:</th>
|
|
43
|
+
<td>
|
|
44
|
+
<% if error.request_params.present? %>
|
|
45
|
+
<pre class="mb-0"><code><%= JSON.pretty_generate(JSON.parse(error.request_params)) rescue error.request_params %></code></pre>
|
|
46
|
+
<% else %>
|
|
47
|
+
<span class="text-muted">N/A</span>
|
|
48
|
+
<% end %>
|
|
49
|
+
</td>
|
|
50
|
+
</tr>
|
|
51
|
+
<tr>
|
|
52
|
+
<th>User Agent:</th>
|
|
53
|
+
<td>
|
|
54
|
+
<% if error.user_agent.present? %>
|
|
55
|
+
<% ua_info = parse_user_agent(error.user_agent) %>
|
|
56
|
+
<div class="mb-2">
|
|
57
|
+
<%= browser_icon(ua_info) %>
|
|
58
|
+
<strong><%= ua_info[:browser_name] %></strong>
|
|
59
|
+
<% if ua_info[:browser_version].present? %>
|
|
60
|
+
<span class="text-muted"><%= ua_info[:browser_version] %></span>
|
|
61
|
+
<% end %>
|
|
62
|
+
<span class="ms-2"><%= os_icon(ua_info) %> <%= ua_info[:os_name] %></span>
|
|
63
|
+
<span class="ms-2"><%= device_icon(ua_info) %> <%= ua_info[:device_type] %></span>
|
|
64
|
+
</div>
|
|
65
|
+
<details>
|
|
66
|
+
<summary class="text-muted" style="cursor: pointer;">
|
|
67
|
+
<small>Raw User Agent</small>
|
|
68
|
+
</summary>
|
|
69
|
+
<small class="text-muted"><%= error.user_agent %></small>
|
|
70
|
+
</details>
|
|
71
|
+
<% else %>
|
|
72
|
+
<span class="text-muted">N/A</span>
|
|
73
|
+
<% end %>
|
|
74
|
+
</td>
|
|
75
|
+
</tr>
|
|
76
|
+
<tr>
|
|
77
|
+
<th>IP Address:</th>
|
|
78
|
+
<td><code><%= error.ip_address || 'N/A' %></code></td>
|
|
79
|
+
</tr>
|
|
80
|
+
<% curl_cmd = RailsErrorDashboard::Services::CurlGenerator.call(error) %>
|
|
81
|
+
<% if curl_cmd.present? %>
|
|
82
|
+
<tr>
|
|
83
|
+
<th>Replay Request:</th>
|
|
84
|
+
<td>
|
|
85
|
+
<button class="btn btn-sm btn-outline-primary"
|
|
86
|
+
onclick="copyToClipboard(this.dataset.curl, this)"
|
|
87
|
+
data-curl="<%= h curl_cmd %>"
|
|
88
|
+
title="Copy curl command to clipboard">
|
|
89
|
+
<i class="bi bi-terminal"></i> Copy as curl
|
|
90
|
+
</button>
|
|
91
|
+
</td>
|
|
92
|
+
</tr>
|
|
93
|
+
<% end %>
|
|
94
|
+
</table>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<% end %>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
function downloadErrorJSON(event) {
|
|
3
|
+
const errorData = {
|
|
4
|
+
id: <%= raw error.id.to_json %>,
|
|
5
|
+
error_type: <%= raw error.error_type.to_json %>,
|
|
6
|
+
message: <%= raw error.message.to_json %>,
|
|
7
|
+
backtrace: <%= raw error.backtrace.to_json %>,
|
|
8
|
+
occurred_at: <%= raw error.occurred_at.to_json %>,
|
|
9
|
+
first_seen_at: <%= raw error.first_seen_at.to_json %>,
|
|
10
|
+
last_seen_at: <%= raw error.last_seen_at.to_json %>,
|
|
11
|
+
occurrence_count: <%= raw error.occurrence_count.to_json %>,
|
|
12
|
+
resolved: <%= raw error.resolved?.to_json %>,
|
|
13
|
+
resolved_at: <%= raw error.resolved_at.to_json %>,
|
|
14
|
+
resolved_by_name: <%= raw error.resolved_by_name.to_json %>,
|
|
15
|
+
platform: <%= raw error.platform.to_json %>,
|
|
16
|
+
user_id: <%= raw error.user_id.to_json %>,
|
|
17
|
+
severity: <%= raw error.severity.to_json %>,
|
|
18
|
+
priority_level: <%= raw (error.priority_level || 0).to_json %>,
|
|
19
|
+
<% if error.respond_to?(:exception_cause) && error.exception_cause.present? %>
|
|
20
|
+
exception_cause: <%= raw error.exception_cause %>,
|
|
21
|
+
<% end %>
|
|
22
|
+
<% if error.respond_to?(:app_version) %>
|
|
23
|
+
app_version: <%= raw error.app_version.to_json %>,
|
|
24
|
+
<% end %>
|
|
25
|
+
<% if error.respond_to?(:git_commit) %>
|
|
26
|
+
git_commit: <%= raw error.git_commit.to_json %>,
|
|
27
|
+
<% end %>
|
|
28
|
+
created_at: <%= raw error.created_at.to_json %>,
|
|
29
|
+
updated_at: <%= raw error.updated_at.to_json %>
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const jsonString = JSON.stringify(errorData, null, 2);
|
|
33
|
+
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
34
|
+
const url = URL.createObjectURL(blob);
|
|
35
|
+
const link = document.createElement('a');
|
|
36
|
+
link.href = url;
|
|
37
|
+
link.download = `error_${errorData.id}_${errorData.error_type.replace(/[^a-zA-Z0-9]/g, '_')}.json`;
|
|
38
|
+
document.body.appendChild(link);
|
|
39
|
+
link.click();
|
|
40
|
+
document.body.removeChild(link);
|
|
41
|
+
URL.revokeObjectURL(url);
|
|
42
|
+
|
|
43
|
+
// Visual feedback
|
|
44
|
+
const button = event.currentTarget;
|
|
45
|
+
const originalHTML = button.innerHTML;
|
|
46
|
+
button.innerHTML = '<i class="bi bi-check"></i> Downloaded!';
|
|
47
|
+
button.classList.remove('btn-outline-secondary');
|
|
48
|
+
button.classList.add('btn-success');
|
|
49
|
+
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
button.innerHTML = originalHTML;
|
|
52
|
+
button.classList.remove('btn-success');
|
|
53
|
+
button.classList.add('btn-outline-secondary');
|
|
54
|
+
}, 2000);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Section Navigation — pill bar with scroll spy
|
|
58
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
59
|
+
var sectionConfig = [
|
|
60
|
+
{ id: 'section-error-info', icon: 'bi-bug-fill', label: 'Error' },
|
|
61
|
+
{ id: 'section-request-context', icon: 'bi-globe', label: 'Request' },
|
|
62
|
+
{ id: 'section-deprecation-warnings', icon: 'bi-exclamation-triangle', label: 'Deprecations' },
|
|
63
|
+
{ id: 'section-n-plus-one', icon: 'bi-arrow-repeat', label: 'N+1' },
|
|
64
|
+
{ id: 'section-cache-health', icon: 'bi-lightning-charge', label: 'Cache' },
|
|
65
|
+
{ id: 'section-breadcrumbs', icon: 'bi-signpost-2', label: 'Breadcrumbs' },
|
|
66
|
+
{ id: 'section-similar-errors', icon: 'bi-diagram-3', label: 'Similar' },
|
|
67
|
+
{ id: 'section-co-occurring', icon: 'bi-intersect', label: 'Co-occurring' },
|
|
68
|
+
{ id: 'section-timeline', icon: 'bi-clock-history', label: 'Timeline' },
|
|
69
|
+
{ id: 'section-discussion', icon: 'bi-chat-dots', label: 'Discussion' },
|
|
70
|
+
{ id: 'section-error-cascades', icon: 'bi-share', label: 'Cascades' },
|
|
71
|
+
{ id: 'section-occurrence-patterns', icon: 'bi-graph-up', label: 'Patterns' }
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
var scrollContainer = document.querySelector('.section-nav-scroll');
|
|
75
|
+
var wrapper = document.getElementById('section-nav-wrapper');
|
|
76
|
+
if (!scrollContainer || !wrapper) return;
|
|
77
|
+
|
|
78
|
+
// Build pills only for sections that exist in the DOM
|
|
79
|
+
var activeSections = [];
|
|
80
|
+
sectionConfig.forEach(function(cfg) {
|
|
81
|
+
if (document.getElementById(cfg.id)) {
|
|
82
|
+
activeSections.push(cfg);
|
|
83
|
+
var pill = document.createElement('a');
|
|
84
|
+
pill.href = '#' + cfg.id;
|
|
85
|
+
pill.className = 'section-nav-pill';
|
|
86
|
+
pill.dataset.section = cfg.id;
|
|
87
|
+
pill.innerHTML = '<i class="bi ' + cfg.icon + '"></i> <span>' + cfg.label + '</span>';
|
|
88
|
+
pill.addEventListener('click', function(e) {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
var target = document.getElementById(cfg.id);
|
|
91
|
+
if (target) {
|
|
92
|
+
var navHeight = wrapper.offsetHeight;
|
|
93
|
+
var y = target.getBoundingClientRect().top + window.pageYOffset - navHeight - 12;
|
|
94
|
+
window.scrollTo({ top: y, behavior: 'smooth' });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
scrollContainer.appendChild(pill);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Hide nav if only 2 or fewer sections (not useful)
|
|
102
|
+
if (activeSections.length <= 2) {
|
|
103
|
+
wrapper.style.display = 'none';
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Scroll spy with Intersection Observer
|
|
108
|
+
var currentActive = null;
|
|
109
|
+
var observer = new IntersectionObserver(function(entries) {
|
|
110
|
+
entries.forEach(function(entry) {
|
|
111
|
+
if (entry.isIntersecting) {
|
|
112
|
+
var id = entry.target.id;
|
|
113
|
+
if (currentActive !== id) {
|
|
114
|
+
currentActive = id;
|
|
115
|
+
document.querySelectorAll('.section-nav-pill').forEach(function(p) {
|
|
116
|
+
p.classList.toggle('active', p.dataset.section === id);
|
|
117
|
+
if (p.dataset.section === id) {
|
|
118
|
+
p.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}, {
|
|
125
|
+
rootMargin: '-80px 0px -60% 0px',
|
|
126
|
+
threshold: 0
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
activeSections.forEach(function(cfg) {
|
|
130
|
+
var el = document.getElementById(cfg.id);
|
|
131
|
+
if (el) observer.observe(el);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Sticky detection — add shadow when stuck
|
|
135
|
+
var sentinel = document.createElement('div');
|
|
136
|
+
sentinel.style.height = '1px';
|
|
137
|
+
sentinel.style.marginBottom = '-1px';
|
|
138
|
+
wrapper.parentNode.insertBefore(sentinel, wrapper);
|
|
139
|
+
|
|
140
|
+
var stickyObserver = new IntersectionObserver(function(entries) {
|
|
141
|
+
wrapper.classList.toggle('is-stuck', !entries[0].isIntersecting);
|
|
142
|
+
}, { threshold: 0 });
|
|
143
|
+
stickyObserver.observe(sentinel);
|
|
144
|
+
|
|
145
|
+
// Keyboard shortcuts: [ previous section, ] next section
|
|
146
|
+
document.addEventListener('keydown', function(e) {
|
|
147
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
|
|
148
|
+
if (e.key === '[' || e.key === ']') {
|
|
149
|
+
var pills = Array.from(document.querySelectorAll('.section-nav-pill'));
|
|
150
|
+
var activeIdx = pills.findIndex(function(p) { return p.classList.contains('active'); });
|
|
151
|
+
var nextIdx = e.key === ']' ? Math.min(activeIdx + 1, pills.length - 1) : Math.max(activeIdx - 1, 0);
|
|
152
|
+
if (nextIdx !== activeIdx) pills[nextIdx].click();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
</script>
|