lapsoss 0.1.0 → 0.3.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +153 -733
  3. data/lib/lapsoss/adapters/appsignal_adapter.rb +7 -8
  4. data/lib/lapsoss/adapters/base.rb +0 -3
  5. data/lib/lapsoss/adapters/bugsnag_adapter.rb +12 -0
  6. data/lib/lapsoss/adapters/insight_hub_adapter.rb +102 -101
  7. data/lib/lapsoss/adapters/logger_adapter.rb +7 -7
  8. data/lib/lapsoss/adapters/rollbar_adapter.rb +93 -54
  9. data/lib/lapsoss/adapters/sentry_adapter.rb +11 -17
  10. data/lib/lapsoss/backtrace_frame.rb +35 -214
  11. data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
  12. data/lib/lapsoss/backtrace_processor.rb +37 -37
  13. data/lib/lapsoss/client.rb +2 -6
  14. data/lib/lapsoss/configuration.rb +25 -22
  15. data/lib/lapsoss/current.rb +9 -1
  16. data/lib/lapsoss/event.rb +30 -6
  17. data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
  18. data/lib/lapsoss/exclusion_configuration.rb +30 -0
  19. data/lib/lapsoss/exclusion_filter.rb +156 -0
  20. data/lib/lapsoss/{exclusions.rb → exclusion_presets.rb} +1 -181
  21. data/lib/lapsoss/fingerprinter.rb +9 -13
  22. data/lib/lapsoss/http_client.rb +42 -8
  23. data/lib/lapsoss/merged_scope.rb +63 -0
  24. data/lib/lapsoss/middleware/base.rb +15 -0
  25. data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
  26. data/lib/lapsoss/middleware/event_enricher.rb +19 -0
  27. data/lib/lapsoss/middleware/event_transformer.rb +19 -0
  28. data/lib/lapsoss/middleware/exception_filter.rb +43 -0
  29. data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
  30. data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
  31. data/lib/lapsoss/middleware/release_tracker.rb +117 -0
  32. data/lib/lapsoss/middleware/sample_filter.rb +23 -0
  33. data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
  34. data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
  35. data/lib/lapsoss/middleware.rb +0 -347
  36. data/lib/lapsoss/pipeline.rb +1 -73
  37. data/lib/lapsoss/pipeline_builder.rb +69 -0
  38. data/lib/lapsoss/rails_error_subscriber.rb +42 -0
  39. data/lib/lapsoss/rails_middleware.rb +78 -0
  40. data/lib/lapsoss/railtie.rb +22 -50
  41. data/lib/lapsoss/registry.rb +34 -20
  42. data/lib/lapsoss/release_providers.rb +110 -0
  43. data/lib/lapsoss/release_tracker.rb +112 -207
  44. data/lib/lapsoss/router.rb +3 -5
  45. data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
  46. data/lib/lapsoss/sampling/base.rb +11 -0
  47. data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
  48. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
  49. data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
  50. data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
  51. data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
  52. data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
  53. data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
  54. data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
  55. data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
  56. data/lib/lapsoss/sampling.rb +0 -326
  57. data/lib/lapsoss/scope.rb +17 -57
  58. data/lib/lapsoss/scrubber.rb +16 -18
  59. data/lib/lapsoss/user_context.rb +18 -198
  60. data/lib/lapsoss/user_context_integrations.rb +39 -0
  61. data/lib/lapsoss/user_context_middleware.rb +50 -0
  62. data/lib/lapsoss/user_context_provider.rb +93 -0
  63. data/lib/lapsoss/utils.rb +13 -0
  64. data/lib/lapsoss/validators.rb +14 -27
  65. data/lib/lapsoss/version.rb +1 -1
  66. data/lib/lapsoss.rb +12 -25
  67. metadata +106 -21
@@ -1,258 +1,79 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lapsoss
4
- class BacktraceFrame
5
- # Backtrace line patterns for different Ruby implementations
6
- BACKTRACE_PATTERNS = [
7
- # Standard Ruby format: filename.rb:123:in `method_name'
8
- /^(?<filename>[^:]+):(?<line>\d+):in [`'](?<method>.*?)[`']$/,
9
-
10
- # Ruby format without method: filename.rb:123
11
- /^(?<filename>[^:]+):(?<line>\d+)$/,
12
-
13
- # JRuby format: filename.rb:123:in method_name
14
- /^(?<filename>[^:]+):(?<line>\d+):in (?<method>.*)$/,
15
-
16
- # Eval'd code: (eval):123:in `method_name'
17
- /^\(eval\):(?<line>\d+):in [`'](?<method>.*?)[`']$/,
18
-
19
- # Block format: filename.rb:123:in `block in method_name'
20
- /^(?<filename>[^:]+):(?<line>\d+):in [`']block (?<block_level>\(\d+\s+levels\)\s+)?in (?<method>.*?)[`']$/,
21
-
22
- # Native extension format: [native_gem] filename.c:123:in `method_name'
23
- /^\[(?<native_gem>[^\]]+)\]\s*(?<filename>[^:]+):(?<line>\d+):in [`'](?<method>.*?)[`']$/,
24
-
25
- # Java backtrace format: org.jruby.Ruby.runScript(Ruby.java:123)
26
- /^(?<method>[^(]+)\((?<filename>[^:)]+):(?<line>\d+)\)$/,
27
-
28
- # Java backtrace format without line number: org.jruby.Ruby.runScript(Ruby.java)
29
- /^(?<method>[^(]+)\((?<filename>[^:)]+)\)$/,
30
-
31
- # Malformed Ruby format with invalid line number: filename.rb:abc:in `method'
32
- /^(?<filename>[^:]+):(?<line>[^:]*):in [`'](?<method>.*?)[`']$/,
33
-
34
- # Malformed Ruby format with missing line number: filename.rb::in `method'
35
- /^(?<filename>[^:]+)::in [`'](?<method>.*?)[`']$/,
36
-
37
- # Malformed Ruby format with missing method: filename.rb:123:in
38
- /^(?<filename>[^:]+):(?<line>\d+):in$/
39
- ].freeze
40
-
41
- # Common paths that indicate library/gem code
42
- LIBRARY_INDICATORS = [
43
- '/gems/',
44
- '/.bundle/',
45
- '/vendor/',
46
- '/ruby/',
47
- '(eval)',
48
- '(irb)',
49
- '/lib/ruby/',
50
- '/rbenv/',
51
- '/rvm/',
52
- '/usr/lib/ruby',
53
- '/System/Library/Frameworks'
54
- ].freeze
55
-
56
- attr_reader :filename, :line_number, :method_name, :in_app, :raw_line
57
- attr_reader :function, :module_name, :code_context, :block_info
58
-
4
+ BacktraceFrame = Data.define(
5
+ :filename,
6
+ :line_number,
7
+ :method_name,
8
+ :in_app,
9
+ :raw_line,
10
+ :function,
11
+ :module_name,
12
+ :code_context,
13
+ :block_info
14
+ ) do
59
15
  # Backward compatibility aliases
60
16
  alias_method :lineno, :line_number
61
17
  alias_method :raw, :raw_line
62
18
 
63
- def initialize(raw_line, in_app_patterns: [], exclude_patterns: [], load_paths: [])
64
- @raw_line = raw_line.to_s.strip
65
- @in_app_patterns = Array(in_app_patterns)
66
- @exclude_patterns = Array(exclude_patterns)
67
- @load_paths = Array(load_paths)
68
-
69
- parse_backtrace_line
70
- determine_app_status
71
- normalize_paths
72
- end
73
-
74
19
  def to_h
75
20
  {
76
- filename: @filename,
77
- line_number: @line_number,
78
- method: @method_name,
79
- function: @function,
80
- module: @module_name,
81
- in_app: @in_app,
82
- code_context: @code_context,
83
- raw: @raw_line
21
+ filename: filename,
22
+ line_number: line_number,
23
+ method: method_name,
24
+ function: function,
25
+ module: module_name,
26
+ in_app: in_app,
27
+ code_context: code_context,
28
+ raw: raw_line
84
29
  }.compact
85
30
  end
86
31
 
87
32
  def add_code_context(processor, context_lines = 3)
88
- return unless @filename && @line_number && File.exist?(@filename)
33
+ return unless filename && line_number && File.exist?(filename)
89
34
 
90
- @code_context = processor.get_code_context(@filename, @line_number, context_lines)
35
+ with(code_context: processor.get_code_context(filename, line_number, context_lines))
91
36
  end
92
37
 
93
38
  def valid?
94
- @filename && (@line_number.nil? || @line_number >= 0)
39
+ filename && (line_number.nil? || line_number >= 0)
95
40
  end
96
41
 
97
42
  def library_frame?
98
- !@in_app
43
+ !in_app
99
44
  end
100
45
 
101
46
  def app_frame?
102
- @in_app
47
+ in_app
103
48
  end
104
49
 
105
- def excluded?
106
- return false if @exclude_patterns.empty?
50
+ def excluded?(exclude_patterns = [])
51
+ return false if exclude_patterns.empty?
107
52
 
108
- @exclude_patterns.any? do |pattern|
53
+ exclude_patterns.any? do |pattern|
109
54
  case pattern
110
55
  when Regexp
111
- @raw_line.match?(pattern)
56
+ raw_line.match?(pattern)
112
57
  when String
113
- @raw_line.include?(pattern)
58
+ raw_line.include?(pattern)
114
59
  else
115
60
  false
116
61
  end
117
62
  end
118
63
  end
119
64
 
120
- def relative_filename
121
- return @filename unless @filename && @load_paths.any?
65
+ def relative_filename(load_paths = [])
66
+ return filename unless filename && load_paths.any?
122
67
 
123
68
  # Try to make path relative to load paths
124
- @load_paths.each do |load_path|
125
- if @filename.start_with?(load_path)
126
- relative = @filename.sub(/^#{Regexp.escape(load_path)}\/?/, '')
69
+ load_paths.each do |load_path|
70
+ if filename.start_with?(load_path)
71
+ relative = filename.sub(%r{^#{Regexp.escape(load_path)}/?}, "")
127
72
  return relative unless relative.empty?
128
73
  end
129
74
  end
130
75
 
131
- @filename
132
- end
133
-
134
- private
135
-
136
- def parse_backtrace_line
137
- BACKTRACE_PATTERNS.each_with_index do |pattern, pattern_index|
138
- match = @raw_line.match(pattern)
139
- next unless match
140
-
141
- @filename = match[:filename]
142
- # Handle malformed line numbers - convert invalid numbers to 0
143
- if match.names.include?('line') && match[:line]
144
- @line_number = match[:line].match?(/^\d+$/) ? match[:line].to_i : 0
145
- else
146
- @line_number = nil
147
- end
148
- @method_name = match.names.include?('method') ? match[:method] : nil
149
- @native_gem = match.names.include?('native_gem') ? match[:native_gem] : nil
150
- @block_level = match.names.include?('block_level') ? match[:block_level] : nil
151
-
152
- # Set default method name for lines without methods (top-level execution)
153
- @method_name = "<main>" if @method_name.nil?
154
-
155
- process_method_info
156
- return
157
- end
158
-
159
- # Fallback: treat entire line as filename if no pattern matches
160
- @filename = @raw_line
161
- @line_number = nil
162
- @method_name = "<main>"
163
- end
164
-
165
- def process_method_info
166
- return unless @method_name
167
-
168
- # Extract module/class and method information
169
- if @method_name.include?('.')
170
- # Class method: Module.method
171
- parts = @method_name.split('.', 2)
172
- @module_name = parts[0] if parts[0] != @method_name
173
- @function = parts[1] || @method_name
174
- elsif @method_name.include?('#')
175
- # Instance method: Module#method
176
- parts = @method_name.split('#', 2)
177
- @module_name = parts[0] if parts[0] != @method_name
178
- @function = parts[1] || @method_name
179
- elsif @method_name.start_with?('block')
180
- # Block method: process specially
181
- @function = @method_name
182
- process_block_info
183
- else
184
- @function = @method_name
185
- end
186
-
187
- # Clean up function name
188
- @function = @function&.strip
189
- @module_name = @module_name&.strip
190
- end
191
-
192
- def process_block_info
193
- return unless @method_name&.include?('block')
194
-
195
- @block_info = {
196
- type: :block,
197
- level: @block_level,
198
- in_method: nil
199
- }
200
-
201
- # Extract the method that contains the block
202
- if @method_name.match(/block (?:\([^)]+\)\s+)?in (.+)/)
203
- @block_info[:in_method] = $1
204
- end
205
- end
206
-
207
- def determine_app_status
208
- return @in_app = false unless @filename
209
-
210
- # Check explicit patterns first
211
- if @in_app_patterns.any?
212
- @in_app = @in_app_patterns.any? do |pattern|
213
- case pattern
214
- when Regexp
215
- @filename.match?(pattern)
216
- when String
217
- @filename.include?(pattern)
218
- else
219
- false
220
- end
221
- end
222
- return
223
- end
224
-
225
- # Default heuristics: check for library indicators
226
- @in_app = !LIBRARY_INDICATORS.any? { |indicator| @filename.include?(indicator) }
227
-
228
- # Special cases
229
- @in_app = false if @native_gem # Native extensions are not app code
230
- @in_app = false if @filename.start_with?('(') && @filename.end_with?(')') # Eval, irb, etc.
231
- end
232
-
233
- def normalize_paths
234
- return unless @filename
235
-
236
- # Expand relative paths
237
- if @filename.start_with?('./')
238
- @filename = File.expand_path(@filename)
239
- end
240
-
241
- # Handle Windows paths on Unix systems (for cross-platform stack traces)
242
- if @filename.include?('\\') && !@filename.include?('/')
243
- @filename = @filename.tr('\\', '/')
244
- end
245
-
246
- # Strip load paths to make traces more readable
247
- if @load_paths.any?
248
- original = @filename
249
- @filename = relative_filename
250
-
251
- # Keep absolute path if relative didn't work well
252
- if @filename.empty? || @filename == '.'
253
- @filename = original
254
- end
255
- end
76
+ filename
256
77
  end
257
78
  end
258
79
  end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ class BacktraceFrameFactory
5
+ # Backtrace line patterns for different Ruby implementations
6
+ BACKTRACE_PATTERNS = [
7
+ # Standard Ruby format: filename.rb:123:in `method_name'
8
+ /^(?<filename>[^:]+):(?<line>\d+):in [`'](?<method>.*?)[`']$/,
9
+
10
+ # Ruby format without method: filename.rb:123
11
+ /^(?<filename>[^:]+):(?<line>\d+)$/,
12
+
13
+ # JRuby format: filename.rb:123:in method_name
14
+ /^(?<filename>[^:]+):(?<line>\d+):in (?<method>.*)$/,
15
+
16
+ # Eval'd code: (eval):123:in `method_name'
17
+ /^\(eval\):(?<line>\d+):in [`'](?<method>.*?)[`']$/,
18
+
19
+ # Block format: filename.rb:123:in `block in method_name'
20
+ /^(?<filename>[^:]+):(?<line>\d+):in [`']block (?<block_level>\(\d+\s+levels\)\s+)?in (?<method>.*?)[`']$/,
21
+
22
+ # Native extension format: [native_gem] filename.c:123:in `method_name'
23
+ /^\[(?<native_gem>[^\]]+)\]\s*(?<filename>[^:]+):(?<line>\d+):in [`'](?<method>.*?)[`']$/,
24
+
25
+ # Java backtrace format: org.jruby.Ruby.runScript(Ruby.java:123)
26
+ /^(?<method>[^(]+)\((?<filename>[^:)]+):(?<line>\d+)\)$/,
27
+
28
+ # Java backtrace format without line number: org.jruby.Ruby.runScript(Ruby.java)
29
+ /^(?<method>[^(]+)\((?<filename>[^:)]+)\)$/,
30
+
31
+ # Malformed Ruby format with invalid line number: filename.rb:abc:in `method'
32
+ /^(?<filename>[^:]+):(?<line>[^:]*):in [`'](?<method>.*?)[`']$/,
33
+
34
+ # Malformed Ruby format with missing line number: filename.rb::in `method'
35
+ /^(?<filename>[^:]+)::in [`'](?<method>.*?)[`']$/,
36
+
37
+ # Malformed Ruby format with missing method: filename.rb:123:in
38
+ /^(?<filename>[^:]+):(?<line>\d+):in$/
39
+ ].freeze
40
+
41
+ # Common paths that indicate library/gem code
42
+ LIBRARY_INDICATORS = [
43
+ "/gems/",
44
+ "/.bundle/",
45
+ "/vendor/",
46
+ "/ruby/",
47
+ "(eval)",
48
+ "(irb)",
49
+ "/lib/ruby/",
50
+ "/rbenv/",
51
+ "/rvm/",
52
+ "/usr/lib/ruby",
53
+ "/System/Library/Frameworks"
54
+ ].freeze
55
+
56
+ def self.from_raw_line(raw_line, in_app_patterns: [], exclude_patterns: [], load_paths: [])
57
+ new(in_app_patterns: in_app_patterns, exclude_patterns: exclude_patterns, load_paths: load_paths)
58
+ .create_frame(raw_line)
59
+ end
60
+
61
+ def initialize(in_app_patterns: [], exclude_patterns: [], load_paths: [])
62
+ @in_app_patterns = Array(in_app_patterns)
63
+ @exclude_patterns = Array(exclude_patterns)
64
+ @load_paths = Array(load_paths)
65
+ end
66
+
67
+ def create_frame(raw_line)
68
+ @raw_line = raw_line.to_s.strip
69
+ parse_backtrace_line
70
+ end
71
+
72
+ private
73
+
74
+ def parse_backtrace_line
75
+ filename, line_number, method_name, function, module_name, block_info = parse_line_components
76
+
77
+ in_app = determine_app_status(filename)
78
+ filename = normalize_path(filename) if filename
79
+
80
+ BacktraceFrame.new(
81
+ filename: filename,
82
+ line_number: line_number,
83
+ method_name: method_name,
84
+ in_app: in_app,
85
+ raw_line: @raw_line,
86
+ function: function,
87
+ module_name: module_name,
88
+ code_context: nil,
89
+ block_info: block_info
90
+ )
91
+ end
92
+
93
+ def parse_line_components
94
+ BACKTRACE_PATTERNS.each do |pattern|
95
+ match = @raw_line.match(pattern)
96
+ next unless match
97
+
98
+ filename = match[:filename]
99
+ # Handle malformed line numbers - convert invalid numbers to 0
100
+ line_number = if match.names.include?("line") && match[:line]
101
+ match[:line].match?(/^\d+$/) ? match[:line].to_i : 0
102
+ end
103
+ method_name = match.names.include?("method") ? match[:method] : nil
104
+ match.names.include?("native_gem") ? match[:native_gem] : nil
105
+ block_level = match.names.include?("block_level") ? match[:block_level] : nil
106
+
107
+ # Set default method name for lines without methods (top-level execution)
108
+ method_name = "<main>" if method_name.nil?
109
+
110
+ function, module_name, block_info = process_method_info(method_name, block_level)
111
+
112
+ return [ filename, line_number, method_name, function, module_name, block_info ]
113
+ end
114
+
115
+ # Fallback: treat entire line as filename if no pattern matches
116
+ [ @raw_line, nil, "<main>", "<main>", nil, nil ]
117
+ end
118
+
119
+ def process_method_info(method_name, block_level)
120
+ return [ nil, nil, nil ] unless method_name
121
+
122
+ function = nil
123
+ module_name = nil
124
+ block_info = nil
125
+
126
+ # Extract module/class and method information
127
+ if method_name.include?(".")
128
+ # Class method: Module.method
129
+ parts = method_name.split(".", 2)
130
+ module_name = parts[0] if parts[0] != method_name
131
+ function = parts[1] || method_name
132
+ elsif method_name.include?("#")
133
+ # Instance method: Module#method
134
+ parts = method_name.split("#", 2)
135
+ module_name = parts[0] if parts[0] != method_name
136
+ function = parts[1] || method_name
137
+ elsif method_name.start_with?("block")
138
+ # Block method: process specially
139
+ function = method_name
140
+ block_info = process_block_info(method_name, block_level)
141
+ else
142
+ function = method_name
143
+ end
144
+
145
+ # Clean up function name
146
+ function = function&.strip
147
+ module_name = module_name&.strip
148
+
149
+ [ function, module_name, block_info ]
150
+ end
151
+
152
+ def process_block_info(method_name, block_level)
153
+ return nil unless method_name&.include?("block")
154
+
155
+ block_info = {
156
+ type: :block,
157
+ level: block_level,
158
+ in_method: nil
159
+ }
160
+
161
+ # Extract the method that contains the block
162
+ block_info[:in_method] = ::Regexp.last_match(1) if method_name =~ /block (?:\([^)]+\)\s+)?in (.+)/
163
+
164
+ block_info
165
+ end
166
+
167
+ def determine_app_status(filename)
168
+ return false unless filename
169
+
170
+ # Check explicit patterns first
171
+ if @in_app_patterns.any?
172
+ return @in_app_patterns.any? do |pattern|
173
+ case pattern
174
+ when Regexp
175
+ filename.match?(pattern)
176
+ when String
177
+ filename.include?(pattern)
178
+ else
179
+ false
180
+ end
181
+ end
182
+ end
183
+
184
+ # Default heuristics: check for library indicators
185
+ in_app = LIBRARY_INDICATORS.none? { |indicator| filename.include?(indicator) }
186
+
187
+ # Special cases
188
+ in_app = false if filename.start_with?("(") && filename.end_with?(")") # Eval, irb, etc.
189
+
190
+ in_app
191
+ end
192
+
193
+ def normalize_path(filename)
194
+ return filename unless filename
195
+
196
+ # Expand relative paths
197
+ filename = File.expand_path(filename) if filename.start_with?("./")
198
+
199
+ # Handle Windows paths on Unix systems (for cross-platform stack traces)
200
+ filename = filename.tr("\\", "/") if filename.include?("\\") && filename.exclude?("/")
201
+
202
+ # Strip load paths to make traces more readable
203
+ return filename unless @load_paths.any?
204
+
205
+ original = filename
206
+ filename = make_relative_filename(filename)
207
+
208
+ # Keep absolute path if relative didn't work well
209
+ filename = original if filename.empty? || filename == "."
210
+
211
+ filename
212
+ end
213
+
214
+ def make_relative_filename(filename)
215
+ return filename unless filename && @load_paths.any?
216
+
217
+ # Try to make path relative to load paths
218
+ @load_paths.each do |load_path|
219
+ if filename.start_with?(load_path)
220
+ relative = filename.sub(%r{^#{Regexp.escape(load_path)}/?}, "")
221
+ return relative unless relative.empty?
222
+ end
223
+ end
224
+
225
+ filename
226
+ end
227
+ end
228
+ end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require "active_support/cache"
5
4
  require "active_support/core_ext/numeric/time"
6
- require_relative "backtrace_frame"
7
5
 
8
6
  module Lapsoss
9
7
  class BacktraceProcessor
@@ -33,19 +31,19 @@ module Lapsoss
33
31
 
34
32
  def initialize(config = {})
35
33
  # Handle different config formats for backward compatibility
36
- if config.respond_to?(:backtrace_context_lines)
37
- # Configuration object passed
38
- config_hash = {
39
- context_lines: config.backtrace_context_lines,
40
- max_frames: config.backtrace_max_frames,
41
- enable_code_context: config.backtrace_enable_code_context,
42
- in_app_patterns: config.backtrace_in_app_patterns,
43
- exclude_patterns: config.backtrace_exclude_patterns,
44
- strip_load_path: config.backtrace_strip_load_path
45
- }
34
+ config_hash = if config.respond_to?(:backtrace_context_lines)
35
+ # Configuration object passed
36
+ {
37
+ context_lines: config.backtrace_context_lines,
38
+ max_frames: config.backtrace_max_frames,
39
+ enable_code_context: config.backtrace_enable_code_context,
40
+ in_app_patterns: config.backtrace_in_app_patterns,
41
+ exclude_patterns: config.backtrace_exclude_patterns,
42
+ strip_load_path: config.backtrace_strip_load_path
43
+ }
46
44
  else
47
- # Hash passed
48
- config_hash = config
45
+ # Hash passed
46
+ config
49
47
  end
50
48
 
51
49
  @config = DEFAULT_CONFIG.merge(config_hash)
@@ -77,17 +75,20 @@ module Lapsoss
77
75
  end
78
76
 
79
77
  # Backward compatibility alias
80
- alias_method :process, :process_backtrace
78
+ alias process process_backtrace
81
79
 
82
80
  def process_exception_backtrace(exception, follow_cause: false)
83
81
  return [] unless exception&.backtrace
84
82
 
85
83
  frames = process_backtrace(exception.backtrace)
86
84
 
87
- # Add exception-specific context
88
- frames.each_with_index do |frame, index|
89
- frame.define_singleton_method(:crash_frame?) { index == 0 }
90
- frame.define_singleton_method(:exception_class) { exception.class.name }
85
+ # Wrap frames with exception-specific context
86
+ frames = frames.map.with_index do |frame, index|
87
+ ExceptionBacktraceFrame.new(
88
+ frame,
89
+ exception_class: exception.class.name,
90
+ is_crash_frame: index.zero?
91
+ )
91
92
  end
92
93
 
93
94
  # Follow exception causes if requested
@@ -100,7 +101,7 @@ module Lapsoss
100
101
  end
101
102
 
102
103
  # Backward compatibility aliases
103
- alias_method :process_exception, :process_exception_backtrace
104
+ alias process_exception process_exception_backtrace
104
105
 
105
106
  def clear_cache!
106
107
  @file_cache.clear
@@ -205,11 +206,11 @@ module Lapsoss
205
206
 
206
207
  # Convert to 0-based index
207
208
  line_index = line_number - 1
208
- return nil if line_index < 0 || line_index >= lines.length
209
+ return nil if line_index.negative? || line_index >= lines.length
209
210
 
210
211
  # Calculate context range
211
- start_line = [0, line_index - context_lines].max
212
- end_line = [lines.length - 1, line_index + context_lines].min
212
+ start_line = [ 0, line_index - context_lines ].max
213
+ end_line = [ lines.length - 1, line_index + context_lines ].min
213
214
 
214
215
  {
215
216
  pre_context: lines[start_line...line_index],
@@ -235,7 +236,7 @@ module Lapsoss
235
236
  def parse_frames(backtrace)
236
237
  load_paths = determine_load_paths
237
238
  frames = backtrace.map do |line|
238
- BacktraceFrame.new(
239
+ BacktraceFrameFactory.from_raw_line(
239
240
  line,
240
241
  in_app_patterns: @config[:in_app_patterns],
241
242
  exclude_patterns: @config[:exclude_patterns],
@@ -249,7 +250,7 @@ module Lapsoss
249
250
 
250
251
  def filter_frames(frames)
251
252
  # Remove excluded frames
252
- frames = frames.reject(&:excluded?)
253
+ frames = frames.reject { |frame| frame.excluded?(@config[:exclude_patterns]) }
253
254
 
254
255
  # Filter gems if configured
255
256
  unless @config[:include_gems_in_context]
@@ -283,10 +284,10 @@ module Lapsoss
283
284
  head_frames = frames.first(head_count)
284
285
 
285
286
  # Get tail frames (original cause)
286
- tail_frames = if tail_count > 0
287
- frames.last(tail_count)
287
+ tail_frames = if tail_count.positive?
288
+ frames.last(tail_count)
288
289
  else
289
- []
290
+ []
290
291
  end
291
292
 
292
293
  head_frames + tail_frames
@@ -297,8 +298,9 @@ module Lapsoss
297
298
  context_frames = frames.select(&:app_frame?)
298
299
  context_frames += frames.select(&:library_frame?).first(3)
299
300
 
300
- context_frames.each do |frame|
301
- frame.add_code_context(self, @config[:context_lines])
301
+ context_frames.each_with_index do |frame, _index|
302
+ updated_frame = frame.add_code_context(self, @config[:context_lines])
303
+ frames[frames.index(frame)] = updated_frame if updated_frame
302
304
  end
303
305
  end
304
306
 
@@ -306,7 +308,7 @@ module Lapsoss
306
308
  seen = Set.new
307
309
  frames.select do |frame|
308
310
  # Create a key based on filename, line, and method
309
- key = [frame.filename, frame.line_number, frame.function].join(':')
311
+ key = [ frame.filename, frame.line_number, frame.function ].join(":")
310
312
 
311
313
  if seen.include?(key)
312
314
  false
@@ -326,18 +328,16 @@ module Lapsoss
326
328
  # Add common Rails paths if in Rails
327
329
  if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
328
330
  paths << Rails.root.to_s
329
- paths << Rails.root.join('app').to_s
330
- paths << Rails.root.join('lib').to_s
331
- paths << Rails.root.join('config').to_s
331
+ paths << Rails.root.join("app").to_s
332
+ paths << Rails.root.join("lib").to_s
333
+ paths << Rails.root.join("config").to_s
332
334
  end
333
335
 
334
336
  # Add current working directory
335
337
  paths << Dir.pwd
336
338
 
337
339
  # Add gem paths
338
- if defined?(Gem)
339
- paths.concat(Gem.path.map { |p| File.join(p, 'gems') })
340
- end
340
+ paths.concat(Gem.path.map { |p| File.join(p, "gems") }) if defined?(Gem)
341
341
 
342
342
  # Sort by length (longest first) for better matching
343
343
  paths.uniq.sort_by(&:length).reverse