Class: RailsErrorDashboard::Services::BreadcrumbCollector
- Inherits:
-
Object
- Object
- RailsErrorDashboard::Services::BreadcrumbCollector
- Defined in:
- lib/rails_error_dashboard/services/breadcrumb_collector.rb
Overview
Pure service: Collect breadcrumbs (request activity trail) for error context
Uses a thread-local ring buffer to capture events during a request lifecycle. Thread.current isolation means no mutex/lock is needed.
SAFETY RULES (HOST_APP_SAFETY.md):
-
Every public method wrapped in rescue => e; nil
-
Never raise, never block, never allocate large objects
-
Messages truncated to 500 chars, metadata values to 200 chars
-
Ring buffer has fixed max size (no unbounded growth)
Defined Under Namespace
Classes: RingBuffer
Constant Summary collapse
- THREAD_KEY =
:red_breadcrumbs- MAX_MESSAGE_LENGTH =
500- MAX_METADATA_VALUE_LENGTH =
200- MAX_METADATA_KEYS =
10- LLM_CATEGORIES =
Categories whose metadata is emitted by LlmCallEvent#to_breadcrumb_metadata.
%w[llm llm_tool].freeze
- LLM_STRUCTURED_METADATA_KEYS =
Keys on llm / llm_tool crumbs whose values are structured (numeric counts, identifiers, durations, costs) and must not be redacted by the ActiveSupport::ParameterFilter substring matcher. Without this whitelist, the default ‘:token` pattern matches `input_tokens` / `output_tokens` and corrupts the LlmSummary rollup, sidebar card, and markdown export tokens column. Sensitive-looking fields like tool_arguments / tool_result / error_message are intentionally NOT in this list — they go through the filter as usual because they can carry user-provided content.
%w[ provider model status input_tokens output_tokens duration_ms tool_name error_class cost_usd ].freeze
Class Method Summary collapse
-
.add(category, message, duration_ms: nil, metadata: nil) ⇒ Object
Add a breadcrumb to the current buffer.
-
.clear_buffer ⇒ Object
Clear the ring buffer (end of request — MUST be called in ensure block).
-
.current_breadcrumbs ⇒ Array<Hash>
Non-destructive read of current breadcrumbs (does NOT clear the buffer) Used by DiagnosticDumpGenerator for on-demand snapshots.
-
.current_buffer ⇒ RingBuffer?
Get the current buffer (for inspection).
-
.filter_sensitive(breadcrumbs) ⇒ Array<Hash>
Filter sensitive data from breadcrumbs before storage Reuses existing SensitiveDataFilter — no new filter logic.
-
.harvest ⇒ Array<Hash>
Harvest breadcrumbs from the current buffer and clear it.
-
.init_buffer ⇒ Object
Initialize a new ring buffer for the current thread (start of request).
Class Method Details
.add(category, message, duration_ms: nil, metadata: nil) ⇒ Object
Add a breadcrumb to the current buffer
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 93 def self.add(category, , duration_ms: nil, metadata: nil) buffer = Thread.current[THREAD_KEY] return unless buffer # Check category filter allowed = RailsErrorDashboard.configuration. if allowed cat_sym = category.to_s.to_sym return unless allowed.include?(cat_sym) end # Build breadcrumb entry with compact keys entry = { t: (Time.now.to_f * 1000).to_i, c: category.to_s, m: () } entry[:d] = duration_ms if duration_ms entry[:meta] = () if .is_a?(Hash) buffer.add(entry) rescue => e RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] BreadcrumbCollector.add failed: #{e.}") nil end |
.clear_buffer ⇒ Object
Clear the ring buffer (end of request — MUST be called in ensure block)
81 82 83 84 85 86 |
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 81 def self.clear_buffer Thread.current[THREAD_KEY] = nil rescue => e RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] BreadcrumbCollector.clear_buffer failed: #{e.}") nil end |
.current_breadcrumbs ⇒ Array<Hash>
Non-destructive read of current breadcrumbs (does NOT clear the buffer) Used by DiagnosticDumpGenerator for on-demand snapshots.
137 138 139 140 141 142 143 144 |
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 137 def self. buffer = Thread.current[THREAD_KEY] return [] unless buffer buffer.to_a rescue => e RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] BreadcrumbCollector.current_breadcrumbs failed: #{e.}") [] end |
.current_buffer ⇒ RingBuffer?
Get the current buffer (for inspection)
148 149 150 151 152 153 |
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 148 def self.current_buffer Thread.current[THREAD_KEY] rescue => e RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] BreadcrumbCollector.current_buffer failed: #{e.}") nil end |
.filter_sensitive(breadcrumbs) ⇒ Array<Hash>
Filter sensitive data from breadcrumbs before storage Reuses existing SensitiveDataFilter — no new filter logic
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 159 def self.filter_sensitive() return [] unless .is_a?(Array) return unless RailsErrorDashboard.configuration.filter_sensitive_data filter = SensitiveDataFilter.parameter_filter return unless filter .map do |crumb| filtered = crumb.dup # Filter message (SQL queries, key=value patterns) if filtered[:m] filtered[:m] = SensitiveDataFilter.send(:filter_message, filter, filtered[:m]) end # Filter metadata values if filtered[:meta].is_a?(Hash) filtered[:meta] = (filter, filtered[:c], filtered[:meta]) end filtered end rescue => e RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] BreadcrumbCollector.filter_sensitive failed: #{e.}") .is_a?(Array) ? : [] end |
.harvest ⇒ Array<Hash>
Harvest breadcrumbs from the current buffer and clear it
122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 122 def self.harvest buffer = Thread.current[THREAD_KEY] return [] unless buffer result = buffer.to_a buffer.clear result rescue => e RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] BreadcrumbCollector.harvest failed: #{e.}") [] end |
.init_buffer ⇒ Object
Initialize a new ring buffer for the current thread (start of request)
72 73 74 75 76 77 78 |
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 72 def self.init_buffer size = RailsErrorDashboard.configuration. || 40 Thread.current[THREAD_KEY] = RingBuffer.new(size) rescue => e RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] BreadcrumbCollector.init_buffer failed: #{e.}") nil end |