Class: RailsErrorDashboard::Services::BreadcrumbCollector

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Class Method Details

.add(category, message, duration_ms: nil, metadata: nil) ⇒ Object

Add a breadcrumb to the current buffer

Parameters:

  • category (String)

    Event category (sql, controller, cache, job, mailer, custom)

  • message (String)

    Human-readable description

  • duration_ms (Float, nil) (defaults to: nil)

    Duration in milliseconds

  • metadata (Hash, nil) (defaults to: nil)

    Optional key-value pairs



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 76

def self.add(category, message, duration_ms: nil, metadata: nil)
  buffer = Thread.current[THREAD_KEY]
  return unless buffer

  # Check category filter
  allowed = RailsErrorDashboard.configuration.breadcrumb_categories
  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: truncate_message(message)
  }

  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.message}")
  nil
end

.clear_bufferObject

Clear the ring buffer (end of request — MUST be called in ensure block)



64
65
66
67
68
69
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 64

def self.clear_buffer
  Thread.current[THREAD_KEY] = nil
rescue => e
  RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] BreadcrumbCollector.clear_buffer failed: #{e.message}")
  nil
end

.current_bufferRingBuffer?

Get the current buffer (for inspection)

Returns:



119
120
121
122
123
124
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 119

def self.current_buffer
  Thread.current[THREAD_KEY]
rescue => e
  RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] BreadcrumbCollector.current_buffer failed: #{e.message}")
  nil
end

.filter_sensitive(breadcrumbs) ⇒ Array<Hash>

Filter sensitive data from breadcrumbs before storage Reuses existing SensitiveDataFilter — no new filter logic

Parameters:

  • breadcrumbs (Array<Hash>)

    Raw breadcrumbs

Returns:

  • (Array<Hash>)

    Filtered breadcrumbs



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 130

def self.filter_sensitive(breadcrumbs)
  return [] unless breadcrumbs.is_a?(Array)
  return breadcrumbs unless RailsErrorDashboard.configuration.filter_sensitive_data

  filter = SensitiveDataFilter.parameter_filter
  return breadcrumbs unless filter

  breadcrumbs.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.filter(filtered[:meta])
    end

    filtered
  end
rescue => e
  RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] BreadcrumbCollector.filter_sensitive failed: #{e.message}")
  breadcrumbs.is_a?(Array) ? breadcrumbs : []
end

.harvestArray<Hash>

Harvest breadcrumbs from the current buffer and clear it

Returns:

  • (Array<Hash>)

    Array of breadcrumb hashes (empty if none)



105
106
107
108
109
110
111
112
113
114
115
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 105

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.message}")
  []
end

.init_bufferObject

Initialize a new ring buffer for the current thread (start of request)



55
56
57
58
59
60
61
# File 'lib/rails_error_dashboard/services/breadcrumb_collector.rb', line 55

def self.init_buffer
  size = RailsErrorDashboard.configuration.breadcrumb_buffer_size || 40
  Thread.current[THREAD_KEY] = RingBuffer.new(size)
rescue => e
  RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] BreadcrumbCollector.init_buffer failed: #{e.message}")
  nil
end