Class: Lapsoss::BacktraceProcessor

Inherits:
Object
  • Object
show all
Defined in:
lib/lapsoss/backtrace_processor.rb

Constant Summary collapse

DEFAULT_CONFIG =
{
  context_lines: 3,
  max_frames: 100,
  enable_code_context: true,
  strip_load_path: true,
  in_app_patterns: [],
  exclude_patterns: [
    # Common test/debug patterns to exclude
    /rspec/,
    /minitest/,
    /test-unit/,
    /cucumber/,
    /pry/,
    /byebug/,
    /debug/,
    /<internal:/,
    /kernel_require\.rb/
  ],
  dedupe_frames: true,
  include_gems_in_context: false
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = {}) ⇒ BacktraceProcessor

Returns a new instance of BacktraceProcessor.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/lapsoss/backtrace_processor.rb', line 33

def initialize(config = {})
  # Handle different config formats for backward compatibility
  config_hash = if config.respond_to?(:backtrace_context_lines)
                  # Configuration object passed
                  {
                    context_lines: config.backtrace_context_lines,
                    max_frames: config.backtrace_max_frames,
                    enable_code_context: config.backtrace_enable_code_context,
                    in_app_patterns: config.backtrace_in_app_patterns,
                    exclude_patterns: config.backtrace_exclude_patterns,
                    strip_load_path: config.backtrace_strip_load_path
                  }
  else
                  # Hash passed
                  config
  end

  @config = DEFAULT_CONFIG.merge(config_hash)
  @file_cache = ActiveSupport::Cache::MemoryStore.new(
    size: (@config[:file_cache_size] || 100) * 1024 * 1024, # Convert to bytes
    expires_in: 1.hour
  )
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



31
32
33
# File 'lib/lapsoss/backtrace_processor.rb', line 31

def config
  @config
end

Instance Method Details

#clear_cacheObject



193
194
195
# File 'lib/lapsoss/backtrace_processor.rb', line 193

def clear_cache
  @file_cache.clear
end

#clear_cache!Object



107
108
109
# File 'lib/lapsoss/backtrace_processor.rb', line 107

def clear_cache!
  @file_cache.clear
end

#format_frames(frames, format = :sentry) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/lapsoss/backtrace_processor.rb', line 115

def format_frames(frames, format = :sentry)
  case format
  when :sentry
    to_sentry_format(frames)
  when :rollbar
    to_rollbar_format(frames)
  when :bugsnag
    to_bugsnag_format(frames)
  else
    to_sentry_format(frames)
  end
end

#get_code_context(filename, line_number, context_lines = 3) ⇒ Object

Get code context around a specific line number using ActiveSupport::Cache



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/lapsoss/backtrace_processor.rb', line 198

def get_code_context(filename, line_number, context_lines = 3)
  return nil unless filename

  lines = @file_cache.fetch(filename) do
    read_file_safely(filename)
  end

  return nil unless lines

  # Convert to 0-based index
  line_index = line_number - 1
  return nil if line_index.negative? || line_index >= lines.length

  # Calculate context range
  start_line = [ 0, line_index - context_lines ].max
  end_line = [ lines.length - 1, line_index + context_lines ].min

  {
    pre_context: lines[start_line...line_index],
    context_line: lines[line_index],
    post_context: lines[(line_index + 1)..end_line],
    line_number: line_number,
    start_line: start_line + 1,
    end_line: end_line + 1
  }
rescue StandardError
  # Return nil on any file read error
  nil
end

#process_backtrace(backtrace) ⇒ Object Also known as: process



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/lapsoss/backtrace_processor.rb', line 57

def process_backtrace(backtrace)
  return [] unless backtrace&.any?

  # Parse all frames
  frames = parse_frames(backtrace)

  # Apply filtering
  frames = filter_frames(frames)

  # Limit frame count
  frames = limit_frames(frames)

  # Add code context if enabled
  add_code_context(frames) if @config[:enable_code_context]

  # Deduplicate if enabled
  frames = dedupe_frames(frames) if @config[:dedupe_frames]

  frames
end

#process_exception_backtrace(exception, follow_cause: false) ⇒ Object Also known as: process_exception



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/lapsoss/backtrace_processor.rb', line 81

def process_exception_backtrace(exception, follow_cause: false)
  return [] unless exception&.backtrace

  frames = process_backtrace(exception.backtrace)

  # Wrap frames with exception-specific context
  frames = frames.map.with_index do |frame, index|
    ExceptionBacktraceFrame.new(
      frame,
      exception_class: exception.class.name,
      is_crash_frame: index.zero?
    )
  end

  # Follow exception causes if requested
  if follow_cause && exception.respond_to?(:cause) && exception.cause
    cause_frames = process_exception_backtrace(exception.cause, follow_cause: true)
    frames.concat(cause_frames)
  end

  frames
end

#statsObject



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/lapsoss/backtrace_processor.rb', line 180

def stats
  {
    file_cache: {
      # ActiveSupport::Cache::MemoryStore doesn't expose detailed stats
      # but we can provide basic info
      type: "ActiveSupport::Cache::MemoryStore",
      configured_size: @file_cache.options[:size]
    },
    config: @config,
    load_paths: @load_paths
  }
end

#to_bugsnag_format(frames) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/lapsoss/backtrace_processor.rb', line 160

def to_bugsnag_format(frames)
  frames.map do |frame|
    data = {
      file: frame.filename,
      lineNumber: frame.line_number,
      method: frame.function || frame.method_name,
      inProject: frame.in_app
    }.compact

    # Add code context for Bugsnag/Insight Hub
    if frame.code_context
      data[:code] = {
        frame.code_context[:line_number] => frame.code_context[:context_line]
      }
    end

    data
  end
end

#to_hash_array(frames) ⇒ Object



111
112
113
# File 'lib/lapsoss/backtrace_processor.rb', line 111

def to_hash_array(frames)
  frames.map(&:to_h)
end

#to_rollbar_format(frames) ⇒ Object



149
150
151
152
153
154
155
156
157
158
# File 'lib/lapsoss/backtrace_processor.rb', line 149

def to_rollbar_format(frames)
  frames.map do |frame|
    {
      filename: frame.filename,
      lineno: frame.line_number,
      method: frame.function || frame.method_name,
      code: frame.code_context&.dig(:context_line)
    }.compact
  end
end

#to_sentry_format(frames) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/lapsoss/backtrace_processor.rb', line 128

def to_sentry_format(frames)
  frames.map do |frame|
    data = {
      filename: frame.filename,
      lineno: frame.line_number,
      function: frame.function || frame.method_name,
      module: frame.module_name,
      in_app: frame.in_app
    }.compact

    # Add code context for Sentry
    if frame.code_context
      data[:pre_context] = frame.code_context[:pre_context]
      data[:context_line] = frame.code_context[:context_line]
      data[:post_context] = frame.code_context[:post_context]
    end

    data
  end.reverse # Sentry expects oldest frame first
end