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.
- checksums.yaml +4 -4
- data/README.md +153 -733
- data/lib/lapsoss/adapters/appsignal_adapter.rb +7 -8
- data/lib/lapsoss/adapters/base.rb +0 -3
- data/lib/lapsoss/adapters/bugsnag_adapter.rb +12 -0
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +102 -101
- data/lib/lapsoss/adapters/logger_adapter.rb +7 -7
- data/lib/lapsoss/adapters/rollbar_adapter.rb +93 -54
- data/lib/lapsoss/adapters/sentry_adapter.rb +11 -17
- data/lib/lapsoss/backtrace_frame.rb +35 -214
- data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
- data/lib/lapsoss/backtrace_processor.rb +37 -37
- data/lib/lapsoss/client.rb +2 -6
- data/lib/lapsoss/configuration.rb +25 -22
- data/lib/lapsoss/current.rb +9 -1
- data/lib/lapsoss/event.rb +30 -6
- data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
- data/lib/lapsoss/exclusion_configuration.rb +30 -0
- data/lib/lapsoss/exclusion_filter.rb +156 -0
- data/lib/lapsoss/{exclusions.rb → exclusion_presets.rb} +1 -181
- data/lib/lapsoss/fingerprinter.rb +9 -13
- data/lib/lapsoss/http_client.rb +42 -8
- data/lib/lapsoss/merged_scope.rb +63 -0
- data/lib/lapsoss/middleware/base.rb +15 -0
- data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
- data/lib/lapsoss/middleware/event_enricher.rb +19 -0
- data/lib/lapsoss/middleware/event_transformer.rb +19 -0
- data/lib/lapsoss/middleware/exception_filter.rb +43 -0
- data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
- data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
- data/lib/lapsoss/middleware/release_tracker.rb +117 -0
- data/lib/lapsoss/middleware/sample_filter.rb +23 -0
- data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
- data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
- data/lib/lapsoss/middleware.rb +0 -347
- data/lib/lapsoss/pipeline.rb +1 -73
- data/lib/lapsoss/pipeline_builder.rb +69 -0
- data/lib/lapsoss/rails_error_subscriber.rb +42 -0
- data/lib/lapsoss/rails_middleware.rb +78 -0
- data/lib/lapsoss/railtie.rb +22 -50
- data/lib/lapsoss/registry.rb +34 -20
- data/lib/lapsoss/release_providers.rb +110 -0
- data/lib/lapsoss/release_tracker.rb +112 -207
- data/lib/lapsoss/router.rb +3 -5
- data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
- data/lib/lapsoss/sampling/base.rb +11 -0
- data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
- data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
- data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
- data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
- data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
- data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
- data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
- data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
- data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
- data/lib/lapsoss/sampling.rb +0 -326
- data/lib/lapsoss/scope.rb +17 -57
- data/lib/lapsoss/scrubber.rb +16 -18
- data/lib/lapsoss/user_context.rb +18 -198
- data/lib/lapsoss/user_context_integrations.rb +39 -0
- data/lib/lapsoss/user_context_middleware.rb +50 -0
- data/lib/lapsoss/user_context_provider.rb +93 -0
- data/lib/lapsoss/utils.rb +13 -0
- data/lib/lapsoss/validators.rb +14 -27
- data/lib/lapsoss/version.rb +1 -1
- data/lib/lapsoss.rb +12 -25
- metadata +106 -21
@@ -1,258 +1,79 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Lapsoss
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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:
|
77
|
-
line_number:
|
78
|
-
method:
|
79
|
-
function:
|
80
|
-
module:
|
81
|
-
in_app:
|
82
|
-
code_context:
|
83
|
-
raw:
|
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
|
33
|
+
return unless filename && line_number && File.exist?(filename)
|
89
34
|
|
90
|
-
|
35
|
+
with(code_context: processor.get_code_context(filename, line_number, context_lines))
|
91
36
|
end
|
92
37
|
|
93
38
|
def valid?
|
94
|
-
|
39
|
+
filename && (line_number.nil? || line_number >= 0)
|
95
40
|
end
|
96
41
|
|
97
42
|
def library_frame?
|
98
|
-
|
43
|
+
!in_app
|
99
44
|
end
|
100
45
|
|
101
46
|
def app_frame?
|
102
|
-
|
47
|
+
in_app
|
103
48
|
end
|
104
49
|
|
105
|
-
def excluded?
|
106
|
-
return false if
|
50
|
+
def excluded?(exclude_patterns = [])
|
51
|
+
return false if exclude_patterns.empty?
|
107
52
|
|
108
|
-
|
53
|
+
exclude_patterns.any? do |pattern|
|
109
54
|
case pattern
|
110
55
|
when Regexp
|
111
|
-
|
56
|
+
raw_line.match?(pattern)
|
112
57
|
when String
|
113
|
-
|
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
|
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
|
-
|
125
|
-
if
|
126
|
-
relative =
|
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
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
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
|
-
#
|
88
|
-
frames.
|
89
|
-
|
90
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
287
|
-
|
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.
|
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(
|
330
|
-
paths << Rails.root.join(
|
331
|
-
paths << Rails.root.join(
|
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
|