Module: Alplus::Stack
- Defined in:
- lib/alplus/stack.rb
Overview
Maps a Ruby backtrace to the wire frames[] shape
({file, function, lineno, in_app}) with in-app vs library detection.
Prefers Exception#backtrace_locations (structured, available since
Ruby 2.0) over parsing #backtrace strings; falls back to string
parsing only for backtraces that don't expose locations (e.g. a
synthetic backtrace assigned by hand).
Constant Summary collapse
- LIBRARY_MARKERS =
Path fragments that mark a frame as library code regardless of
app_dirs: installed gems, the Ruby stdlib, and common version manager layouts. Checked beforeapp_dirs, so a gem vendored inside the app directory is still treated as library code. [ "/gems/", "/lib/ruby/", "/vendor/bundle/", "/.rbenv/", "/.rvm/", "/.asdf/", "<internal:" ].freeze
- LINE_PATTERN =
/\A(.+):(\d+):in [`'"](.+)['"]\z/.freeze
- MAX_SOURCE_LINE_CHARS =
Longest source line kept in
pre_context/context_line/post_context(issue: source-context frames) -- a single absurdly long line (minified/generated code) is truncated rather than blowing up the envelope. 500- MAX_CACHED_SOURCE_FILES =
Per-process cache of a source file's lines. A deep in-app backtrace, or an error storm, otherwise re-reads the same files on the capturing thread on every event. Source does not change within a process, so each file is read at most once. Bounded (FIFO eviction) so it cannot grow without limit.
256
Class Method Summary collapse
- .build_frame(path, lineno, label, app_dirs, context_lines = 0) ⇒ Object
-
.cached_source_lines(path) ⇒ Object
Returns
path's lines, reading from disk at most once per process. - .cap_source_line(line) ⇒ Object
- .frame_from_line(line, app_dirs, context_lines = 0) ⇒ Object
-
.frames_for(exception, app_dirs: [], context_lines: 0) ⇒ Object
context_lines:(default0, i.e. disabled) isconfig.context_linesat call sites -- seeEnvelope.exception_item. - .in_app?(path, app_dirs) ⇒ Boolean
-
.source_context(path, lineno, context_lines) ⇒ Object
Reads
context_lineslines before/afterlineno(1-indexed, as backtraces report it) frompath.
Class Method Details
.build_frame(path, lineno, label, app_dirs, context_lines = 0) ⇒ Object
60 61 62 63 64 65 |
# File 'lib/alplus/stack.rb', line 60 def build_frame(path, lineno, label, app_dirs, context_lines = 0) in_app = in_app?(path, app_dirs) frame = { file: path, lineno: lineno, function: label, in_app: in_app } frame.merge!(source_context(path, lineno, context_lines)) if in_app && context_lines.to_i.positive? frame.compact end |
.cached_source_lines(path) ⇒ Object
Returns path's lines, reading from disk at most once per process.
FIFO-evicts the oldest entry past the cap. Holds the mutex across the
read so concurrent captures of the same file do not each read it.
100 101 102 103 104 105 106 107 108 109 |
# File 'lib/alplus/stack.rb', line 100 def cached_source_lines(path) SOURCE_CACHE_MUTEX.synchronize do return SOURCE_CACHE[path] if SOURCE_CACHE.key?(path) lines = File.readlines(path) SOURCE_CACHE[path] = lines SOURCE_CACHE.shift if SOURCE_CACHE.size > MAX_CACHED_SOURCE_FILES lines end end |
.cap_source_line(line) ⇒ Object
111 112 113 114 |
# File 'lib/alplus/stack.rb', line 111 def cap_source_line(line) line = line.chomp line.length > MAX_SOURCE_LINE_CHARS ? line[0, MAX_SOURCE_LINE_CHARS] : line end |
.frame_from_line(line, app_dirs, context_lines = 0) ⇒ Object
67 68 69 70 71 72 |
# File 'lib/alplus/stack.rb', line 67 def frame_from_line(line, app_dirs, context_lines = 0) match = LINE_PATTERN.match(line) return nil unless match build_frame(match[1], match[2].to_i, match[3], app_dirs, context_lines) end |
.frames_for(exception, app_dirs: [], context_lines: 0) ⇒ Object
context_lines: (default 0, i.e. disabled) is config.context_lines
at call sites -- see Envelope.exception_item. Source context is only
ever attached to in_app frames: library/gem frames have no value to
a host app's developer and reading arbitrary gem source is wasted
work.
51 52 53 54 55 56 57 58 |
# File 'lib/alplus/stack.rb', line 51 def frames_for(exception, app_dirs: [], context_lines: 0) locations = exception.respond_to?(:backtrace_locations) ? exception.backtrace_locations : nil if locations locations.map { |loc| build_frame(loc.path, loc.lineno, loc.label, app_dirs, context_lines) } else Array(exception.backtrace).filter_map { |line| frame_from_line(line, app_dirs, context_lines) } end end |
.in_app?(path, app_dirs) ⇒ Boolean
116 117 118 119 120 121 122 |
# File 'lib/alplus/stack.rb', line 116 def in_app?(path, app_dirs) return false if path.nil? return false if LIBRARY_MARKERS.any? { |marker| path.include?(marker) } return true if app_dirs.empty? app_dirs.any? { |dir| path.start_with?(dir.to_s) } end |
.source_context(path, lineno, context_lines) ⇒ Object
Reads context_lines lines before/after lineno (1-indexed, as
backtraces report it) from path. Returns {} (attaching nothing)
for a missing/unreadable file or an out-of-range line number --
never raises, matching every other fail-safe boundary in this SDK.
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/alplus/stack.rb', line 78 def source_context(path, lineno, context_lines) return {} unless path && File.file?(path) && File.readable?(path) lines = cached_source_lines(path) index = lineno - 1 return {} unless index >= 0 && index < lines.length start_index = [index - context_lines, 0].max end_index = [index + context_lines, lines.length - 1].min { pre_context: lines[start_index...index].map { |line| cap_source_line(line) }, context_line: cap_source_line(lines[index]), post_context: lines[(index + 1)..end_index].map { |line| cap_source_line(line) } } rescue StandardError {} end |