rails_error_dashboard 0.1.29 → 0.1.30
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 +34 -6
- data/app/controllers/rails_error_dashboard/errors_controller.rb +22 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +79 -7
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +149 -0
- data/app/models/rails_error_dashboard/application.rb +1 -1
- data/app/models/rails_error_dashboard/error_log.rb +44 -16
- data/app/views/layouts/rails_error_dashboard.html.erb +66 -1237
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +10 -2
- data/app/views/rails_error_dashboard/errors/_source_code.html.erb +76 -0
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +18 -82
- data/app/views/rails_error_dashboard/errors/index.html.erb +64 -31
- data/app/views/rails_error_dashboard/errors/overview.html.erb +181 -3
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +2 -1
- data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +286 -0
- data/app/views/rails_error_dashboard/errors/settings.html.erb +146 -480
- data/app/views/rails_error_dashboard/errors/show.html.erb +44 -20
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +188 -0
- data/db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb +5 -0
- data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +3 -0
- data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +3 -0
- data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +4 -0
- data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +3 -0
- data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +3 -0
- data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +3 -0
- data/db/migrate/20251225100236_create_error_occurrences.rb +3 -0
- data/db/migrate/20251225101920_create_cascade_patterns.rb +3 -0
- data/db/migrate/20251225102500_create_error_baselines.rb +3 -0
- data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +3 -0
- data/db/migrate/20251226020100_create_error_comments.rb +3 -0
- data/db/migrate/20251229111223_add_additional_performance_indexes.rb +4 -0
- data/db/migrate/20260106094220_create_rails_error_dashboard_applications.rb +3 -0
- data/db/migrate/20260106094233_add_application_to_error_logs.rb +3 -0
- data/db/migrate/20260106094318_finalize_application_foreign_key.rb +5 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +32 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +37 -4
- data/lib/rails_error_dashboard/configuration.rb +160 -3
- data/lib/rails_error_dashboard/configuration_error.rb +24 -0
- data/lib/rails_error_dashboard/engine.rb +17 -0
- data/lib/rails_error_dashboard/helpers/user_model_detector.rb +138 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +19 -4
- data/lib/rails_error_dashboard/queries/errors_list.rb +20 -8
- data/lib/rails_error_dashboard/services/error_normalizer.rb +143 -0
- data/lib/rails_error_dashboard/services/git_blame_reader.rb +195 -0
- data/lib/rails_error_dashboard/services/github_link_generator.rb +159 -0
- data/lib/rails_error_dashboard/services/source_code_reader.rb +214 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +6 -0
- metadata +13 -10
- data/app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss +0 -107
- data/app/assets/stylesheets/rails_error_dashboard/_components.scss +0 -625
- data/app/assets/stylesheets/rails_error_dashboard/_layout.scss +0 -257
- data/app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss +0 -203
- data/app/assets/stylesheets/rails_error_dashboard/application.css +0 -15
- data/app/assets/stylesheets/rails_error_dashboard/application.css.map +0 -7
- data/app/assets/stylesheets/rails_error_dashboard/application.scss +0 -61
- data/app/views/layouts/rails_error_dashboard/application.html.erb +0 -55
|
@@ -75,9 +75,17 @@
|
|
|
75
75
|
</td>
|
|
76
76
|
<td onclick="window.location='<%= error_path(error) %>';">
|
|
77
77
|
<% if error.resolved? %>
|
|
78
|
-
<i class="bi bi-check-circle-fill text-success"
|
|
78
|
+
<i class="bi bi-check-circle-fill text-success"
|
|
79
|
+
data-bs-toggle="tooltip"
|
|
80
|
+
data-bs-placement="left"
|
|
81
|
+
data-bs-html="true"
|
|
82
|
+
title="<%= if error.resolution_comment.present?
|
|
83
|
+
"<strong>Resolved</strong><br><em>#{error.resolution_comment.truncate(100).gsub('"', '"').gsub("'", ''')}</em>"
|
|
84
|
+
else
|
|
85
|
+
'Resolved'
|
|
86
|
+
end %>"></i>
|
|
79
87
|
<% else %>
|
|
80
|
-
<i class="bi bi-exclamation-circle-fill text-danger" title="Unresolved"></i>
|
|
88
|
+
<i class="bi bi-exclamation-circle-fill text-danger" data-bs-toggle="tooltip" title="Unresolved"></i>
|
|
81
89
|
<% end %>
|
|
82
90
|
</td>
|
|
83
91
|
<td onclick="event.stopPropagation();">
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<%# Displays source code for a backtrace frame with git blame and repository links %>
|
|
2
|
+
<%# Usage: render "source_code", frame: frame, error: @error, index: 0 %>
|
|
3
|
+
|
|
4
|
+
<% if RailsErrorDashboard.configuration.enable_source_code_integration %>
|
|
5
|
+
<% source_data = read_source_code(frame) %>
|
|
6
|
+
<% blame_data = read_git_blame(frame) if RailsErrorDashboard.configuration.enable_git_blame %>
|
|
7
|
+
<% repo_link = generate_repository_link(frame, error) %>
|
|
8
|
+
|
|
9
|
+
<% if source_data && source_data[:lines].present? %>
|
|
10
|
+
<div class="source-code-viewer mt-2 mb-3 border rounded shadow-sm">
|
|
11
|
+
<!-- Header with file info and actions -->
|
|
12
|
+
<div class="source-code-header bg-light border-bottom">
|
|
13
|
+
<!-- Top row: File path and View Source button -->
|
|
14
|
+
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-bottom">
|
|
15
|
+
<div class="d-flex align-items-center gap-2">
|
|
16
|
+
<i class="bi bi-file-code text-primary"></i>
|
|
17
|
+
<span class="font-monospace source-file-path fw-medium" style="font-size: 0.9rem;">
|
|
18
|
+
<%= frame[:short_path] %>:<%= frame[:line_number] %>
|
|
19
|
+
</span>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<% if repo_link %>
|
|
23
|
+
<%= link_to repo_link, target: "_blank", rel: "noopener",
|
|
24
|
+
class: "btn btn-sm btn-primary",
|
|
25
|
+
title: "View on #{repo_link.include?('github') ? 'GitHub' : repo_link.include?('gitlab') ? 'GitLab' : 'Bitbucket'}" do %>
|
|
26
|
+
<i class="bi bi-box-arrow-up-right"></i> View Source
|
|
27
|
+
<% end %>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Bottom row: Git blame info (if available) -->
|
|
32
|
+
<% if blame_data %>
|
|
33
|
+
<div class="git-blame-info px-3 py-2 bg-white">
|
|
34
|
+
<div class="d-flex align-items-center gap-3 text-muted" style="font-size: 0.85rem;">
|
|
35
|
+
<div class="d-flex align-items-center gap-1">
|
|
36
|
+
<i class="bi bi-person-circle"></i>
|
|
37
|
+
<span><%= blame_data[:author] %></span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="d-flex align-items-center gap-1">
|
|
40
|
+
<i class="bi bi-clock"></i>
|
|
41
|
+
<span><%= time_ago_in_words(blame_data[:date]) %> ago</span>
|
|
42
|
+
</div>
|
|
43
|
+
<% if blame_data[:commit_message] %>
|
|
44
|
+
<div class="d-flex align-items-center gap-1 flex-grow-1">
|
|
45
|
+
<i class="bi bi-chat-left-text"></i>
|
|
46
|
+
<span class="text-truncate" style="max-width: 400px;" title="<%= blame_data[:commit_message] %>">
|
|
47
|
+
<%= blame_data[:commit_message] %>
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
<% end %>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Source code lines with syntax highlighting -->
|
|
57
|
+
<div class="source-code-content bg-white">
|
|
58
|
+
<div class="code-block" style="max-height: 500px; overflow-y: auto; overflow-x: auto;">
|
|
59
|
+
<%
|
|
60
|
+
# Find the error line number for highlighting
|
|
61
|
+
error_line_number = source_data[:lines].find { |l| l[:highlight] }&.dig(:number)
|
|
62
|
+
# Get start line number (first line in context)
|
|
63
|
+
start_line = source_data[:lines].first[:number]
|
|
64
|
+
%>
|
|
65
|
+
<pre class="mb-0"><code class="language-<%= source_data[:language] || 'plaintext' %>" data-error-line="<%= error_line_number %>" data-start-line="<%= start_line %>"><%= source_data[:lines].map { |l| l[:content] }.join("\n") %></code></pre>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<% elsif source_data && source_data[:error] %>
|
|
70
|
+
<!-- Error message if source couldn't be read -->
|
|
71
|
+
<div class="alert alert-warning mt-2 mb-2 py-2">
|
|
72
|
+
<i class="bi bi-exclamation-triangle"></i>
|
|
73
|
+
<small>Could not read source: <%= source_data[:error] %></small>
|
|
74
|
+
</div>
|
|
75
|
+
<% end %>
|
|
76
|
+
<% end %>
|
|
@@ -74,96 +74,32 @@
|
|
|
74
74
|
<% end %>
|
|
75
75
|
|
|
76
76
|
<% if error.resolved? %>
|
|
77
|
-
<span class="badge bg-success"
|
|
77
|
+
<span class="badge bg-success"
|
|
78
|
+
<% if error.resolution_comment.present? %>
|
|
79
|
+
data-bs-toggle="tooltip"
|
|
80
|
+
data-bs-placement="top"
|
|
81
|
+
data-bs-html="true"
|
|
82
|
+
title="<strong>Resolution Notes:</strong><br><%= error.resolution_comment.truncate(150).gsub('"', '"') %>"
|
|
83
|
+
<% end %>>
|
|
78
84
|
<i class="bi bi-check-circle"></i> Resolved
|
|
79
85
|
</span>
|
|
80
86
|
<% end %>
|
|
81
87
|
</div>
|
|
88
|
+
|
|
89
|
+
<% if error.resolved? && error.resolution_comment.present? %>
|
|
90
|
+
<div class="mt-2 p-2 bg-success bg-opacity-10 border border-success rounded">
|
|
91
|
+
<small class="text-success fw-bold">
|
|
92
|
+
<i class="bi bi-journal-text"></i> Resolution Notes:
|
|
93
|
+
</small>
|
|
94
|
+
<div class="mb-0 small text-muted mt-1">
|
|
95
|
+
<%= auto_link_urls(truncate(error.resolution_comment, length: 200), error: error).html_safe %>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<% end %>
|
|
82
99
|
</div>
|
|
83
100
|
</div>
|
|
84
101
|
<% end %>
|
|
85
102
|
</div>
|
|
86
103
|
</div>
|
|
87
104
|
</div>
|
|
88
|
-
|
|
89
|
-
<style>
|
|
90
|
-
.timeline {
|
|
91
|
-
position: relative;
|
|
92
|
-
padding-left: 0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.timeline-item {
|
|
96
|
-
position: relative;
|
|
97
|
-
padding-left: 40px;
|
|
98
|
-
padding-bottom: 30px;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.timeline-item-last {
|
|
102
|
-
padding-bottom: 0;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.timeline-item::before {
|
|
106
|
-
content: '';
|
|
107
|
-
position: absolute;
|
|
108
|
-
left: 11px;
|
|
109
|
-
top: 30px;
|
|
110
|
-
bottom: 0;
|
|
111
|
-
width: 2px;
|
|
112
|
-
background: var(--bs-border-color);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.timeline-item-last::before {
|
|
116
|
-
display: none;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
.timeline-marker {
|
|
120
|
-
position: absolute;
|
|
121
|
-
left: 0;
|
|
122
|
-
top: 0;
|
|
123
|
-
width: 24px;
|
|
124
|
-
height: 24px;
|
|
125
|
-
display: flex;
|
|
126
|
-
align-items: center;
|
|
127
|
-
justify-content: center;
|
|
128
|
-
background: white;
|
|
129
|
-
border: 2px solid var(--bs-border-color);
|
|
130
|
-
border-radius: 50%;
|
|
131
|
-
z-index: 1;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.timeline-marker-current {
|
|
135
|
-
border-color: var(--bs-danger);
|
|
136
|
-
background: var(--bs-danger-bg-subtle);
|
|
137
|
-
box-shadow: 0 0 0 4px var(--bs-danger-bg-subtle);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.timeline-marker i {
|
|
141
|
-
font-size: 12px;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.timeline-content {
|
|
145
|
-
background: var(--bs-body-bg);
|
|
146
|
-
padding: 12px;
|
|
147
|
-
border-radius: 8px;
|
|
148
|
-
border: 1px solid var(--bs-border-color);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.timeline-item:hover .timeline-content {
|
|
152
|
-
border-color: var(--bs-primary);
|
|
153
|
-
background: var(--bs-primary-bg-subtle);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/* Dark mode support */
|
|
157
|
-
body.dark-mode .timeline-marker,
|
|
158
|
-
html[data-theme="dark"] body .timeline-marker {
|
|
159
|
-
background: var(--card-bg);
|
|
160
|
-
border-color: var(--border-color);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
body.dark-mode .timeline-marker-current,
|
|
164
|
-
html[data-theme="dark"] body .timeline-marker-current {
|
|
165
|
-
border-color: var(--bs-danger);
|
|
166
|
-
background: rgba(220, 53, 69, 0.2);
|
|
167
|
-
}
|
|
168
|
-
</style>
|
|
169
105
|
<% end %>
|
|
@@ -148,6 +148,10 @@
|
|
|
148
148
|
active_filters << { label: "Unassigned", param: :assigned_to }
|
|
149
149
|
elsif params[:assigned_to] == '__assigned__'
|
|
150
150
|
active_filters << { label: "Assigned", param: :assigned_to }
|
|
151
|
+
# Show assignee name filter if present
|
|
152
|
+
if params[:assignee_name].present?
|
|
153
|
+
active_filters << { label: "Assignee: #{params[:assignee_name]}", param: :assignee_name }
|
|
154
|
+
end
|
|
151
155
|
end
|
|
152
156
|
%>
|
|
153
157
|
|
|
@@ -185,8 +189,6 @@
|
|
|
185
189
|
class: "btn btn-sm #{params[:assigned_to].blank? ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
186
190
|
<%= link_to "Unassigned", errors_path(assigned_to: '__unassigned__'),
|
|
187
191
|
class: "btn btn-sm #{params[:assigned_to] == '__unassigned__' ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
188
|
-
<%= link_to "My Errors", errors_path(assigned_to: current_user_name),
|
|
189
|
-
class: "btn btn-sm #{params[:assigned_to] == current_user_name ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
190
192
|
</div>
|
|
191
193
|
|
|
192
194
|
<%= form_with url: errors_path, method: :get, class: "row g-3", data: { turbo: false } do %>
|
|
@@ -268,44 +270,54 @@
|
|
|
268
270
|
['All Assignments', ''],
|
|
269
271
|
['Unassigned', '__unassigned__'],
|
|
270
272
|
['Assigned', '__assigned__']
|
|
271
|
-
], params[:assigned_to]), class: "form-select" %>
|
|
273
|
+
], params[:assigned_to]), class: "form-select", id: "assigned_to_filter" %>
|
|
272
274
|
</div>
|
|
273
275
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
['
|
|
278
|
-
[
|
|
279
|
-
|
|
280
|
-
['⚪ Low (P0)', 0]
|
|
281
|
-
], params[:priority_level]), class: "form-select" %>
|
|
276
|
+
<!-- Assignee Name Filter - Only shown when "Assigned" is selected -->
|
|
277
|
+
<div class="col-md-2" id="assignee_name_filter" style="<%= params[:assigned_to] == '__assigned__' ? '' : 'display: none;' %>">
|
|
278
|
+
<%= select_tag :assignee_name, options_for_select(
|
|
279
|
+
[['All Assignees', '']] + @assignees.map { |name| [name, name] },
|
|
280
|
+
params[:assignee_name]
|
|
281
|
+
), class: "form-select", placeholder: "Filter by assignee..." %>
|
|
282
282
|
</div>
|
|
283
283
|
|
|
284
|
-
<div class="col-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
# If unresolved param is explicitly "false" or "0", uncheck it
|
|
290
|
-
is_checked = if params[:unresolved].nil?
|
|
291
|
-
true # Default to checked when first loading
|
|
292
|
-
else
|
|
293
|
-
params[:unresolved] != "false" && params[:unresolved] != "0"
|
|
294
|
-
end
|
|
295
|
-
%>
|
|
296
|
-
<%= check_box_tag :unresolved, "1", is_checked, class: "form-check-input", id: "unresolved_checkbox" %>
|
|
297
|
-
<%= label_tag :unresolved, "Unresolved only", class: "form-check-label" %>
|
|
298
|
-
</div>
|
|
284
|
+
<div class="col-md-2">
|
|
285
|
+
<%= select_tag :priority_level, options_for_select(
|
|
286
|
+
[['All Priorities', '']] + RailsErrorDashboard::ErrorLog.priority_options(include_emoji: true),
|
|
287
|
+
params[:priority_level]
|
|
288
|
+
), class: "form-select" %>
|
|
299
289
|
</div>
|
|
300
290
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
291
|
+
<!-- Checkboxes on their own row -->
|
|
292
|
+
<div class="col-12 mt-2">
|
|
293
|
+
<div class="row g-3">
|
|
294
|
+
<div class="col-auto">
|
|
295
|
+
<div class="form-check">
|
|
296
|
+
<%
|
|
297
|
+
# Determine checkbox state based on params
|
|
298
|
+
# If unresolved param is missing or nil, default to checked (true)
|
|
299
|
+
# If unresolved param is explicitly "false" or "0", uncheck it
|
|
300
|
+
is_checked = if params[:unresolved].nil?
|
|
301
|
+
true # Default to checked when first loading
|
|
302
|
+
else
|
|
303
|
+
params[:unresolved] != "false" && params[:unresolved] != "0"
|
|
304
|
+
end
|
|
305
|
+
%>
|
|
306
|
+
<%= check_box_tag :unresolved, "1", is_checked, class: "form-check-input", id: "unresolved_checkbox" %>
|
|
307
|
+
<%= label_tag :unresolved, "Unresolved only", class: "form-check-label" %>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div class="col-auto">
|
|
312
|
+
<div class="form-check">
|
|
313
|
+
<%= check_box_tag :hide_snoozed, "1", params[:hide_snoozed] == "1", class: "form-check-input" %>
|
|
314
|
+
<%= label_tag :hide_snoozed, "Hide snoozed", class: "form-check-label" %>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
305
317
|
</div>
|
|
306
318
|
</div>
|
|
307
319
|
|
|
308
|
-
<div class="col-12">
|
|
320
|
+
<div class="col-12 mt-3">
|
|
309
321
|
<%= submit_tag "Apply Filters", class: "btn btn-primary" %>
|
|
310
322
|
<%= link_to "Clear", errors_path, class: "btn btn-outline-secondary" %>
|
|
311
323
|
</div>
|
|
@@ -600,4 +612,25 @@
|
|
|
600
612
|
document.title = `(${unresolvedCount}) ${document.title}`;
|
|
601
613
|
}
|
|
602
614
|
});
|
|
615
|
+
|
|
616
|
+
// Toggle assignee name filter visibility based on assigned_to selection
|
|
617
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
618
|
+
const assignedToFilter = document.getElementById('assigned_to_filter');
|
|
619
|
+
const assigneeNameFilter = document.getElementById('assignee_name_filter');
|
|
620
|
+
|
|
621
|
+
if (assignedToFilter && assigneeNameFilter) {
|
|
622
|
+
assignedToFilter.addEventListener('change', function() {
|
|
623
|
+
if (this.value === '__assigned__') {
|
|
624
|
+
assigneeNameFilter.style.display = 'block';
|
|
625
|
+
} else {
|
|
626
|
+
assigneeNameFilter.style.display = 'none';
|
|
627
|
+
// Reset assignee name filter when hiding
|
|
628
|
+
const assigneeNameSelect = assigneeNameFilter.querySelector('select');
|
|
629
|
+
if (assigneeNameSelect) {
|
|
630
|
+
assigneeNameSelect.value = '';
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
});
|
|
603
636
|
</script>
|
|
@@ -45,8 +45,23 @@
|
|
|
45
45
|
</div>
|
|
46
46
|
</div>
|
|
47
47
|
|
|
48
|
+
<!-- Unresolved Errors Card -->
|
|
49
|
+
<div class="col-12 col-md-6 col-lg-4">
|
|
50
|
+
<div class="card stat-card h-100">
|
|
51
|
+
<div class="card-body text-center">
|
|
52
|
+
<div class="stat-label mb-2">UNRESOLVED ERRORS</div>
|
|
53
|
+
<div class="stat-value text-danger">
|
|
54
|
+
<%= @stats[:unresolved] %>
|
|
55
|
+
</div>
|
|
56
|
+
<small class="text-muted">
|
|
57
|
+
Pending resolution
|
|
58
|
+
</small>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
48
63
|
<!-- Trend Card -->
|
|
49
|
-
<div class="col-12 col-lg-4">
|
|
64
|
+
<div class="col-12 col-md-6 col-lg-4">
|
|
50
65
|
<div class="card stat-card h-100">
|
|
51
66
|
<div class="card-body text-center">
|
|
52
67
|
<div class="stat-label mb-2">ERROR TREND</div>
|
|
@@ -60,15 +75,58 @@
|
|
|
60
75
|
</div>
|
|
61
76
|
</div>
|
|
62
77
|
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Resolution Rate Card -->
|
|
80
|
+
<div class="col-12 col-md-6 col-lg-4">
|
|
81
|
+
<div class="card stat-card h-100">
|
|
82
|
+
<div class="card-body text-center">
|
|
83
|
+
<div class="stat-label mb-2">RESOLUTION RATE</div>
|
|
84
|
+
<%
|
|
85
|
+
total_errors = @stats[:resolved] + @stats[:unresolved]
|
|
86
|
+
resolution_rate = total_errors > 0 ? ((@stats[:resolved].to_f / total_errors) * 100).round(1) : 0.0
|
|
87
|
+
%>
|
|
88
|
+
<div class="stat-value <%= resolution_rate >= 80 ? 'text-success' : resolution_rate >= 50 ? 'text-warning' : 'text-danger' %>">
|
|
89
|
+
<%= resolution_rate %>%
|
|
90
|
+
</div>
|
|
91
|
+
<small class="text-muted">
|
|
92
|
+
<%= @stats[:resolved] %> of <%= total_errors %> resolved
|
|
93
|
+
</small>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<!-- Average Resolution Time Card -->
|
|
99
|
+
<div class="col-12 col-md-6 col-lg-4">
|
|
100
|
+
<div class="card stat-card h-100">
|
|
101
|
+
<div class="card-body text-center">
|
|
102
|
+
<div class="stat-label mb-2">AVG RESOLUTION TIME</div>
|
|
103
|
+
<% if @stats[:average_resolution_time].present? %>
|
|
104
|
+
<div class="stat-value text-warning">
|
|
105
|
+
<%= @stats[:average_resolution_time] %>h
|
|
106
|
+
</div>
|
|
107
|
+
<small class="text-muted">
|
|
108
|
+
Mean time to resolution
|
|
109
|
+
</small>
|
|
110
|
+
<% else %>
|
|
111
|
+
<div class="stat-value text-muted">
|
|
112
|
+
--
|
|
113
|
+
</div>
|
|
114
|
+
<small class="text-muted">
|
|
115
|
+
No resolved errors yet
|
|
116
|
+
</small>
|
|
117
|
+
<% end %>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
63
121
|
</div>
|
|
64
122
|
|
|
65
|
-
<!-- Top
|
|
123
|
+
<!-- Top 6 Errors by Impact (Mobile: stack, Desktop: 2 columns) -->
|
|
66
124
|
<% if @stats[:top_errors_by_impact].any? %>
|
|
67
125
|
<div class="card mb-4">
|
|
68
126
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
69
127
|
<h5 class="mb-0">
|
|
70
128
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
71
|
-
Top
|
|
129
|
+
Top 6 Errors by Impact
|
|
72
130
|
</h5>
|
|
73
131
|
<%= link_to "View All Errors →", errors_path, class: "btn btn-sm btn-outline-primary" %>
|
|
74
132
|
</div>
|
|
@@ -174,6 +232,126 @@
|
|
|
174
232
|
</div>
|
|
175
233
|
<% end %>
|
|
176
234
|
|
|
235
|
+
<!-- Correlation Summary (Last 7 days) -->
|
|
236
|
+
<% if RailsErrorDashboard.configuration.enable_error_correlation && (@problematic_releases.any? || @time_correlated_errors.any? || @multi_error_users.any?) %>
|
|
237
|
+
<%
|
|
238
|
+
# Calculate column width based on number of cards present
|
|
239
|
+
cards_present = [@problematic_releases.any?, @time_correlated_errors.any?, @multi_error_users.any?].count(true)
|
|
240
|
+
col_class = case cards_present
|
|
241
|
+
when 1 then "col-12"
|
|
242
|
+
when 2 then "col-12 col-lg-6"
|
|
243
|
+
else "col-12 col-lg-4"
|
|
244
|
+
end
|
|
245
|
+
%>
|
|
246
|
+
<div class="card mb-4">
|
|
247
|
+
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
248
|
+
<h5 class="mb-0">
|
|
249
|
+
<i class="bi bi-diagram-3 me-2"></i>
|
|
250
|
+
Error Correlation Insights
|
|
251
|
+
</h5>
|
|
252
|
+
<%= link_to "Full Analysis →", correlation_errors_path, class: "btn btn-sm btn-outline-primary" %>
|
|
253
|
+
</div>
|
|
254
|
+
<div class="card-body">
|
|
255
|
+
<div class="row g-3">
|
|
256
|
+
<!-- Problematic Releases -->
|
|
257
|
+
<% if @problematic_releases.any? %>
|
|
258
|
+
<div class="<%= col_class %>">
|
|
259
|
+
<div class="card h-100 border-warning">
|
|
260
|
+
<div class="card-body">
|
|
261
|
+
<h6 class="mb-3">
|
|
262
|
+
<i class="bi bi-tag me-2"></i>
|
|
263
|
+
Problematic Releases
|
|
264
|
+
</h6>
|
|
265
|
+
<div class="list-group list-group-flush">
|
|
266
|
+
<% @problematic_releases.each do |release| %>
|
|
267
|
+
<div class="list-group-item px-0 py-2">
|
|
268
|
+
<div class="d-flex justify-content-between align-items-start">
|
|
269
|
+
<div class="flex-grow-1">
|
|
270
|
+
<code class="small"><%= release[:version] || release[:git_sha]&.truncate(8, omission: '') %></code>
|
|
271
|
+
<div class="small text-muted">
|
|
272
|
+
<%= release[:error_count] %> error<%= release[:error_count] != 1 ? 's' : '' %> •
|
|
273
|
+
<%= release[:unique_types] %> type<%= release[:unique_types] != 1 ? 's' : '' %>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
<span class="badge bg-danger"><%= release[:severity_score] %></span>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
<% end %>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
<% end %>
|
|
285
|
+
|
|
286
|
+
<!-- Time-Correlated Errors -->
|
|
287
|
+
<% if @time_correlated_errors.any? %>
|
|
288
|
+
<div class="<%= col_class %>">
|
|
289
|
+
<div class="card h-100 border-info">
|
|
290
|
+
<div class="card-body">
|
|
291
|
+
<h6 class="mb-3">
|
|
292
|
+
<i class="bi bi-clock-history me-2"></i>
|
|
293
|
+
Time-Correlated Errors
|
|
294
|
+
</h6>
|
|
295
|
+
<div class="list-group list-group-flush">
|
|
296
|
+
<% @time_correlated_errors.each do |group| %>
|
|
297
|
+
<div class="list-group-item px-0 py-2">
|
|
298
|
+
<div class="small">
|
|
299
|
+
<strong><%= group[:error_types].size %> error types</strong>
|
|
300
|
+
<div class="text-muted">
|
|
301
|
+
occurred together <%= group[:occurrences] %> time<%= group[:occurrences] != 1 ? 's' : '' %>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="mt-1">
|
|
304
|
+
<% group[:error_types].first(2).each do |type| %>
|
|
305
|
+
<code class="small d-block text-truncate"><%= type %></code>
|
|
306
|
+
<% end %>
|
|
307
|
+
<% if group[:error_types].size > 2 %>
|
|
308
|
+
<small class="text-muted">+<%= group[:error_types].size - 2 %> more</small>
|
|
309
|
+
<% end %>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
<% end %>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
<% end %>
|
|
319
|
+
|
|
320
|
+
<!-- Multi-Error Users -->
|
|
321
|
+
<% if @multi_error_users.any? %>
|
|
322
|
+
<div class="<%= col_class %>">
|
|
323
|
+
<div class="card h-100 border-danger">
|
|
324
|
+
<div class="card-body">
|
|
325
|
+
<h6 class="mb-3">
|
|
326
|
+
<i class="bi bi-people-fill me-2"></i>
|
|
327
|
+
Users with Multiple Errors
|
|
328
|
+
</h6>
|
|
329
|
+
<div class="list-group list-group-flush">
|
|
330
|
+
<% @multi_error_users.each do |user| %>
|
|
331
|
+
<div class="list-group-item px-0 py-2">
|
|
332
|
+
<div class="d-flex justify-content-between align-items-start">
|
|
333
|
+
<div class="flex-grow-1">
|
|
334
|
+
<div class="small">
|
|
335
|
+
User ID: <strong><%= user[:user_id] %></strong>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="small text-muted">
|
|
338
|
+
<%= user[:error_types].size %> different error type<%= user[:error_types].size != 1 ? 's' : '' %>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
<span class="badge bg-danger"><%= user[:total_errors] %></span>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
<% end %>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
<% end %>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
<% end %>
|
|
354
|
+
|
|
177
355
|
<!-- Critical Alerts (Last hour) -->
|
|
178
356
|
<% if @critical_alerts.any? %>
|
|
179
357
|
<div class="alert alert-danger border-danger mb-4" role="alert">
|
|
@@ -378,7 +378,7 @@
|
|
|
378
378
|
const times = <%= raw @resolution_times.values.map { |v| v || 0 }.to_json %>;
|
|
379
379
|
|
|
380
380
|
new Chart(resolutionTimeCtx, {
|
|
381
|
-
type: '
|
|
381
|
+
type: 'bar',
|
|
382
382
|
data: {
|
|
383
383
|
labels: platforms,
|
|
384
384
|
datasets: [{
|
|
@@ -390,6 +390,7 @@
|
|
|
390
390
|
}]
|
|
391
391
|
},
|
|
392
392
|
options: {
|
|
393
|
+
indexAxis: 'y', // Horizontal bar chart
|
|
393
394
|
responsive: true,
|
|
394
395
|
maintainAspectRatio: false,
|
|
395
396
|
plugins: {
|