rails_error_dashboard 0.6.4 → 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 +99 -4
- data/app/controllers/rails_error_dashboard/errors_controller.rb +41 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +2 -0
- 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 +63 -5
- data/app/views/rails_error_dashboard/errors/_llm_summary.html.erb +97 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +8 -0
- 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/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 +12 -2
|
@@ -210,13 +210,71 @@
|
|
|
210
210
|
</thead>
|
|
211
211
|
<tbody>
|
|
212
212
|
<% breadcrumbs.each_with_index do |crumb, i| %>
|
|
213
|
-
|
|
213
|
+
<% cat = crumb["c"].to_s %>
|
|
214
|
+
<% meta = crumb["meta"].is_a?(Hash) ? crumb["meta"] : {} %>
|
|
215
|
+
<% is_llm = cat == "llm" %>
|
|
216
|
+
<% is_tool = cat == "llm_tool" %>
|
|
217
|
+
<% prev_cat = i > 0 ? breadcrumbs[i - 1]["c"].to_s : nil %>
|
|
218
|
+
<% nested = is_tool && (prev_cat == "llm" || prev_cat == "llm_tool") %>
|
|
219
|
+
<tr<%= nested ? ' class="llm-tool-row"'.html_safe : "".html_safe %>>
|
|
214
220
|
<td class="text-muted"><%= i + 1 %></td>
|
|
215
|
-
<td><span class="badge bg-<%= breadcrumb_badge_color(
|
|
216
|
-
<td
|
|
221
|
+
<td><span class="badge bg-<%= breadcrumb_badge_color(cat) %>"><%= cat %></span></td>
|
|
222
|
+
<td<%= ' style="padding-left: 2rem;"'.html_safe if nested %>>
|
|
223
|
+
<% if nested %><i class="bi bi-arrow-return-right text-muted me-1" aria-hidden="true"></i><% end %>
|
|
217
224
|
<code style="word-break: break-all; font-size: 0.85em;"><%= truncate(crumb["m"].to_s, length: 200) %></code>
|
|
218
|
-
<% if
|
|
219
|
-
|
|
225
|
+
<% if is_llm %>
|
|
226
|
+
<% provider = meta["provider"].presence %>
|
|
227
|
+
<% model = meta["model"].presence %>
|
|
228
|
+
<% in_tok = meta["input_tokens"].presence %>
|
|
229
|
+
<% out_tok = meta["output_tokens"].presence %>
|
|
230
|
+
<% cost = meta["cost_usd"].presence %>
|
|
231
|
+
<% status = meta["status"].presence %>
|
|
232
|
+
<% err_class = meta["error_class"].presence %>
|
|
233
|
+
<% err_msg = meta["error_message"].presence %>
|
|
234
|
+
<br><small class="text-muted">
|
|
235
|
+
<% if provider || model %>
|
|
236
|
+
<i class="bi bi-cpu" aria-hidden="true"></i>
|
|
237
|
+
<%= [ provider, model ].compact.join(" · ") %>
|
|
238
|
+
<% end %>
|
|
239
|
+
<% if in_tok || out_tok %>
|
|
240
|
+
· <i class="bi bi-input-cursor-text" aria-hidden="true"></i>
|
|
241
|
+
in:<strong><%= in_tok || "?" %></strong>
|
|
242
|
+
/ out:<strong><%= out_tok || "?" %></strong>
|
|
243
|
+
<% end %>
|
|
244
|
+
<% if cost %>
|
|
245
|
+
· <i class="bi bi-currency-dollar" aria-hidden="true"></i>
|
|
246
|
+
<strong>$<%= "%.6f" % cost.to_f %></strong>
|
|
247
|
+
<% end %>
|
|
248
|
+
<% if status && status != "success" %>
|
|
249
|
+
· <span class="badge bg-danger"><%= status %></span>
|
|
250
|
+
<% end %>
|
|
251
|
+
</small>
|
|
252
|
+
<% if err_class || err_msg %>
|
|
253
|
+
<br><small class="text-danger">
|
|
254
|
+
<i class="bi bi-exclamation-triangle" aria-hidden="true"></i>
|
|
255
|
+
<%= [ err_class, err_msg ].compact.join(": ") %>
|
|
256
|
+
</small>
|
|
257
|
+
<% end %>
|
|
258
|
+
<% elsif is_tool %>
|
|
259
|
+
<% tool_name = meta["tool_name"].presence %>
|
|
260
|
+
<% tool_args = meta["tool_arguments"].presence %>
|
|
261
|
+
<% tool_result = meta["tool_result"].presence %>
|
|
262
|
+
<% if tool_name || tool_args || tool_result %>
|
|
263
|
+
<br><small class="text-muted">
|
|
264
|
+
<% if tool_name %>
|
|
265
|
+
<i class="bi bi-wrench-adjustable" aria-hidden="true"></i>
|
|
266
|
+
<strong><%= tool_name %></strong>
|
|
267
|
+
<% end %>
|
|
268
|
+
<% if tool_args %>
|
|
269
|
+
· args: <code style="font-size: 0.85em;"><%= truncate(tool_args, length: 120) %></code>
|
|
270
|
+
<% end %>
|
|
271
|
+
<% if tool_result %>
|
|
272
|
+
· result: <code style="font-size: 0.85em;"><%= truncate(tool_result, length: 120) %></code>
|
|
273
|
+
<% end %>
|
|
274
|
+
</small>
|
|
275
|
+
<% end %>
|
|
276
|
+
<% elsif meta.present? %>
|
|
277
|
+
<br><small class="text-muted"><%= meta.inspect %></small>
|
|
220
278
|
<% end %>
|
|
221
279
|
</td>
|
|
222
280
|
<td>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<% if RailsErrorDashboard.configuration.enable_llm_observability &&
|
|
2
|
+
RailsErrorDashboard.configuration.enable_breadcrumbs &&
|
|
3
|
+
error.respond_to?(:breadcrumbs) && error.breadcrumbs.present? %>
|
|
4
|
+
<% breadcrumbs = JSON.parse(error.breadcrumbs) rescue [] %>
|
|
5
|
+
<% breadcrumbs = [] unless breadcrumbs.is_a?(Array) %>
|
|
6
|
+
<% llm_summary = RailsErrorDashboard::Services::LlmSummary.call(breadcrumbs) %>
|
|
7
|
+
<% if llm_summary %>
|
|
8
|
+
<div class="card mb-4" id="section-llm-summary">
|
|
9
|
+
<div class="card-header">
|
|
10
|
+
<h5 class="mb-0">
|
|
11
|
+
<i class="bi bi-robot text-info" aria-hidden="true"></i> LLM Calls
|
|
12
|
+
<span class="badge bg-info text-dark"><%= llm_summary[:total_calls] %></span>
|
|
13
|
+
</h5>
|
|
14
|
+
<small class="text-muted">Aggregated from request breadcrumbs</small>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="card-body">
|
|
17
|
+
<div class="row text-center mb-3">
|
|
18
|
+
<div class="col-4">
|
|
19
|
+
<div class="fs-4 fw-bold"><%= llm_summary[:total_calls] %></div>
|
|
20
|
+
<small class="text-muted">Calls</small>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="col-4">
|
|
23
|
+
<div class="fs-4 fw-bold"><%= number_with_delimiter(llm_summary[:total_tokens]) %></div>
|
|
24
|
+
<small class="text-muted">Tokens</small>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="col-4">
|
|
27
|
+
<div class="fs-4 fw-bold">
|
|
28
|
+
<% if llm_summary[:total_cost_usd] > 0 %>
|
|
29
|
+
$<%= "%.4f" % llm_summary[:total_cost_usd] %>
|
|
30
|
+
<% else %>
|
|
31
|
+
<span class="text-muted">--</span>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
<small class="text-muted">Cost</small>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="mb-3">
|
|
39
|
+
<div class="d-flex justify-content-between mb-1">
|
|
40
|
+
<small class="text-muted">Input</small>
|
|
41
|
+
<small><strong><%= number_with_delimiter(llm_summary[:total_input_tokens]) %></strong></small>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="d-flex justify-content-between mb-1">
|
|
44
|
+
<small class="text-muted">Output</small>
|
|
45
|
+
<small><strong><%= number_with_delimiter(llm_summary[:total_output_tokens]) %></strong></small>
|
|
46
|
+
</div>
|
|
47
|
+
<% if llm_summary[:total_tool_calls] > 0 %>
|
|
48
|
+
<div class="d-flex justify-content-between mb-1">
|
|
49
|
+
<small class="text-muted">Tool calls</small>
|
|
50
|
+
<small><strong><%= llm_summary[:total_tool_calls] %></strong></small>
|
|
51
|
+
</div>
|
|
52
|
+
<% end %>
|
|
53
|
+
<div class="d-flex justify-content-between mb-1">
|
|
54
|
+
<small class="text-muted">Total time</small>
|
|
55
|
+
<small><strong><%= "%.1f" % llm_summary[:total_duration_ms] %>ms</strong></small>
|
|
56
|
+
</div>
|
|
57
|
+
<% if llm_summary[:error_count] > 0 %>
|
|
58
|
+
<div class="d-flex justify-content-between mb-1">
|
|
59
|
+
<small class="text-muted">Errors</small>
|
|
60
|
+
<small><span class="badge bg-danger"><%= llm_summary[:error_count] %></span></small>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<% if llm_summary[:by_model].any? %>
|
|
66
|
+
<hr class="my-2">
|
|
67
|
+
<small class="text-muted d-block mb-2">By model</small>
|
|
68
|
+
<% llm_summary[:by_model].each do |row| %>
|
|
69
|
+
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
70
|
+
<small style="word-break: break-all;">
|
|
71
|
+
<% if row[:provider].present? %>
|
|
72
|
+
<span class="text-muted"><%= row[:provider] %>·</span>
|
|
73
|
+
<% end %>
|
|
74
|
+
<strong><%= row[:model].presence || "unknown" %></strong>
|
|
75
|
+
</small>
|
|
76
|
+
<small class="text-muted">
|
|
77
|
+
<%= row[:calls] %> · <%= number_with_delimiter(row[:tokens]) %>tok
|
|
78
|
+
<% if row[:cost_usd] > 0 %>
|
|
79
|
+
· $<%= "%.4f" % row[:cost_usd] %>
|
|
80
|
+
<% end %>
|
|
81
|
+
</small>
|
|
82
|
+
</div>
|
|
83
|
+
<% end %>
|
|
84
|
+
<% end %>
|
|
85
|
+
|
|
86
|
+
<% if llm_summary[:error_count] > 0 %>
|
|
87
|
+
<div class="alert alert-danger py-1 px-2 mb-0 mt-2">
|
|
88
|
+
<small>
|
|
89
|
+
<i class="bi bi-exclamation-triangle" aria-hidden="true"></i>
|
|
90
|
+
<%= llm_summary[:error_count] %> failed call<%= llm_summary[:error_count] == 1 ? "" : "s" %> — see breadcrumbs trail for details.
|
|
91
|
+
</small>
|
|
92
|
+
</div>
|
|
93
|
+
<% end %>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<% end %>
|
|
97
|
+
<% end %>
|
|
@@ -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 %>
|
|
@@ -226,6 +233,7 @@
|
|
|
226
233
|
</div>
|
|
227
234
|
|
|
228
235
|
<%= render "modals", error: @error %>
|
|
236
|
+
<%= render "ai_help_panel", error: @error if RailsErrorDashboard.configuration.llm_configured? %>
|
|
229
237
|
|
|
230
238
|
<%= red_javascript_tag do %>
|
|
231
239
|
window.switchTab = function(tabId) {
|
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
|