rails_error_dashboard 0.6.4 → 0.7.1

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.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +99 -4
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +41 -0
  4. data/app/helpers/rails_error_dashboard/application_helper.rb +2 -0
  5. data/app/views/layouts/rails_error_dashboard.html.erb +307 -0
  6. data/app/views/rails_error_dashboard/errors/_ai_help_panel.html.erb +36 -0
  7. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +63 -5
  8. data/app/views/rails_error_dashboard/errors/_llm_summary.html.erb +97 -0
  9. data/app/views/rails_error_dashboard/errors/show.html.erb +8 -0
  10. data/config/routes.rb +1 -0
  11. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +27 -0
  12. data/lib/rails_error_dashboard/configuration.rb +101 -1
  13. data/lib/rails_error_dashboard/engine.rb +14 -0
  14. data/lib/rails_error_dashboard/integrations/llm_middleware.rb +276 -0
  15. data/lib/rails_error_dashboard/integrations/llm_span_processor.rb +181 -0
  16. data/lib/rails_error_dashboard/integrations/o_tel.rb +45 -0
  17. data/lib/rails_error_dashboard/services/breadcrumb_collector.rb +34 -1
  18. data/lib/rails_error_dashboard/services/llm_client.rb +368 -0
  19. data/lib/rails_error_dashboard/services/llm_cost_estimator.rb +85 -0
  20. data/lib/rails_error_dashboard/services/llm_summary.rb +91 -0
  21. data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +87 -0
  22. data/lib/rails_error_dashboard/subscribers/llm_call_subscriber.rb +150 -0
  23. data/lib/rails_error_dashboard/value_objects/llm_call_event.rb +92 -0
  24. data/lib/rails_error_dashboard/version.rb +1 -1
  25. data/lib/rails_error_dashboard.rb +8 -0
  26. metadata +12 -2
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Subscribers
5
+ # ActiveSupport::Notifications subscriber for manually-instrumented LLM
6
+ # calls. The Tier 3 path — for hosts that don't run OpenTelemetry AND
7
+ # don't use Faraday-based LLM SDKs (e.g., direct Net::HTTP, gRPC clients,
8
+ # custom adapters, or hosts that want to layer in extra LLM activity that
9
+ # the automatic paths can't see, like local inference servers).
10
+ #
11
+ # Usage in the host app:
12
+ #
13
+ # ActiveSupport::Notifications.instrument("red.llm_call",
14
+ # provider: "ollama",
15
+ # model: "llama3:8b",
16
+ # input_tokens: 1200,
17
+ # output_tokens: 350
18
+ # ) do
19
+ # # ... call your LLM ...
20
+ # end
21
+ #
22
+ # # Tool execution
23
+ # ActiveSupport::Notifications.instrument("red.llm_tool_call",
24
+ # tool_name: "search_database",
25
+ # tool_arguments: { query: "..." },
26
+ # tool_result: "[...]"
27
+ # ) do
28
+ # # ... execute tool ...
29
+ # end
30
+ #
31
+ # Payload contract = LlmCallEvent constructor kwargs:
32
+ # :provider, :model, :status, :input_tokens, :output_tokens,
33
+ # :duration_ms, :error_class, :error_message, :tool_name,
34
+ # :tool_arguments, :tool_result, :cost_usd_estimate
35
+ #
36
+ # Duration: defaults to `event.duration` (host wraps `.instrument` around
37
+ # the work); payload `:duration_ms` overrides if explicitly supplied.
38
+ # Cost: auto-estimated from provider/model/tokens unless payload supplies
39
+ # `:cost_usd_estimate`.
40
+ #
41
+ # SAFETY RULES (HOST_APP_SAFETY.md):
42
+ # - Every callback wrapped in rescue => e; nil
43
+ # - Never raise from subscriber callbacks
44
+ # - Skip if buffer is nil (not in a request context)
45
+ # - Re-read config on every event (host may toggle at runtime)
46
+ class LlmCallSubscriber
47
+ CHAT_EVENT = "red.llm_call"
48
+ TOOL_EVENT = "red.llm_tool_call"
49
+ EVENTS = [ CHAT_EVENT, TOOL_EVENT ].freeze
50
+
51
+ @subscriptions = []
52
+
53
+ class << self
54
+ attr_reader :subscriptions
55
+
56
+ # Idempotent — re-subscribing first tears down previous subscriptions
57
+ # so Spring reloads / repeated engine boots don't pile up duplicates.
58
+ def subscribe!
59
+ unsubscribe!
60
+ @subscriptions = EVENTS.map { |name| subscribe_event(name) }
61
+ end
62
+
63
+ def unsubscribe!
64
+ (@subscriptions || []).each do |sub|
65
+ ActiveSupport::Notifications.unsubscribe(sub) if sub
66
+ rescue StandardError
67
+ nil
68
+ end
69
+ @subscriptions = []
70
+ end
71
+
72
+ private
73
+
74
+ def subscribe_event(event_name)
75
+ ActiveSupport::Notifications.subscribe(event_name) do |*args|
76
+ event = ActiveSupport::Notifications::Event.new(*args)
77
+ handle_event(event, event_name)
78
+ rescue StandardError => e
79
+ RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] LlmCallSubscriber callback failed: #{e.message}")
80
+ nil
81
+ end
82
+ end
83
+
84
+ def handle_event(event, event_name)
85
+ return unless RailsErrorDashboard.configuration.enable_llm_observability
86
+ return unless RailsErrorDashboard.configuration.enable_breadcrumbs
87
+ return unless Services::BreadcrumbCollector.current_buffer
88
+
89
+ payload = event.payload || {}
90
+ # Symbolize-ish access — hosts may pass either symbol or string keys
91
+ payload = payload.transform_keys(&:to_sym) if payload.respond_to?(:transform_keys)
92
+
93
+ tool_name = payload[:tool_name] || (event_name == TOOL_EVENT ? "unknown" : nil)
94
+ provider = payload[:provider]
95
+ model = payload[:model]
96
+
97
+ duration_ms = payload[:duration_ms]
98
+ duration_ms ||= event.duration if event.respond_to?(:duration)
99
+
100
+ status = normalize_status(payload[:status], payload[:error_class])
101
+
102
+ cost = payload[:cost_usd_estimate]
103
+ if cost.nil? && tool_name.nil? && status == :success && model
104
+ cost = Services::LlmCostEstimator.estimate(
105
+ provider: provider,
106
+ model: model,
107
+ input_tokens: payload[:input_tokens],
108
+ output_tokens: payload[:output_tokens]
109
+ )
110
+ end
111
+
112
+ llm_event = ValueObjects::LlmCallEvent.new(
113
+ provider: provider || "unknown",
114
+ model: model || "unknown",
115
+ status: status,
116
+ input_tokens: payload[:input_tokens],
117
+ output_tokens: payload[:output_tokens],
118
+ duration_ms: duration_ms,
119
+ error_class: payload[:error_class],
120
+ error_message: payload[:error_message],
121
+ tool_name: tool_name,
122
+ tool_arguments: payload[:tool_arguments],
123
+ tool_result: payload[:tool_result],
124
+ cost_usd_estimate: cost
125
+ )
126
+
127
+ category = llm_event.tool_call? ? "llm_tool" : "llm"
128
+
129
+ Services::BreadcrumbCollector.add(
130
+ category,
131
+ llm_event.to_breadcrumb_message,
132
+ duration_ms: llm_event.duration_ms,
133
+ metadata: llm_event.to_breadcrumb_metadata
134
+ )
135
+ end
136
+
137
+ # Status precedence: explicit payload status (if valid) → :error when
138
+ # error_class present → :success.
139
+ def normalize_status(payload_status, error_class)
140
+ if payload_status
141
+ sym = payload_status.to_sym rescue nil
142
+ return sym if ValueObjects::LlmCallEvent::STATUSES.include?(sym)
143
+ end
144
+ return :error if error_class
145
+ :success
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module ValueObjects
5
+ # Immutable value object representing a single LLM call observed in the
6
+ # host application. One canonical shape that all capture paths (OTel
7
+ # SpanProcessor, Faraday middleware, future native subscribers) normalize
8
+ # to before handing off to BreadcrumbCollector.
9
+ #
10
+ # Fields are read-only after initialization. Unknown / unavailable fields
11
+ # are nil — never raise, never block, never allocate large strings.
12
+ class LlmCallEvent
13
+ STATUSES = [ :success, :error, :timeout ].freeze
14
+ MAX_TOOL_ARG_LENGTH = 500
15
+ MAX_TOOL_RESULT_LENGTH = 500
16
+ MAX_ERROR_MESSAGE_LENGTH = 200
17
+
18
+ attr_reader :provider, :model, :input_tokens, :output_tokens,
19
+ :duration_ms, :status, :error_class, :error_message,
20
+ :tool_name, :tool_arguments_truncated, :tool_result_truncated,
21
+ :cost_usd_estimate
22
+
23
+ def initialize(provider:, model:, status:,
24
+ input_tokens: nil, output_tokens: nil, duration_ms: nil,
25
+ error_class: nil, error_message: nil,
26
+ tool_name: nil, tool_arguments: nil, tool_result: nil,
27
+ cost_usd_estimate: nil)
28
+ @provider = provider.to_s
29
+ @model = model.to_s
30
+ @status = STATUSES.include?(status) ? status : :success
31
+ @input_tokens = input_tokens
32
+ @output_tokens = output_tokens
33
+ @duration_ms = duration_ms
34
+ @error_class = error_class
35
+ @error_message = truncate(error_message, MAX_ERROR_MESSAGE_LENGTH)
36
+ @tool_name = tool_name
37
+ @tool_arguments_truncated = truncate(tool_arguments, MAX_TOOL_ARG_LENGTH)
38
+ @tool_result_truncated = truncate(tool_result, MAX_TOOL_RESULT_LENGTH)
39
+ @cost_usd_estimate = cost_usd_estimate
40
+ freeze
41
+ end
42
+
43
+ def tool_call?
44
+ !@tool_name.nil?
45
+ end
46
+
47
+ # Hash shape passed to BreadcrumbCollector.add(..., metadata:).
48
+ # Only includes non-nil keys — keeps the breadcrumb JSON compact.
49
+ def to_breadcrumb_metadata
50
+ {
51
+ provider: @provider,
52
+ model: @model,
53
+ status: @status.to_s,
54
+ input_tokens: @input_tokens,
55
+ output_tokens: @output_tokens,
56
+ duration_ms: @duration_ms,
57
+ error_class: @error_class,
58
+ error_message: @error_message,
59
+ tool_name: @tool_name,
60
+ tool_arguments: @tool_arguments_truncated,
61
+ tool_result: @tool_result_truncated,
62
+ cost_usd: @cost_usd_estimate
63
+ }.compact
64
+ end
65
+
66
+ # Short human-readable message for the breadcrumb (rendered in UI).
67
+ def to_breadcrumb_message
68
+ if tool_call?
69
+ "tool: #{@tool_name}"
70
+ else
71
+ parts = [ @provider, @model ]
72
+ if @input_tokens && @output_tokens
73
+ parts << "in:#{@input_tokens}/out:#{@output_tokens}"
74
+ end
75
+ parts << @status.to_s if @status != :success
76
+ parts.compact.join(" · ")
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def truncate(value, limit)
83
+ return nil if value.nil?
84
+ str = value.to_s
85
+ return str if str.length <= limit
86
+ "#{str[0, limit]}…"
87
+ rescue
88
+ nil
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.6.4"
2
+ VERSION = "0.7.1"
3
3
  end
@@ -19,7 +19,12 @@ begin; require "turbo-rails"; rescue LoadError; end
19
19
 
20
20
  # Core library files
21
21
  require "rails_error_dashboard/value_objects/error_context"
22
+ require "rails_error_dashboard/value_objects/llm_call_event"
23
+ require "rails_error_dashboard/integrations/o_tel"
24
+ require "rails_error_dashboard/integrations/llm_span_processor"
25
+ require "rails_error_dashboard/integrations/llm_middleware"
22
26
  require "rails_error_dashboard/helpers/user_model_detector"
27
+ require "rails_error_dashboard/services/llm_cost_estimator"
23
28
  require "rails_error_dashboard/services/platform_detector"
24
29
  require "rails_error_dashboard/services/backtrace_parser"
25
30
  require "rails_error_dashboard/services/similarity_calculator"
@@ -57,12 +62,14 @@ require "rails_error_dashboard/services/n_plus_one_detector"
57
62
  require "rails_error_dashboard/services/curl_generator"
58
63
  require "rails_error_dashboard/services/rspec_generator"
59
64
  require "rails_error_dashboard/services/markdown_error_formatter"
65
+ require "rails_error_dashboard/services/llm_client"
60
66
  require "rails_error_dashboard/services/issue_tracker_client"
61
67
  require "rails_error_dashboard/services/github_issue_client"
62
68
  require "rails_error_dashboard/services/gitlab_issue_client"
63
69
  require "rails_error_dashboard/services/codeberg_issue_client"
64
70
  require "rails_error_dashboard/services/database_health_inspector"
65
71
  require "rails_error_dashboard/services/cache_analyzer"
72
+ require "rails_error_dashboard/services/llm_summary"
66
73
  require "rails_error_dashboard/services/variable_serializer"
67
74
  require "rails_error_dashboard/services/local_variable_capturer"
68
75
  require "rails_error_dashboard/services/swallowed_exception_tracker"
@@ -75,6 +82,7 @@ require "rails_error_dashboard/subscribers/breadcrumb_subscriber"
75
82
  require "rails_error_dashboard/subscribers/rack_attack_subscriber"
76
83
  require "rails_error_dashboard/subscribers/action_cable_subscriber"
77
84
  require "rails_error_dashboard/subscribers/active_storage_subscriber"
85
+ require "rails_error_dashboard/subscribers/llm_call_subscriber"
78
86
  require "rails_error_dashboard/subscribers/issue_tracker_subscriber"
79
87
  require "rails_error_dashboard/queries/co_occurring_errors"
80
88
  require "rails_error_dashboard/queries/action_cable_summary"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_error_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.4
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -281,6 +281,7 @@ files:
281
281
  - app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb
282
282
  - app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb
283
283
  - app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb
284
+ - app/views/rails_error_dashboard/errors/_ai_help_panel.html.erb
284
285
  - app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb
285
286
  - app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb
286
287
  - app/views/rails_error_dashboard/errors/_discussion.html.erb
@@ -289,6 +290,7 @@ files:
289
290
  - app/views/rails_error_dashboard/errors/_error_row.html.erb
290
291
  - app/views/rails_error_dashboard/errors/_instance_variables.html.erb
291
292
  - app/views/rails_error_dashboard/errors/_issue_section.html.erb
293
+ - app/views/rails_error_dashboard/errors/_llm_summary.html.erb
292
294
  - app/views/rails_error_dashboard/errors/_local_variables.html.erb
293
295
  - app/views/rails_error_dashboard/errors/_modals.html.erb
294
296
  - app/views/rails_error_dashboard/errors/_pattern_insights.html.erb
@@ -392,6 +394,9 @@ files:
392
394
  - lib/rails_error_dashboard/engine.rb
393
395
  - lib/rails_error_dashboard/error_reporter.rb
394
396
  - lib/rails_error_dashboard/helpers/user_model_detector.rb
397
+ - lib/rails_error_dashboard/integrations/llm_middleware.rb
398
+ - lib/rails_error_dashboard/integrations/llm_span_processor.rb
399
+ - lib/rails_error_dashboard/integrations/o_tel.rb
395
400
  - lib/rails_error_dashboard/logger.rb
396
401
  - lib/rails_error_dashboard/manual_error_reporter.rb
397
402
  - lib/rails_error_dashboard/middleware/error_catcher.rb
@@ -455,6 +460,9 @@ files:
455
460
  - lib/rails_error_dashboard/services/gitlab_issue_client.rb
456
461
  - lib/rails_error_dashboard/services/issue_body_formatter.rb
457
462
  - lib/rails_error_dashboard/services/issue_tracker_client.rb
463
+ - lib/rails_error_dashboard/services/llm_client.rb
464
+ - lib/rails_error_dashboard/services/llm_cost_estimator.rb
465
+ - lib/rails_error_dashboard/services/llm_summary.rb
458
466
  - lib/rails_error_dashboard/services/local_variable_capturer.rb
459
467
  - lib/rails_error_dashboard/services/markdown_error_formatter.rb
460
468
  - lib/rails_error_dashboard/services/n_plus_one_detector.rb
@@ -480,9 +488,11 @@ files:
480
488
  - lib/rails_error_dashboard/subscribers/active_storage_subscriber.rb
481
489
  - lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb
482
490
  - lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb
491
+ - lib/rails_error_dashboard/subscribers/llm_call_subscriber.rb
483
492
  - lib/rails_error_dashboard/subscribers/rack_attack_subscriber.rb
484
493
  - lib/rails_error_dashboard/test_error.rb
485
494
  - lib/rails_error_dashboard/value_objects/error_context.rb
495
+ - lib/rails_error_dashboard/value_objects/llm_call_event.rb
486
496
  - lib/rails_error_dashboard/version.rb
487
497
  - lib/tasks/error_dashboard.rake
488
498
  - lib/tasks/rails_error_dashboard_tasks.rake
@@ -498,7 +508,7 @@ metadata:
498
508
  funding_uri: https://github.com/sponsors/AnjanJ
499
509
  post_install_message: |
500
510
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
501
- RED (Rails Error Dashboard) v0.6.4
511
+ RED (Rails Error Dashboard) v0.7.1
502
512
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
503
513
 
504
514
  First install: