rails_error_dashboard 0.6.3 → 0.7.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 +101 -6
- data/app/controllers/rails_error_dashboard/application_controller.rb +8 -2
- data/app/controllers/rails_error_dashboard/errors_controller.rb +66 -14
- data/app/helpers/rails_error_dashboard/application_helper.rb +42 -10
- data/app/views/layouts/rails_error_dashboard.html.erb +307 -0
- data/app/views/rails_error_dashboard/errors/_ai_help_panel.html.erb +36 -0
- data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +64 -5
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +2 -2
- data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +1 -0
- data/app/views/rails_error_dashboard/errors/_llm_summary.html.erb +97 -0
- data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +1 -0
- data/app/views/rails_error_dashboard/errors/_modals.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +21 -20
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +33 -19
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +1 -2
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +5 -1
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +16 -4
- data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +7 -3
- data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/index.html.erb +7 -1
- data/app/views/rails_error_dashboard/errors/overview.html.erb +2 -2
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +3 -1
- data/app/views/rails_error_dashboard/errors/releases.html.erb +3 -1
- data/app/views/rails_error_dashboard/errors/settings.html.erb +0 -1
- data/app/views/rails_error_dashboard/errors/show.html.erb +12 -2
- data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +5 -1
- data/app/views/rails_error_dashboard/errors/user_impact.html.erb +3 -1
- data/config/routes.rb +1 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +27 -0
- data/lib/rails_error_dashboard/configuration.rb +101 -1
- data/lib/rails_error_dashboard/engine.rb +14 -0
- data/lib/rails_error_dashboard/integrations/llm_middleware.rb +276 -0
- data/lib/rails_error_dashboard/integrations/llm_span_processor.rb +181 -0
- data/lib/rails_error_dashboard/integrations/o_tel.rb +45 -0
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +4 -1
- data/lib/rails_error_dashboard/queries/error_correlation.rb +17 -13
- data/lib/rails_error_dashboard/queries/errors_list.rb +14 -0
- data/lib/rails_error_dashboard/services/cascade_detector.rb +28 -18
- data/lib/rails_error_dashboard/services/llm_client.rb +368 -0
- data/lib/rails_error_dashboard/services/llm_cost_estimator.rb +85 -0
- data/lib/rails_error_dashboard/services/llm_summary.rb +91 -0
- data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +87 -0
- data/lib/rails_error_dashboard/subscribers/llm_call_subscriber.rb +150 -0
- data/lib/rails_error_dashboard/value_objects/llm_call_event.rb +92 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +8 -0
- metadata +13 -3
|
@@ -35,37 +35,49 @@
|
|
|
35
35
|
<div class="mb-3">
|
|
36
36
|
<small class="metadata-label d-block mb-1">Severity Level</small>
|
|
37
37
|
<% severity = error.severity %>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
<%= link_to errors_path(severity: severity, unresolved: '0'), class: "text-decoration-none", title: "View all #{severity} errors" do %>
|
|
39
|
+
<% if severity == :critical %>
|
|
40
|
+
<span class="badge bg-danger fs-6">CRITICAL</span>
|
|
41
|
+
<% elsif severity == :high %>
|
|
42
|
+
<span class="badge bg-warning text-dark fs-6">HIGH</span>
|
|
43
|
+
<% elsif severity == :medium %>
|
|
44
|
+
<span class="badge bg-info text-dark fs-6">MEDIUM</span>
|
|
45
|
+
<% else %>
|
|
46
|
+
<span class="badge bg-secondary fs-6">LOW</span>
|
|
47
|
+
<% end %>
|
|
46
48
|
<% end %>
|
|
47
49
|
</div>
|
|
48
50
|
|
|
49
51
|
<div class="mb-3">
|
|
50
52
|
<small class="metadata-label d-block mb-1">Platform</small>
|
|
51
|
-
<% if error.platform
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
<% if error.platform.present? %>
|
|
54
|
+
<%= link_to errors_path(platform: error.platform, unresolved: '0'), class: "text-decoration-none", title: "View all #{error.platform} errors" do %>
|
|
55
|
+
<% if error.platform == 'iOS' %>
|
|
56
|
+
<span class="badge badge-ios"><i class="bi bi-apple"></i> iOS</span>
|
|
57
|
+
<% elsif error.platform == 'Android' %>
|
|
58
|
+
<span class="badge badge-android"><i class="bi bi-android2"></i> Android</span>
|
|
59
|
+
<% elsif error.platform == 'Web' %>
|
|
60
|
+
<span class="badge badge-web"><i class="bi bi-globe"></i> Web</span>
|
|
61
|
+
<% else %>
|
|
62
|
+
<span class="badge badge-api"><i class="bi bi-server"></i> <%= error.platform %></span>
|
|
63
|
+
<% end %>
|
|
64
|
+
<% end %>
|
|
57
65
|
<% else %>
|
|
58
|
-
<span class="badge badge-api"><i class="bi bi-server"></i>
|
|
66
|
+
<span class="badge badge-api"><i class="bi bi-server"></i> API</span>
|
|
59
67
|
<% end %>
|
|
60
68
|
</div>
|
|
61
69
|
|
|
62
70
|
<div class="mb-3">
|
|
63
71
|
<small class="metadata-label d-block mb-1">User</small>
|
|
64
72
|
<% if error.respond_to?(:user) && error.user %>
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
<%= link_to errors_path(user_id: error.user_id, unresolved: '0'), class: "text-decoration-none", title: "View all errors for this user" do %>
|
|
74
|
+
<strong><%= error.user.email %></strong><br>
|
|
75
|
+
<small class="text-muted">ID: <%= error.user_id %></small>
|
|
76
|
+
<% end %>
|
|
67
77
|
<% elsif error.user_id %>
|
|
68
|
-
|
|
78
|
+
<%= link_to errors_path(user_id: error.user_id, unresolved: '0'), class: "text-decoration-none text-muted", title: "View all errors for this user" do %>
|
|
79
|
+
User ID: <%= error.user_id %>
|
|
80
|
+
<% end %>
|
|
69
81
|
<% else %>
|
|
70
82
|
<span class="text-muted">Guest / Unauthenticated</span>
|
|
71
83
|
<% end %>
|
|
@@ -249,7 +261,9 @@
|
|
|
249
261
|
<% if error.app_version.present? %>
|
|
250
262
|
<div class="mb-1">
|
|
251
263
|
<small class="text-muted">App Version:</small>
|
|
252
|
-
|
|
264
|
+
<%= link_to errors_path(app_version: error.app_version, unresolved: '0'), class: "ms-1 text-decoration-none", title: "View all errors in #{error.app_version}" do %>
|
|
265
|
+
<code><%= error.app_version %></code>
|
|
266
|
+
<% end %>
|
|
253
267
|
</div>
|
|
254
268
|
<% end %>
|
|
255
269
|
|
|
@@ -78,8 +78,7 @@
|
|
|
78
78
|
<% if error.resolution_comment.present? %>
|
|
79
79
|
data-bs-toggle="tooltip"
|
|
80
80
|
data-bs-placement="top"
|
|
81
|
-
|
|
82
|
-
title="<strong>Resolution Notes:</strong><br><%= error.resolution_comment.truncate(150).gsub('"', '"') %>"
|
|
81
|
+
title="Resolution Notes: <%= error.resolution_comment.truncate(150) %>"
|
|
83
82
|
<% end %>>
|
|
84
83
|
<i class="bi bi-check-circle"></i> Resolved
|
|
85
84
|
</span>
|
|
@@ -339,7 +339,11 @@
|
|
|
339
339
|
<tbody>
|
|
340
340
|
<% @recurring_data[:high_frequency_errors].first(10).each do |error| %>
|
|
341
341
|
<tr>
|
|
342
|
-
<td
|
|
342
|
+
<td>
|
|
343
|
+
<%= link_to errors_path(error_type: error[:error_type], unresolved: '0'), class: "text-decoration-none" do %>
|
|
344
|
+
<code class="small"><%= error[:error_type] %></code>
|
|
345
|
+
<% end %>
|
|
346
|
+
</td>
|
|
343
347
|
<td><span class="badge bg-danger"><%= error[:total_occurrences] %></span></td>
|
|
344
348
|
<td><%= error[:duration_days] %> days</td>
|
|
345
349
|
<td><small class="text-muted"><%= local_time(error[:first_seen], format: :date_only) %></small></td>
|
|
@@ -90,7 +90,11 @@
|
|
|
90
90
|
<tbody>
|
|
91
91
|
<% @problematic_releases.each do |release| %>
|
|
92
92
|
<tr>
|
|
93
|
-
<td
|
|
93
|
+
<td>
|
|
94
|
+
<%= link_to errors_path(app_version: release[:version], unresolved: '0'), class: "text-decoration-none" do %>
|
|
95
|
+
<code><%= release[:version] %></code>
|
|
96
|
+
<% end %>
|
|
97
|
+
</td>
|
|
94
98
|
<td><strong class="text-danger"><%= release[:error_count] %></strong></td>
|
|
95
99
|
<td>
|
|
96
100
|
<span class="badge bg-danger">
|
|
@@ -105,7 +109,7 @@
|
|
|
105
109
|
<% end %>
|
|
106
110
|
</td>
|
|
107
111
|
<td>
|
|
108
|
-
<%= link_to "View", errors_path(
|
|
112
|
+
<%= link_to "View", errors_path(app_version: release[:version], unresolved: '0'), class: "btn btn-sm btn-outline-primary" %>
|
|
109
113
|
</td>
|
|
110
114
|
</tr>
|
|
111
115
|
<% end %>
|
|
@@ -141,7 +145,11 @@
|
|
|
141
145
|
<tbody>
|
|
142
146
|
<% @errors_by_version.sort_by { |_, v| -v[:count] }.first(10).each do |version, data| %>
|
|
143
147
|
<tr>
|
|
144
|
-
<td
|
|
148
|
+
<td>
|
|
149
|
+
<%= link_to errors_path(app_version: version, unresolved: '0'), class: "text-decoration-none" do %>
|
|
150
|
+
<code><%= version %></code>
|
|
151
|
+
<% end %>
|
|
152
|
+
</td>
|
|
145
153
|
<td><%= data[:count] %></td>
|
|
146
154
|
<td><%= data[:error_types] %></td>
|
|
147
155
|
<td>
|
|
@@ -185,7 +193,11 @@
|
|
|
185
193
|
<tbody>
|
|
186
194
|
<% @errors_by_git_sha.sort_by { |_, v| -v[:count] }.first(10).each do |sha, data| %>
|
|
187
195
|
<tr>
|
|
188
|
-
<td
|
|
196
|
+
<td>
|
|
197
|
+
<%= link_to errors_path(git_sha: sha, unresolved: '0'), class: "text-decoration-none" do %>
|
|
198
|
+
<code class="small"><%= sha[0..7] %></code>
|
|
199
|
+
<% end %>
|
|
200
|
+
</td>
|
|
189
201
|
<td><%= data[:count] %></td>
|
|
190
202
|
<td><%= data[:error_types] %></td>
|
|
191
203
|
<td>
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
<%= number_with_delimiter(table[:dead_tuples]) %>
|
|
151
151
|
<% end %>
|
|
152
152
|
</td>
|
|
153
|
-
<td><%= table[:last_autovacuum] ? local_time_ago(
|
|
153
|
+
<td><%= table[:last_autovacuum] ? local_time_ago(parse_pg_timestamp(table[:last_autovacuum])) : "Never" %></td>
|
|
154
154
|
</tr>
|
|
155
155
|
<% end %>
|
|
156
156
|
</tbody>
|
|
@@ -203,7 +203,7 @@
|
|
|
203
203
|
<%= number_with_delimiter(table[:dead_tuples]) %>
|
|
204
204
|
<% end %>
|
|
205
205
|
</td>
|
|
206
|
-
<td><%= table[:last_autovacuum] ? local_time_ago(
|
|
206
|
+
<td><%= table[:last_autovacuum] ? local_time_ago(parse_pg_timestamp(table[:last_autovacuum])) : "Never" %></td>
|
|
207
207
|
</tr>
|
|
208
208
|
<% end %>
|
|
209
209
|
</tbody>
|
|
@@ -338,7 +338,11 @@
|
|
|
338
338
|
<% @entries.each do |entry| %>
|
|
339
339
|
<tr>
|
|
340
340
|
<td><%= link_to "##{entry[:error_id]}", error_path(entry[:error_id]), class: "text-decoration-none" %></td>
|
|
341
|
-
<td
|
|
341
|
+
<td>
|
|
342
|
+
<%= link_to errors_path(error_type: entry[:error_type], unresolved: '0'), class: "text-decoration-none" do %>
|
|
343
|
+
<code><%= truncate(entry[:error_type].to_s, length: 40) %></code>
|
|
344
|
+
<% end %>
|
|
345
|
+
</td>
|
|
342
346
|
<td>
|
|
343
347
|
<% util = entry[:utilization] %>
|
|
344
348
|
<% util_badge = util >= 80 ? "danger" : (util >= 60 ? "warning" : "success") %>
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
<small class="text-muted d-block">GC</small>
|
|
140
140
|
<small>
|
|
141
141
|
<% gc_health = health["gc"] || {} %>
|
|
142
|
-
Live: <%= gc_health["heap_live_slots"]
|
|
142
|
+
Live: <%= gc_health["heap_live_slots"] ? number_with_delimiter(gc_health["heap_live_slots"]) : "?" %> /
|
|
143
143
|
Major GC: <%= gc_health["major_gc_count"] || "?" %>
|
|
144
144
|
</small>
|
|
145
145
|
</div>
|
|
@@ -172,7 +172,13 @@
|
|
|
172
172
|
active_filters << { label: "Timeframe: #{params[:timeframe].humanize}", param: :timeframe } if params[:timeframe].present?
|
|
173
173
|
active_filters << { label: "Frequency: #{params[:frequency].humanize}", param: :frequency } if params[:frequency].present?
|
|
174
174
|
active_filters << { label: "Status: #{params[:status].humanize}", param: :status } if params[:status].present?
|
|
175
|
-
|
|
175
|
+
if params[:priority_level].present?
|
|
176
|
+
# priority_level is the integer key (3=Critical/P0, 0=Low/P3); look up the short_label
|
|
177
|
+
# rather than interpolating the raw integer, which inverts the P-number.
|
|
178
|
+
priority_data = RailsErrorDashboard::ErrorLog::PRIORITY_LEVELS[params[:priority_level].to_i]
|
|
179
|
+
priority_short = priority_data ? priority_data[:short_label] : "P?"
|
|
180
|
+
active_filters << { label: "Priority: #{priority_short}", param: :priority_level }
|
|
181
|
+
end
|
|
176
182
|
if params[:assigned_to] == '__unassigned__'
|
|
177
183
|
active_filters << { label: "Unassigned", param: :assigned_to }
|
|
178
184
|
elsif params[:assigned_to] == '__assigned__'
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); margin-bottom: var(--space-6);">
|
|
93
93
|
<!-- Affected Users -->
|
|
94
94
|
<div class="card stat-card" style="padding: var(--space-5) var(--space-6);">
|
|
95
|
-
<div class="stat-label" style="margin-bottom: 4px;">Affected Users</div>
|
|
95
|
+
<div class="stat-label" style="margin-bottom: 4px;">Affected Users (Today)</div>
|
|
96
96
|
<div style="display: flex; align-items: baseline; gap: 8px;">
|
|
97
97
|
<span class="stat-value"><%= @stats[:affected_users_today] %></span>
|
|
98
98
|
<% if @stats[:affected_users_change] != 0 %>
|
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
<div style="font-size: 12px; color: var(--text-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><%= error[:message]&.truncate(80) %></div>
|
|
150
150
|
</div>
|
|
151
151
|
<div style="text-align: right; flex-shrink: 0;">
|
|
152
|
-
<div style="font-size: 14px; font-weight: 600; color: var(--text-primary); font-variant-numeric: tabular-nums;"><%= error[:occurrence_count]
|
|
152
|
+
<div style="font-size: 14px; font-weight: 600; color: var(--text-primary); font-variant-numeric: tabular-nums;"><%= number_with_delimiter(error[:occurrence_count]) %></div>
|
|
153
153
|
<div style="font-size: 11px; color: var(--text-tertiary);"><%= error[:affected_users] %> users</div>
|
|
154
154
|
</div>
|
|
155
155
|
<% end %>
|
|
@@ -227,7 +227,9 @@
|
|
|
227
227
|
<% @cross_platform_errors.first(10).each do |error| %>
|
|
228
228
|
<tr>
|
|
229
229
|
<td>
|
|
230
|
-
|
|
230
|
+
<%= link_to errors_path(error_type: error[:error_type], unresolved: '0'), class: "text-decoration-none" do %>
|
|
231
|
+
<code class="small"><%= error[:error_type] %></code>
|
|
232
|
+
<% end %>
|
|
231
233
|
</td>
|
|
232
234
|
<td>
|
|
233
235
|
<% error[:platforms].each do |platform| %>
|
|
@@ -185,7 +185,9 @@
|
|
|
185
185
|
<% @releases.each do |release| %>
|
|
186
186
|
<tr class="<%= 'table-active' if release[:current] %>">
|
|
187
187
|
<td>
|
|
188
|
-
|
|
188
|
+
<%= link_to errors_path(app_version: release[:version], unresolved: '0'), class: "text-decoration-none" do %>
|
|
189
|
+
<strong><%= release[:version] %></strong>
|
|
190
|
+
<% end %>
|
|
189
191
|
<% if release[:current] %>
|
|
190
192
|
<span class="badge bg-primary ms-1">Current</span>
|
|
191
193
|
<% end %>
|
|
@@ -213,7 +213,6 @@
|
|
|
213
213
|
<div class="card" style="margin-bottom: var(--space-4);">
|
|
214
214
|
<div style="padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--border-primary);">
|
|
215
215
|
<span style="font-size: 14px; font-weight: 600; color: var(--text-primary);"><i class="bi bi-puzzle" style="margin-right: 6px;"></i> Active Plugins</span>
|
|
216
|
-
</h5>
|
|
217
216
|
</div>
|
|
218
217
|
<div class="card-body">
|
|
219
218
|
<% if RailsErrorDashboard::PluginRegistry.any? %>
|
|
@@ -80,6 +80,11 @@
|
|
|
80
80
|
<button type="button" class="btn" data-red-action="copy-markdown" data-markdown="<%= j @error_markdown %>">
|
|
81
81
|
<i class="bi bi-clipboard" style="margin-right: 4px;"></i>Copy for LLM
|
|
82
82
|
</button>
|
|
83
|
+
<% if RailsErrorDashboard.configuration.llm_configured? %>
|
|
84
|
+
<button type="button" class="btn" data-red-action="open-ai-help">
|
|
85
|
+
<i class="bi bi-stars" style="margin-right: 4px;"></i>AI Help
|
|
86
|
+
</button>
|
|
87
|
+
<% end %>
|
|
83
88
|
<button type="button" class="btn" data-red-action="download-json" title="Export JSON">
|
|
84
89
|
<i class="bi bi-download"></i>
|
|
85
90
|
</button>
|
|
@@ -165,6 +170,8 @@
|
|
|
165
170
|
<div class="error-sidebar" style="display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; overflow-wrap: break-word;">
|
|
166
171
|
<%= render "sidebar_metadata", error: @error, related_errors: @related_errors %>
|
|
167
172
|
|
|
173
|
+
<%= render "llm_summary", error: @error %>
|
|
174
|
+
|
|
168
175
|
<!-- Baseline Statistics -->
|
|
169
176
|
<% if RailsErrorDashboard.configuration.enable_baseline_alerts %>
|
|
170
177
|
<% baselines = @error.baselines %>
|
|
@@ -211,8 +218,10 @@
|
|
|
211
218
|
<i class="bi bi-person"></i> View User's Errors
|
|
212
219
|
<% end %>
|
|
213
220
|
<% end %>
|
|
214
|
-
|
|
215
|
-
|
|
221
|
+
<% if @error.platform.present? %>
|
|
222
|
+
<%= link_to errors_path(app_context.merge(platform: @error.platform, unresolved: '0')), class: "btn btn-sm" do %>
|
|
223
|
+
<i class="bi bi-phone"></i> View <%= @error.platform %> Errors
|
|
224
|
+
<% end %>
|
|
216
225
|
<% end %>
|
|
217
226
|
<%= link_to analytics_errors_path(app_context), class: "btn btn-sm" do %>
|
|
218
227
|
<i class="bi bi-bar-chart-line"></i> Analytics
|
|
@@ -224,6 +233,7 @@
|
|
|
224
233
|
</div>
|
|
225
234
|
|
|
226
235
|
<%= render "modals", error: @error %>
|
|
236
|
+
<%= render "ai_help_panel", error: @error if RailsErrorDashboard.configuration.llm_configured? %>
|
|
227
237
|
|
|
228
238
|
<%= red_javascript_tag do %>
|
|
229
239
|
window.switchTab = function(tabId) {
|
|
@@ -93,7 +93,11 @@
|
|
|
93
93
|
<tbody>
|
|
94
94
|
<% @entries.each do |entry| %>
|
|
95
95
|
<tr>
|
|
96
|
-
<td
|
|
96
|
+
<td>
|
|
97
|
+
<%= link_to errors_path(error_type: entry[:exception_class], unresolved: '0'), class: "text-decoration-none", title: "View any unrescued #{entry[:exception_class]} errors that leaked into error_log" do %>
|
|
98
|
+
<code style="font-size: 0.85em;"><%= entry[:exception_class] %></code>
|
|
99
|
+
<% end %>
|
|
100
|
+
</td>
|
|
97
101
|
<td style="color: var(--text-secondary); font-size: 12px; word-break: break-all;"><%= entry[:raise_location] %></td>
|
|
98
102
|
<td style="color: var(--text-secondary); font-size: 12px; word-break: break-all;"><%= entry[:rescue_location] || "Unknown" %></td>
|
|
99
103
|
<td><span class="badge bg-info text-dark"><%= number_with_delimiter(entry[:raise_count]) %></span></td>
|
|
@@ -107,7 +107,9 @@
|
|
|
107
107
|
<tr>
|
|
108
108
|
<td><strong><%= (@pagy.from || 1) + index %></strong></td>
|
|
109
109
|
<td>
|
|
110
|
-
|
|
110
|
+
<%= link_to errors_path(error_type: entry[:error_type], unresolved: '0'), class: "text-decoration-none" do %>
|
|
111
|
+
<strong><%= entry[:error_type] %></strong>
|
|
112
|
+
<% end %>
|
|
111
113
|
<br><small class="text-muted"><%= entry[:message] %></small>
|
|
112
114
|
</td>
|
|
113
115
|
<td>
|
data/config/routes.rb
CHANGED
|
@@ -456,6 +456,33 @@ RailsErrorDashboard.configure do |config|
|
|
|
456
456
|
# Codeberg: "https://codeberg.org/username/repo"
|
|
457
457
|
# config.git_repository_url = ENV["GIT_REPOSITORY_URL"]
|
|
458
458
|
|
|
459
|
+
# ============================================================================
|
|
460
|
+
# AI HELP (OpenAI / Anthropic)
|
|
461
|
+
# ============================================================================
|
|
462
|
+
#
|
|
463
|
+
# When provider and API key are configured, error detail pages show an
|
|
464
|
+
# "AI Help" drawer where dashboard users can ask questions about the current
|
|
465
|
+
# error. Error details, backtrace, and related dashboard context are sent to
|
|
466
|
+
# the configured provider.
|
|
467
|
+
#
|
|
468
|
+
# OpenAI uses the Responses API by default and supports GPT-5 and GPT-4 family
|
|
469
|
+
# models such as "gpt-5" and "gpt-4.1". Set llm_openai_endpoint to
|
|
470
|
+
# :chat_completions only if you need the older Chat Completions endpoint.
|
|
471
|
+
#
|
|
472
|
+
# config.llm_provider = :openai
|
|
473
|
+
# config.llm_api_key = -> { Rails.application.credentials.dig(:openai, :api_key) }
|
|
474
|
+
# config.llm_model = "gpt-5"
|
|
475
|
+
# config.llm_openai_endpoint = :auto # :auto, :responses, or :chat_completions
|
|
476
|
+
#
|
|
477
|
+
# config.llm_provider = :anthropic
|
|
478
|
+
# config.llm_api_key = -> { Rails.application.credentials.dig(:anthropic, :api_key) }
|
|
479
|
+
# config.llm_model = "claude-sonnet-4-20250514"
|
|
480
|
+
#
|
|
481
|
+
# Shared options:
|
|
482
|
+
# config.llm_timeout_seconds = 30
|
|
483
|
+
# config.llm_max_output_tokens = 900
|
|
484
|
+
# config.llm_system_prompt = "Prefer concise answers with file-level next steps."
|
|
485
|
+
|
|
459
486
|
# ============================================================================
|
|
460
487
|
# ISSUE TRACKING (GitHub / GitLab / Codeberg)
|
|
461
488
|
# ============================================================================
|
|
@@ -184,9 +184,23 @@ module RailsErrorDashboard
|
|
|
184
184
|
# ActiveStorage event tracking (requires enable_breadcrumbs = true)
|
|
185
185
|
attr_accessor :enable_activestorage_tracking # Master switch (default: false)
|
|
186
186
|
|
|
187
|
+
# LLM observability (requires enable_breadcrumbs = true)
|
|
188
|
+
attr_accessor :enable_llm_observability # Master switch (default: false)
|
|
189
|
+
attr_accessor :llm_observability_content_capture # Capture prompt/completion text (default: false — PII risk)
|
|
190
|
+
attr_accessor :llm_pricing_overrides # Hash of { "model-name" => { input: usd_per_1m, output: usd_per_1m } }
|
|
191
|
+
|
|
187
192
|
# Dashboard UI appearance
|
|
188
193
|
attr_accessor :accent_color # :crimson (default), :ruby, :ember, :violet
|
|
189
194
|
|
|
195
|
+
# LLM-powered AI help (disabled unless provider and API key are configured)
|
|
196
|
+
attr_accessor :llm_provider # :openai or :anthropic
|
|
197
|
+
attr_accessor :llm_api_key # String or lambda/proc
|
|
198
|
+
attr_accessor :llm_model # e.g. "gpt-5", "gpt-4.1", "claude-sonnet-4-20250514"
|
|
199
|
+
attr_accessor :llm_openai_endpoint # :responses, :chat_completions, or :auto
|
|
200
|
+
attr_accessor :llm_timeout_seconds # HTTP timeout for provider calls
|
|
201
|
+
attr_accessor :llm_max_output_tokens # Response length cap
|
|
202
|
+
attr_accessor :llm_system_prompt # Optional prompt override/addition
|
|
203
|
+
|
|
190
204
|
# Notification callbacks (managed via helper methods, not set directly)
|
|
191
205
|
attr_reader :notification_callbacks
|
|
192
206
|
|
|
@@ -304,7 +318,7 @@ module RailsErrorDashboard
|
|
|
304
318
|
# Breadcrumbs defaults - OFF by default (opt-in)
|
|
305
319
|
@enable_breadcrumbs = false # Master switch
|
|
306
320
|
@breadcrumb_buffer_size = 40 # Max events per request (Sentry uses 100, we're conservative)
|
|
307
|
-
@breadcrumb_categories = nil # nil = all; or [:sql, :controller, :cache, :job, :mailer, :custom, :deprecation]
|
|
321
|
+
@breadcrumb_categories = nil # nil = all; or [:sql, :controller, :cache, :job, :mailer, :custom, :deprecation, :llm, :llm_tool]
|
|
308
322
|
|
|
309
323
|
# N+1 query detection defaults - ON by default (lightweight display-time analysis)
|
|
310
324
|
@enable_n_plus_one_detection = true # Analyze SQL breadcrumbs for repeated patterns
|
|
@@ -352,6 +366,12 @@ module RailsErrorDashboard
|
|
|
352
366
|
# ActiveStorage event tracking defaults - OFF by default (opt-in, requires breadcrumbs)
|
|
353
367
|
@enable_activestorage_tracking = false
|
|
354
368
|
|
|
369
|
+
# LLM observability defaults - OFF by default (opt-in, requires breadcrumbs)
|
|
370
|
+
# content_capture stays OFF even when master switch is on (privacy: prompts may contain PII/secrets)
|
|
371
|
+
@enable_llm_observability = false
|
|
372
|
+
@llm_observability_content_capture = false
|
|
373
|
+
@llm_pricing_overrides = {}
|
|
374
|
+
|
|
355
375
|
# Internal logging defaults - SILENT by default
|
|
356
376
|
@enable_internal_logging = false # Opt-in for debugging
|
|
357
377
|
@log_level = :silent # Silent by default, use :debug, :info, :warn, :error, or :silent
|
|
@@ -359,6 +379,15 @@ module RailsErrorDashboard
|
|
|
359
379
|
# Dashboard UI
|
|
360
380
|
@accent_color = :crimson # :crimson, :ruby, :ember, :violet
|
|
361
381
|
|
|
382
|
+
# LLM-powered AI help defaults - OFF until provider and API key are configured
|
|
383
|
+
@llm_provider = ENV["RED_LLM_PROVIDER"]&.to_sym
|
|
384
|
+
@llm_api_key = ENV["RED_LLM_API_KEY"]
|
|
385
|
+
@llm_model = ENV["RED_LLM_MODEL"]
|
|
386
|
+
@llm_openai_endpoint = (ENV["RED_LLM_OPENAI_ENDPOINT"] || "auto").to_sym
|
|
387
|
+
@llm_timeout_seconds = 30
|
|
388
|
+
@llm_max_output_tokens = 900
|
|
389
|
+
@llm_system_prompt = nil
|
|
390
|
+
|
|
362
391
|
@notification_callbacks = {
|
|
363
392
|
error_logged: [],
|
|
364
393
|
critical_error: [],
|
|
@@ -517,6 +546,13 @@ module RailsErrorDashboard
|
|
|
517
546
|
@enable_activestorage_tracking = false
|
|
518
547
|
end
|
|
519
548
|
|
|
549
|
+
# Validate llm observability requires breadcrumbs
|
|
550
|
+
if enable_llm_observability && !enable_breadcrumbs
|
|
551
|
+
warnings << "enable_llm_observability requires enable_breadcrumbs = true. " \
|
|
552
|
+
"LLM observability has been auto-disabled."
|
|
553
|
+
@enable_llm_observability = false
|
|
554
|
+
end
|
|
555
|
+
|
|
520
556
|
# Skip credential/service-dependent validations during Docker builds.
|
|
521
557
|
# SECRET_KEY_BASE_DUMMY=1 means no credentials or external services available.
|
|
522
558
|
build_env = ENV["SECRET_KEY_BASE_DUMMY"].present?
|
|
@@ -582,6 +618,33 @@ module RailsErrorDashboard
|
|
|
582
618
|
end
|
|
583
619
|
end
|
|
584
620
|
|
|
621
|
+
# Validate LLM configuration only when partially or fully configured.
|
|
622
|
+
if llm_provider.present? || effective_llm_api_key.present? || llm_model.present?
|
|
623
|
+
valid_llm_providers = %i[openai anthropic]
|
|
624
|
+
unless effective_llm_provider && valid_llm_providers.include?(effective_llm_provider)
|
|
625
|
+
errors << "llm_provider must be one of #{valid_llm_providers.inspect} (got: #{llm_provider.inspect})"
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
if effective_llm_api_key.blank? && !build_env
|
|
629
|
+
warnings << "llm_provider is configured but no LLM API key is set. " \
|
|
630
|
+
"Set llm_api_key or RED_LLM_API_KEY to enable AI Help."
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
valid_openai_endpoints = %i[auto responses chat_completions]
|
|
634
|
+
if llm_openai_endpoint && !valid_openai_endpoints.include?(llm_openai_endpoint.to_sym)
|
|
635
|
+
errors << "llm_openai_endpoint must be one of #{valid_openai_endpoints.inspect} " \
|
|
636
|
+
"(got: #{llm_openai_endpoint.inspect})"
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
if llm_timeout_seconds && llm_timeout_seconds.to_i < 1
|
|
640
|
+
errors << "llm_timeout_seconds must be at least 1 (got: #{llm_timeout_seconds})"
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
if llm_max_output_tokens && llm_max_output_tokens.to_i < 1
|
|
644
|
+
errors << "llm_max_output_tokens must be at least 1 (got: #{llm_max_output_tokens})"
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
585
648
|
# Validate total_users_for_impact (must be positive if set)
|
|
586
649
|
if total_users_for_impact && total_users_for_impact < 1
|
|
587
650
|
errors << "total_users_for_impact must be at least 1 (got: #{total_users_for_impact})"
|
|
@@ -686,6 +749,43 @@ module RailsErrorDashboard
|
|
|
686
749
|
end
|
|
687
750
|
end
|
|
688
751
|
|
|
752
|
+
# Whether the dashboard can show AI Help controls.
|
|
753
|
+
#
|
|
754
|
+
# @return [Boolean]
|
|
755
|
+
def llm_configured?
|
|
756
|
+
effective_llm_provider.present? && effective_llm_api_key.present?
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
# Resolve the configured LLM provider.
|
|
760
|
+
#
|
|
761
|
+
# @return [Symbol, nil] :openai, :anthropic, or nil
|
|
762
|
+
def effective_llm_provider
|
|
763
|
+
provider = llm_provider.presence
|
|
764
|
+
provider&.to_sym
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# Resolve the configured LLM API key (supports string or lambda).
|
|
768
|
+
#
|
|
769
|
+
# @return [String, nil]
|
|
770
|
+
def effective_llm_api_key
|
|
771
|
+
return nil if llm_api_key.nil?
|
|
772
|
+
llm_api_key.respond_to?(:call) ? llm_api_key.call : llm_api_key
|
|
773
|
+
rescue => e
|
|
774
|
+
nil
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# Resolve the configured model with provider-specific defaults.
|
|
778
|
+
#
|
|
779
|
+
# @return [String, nil]
|
|
780
|
+
def effective_llm_model
|
|
781
|
+
return llm_model if llm_model.present?
|
|
782
|
+
|
|
783
|
+
case effective_llm_provider
|
|
784
|
+
when :openai then "gpt-5"
|
|
785
|
+
when :anthropic then "claude-sonnet-4-20250514"
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
|
|
689
789
|
# Detect the engine's mount path from the host app routes.
|
|
690
790
|
# Falls back to "/red" if detection fails.
|
|
691
791
|
#
|
|
@@ -101,6 +101,20 @@ module RailsErrorDashboard
|
|
|
101
101
|
RailsErrorDashboard::Subscribers::ActiveStorageSubscriber.subscribe!
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# Register OpenTelemetry SpanProcessor for LLM observability — Tier 1 path
|
|
105
|
+
# for hosts already running OTel (ruby_llm, thoughtbot/instrumentation).
|
|
106
|
+
# Internally guards on Integrations::OTel.available? + tracer provider
|
|
107
|
+
# capability, so this is safe to call unconditionally.
|
|
108
|
+
RailsErrorDashboard::Integrations::LlmSpanProcessor.register!
|
|
109
|
+
|
|
110
|
+
# Subscribe to red.llm_call / red.llm_tool_call AS::Notifications — Tier 3
|
|
111
|
+
# path for hosts using direct Net::HTTP / gRPC / local inference servers
|
|
112
|
+
# that aren't covered by OTel or the Faraday middleware.
|
|
113
|
+
if RailsErrorDashboard.configuration.enable_llm_observability &&
|
|
114
|
+
RailsErrorDashboard.configuration.enable_breadcrumbs
|
|
115
|
+
RailsErrorDashboard::Subscribers::LlmCallSubscriber.subscribe!
|
|
116
|
+
end
|
|
117
|
+
|
|
104
118
|
# Enable TracePoint(:raise) for local variable and/or instance variable capture
|
|
105
119
|
if RailsErrorDashboard.configuration.enable_local_variables ||
|
|
106
120
|
RailsErrorDashboard.configuration.enable_instance_variables
|