lapsoss 0.2.0 → 0.3.1
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 +22 -22
- data/lib/lapsoss/adapters/base.rb +0 -3
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +108 -104
- data/lib/lapsoss/adapters/logger_adapter.rb +1 -1
- data/lib/lapsoss/adapters/rollbar_adapter.rb +108 -68
- data/lib/lapsoss/adapters/sentry_adapter.rb +24 -24
- data/lib/lapsoss/backtrace_frame.rb +37 -206
- data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
- data/lib/lapsoss/backtrace_processor.rb +27 -23
- data/lib/lapsoss/client.rb +2 -4
- data/lib/lapsoss/configuration.rb +28 -32
- data/lib/lapsoss/current.rb +10 -2
- data/lib/lapsoss/event.rb +28 -5
- data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
- data/lib/lapsoss/exclusion_configuration.rb +30 -0
- data/lib/lapsoss/exclusion_filter.rb +0 -273
- data/lib/lapsoss/exclusion_presets.rb +249 -0
- data/lib/lapsoss/fingerprinter.rb +28 -28
- data/lib/lapsoss/http_client.rb +8 -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/pipeline.rb +0 -68
- 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 +18 -5
- data/lib/lapsoss/release_providers.rb +110 -0
- data/lib/lapsoss/release_tracker.rb +159 -232
- 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/scope.rb +12 -48
- data/lib/lapsoss/scrubber.rb +7 -7
- data/lib/lapsoss/user_context.rb +30 -203
- 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 +15 -15
- data/lib/lapsoss/version.rb +1 -1
- data/lib/lapsoss.rb +3 -3
- metadata +60 -7
- data/lib/lapsoss/middleware.rb +0 -345
- data/lib/lapsoss/sampling.rb +0 -328
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "securerandom"
|
4
4
|
|
5
5
|
module Lapsoss
|
6
6
|
module Adapters
|
7
7
|
class SentryAdapter < Base
|
8
8
|
PROTOCOL_VERSION = 7
|
9
|
-
CONTENT_TYPE =
|
9
|
+
CONTENT_TYPE = "application/x-sentry-envelope"
|
10
10
|
GZIP_THRESHOLD = 1024 * 30 # 30KB
|
11
11
|
USER_AGENT = "lapsoss/#{Lapsoss::VERSION}".freeze
|
12
12
|
|
@@ -60,14 +60,14 @@ module Lapsoss
|
|
60
60
|
header = {
|
61
61
|
event_id: event.context[:event_id] || SecureRandom.uuid,
|
62
62
|
sent_at: Time.now.iso8601,
|
63
|
-
sdk: { name:
|
63
|
+
sdk: { name: "lapsoss", version: Lapsoss::VERSION }
|
64
64
|
}
|
65
65
|
|
66
|
-
item_type = event.type == :transaction ?
|
67
|
-
item_header = { type: item_type, content_type:
|
66
|
+
item_type = event.type == :transaction ? "transaction" : "event"
|
67
|
+
item_header = { type: item_type, content_type: "application/json" }
|
68
68
|
item_payload = build_event_payload(event)
|
69
69
|
|
70
|
-
[header, item_header, item_payload]
|
70
|
+
[ header, item_header, item_payload ]
|
71
71
|
end
|
72
72
|
|
73
73
|
def serialize_envelope(envelope)
|
@@ -80,15 +80,15 @@ module Lapsoss
|
|
80
80
|
].join("\n")
|
81
81
|
|
82
82
|
if body.bytesize >= GZIP_THRESHOLD
|
83
|
-
[Zlib.gzip(body), true]
|
83
|
+
[ Zlib.gzip(body), true ]
|
84
84
|
else
|
85
|
-
[body, false]
|
85
|
+
[ body, false ]
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
89
89
|
def build_event_payload(event)
|
90
90
|
{
|
91
|
-
platform:
|
91
|
+
platform: "ruby",
|
92
92
|
level: map_level(event.level),
|
93
93
|
timestamp: event.timestamp.to_f,
|
94
94
|
environment: @settings[:environment],
|
@@ -105,11 +105,11 @@ module Lapsoss
|
|
105
105
|
when :exception
|
106
106
|
{
|
107
107
|
exception: {
|
108
|
-
values: [{
|
108
|
+
values: [ {
|
109
109
|
type: event.exception.class.name,
|
110
110
|
value: event.exception.message,
|
111
111
|
stacktrace: { frames: parse_backtrace(event.exception.backtrace) }
|
112
|
-
}]
|
112
|
+
} ]
|
113
113
|
}
|
114
114
|
}
|
115
115
|
when :message
|
@@ -121,9 +121,9 @@ module Lapsoss
|
|
121
121
|
|
122
122
|
def build_headers(compressed)
|
123
123
|
{
|
124
|
-
|
125
|
-
|
126
|
-
|
124
|
+
"Content-Type" => CONTENT_TYPE,
|
125
|
+
"X-Sentry-Auth" => auth_header,
|
126
|
+
"Content-Encoding" => ("gzip" if compressed)
|
127
127
|
}.compact
|
128
128
|
end
|
129
129
|
|
@@ -142,8 +142,8 @@ module Lapsoss
|
|
142
142
|
# - Custom service: https://public_key@custom.com/api/v1/errors -> /api/v1/errors
|
143
143
|
|
144
144
|
# Extract project ID for auth header (usually the last path segment)
|
145
|
-
path_parts = uri.path.split(
|
146
|
-
project_id = path_parts.last ||
|
145
|
+
path_parts = uri.path.split("/").reject(&:empty?)
|
146
|
+
project_id = path_parts.last || "unknown"
|
147
147
|
|
148
148
|
# Use the DSN path directly - this is what the service expects
|
149
149
|
api_path = uri.path
|
@@ -172,19 +172,19 @@ module Lapsoss
|
|
172
172
|
|
173
173
|
def map_level(level)
|
174
174
|
case level
|
175
|
-
when :debug then
|
176
|
-
when :info then
|
177
|
-
when :warn, :warning then
|
178
|
-
when :error then
|
179
|
-
when :fatal then
|
180
|
-
else
|
175
|
+
when :debug then "debug"
|
176
|
+
when :info then "info"
|
177
|
+
when :warn, :warning then "warning"
|
178
|
+
when :error then "error"
|
179
|
+
when :fatal then "fatal"
|
180
|
+
else "info"
|
181
181
|
end
|
182
182
|
end
|
183
183
|
|
184
184
|
def validate_settings!
|
185
|
-
raise ValidationError,
|
185
|
+
raise ValidationError, "Sentry DSN is required" unless @settings[:dsn]
|
186
186
|
|
187
|
-
validate_dsn!(@settings[:dsn],
|
187
|
+
validate_dsn!(@settings[:dsn], "Sentry DSN")
|
188
188
|
end
|
189
189
|
end
|
190
190
|
end
|
@@ -1,248 +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, :function, :module_name, :code_context,
|
57
|
-
: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
|
-
|
61
|
-
|
62
|
-
|
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
|
16
|
+
alias_method :lineno, :line_number
|
17
|
+
alias_method :raw, :raw_line
|
73
18
|
|
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
|
-
@line_number = if match.names.include?('line') && match[:line]
|
144
|
-
match[:line].match?(/^\d+$/) ? match[:line].to_i : 0
|
145
|
-
end
|
146
|
-
@method_name = match.names.include?('method') ? match[:method] : nil
|
147
|
-
@native_gem = match.names.include?('native_gem') ? match[:native_gem] : nil
|
148
|
-
@block_level = match.names.include?('block_level') ? match[:block_level] : nil
|
149
|
-
|
150
|
-
# Set default method name for lines without methods (top-level execution)
|
151
|
-
@method_name = '<main>' if @method_name.nil?
|
152
|
-
|
153
|
-
process_method_info
|
154
|
-
return
|
155
|
-
end
|
156
|
-
|
157
|
-
# Fallback: treat entire line as filename if no pattern matches
|
158
|
-
@filename = @raw_line
|
159
|
-
@line_number = nil
|
160
|
-
@method_name = '<main>'
|
161
|
-
end
|
162
|
-
|
163
|
-
def process_method_info
|
164
|
-
return unless @method_name
|
165
|
-
|
166
|
-
# Extract module/class and method information
|
167
|
-
if @method_name.include?('.')
|
168
|
-
# Class method: Module.method
|
169
|
-
parts = @method_name.split('.', 2)
|
170
|
-
@module_name = parts[0] if parts[0] != @method_name
|
171
|
-
@function = parts[1] || @method_name
|
172
|
-
elsif @method_name.include?('#')
|
173
|
-
# Instance method: Module#method
|
174
|
-
parts = @method_name.split('#', 2)
|
175
|
-
@module_name = parts[0] if parts[0] != @method_name
|
176
|
-
@function = parts[1] || @method_name
|
177
|
-
elsif @method_name.start_with?('block')
|
178
|
-
# Block method: process specially
|
179
|
-
@function = @method_name
|
180
|
-
process_block_info
|
181
|
-
else
|
182
|
-
@function = @method_name
|
183
|
-
end
|
184
|
-
|
185
|
-
# Clean up function name
|
186
|
-
@function = @function&.strip
|
187
|
-
@module_name = @module_name&.strip
|
188
|
-
end
|
189
|
-
|
190
|
-
def process_block_info
|
191
|
-
return unless @method_name&.include?('block')
|
192
|
-
|
193
|
-
@block_info = {
|
194
|
-
type: :block,
|
195
|
-
level: @block_level,
|
196
|
-
in_method: nil
|
197
|
-
}
|
198
|
-
|
199
|
-
# Extract the method that contains the block
|
200
|
-
@block_info[:in_method] = ::Regexp.last_match(1) if @method_name =~ /block (?:\([^)]+\)\s+)?in (.+)/
|
201
|
-
end
|
202
|
-
|
203
|
-
def determine_app_status
|
204
|
-
return @in_app = false unless @filename
|
205
|
-
|
206
|
-
# Check explicit patterns first
|
207
|
-
if @in_app_patterns.any?
|
208
|
-
@in_app = @in_app_patterns.any? do |pattern|
|
209
|
-
case pattern
|
210
|
-
when Regexp
|
211
|
-
@filename.match?(pattern)
|
212
|
-
when String
|
213
|
-
@filename.include?(pattern)
|
214
|
-
else
|
215
|
-
false
|
216
|
-
end
|
217
|
-
end
|
218
|
-
return
|
219
|
-
end
|
220
|
-
|
221
|
-
# Default heuristics: check for library indicators
|
222
|
-
@in_app = LIBRARY_INDICATORS.none? { |indicator| @filename.include?(indicator) }
|
223
|
-
|
224
|
-
# Special cases
|
225
|
-
@in_app = false if @native_gem # Native extensions are not app code
|
226
|
-
@in_app = false if @filename.start_with?('(') && @filename.end_with?(')') # Eval, irb, etc.
|
227
|
-
end
|
228
|
-
|
229
|
-
def normalize_paths
|
230
|
-
return unless @filename
|
231
|
-
|
232
|
-
# Expand relative paths
|
233
|
-
@filename = File.expand_path(@filename) if @filename.start_with?('./')
|
234
|
-
|
235
|
-
# Handle Windows paths on Unix systems (for cross-platform stack traces)
|
236
|
-
@filename = @filename.tr('\\', '/') if @filename.include?('\\') && @filename.exclude?('/')
|
237
|
-
|
238
|
-
# Strip load paths to make traces more readable
|
239
|
-
return unless @load_paths.any?
|
240
|
-
|
241
|
-
original = @filename
|
242
|
-
@filename = relative_filename
|
243
|
-
|
244
|
-
# Keep absolute path if relative didn't work well
|
245
|
-
@filename = original if @filename.empty? || @filename == '.'
|
76
|
+
filename
|
246
77
|
end
|
247
78
|
end
|
248
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
|