Module: Restless::StackFrames
- Defined in:
- lib/restless/stack_frames.rb
Overview
CONTRACT.md FP-043, FP-044, FP-045. The Ruby dialect of stack parsing.
FP-044 makes frame parsing and the skip list explicitly per-language: only
the OUTPUT shape is contract surface. This module is therefore the one
place in the SDK that knows what a Ruby backtrace looks like, and the
shared conformance vectors deliberately do not cover it (FP-046); see
test/test_stack_frames.rb.
Constant Summary collapse
- FRAME_RE =
Ruby <= 3.3: "path.rb:12:in `method'" Ruby >= 3.4: "path.rb:12:in 'Klass#method'"
/\A(.+):(\d+):in [`'](.+)'\z/.freeze
- FRAME_NO_FN_RE =
Some frames carry no method label at all.
/\A(.+):(\d+)\z/.freeze
- BLOCK_LEVELS_RE =
block (3 levels) in handler->block in handler. The nesting count is closer to a line number than to an identity: it moves when somebody wraps the throw site in one moreeach, which FP-041 says must not split a group. /\Ablock \(\d+ levels\) in /.freeze
- RECEIVER_RE =
Ruby 3.4 changed backtrace method labels to carry the receiver:
detonatebecameExploder.detonate, andrunbecameExploder#run. Stripping it back off is not cosmetic. Without it the SAME crash in the SAME file fingerprints differently depending on which Ruby the service happens to run, so a routine runtime upgrade silently splits every existing 5xx group and orphans the Agent Recovery guidance attached to it. FP-041 forbids exactly that churn for line numbers; a runtime version is no different.Stripping rather than keeping, because that reproduces the label Ruby 3.3 and earlier already emit, so no stored key moves.
/\A(?:[A-Z]\w*(?:::[A-Z]\w*)*)[.#]/.freeze
- SDK_DIR =
FP-044. The Ruby equivalent of
node_modules/node:internal/@restlessai/sdk.Everything here is matched by FILE PATH, never by module or class name. A name check would also skip a customer's own
Restless-flavoured code and, worse, would not skip this gem when it is vendored under a different constant. The SDK's own directory is resolved from__dir__, so it is correct however the gem was installed. File.("..", __dir__).freeze
- STDLIB_DIRS =
[ RbConfig::CONFIG["rubylibdir"], RbConfig::CONFIG["rubyarchdir"], RbConfig::CONFIG["sitelibdir"], RbConfig::CONFIG["vendorlibdir"] ].compact.reject(&:empty?).map { |d| File.(d) }.freeze
- GEM_DIRS =
begin dirs = [] begin dirs.concat(Array(Gem.path)) if defined?(Gem) dirs << Gem.dir if defined?(Gem) && Gem.respond_to?(:dir) rescue StandardError # Gem may not be loaded at all; the "/gems/" fallback below covers it. end dirs.compact.uniq.map { |d| File.(d) }.freeze end
Class Method Summary collapse
-
.from_exception(error) ⇒ Object
Convenience for adapters: fingerprint-ready frame from an exception.
- .normalize_fn(name) ⇒ Object
- .parse_frame(line) ⇒ Object
- .skip_path?(path) ⇒ Boolean
-
.strip_receiver(name) ⇒ Object
Applied after the block normalization, and to the method portion of a
block in Xlabel as well as a bare one, since 3.4 qualifies both. -
.top_user_frame(backtrace) ⇒ Object
FP-043.
Class Method Details
.from_exception(error) ⇒ Object
Convenience for adapters: fingerprint-ready frame from an exception.
138 139 140 141 142 143 144 |
# File 'lib/restless/stack_frames.rb', line 138 def from_exception(error) return nil unless error.respond_to?(:backtrace) top_user_frame(error.backtrace) rescue StandardError nil end |
.normalize_fn(name) ⇒ Object
109 110 111 112 113 |
# File 'lib/restless/stack_frames.rb', line 109 def normalize_fn(name) cleaned = name.sub(BLOCK_LEVELS_RE, "block in ") cleaned = strip_receiver(cleaned) cleaned.empty? ? "anonymous" : cleaned end |
.parse_frame(line) ⇒ Object
97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/restless/stack_frames.rb', line 97 def parse_frame(line) if (m = FRAME_RE.match(line)) return { file: m[1], fn: normalize_fn(m[3]) } end if (m = FRAME_NO_FN_RE.match(line)) # FP-045. return { file: m[1], fn: "anonymous" } end nil end |
.skip_path?(path) ⇒ Boolean
125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/restless/stack_frames.rb', line 125 def skip_path?(path) return true if path.nil? || path.empty? # Ruby 3.x synthesises frames like "<internal:kernel>:90:in `tap'". return true if path.start_with?("<internal:") return true if path.start_with?(SDK_DIR) return true if path.include?("/gems/") return true if STDLIB_DIRS.any? { |dir| path.start_with?(dir) } return true if GEM_DIRS.any? { |dir| path.start_with?(dir) } false end |
.strip_receiver(name) ⇒ Object
Applied after the block normalization, and to the method portion of a
block in X label as well as a bare one, since 3.4 qualifies both.
117 118 119 120 121 122 123 |
# File 'lib/restless/stack_frames.rb', line 117 def strip_receiver(name) if name.start_with?("block in ") "block in " + name.sub(/\Ablock in /, "").sub(RECEIVER_RE, "") else name.sub(RECEIVER_RE, "") end end |
.top_user_frame(backtrace) ⇒ Object
FP-043. The frame NEAREST THE THROW SITE that is not vendor, runtime or SDK code.
Ruby's Exception#backtrace is innermost-FIRST (index 0 is where the
exception was raised), like a v8 Error.stack and unlike a Python
traceback, so the walk goes FORWARDS. Implementing this positionally in
the wrong direction returns the Rack entry point for every crash in the
process, which collapses every 500 into one fingerprint group and
defeats the strategy entirely. Verified empirically in
test/test_stack_frames.rb, not assumed.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/restless/stack_frames.rb', line 81 def top_user_frame(backtrace) return nil if backtrace.nil? Array(backtrace).each do |raw| frame = parse_frame(raw.to_s) next if frame.nil? next if skip_path?(frame[:file]) return { file: Fingerprint.project_relative(frame[:file]), fn: frame[:fn] } end nil end |