rails_error_dashboard 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +858 -0
- data/Rakefile +3 -0
- data/app/assets/stylesheets/rails_error_dashboard/application.css +15 -0
- data/app/controllers/rails_error_dashboard/application_controller.rb +12 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +123 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +4 -0
- data/app/jobs/rails_error_dashboard/application_job.rb +4 -0
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +116 -0
- data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +19 -0
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +105 -0
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +166 -0
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +108 -0
- data/app/mailers/rails_error_dashboard/application_mailer.rb +8 -0
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +27 -0
- data/app/models/rails_error_dashboard/application_record.rb +5 -0
- data/app/models/rails_error_dashboard/error_log.rb +185 -0
- data/app/models/rails_error_dashboard/error_logs_record.rb +34 -0
- data/app/views/layouts/rails_error_dashboard/application.html.erb +17 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +351 -0
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +200 -0
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +32 -0
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +237 -0
- data/app/views/rails_error_dashboard/errors/index.html.erb +334 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +294 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb +40 -0
- data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +13 -0
- data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +10 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +27 -0
- data/lib/generators/rails_error_dashboard/install/templates/README +33 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +64 -0
- data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +40 -0
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +60 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +134 -0
- data/lib/rails_error_dashboard/commands/resolve_error.rb +35 -0
- data/lib/rails_error_dashboard/configuration.rb +83 -0
- data/lib/rails_error_dashboard/engine.rb +20 -0
- data/lib/rails_error_dashboard/error_reporter.rb +35 -0
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +41 -0
- data/lib/rails_error_dashboard/plugin.rb +98 -0
- data/lib/rails_error_dashboard/plugin_registry.rb +88 -0
- data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +96 -0
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +122 -0
- data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +78 -0
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +108 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +37 -0
- data/lib/rails_error_dashboard/queries/developer_insights.rb +277 -0
- data/lib/rails_error_dashboard/queries/errors_list.rb +66 -0
- data/lib/rails_error_dashboard/queries/errors_list_v2.rb +149 -0
- data/lib/rails_error_dashboard/queries/filter_options.rb +21 -0
- data/lib/rails_error_dashboard/services/platform_detector.rb +41 -0
- data/lib/rails_error_dashboard/value_objects/error_context.rb +148 -0
- data/lib/rails_error_dashboard/version.rb +3 -0
- data/lib/rails_error_dashboard.rb +60 -0
- data/lib/tasks/rails_error_dashboard_tasks.rake +4 -0
- metadata +318 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
<div class="py-4">
|
|
2
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
3
|
+
<h2 class="mb-0"><i class="bi bi-bug-fill text-primary"></i> Error Overview</h2>
|
|
4
|
+
<div class="text-muted">
|
|
5
|
+
<small>Last updated: <%= Time.current.strftime("%B %d, %Y %I:%M %p") %></small>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<!-- 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>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
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>
|
|
67
|
+
<% end %>
|
|
68
|
+
<% else %>
|
|
69
|
+
<p class="text-muted mb-0">No errors recorded yet</p>
|
|
70
|
+
<% end %>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
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>
|
|
87
|
+
<% end %>
|
|
88
|
+
<% else %>
|
|
89
|
+
<p class="text-muted mb-0">No errors recorded yet</p>
|
|
90
|
+
<% end %>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<!-- Filters -->
|
|
97
|
+
<div class="card mb-4">
|
|
98
|
+
<div class="card-header bg-white">
|
|
99
|
+
<h5 class="mb-0">Filters & Search</h5>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="card-body">
|
|
102
|
+
<%= form_with url: errors_path, method: :get, class: "row g-3" do %>
|
|
103
|
+
<div class="col-md-3">
|
|
104
|
+
<%= text_field_tag :search, params[:search], placeholder: "Search errors...", class: "form-control" %>
|
|
105
|
+
</div>
|
|
106
|
+
<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
|
+
<%= select_tag :error_type, options_for_select([['All Types', '']] + @error_types.map { |t| [t, t] }, params[:error_type]), class: "form-select" %>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="col-md-2">
|
|
116
|
+
<div class="form-check">
|
|
117
|
+
<%= check_box_tag :unresolved, true, params[:unresolved] == 'true', class: "form-check-input" %>
|
|
118
|
+
<%= label_tag :unresolved, "Unresolved only", class: "form-check-label" %>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="col-12">
|
|
122
|
+
<%= submit_tag "Apply Filters", class: "btn btn-primary" %>
|
|
123
|
+
<%= link_to "Clear", errors_path, class: "btn btn-outline-secondary" %>
|
|
124
|
+
</div>
|
|
125
|
+
<% end %>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Error List Table -->
|
|
130
|
+
<div class="card">
|
|
131
|
+
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
132
|
+
<h5 class="mb-0">Recent Errors</h5>
|
|
133
|
+
<small class="text-muted"><%== pagy_info(@pagy) %></small>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- Batch Actions Toolbar -->
|
|
137
|
+
<div class="card-body border-bottom" id="batch-actions-toolbar" style="display: none;">
|
|
138
|
+
<%= form_with url: batch_action_errors_path, method: :post, id: "batch-form" do |f| %>
|
|
139
|
+
<div class="row align-items-center">
|
|
140
|
+
<div class="col-auto">
|
|
141
|
+
<span id="selected-count" class="fw-bold">0 selected</span>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="col-auto">
|
|
144
|
+
<%= button_tag type: "submit", name: "action_type", value: "resolve", class: "btn btn-sm btn-success" do %>
|
|
145
|
+
<i class="bi bi-check-circle"></i> Resolve Selected
|
|
146
|
+
<% end %>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="col-auto">
|
|
149
|
+
<%= button_tag type: "submit", name: "action_type", value: "delete", class: "btn btn-sm btn-danger", data: { confirm: "Are you sure you want to delete the selected errors?" } do %>
|
|
150
|
+
<i class="bi bi-trash"></i> Delete Selected
|
|
151
|
+
<% end %>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="col-auto">
|
|
154
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-selection">
|
|
155
|
+
Clear Selection
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
<% end %>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="card-body p-0">
|
|
163
|
+
<% if @errors.any? %>
|
|
164
|
+
<div class="table-responsive">
|
|
165
|
+
<table class="table table-hover mb-0">
|
|
166
|
+
<thead class="table-light">
|
|
167
|
+
<tr>
|
|
168
|
+
<th style="width: 40px;">
|
|
169
|
+
<input type="checkbox" id="select-all" class="form-check-input">
|
|
170
|
+
</th>
|
|
171
|
+
<th>Time</th>
|
|
172
|
+
<th>Error Type</th>
|
|
173
|
+
<th>Message</th>
|
|
174
|
+
<th>Platform</th>
|
|
175
|
+
<th>Environment</th>
|
|
176
|
+
<th>User</th>
|
|
177
|
+
<th>Status</th>
|
|
178
|
+
<th></th>
|
|
179
|
+
</tr>
|
|
180
|
+
</thead>
|
|
181
|
+
<tbody>
|
|
182
|
+
<% @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>
|
|
232
|
+
<% end %>
|
|
233
|
+
</tbody>
|
|
234
|
+
</table>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<!-- Pagination -->
|
|
238
|
+
<div class="p-3">
|
|
239
|
+
<%== pagy_bootstrap_nav(@pagy) if @pagy.pages > 1 %>
|
|
240
|
+
</div>
|
|
241
|
+
<% else %>
|
|
242
|
+
<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>
|
|
245
|
+
</div>
|
|
246
|
+
<% end %>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<script>
|
|
252
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
253
|
+
const selectAllCheckbox = document.getElementById('select-all');
|
|
254
|
+
const errorCheckboxes = document.querySelectorAll('.error-checkbox');
|
|
255
|
+
const batchToolbar = document.getElementById('batch-actions-toolbar');
|
|
256
|
+
const selectedCountSpan = document.getElementById('selected-count');
|
|
257
|
+
const batchForm = document.getElementById('batch-form');
|
|
258
|
+
const clearSelectionBtn = document.getElementById('clear-selection');
|
|
259
|
+
|
|
260
|
+
function updateBatchToolbar() {
|
|
261
|
+
const checkedBoxes = document.querySelectorAll('.error-checkbox:checked');
|
|
262
|
+
const count = checkedBoxes.length;
|
|
263
|
+
|
|
264
|
+
if (count > 0) {
|
|
265
|
+
batchToolbar.style.display = 'block';
|
|
266
|
+
selectedCountSpan.textContent = `${count} selected`;
|
|
267
|
+
} else {
|
|
268
|
+
batchToolbar.style.display = 'none';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Select all checkbox
|
|
273
|
+
if (selectAllCheckbox) {
|
|
274
|
+
selectAllCheckbox.addEventListener('change', function() {
|
|
275
|
+
errorCheckboxes.forEach(checkbox => {
|
|
276
|
+
checkbox.checked = this.checked;
|
|
277
|
+
});
|
|
278
|
+
updateBatchToolbar();
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Individual checkboxes
|
|
283
|
+
errorCheckboxes.forEach(checkbox => {
|
|
284
|
+
checkbox.addEventListener('change', function() {
|
|
285
|
+
updateBatchToolbar();
|
|
286
|
+
|
|
287
|
+
// Update "select all" checkbox state
|
|
288
|
+
const allChecked = Array.from(errorCheckboxes).every(cb => cb.checked);
|
|
289
|
+
const someChecked = Array.from(errorCheckboxes).some(cb => cb.checked);
|
|
290
|
+
|
|
291
|
+
if (selectAllCheckbox) {
|
|
292
|
+
selectAllCheckbox.checked = allChecked;
|
|
293
|
+
selectAllCheckbox.indeterminate = someChecked && !allChecked;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Clear selection button
|
|
299
|
+
if (clearSelectionBtn) {
|
|
300
|
+
clearSelectionBtn.addEventListener('click', function() {
|
|
301
|
+
errorCheckboxes.forEach(checkbox => {
|
|
302
|
+
checkbox.checked = false;
|
|
303
|
+
});
|
|
304
|
+
if (selectAllCheckbox) {
|
|
305
|
+
selectAllCheckbox.checked = false;
|
|
306
|
+
selectAllCheckbox.indeterminate = false;
|
|
307
|
+
}
|
|
308
|
+
updateBatchToolbar();
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Form submission - add selected error IDs
|
|
313
|
+
if (batchForm) {
|
|
314
|
+
batchForm.addEventListener('submit', function(e) {
|
|
315
|
+
const checkedBoxes = document.querySelectorAll('.error-checkbox:checked');
|
|
316
|
+
|
|
317
|
+
if (checkedBoxes.length === 0) {
|
|
318
|
+
e.preventDefault();
|
|
319
|
+
alert('Please select at least one error');
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Add hidden inputs for each selected error ID
|
|
324
|
+
checkedBoxes.forEach(checkbox => {
|
|
325
|
+
const input = document.createElement('input');
|
|
326
|
+
input.type = 'hidden';
|
|
327
|
+
input.name = 'error_ids[]';
|
|
328
|
+
input.value = checkbox.value;
|
|
329
|
+
this.appendChild(input);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
</script>
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<div class="py-4">
|
|
2
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
3
|
+
<div>
|
|
4
|
+
<%= link_to errors_path, class: "btn btn-outline-secondary btn-sm mb-2" do %>
|
|
5
|
+
<i class="bi bi-arrow-left"></i> Back to Errors
|
|
6
|
+
<% end %>
|
|
7
|
+
<h2 class="mb-0">Error Details</h2>
|
|
8
|
+
</div>
|
|
9
|
+
<div>
|
|
10
|
+
<% if @error.resolved? %>
|
|
11
|
+
<span class="badge bg-success fs-6">
|
|
12
|
+
<i class="bi bi-check-circle"></i> Resolved
|
|
13
|
+
</span>
|
|
14
|
+
<% else %>
|
|
15
|
+
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#resolveModal">
|
|
16
|
+
<i class="bi bi-check-circle"></i> Mark as Resolved
|
|
17
|
+
</button>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="row g-4">
|
|
23
|
+
<!-- Error Information -->
|
|
24
|
+
<div class="col-md-8">
|
|
25
|
+
<div class="card mb-4">
|
|
26
|
+
<div class="card-header bg-danger text-white">
|
|
27
|
+
<h5 class="mb-0"><i class="bi bi-bug-fill"></i> <%= @error.error_type %></h5>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="card-body">
|
|
30
|
+
<h6 class="text-muted mb-3">Error Message:</h6>
|
|
31
|
+
<div class="alert alert-danger">
|
|
32
|
+
<%= @error.message %>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<h6 class="text-muted mb-2 mt-4">Backtrace:</h6>
|
|
36
|
+
<% if @error.backtrace.present? %>
|
|
37
|
+
<div class="bg-dark text-light p-3 rounded" style="max-height: 400px; overflow-y: auto;">
|
|
38
|
+
<pre class="mb-0"><code><%= @error.backtrace %></code></pre>
|
|
39
|
+
</div>
|
|
40
|
+
<% else %>
|
|
41
|
+
<p class="text-muted">No backtrace available</p>
|
|
42
|
+
<% end %>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- Request Context -->
|
|
47
|
+
<div class="card mb-4">
|
|
48
|
+
<div class="card-header bg-white">
|
|
49
|
+
<h5 class="mb-0"><i class="bi bi-globe"></i> Request Context</h5>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="card-body">
|
|
52
|
+
<table class="table table-sm">
|
|
53
|
+
<tr>
|
|
54
|
+
<th width="200">Request URL:</th>
|
|
55
|
+
<td><code><%= @error.request_url || 'N/A' %></code></td>
|
|
56
|
+
</tr>
|
|
57
|
+
<tr>
|
|
58
|
+
<th>Request Params:</th>
|
|
59
|
+
<td>
|
|
60
|
+
<% if @error.request_params.present? %>
|
|
61
|
+
<pre class="mb-0"><code><%= JSON.pretty_generate(JSON.parse(@error.request_params)) rescue @error.request_params %></code></pre>
|
|
62
|
+
<% else %>
|
|
63
|
+
<span class="text-muted">N/A</span>
|
|
64
|
+
<% end %>
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
<tr>
|
|
68
|
+
<th>User Agent:</th>
|
|
69
|
+
<td><small><%= @error.user_agent || 'N/A' %></small></td>
|
|
70
|
+
</tr>
|
|
71
|
+
<tr>
|
|
72
|
+
<th>IP Address:</th>
|
|
73
|
+
<td><code><%= @error.ip_address || 'N/A' %></code></td>
|
|
74
|
+
</tr>
|
|
75
|
+
</table>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Related Errors -->
|
|
80
|
+
<% if @related_errors.any? %>
|
|
81
|
+
<div class="card">
|
|
82
|
+
<div class="card-header bg-white">
|
|
83
|
+
<h5 class="mb-0"><i class="bi bi-collection"></i> Related Errors</h5>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="card-body p-0">
|
|
86
|
+
<div class="table-responsive">
|
|
87
|
+
<table class="table table-hover mb-0">
|
|
88
|
+
<thead class="table-light">
|
|
89
|
+
<tr>
|
|
90
|
+
<th>Time</th>
|
|
91
|
+
<th>Message</th>
|
|
92
|
+
<th>Platform</th>
|
|
93
|
+
<th>Status</th>
|
|
94
|
+
<th></th>
|
|
95
|
+
</tr>
|
|
96
|
+
</thead>
|
|
97
|
+
<tbody>
|
|
98
|
+
<% @related_errors.each do |related| %>
|
|
99
|
+
<tr>
|
|
100
|
+
<td><small><%= related.occurred_at.strftime("%m/%d %I:%M%p") %></small></td>
|
|
101
|
+
<td>
|
|
102
|
+
<div class="text-truncate" style="max-width: 400px;">
|
|
103
|
+
<%= related.message %>
|
|
104
|
+
</div>
|
|
105
|
+
</td>
|
|
106
|
+
<td>
|
|
107
|
+
<% if related.platform == 'iOS' %>
|
|
108
|
+
<span class="badge badge-ios">iOS</span>
|
|
109
|
+
<% elsif related.platform == 'Android' %>
|
|
110
|
+
<span class="badge badge-android">Android</span>
|
|
111
|
+
<% else %>
|
|
112
|
+
<span class="badge badge-api"><%= related.platform || 'API' %></span>
|
|
113
|
+
<% end %>
|
|
114
|
+
</td>
|
|
115
|
+
<td>
|
|
116
|
+
<% if related.resolved? %>
|
|
117
|
+
<i class="bi bi-check-circle-fill text-success"></i>
|
|
118
|
+
<% else %>
|
|
119
|
+
<i class="bi bi-exclamation-circle-fill text-danger"></i>
|
|
120
|
+
<% end %>
|
|
121
|
+
</td>
|
|
122
|
+
<td>
|
|
123
|
+
<%= link_to "View", error_path(related), class: "btn btn-sm btn-outline-primary" %>
|
|
124
|
+
</td>
|
|
125
|
+
</tr>
|
|
126
|
+
<% end %>
|
|
127
|
+
</tbody>
|
|
128
|
+
</table>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<% end %>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<!-- Sidebar -->
|
|
136
|
+
<div class="col-md-4">
|
|
137
|
+
<!-- Metadata Card -->
|
|
138
|
+
<div class="card mb-4">
|
|
139
|
+
<div class="card-header bg-white">
|
|
140
|
+
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Metadata</h5>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="card-body">
|
|
143
|
+
<div class="mb-3">
|
|
144
|
+
<small class="text-muted d-block mb-1">Occurred At</small>
|
|
145
|
+
<strong><%= @error.occurred_at.strftime("%B %d, %Y") %></strong><br>
|
|
146
|
+
<small><%= @error.occurred_at.strftime("%I:%M:%S %p %Z") %></small>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div class="mb-3">
|
|
150
|
+
<small class="text-muted d-block mb-1">Environment</small>
|
|
151
|
+
<span class="badge <%= @error.environment == 'production' ? 'bg-danger' : 'bg-info' %>">
|
|
152
|
+
<%= @error.environment.titleize %>
|
|
153
|
+
</span>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="mb-3">
|
|
157
|
+
<small class="text-muted d-block mb-1">Platform</small>
|
|
158
|
+
<% if @error.platform == 'iOS' %>
|
|
159
|
+
<span class="badge badge-ios"><i class="bi bi-phone"></i> iOS</span>
|
|
160
|
+
<% elsif @error.platform == 'Android' %>
|
|
161
|
+
<span class="badge badge-android"><i class="bi bi-phone"></i> Android</span>
|
|
162
|
+
<% else %>
|
|
163
|
+
<span class="badge badge-api"><i class="bi bi-server"></i> <%= @error.platform || 'API' %></span>
|
|
164
|
+
<% end %>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="mb-3">
|
|
168
|
+
<small class="text-muted d-block mb-1">User</small>
|
|
169
|
+
<% if @error.user %>
|
|
170
|
+
<strong><%= @error.user.email %></strong><br>
|
|
171
|
+
<small class="text-muted">ID: <%= @error.user_id %></small>
|
|
172
|
+
<% else %>
|
|
173
|
+
<span class="text-muted">Guest / Unauthenticated</span>
|
|
174
|
+
<% end %>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="mb-3">
|
|
178
|
+
<small class="text-muted d-block mb-1">Status</small>
|
|
179
|
+
<% if @error.resolved? %>
|
|
180
|
+
<span class="badge bg-success">
|
|
181
|
+
<i class="bi bi-check-circle"></i> Resolved
|
|
182
|
+
</span>
|
|
183
|
+
<% if @error.resolved_at.present? %>
|
|
184
|
+
<br>
|
|
185
|
+
<small class="text-muted mt-1 d-block">
|
|
186
|
+
<%= @error.resolved_at.strftime("%B %d, %Y at %I:%M %p") %>
|
|
187
|
+
</small>
|
|
188
|
+
<% end %>
|
|
189
|
+
<% else %>
|
|
190
|
+
<span class="badge bg-danger">
|
|
191
|
+
<i class="bi bi-exclamation-circle"></i> Unresolved
|
|
192
|
+
</span>
|
|
193
|
+
<% end %>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<% if @error.resolved? && @error.resolved_by_name.present? %>
|
|
197
|
+
<div class="mb-3">
|
|
198
|
+
<small class="text-muted d-block mb-1">Resolved By</small>
|
|
199
|
+
<strong><%= @error.resolved_by_name %></strong>
|
|
200
|
+
</div>
|
|
201
|
+
<% end %>
|
|
202
|
+
|
|
203
|
+
<% if @error.resolved? && @error.resolution_reference.present? %>
|
|
204
|
+
<div class="mb-3">
|
|
205
|
+
<small class="text-muted d-block mb-1">Reference</small>
|
|
206
|
+
<code><%= @error.resolution_reference %></code>
|
|
207
|
+
</div>
|
|
208
|
+
<% end %>
|
|
209
|
+
|
|
210
|
+
<% if @error.resolved? && @error.resolution_comment.present? %>
|
|
211
|
+
<div class="mb-3">
|
|
212
|
+
<small class="text-muted d-block mb-1">Resolution Notes</small>
|
|
213
|
+
<div class="alert alert-success mb-0">
|
|
214
|
+
<%= simple_format(@error.resolution_comment) %>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<% end %>
|
|
218
|
+
|
|
219
|
+
<div>
|
|
220
|
+
<small class="text-muted d-block mb-1">Error ID</small>
|
|
221
|
+
<code><%= @error.id %></code>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<!-- Quick Actions -->
|
|
227
|
+
<div class="card">
|
|
228
|
+
<div class="card-header bg-white">
|
|
229
|
+
<h5 class="mb-0"><i class="bi bi-lightning"></i> Quick Actions</h5>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="card-body">
|
|
232
|
+
<div class="d-grid gap-2">
|
|
233
|
+
<%= link_to errors_path(error_type: @error.error_type), class: "btn btn-outline-primary btn-sm" do %>
|
|
234
|
+
<i class="bi bi-filter"></i> View Similar Errors
|
|
235
|
+
<% end %>
|
|
236
|
+
|
|
237
|
+
<% if @error.user %>
|
|
238
|
+
<%= link_to errors_path(user_id: @error.user_id), class: "btn btn-outline-primary btn-sm" do %>
|
|
239
|
+
<i class="bi bi-person"></i> View User's Errors
|
|
240
|
+
<% end %>
|
|
241
|
+
<% end %>
|
|
242
|
+
|
|
243
|
+
<%= link_to errors_path(platform: @error.platform), class: "btn btn-outline-primary btn-sm" do %>
|
|
244
|
+
<i class="bi bi-phone"></i> View <%= @error.platform %> Errors
|
|
245
|
+
<% end %>
|
|
246
|
+
|
|
247
|
+
<%= link_to analytics_errors_path, class: "btn btn-outline-secondary btn-sm" do %>
|
|
248
|
+
<i class="bi bi-graph-up"></i> View Analytics
|
|
249
|
+
<% end %>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<!-- Resolve Error Modal -->
|
|
258
|
+
<div class="modal fade" id="resolveModal" tabindex="-1" aria-labelledby="resolveModalLabel" aria-hidden="true">
|
|
259
|
+
<div class="modal-dialog">
|
|
260
|
+
<div class="modal-content">
|
|
261
|
+
<%= form_with url: resolve_error_path(@error), method: :patch, class: "modal-content" do |f| %>
|
|
262
|
+
<div class="modal-header">
|
|
263
|
+
<h5 class="modal-title" id="resolveModalLabel">
|
|
264
|
+
<i class="bi bi-check-circle"></i> Mark Error as Resolved
|
|
265
|
+
</h5>
|
|
266
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="modal-body">
|
|
269
|
+
<div class="mb-3">
|
|
270
|
+
<label for="resolved_by_name" class="form-label">Your Name <span class="text-danger">*</span></label>
|
|
271
|
+
<%= text_field_tag :resolved_by_name, nil, class: "form-control", placeholder: "e.g., John Doe", required: true %>
|
|
272
|
+
<small class="text-muted">Who is resolving this error?</small>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div class="mb-3">
|
|
276
|
+
<label for="resolution_reference" class="form-label">Reference (Optional)</label>
|
|
277
|
+
<%= text_field_tag :resolution_reference, nil, class: "form-control", placeholder: "e.g., JIRA-123, PR #456, GitHub Issue #789" %>
|
|
278
|
+
<small class="text-muted">Link to ticket, PR, or issue</small>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div class="mb-3">
|
|
282
|
+
<label for="resolution_comment" class="form-label">Resolution Notes (Optional)</label>
|
|
283
|
+
<%= text_area_tag :resolution_comment, nil, class: "form-control", rows: 4, placeholder: "Describe what was done to fix this error..." %>
|
|
284
|
+
<small class="text-muted">What was done to resolve this error?</small>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
<div class="modal-footer">
|
|
288
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
289
|
+
<%= submit_tag "Mark as Resolved", class: "btn btn-success" %>
|
|
290
|
+
</div>
|
|
291
|
+
<% end %>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
data/config/routes.rb
ADDED