Module: Errsight::Backtrace
- Defined in:
- lib/errsight/backtrace.rb
Overview
Parses Ruby backtrace strings into structured frames and classifies each frame as in_app (customer code) vs framework/gem.
Sentry-ruby was the reference for the parser regex and in_app strategy (study + reimplement, not copy). Their ‘Sentry::Backtrace` handles the same edge cases — JIT frames, eval’d code, native extensions — so the patterns are similar by necessity. The implementation is ours; the bug surface is ours to maintain.
Constant Summary collapse
- RUBY_FRAME =
Ruby 3.4+ formats methods with single quotes — “in ‘method’”. Earlier Ruby uses backticks — “in ‘method’”. The character class accepts either opener; the closer is always a single quote.
%r{ \A (?<file>.+?) :(?<line>\d+) (?::in\s+ ['`](?<method>.+)' )? \z }x- MAX_FRAMES =
Cap on frames per event. A pathological infinite-recursion crash can produce 10k+ frames; without a cap the event blows past the 512 KB ingestion limit and gets rejected. Sentry caps at 50; we match.
50
Class Method Summary collapse
-
.default_gem_paths ⇒ Object
Bundler.bundle_path can sit inside the project (vendor/bundle), so we include it AND Gem.path.
- .default_project_root ⇒ Object
-
.in_app?(abs_path, project_root, gem_paths) ⇒ Boolean
An “in_app” frame is customer code — the kind of frame the issue UI should highlight, expand, and show source context for.
-
.parse(lines, project_root: nil, gem_paths: nil) ⇒ Object
Parses an Array<String> backtrace into Array<Hash> structured frames, most-recent-first (matching exception.backtrace’s natural order).
- .parse_line(line, project_root:, gem_paths:) ⇒ Object
- .relative_filename(abs_path, project_root) ⇒ Object
-
.reset_defaults! ⇒ Object
Test/dev only: drop the memoized paths so a test that mutates Rails.root or Bundler can re-derive.
Class Method Details
.default_gem_paths ⇒ Object
Bundler.bundle_path can sit inside the project (vendor/bundle), so we include it AND Gem.path. Anything inside any of these is “not customer code.” Memoization is per-process; these paths don’t move at runtime.
97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/errsight/backtrace.rb', line 97 def default_gem_paths return @default_gem_paths if defined?(@default_gem_paths) paths = [] paths.concat(Gem.path.map(&:to_s)) if defined?(::Gem) begin paths << ::Bundler.bundle_path.to_s if defined?(::Bundler) && ::Bundler.respond_to?(:bundle_path) rescue StandardError # Bundler.bundle_path can raise if Bundler isn't fully loaded # (some test harnesses, gem-from-source setups). Ignore. end @default_gem_paths = paths.compact.uniq end |
.default_project_root ⇒ Object
85 86 87 88 89 90 91 |
# File 'lib/errsight/backtrace.rb', line 85 def default_project_root if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root ::Rails.root.to_s else Dir.pwd end end |
.in_app?(abs_path, project_root, gem_paths) ⇒ Boolean
An “in_app” frame is customer code — the kind of frame the issue UI should highlight, expand, and show source context for. Framework frames (Rails, gems) collapse into a “show 14 framework frames” group so the eye lands on what actually broke.
The order of checks matters: gem_paths first because Bundler can vendor gems inside the project tree (vendor/bundle), so a frame at /app/vendor/bundle/gems/foo/foo.rb starts with project_root but is not in_app.
66 67 68 69 70 71 72 73 74 |
# File 'lib/errsight/backtrace.rb', line 66 def in_app?(abs_path, project_root, gem_paths) return false if abs_path.nil? || abs_path.empty? # Internal frames (<internal:...>, <main>, (eval)) aren't files we # can show source context for — treat as not-in-app. return false if abs_path.start_with?("<", "(") return false if gem_paths.any? { |p| p && abs_path.start_with?(p) } return false if project_root.nil? || project_root.empty? abs_path.start_with?(project_root) end |
.parse(lines, project_root: nil, gem_paths: nil) ⇒ Object
Parses an Array<String> backtrace into Array<Hash> structured frames, most-recent-first (matching exception.backtrace’s natural order).
32 33 34 35 36 37 38 39 40 |
# File 'lib/errsight/backtrace.rb', line 32 def parse(lines, project_root: nil, gem_paths: nil) return [] unless lines.is_a?(Array) project_root ||= default_project_root gem_paths ||= default_gem_paths lines.first(MAX_FRAMES).filter_map do |line| parse_line(line, project_root: project_root, gem_paths: gem_paths) end end |
.parse_line(line, project_root:, gem_paths:) ⇒ Object
42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/errsight/backtrace.rb', line 42 def parse_line(line, project_root:, gem_paths:) return nil unless line.is_a?(String) match = RUBY_FRAME.match(line) return nil unless match abs_path = match[:file] { filename: relative_filename(abs_path, project_root), abs_path: abs_path, lineno: match[:line].to_i, function: match[:method], in_app: in_app?(abs_path, project_root, gem_paths) } end |
.relative_filename(abs_path, project_root) ⇒ Object
76 77 78 79 80 81 82 83 |
# File 'lib/errsight/backtrace.rb', line 76 def relative_filename(abs_path, project_root) return abs_path if abs_path.nil? return abs_path if project_root.nil? || project_root.empty? return abs_path unless abs_path.start_with?(project_root) # Strip "<project_root>/" prefix; leaves "app/models/user.rb" etc. rest = abs_path[project_root.size..] rest.sub(%r{\A/}, "") end |
.reset_defaults! ⇒ Object
Test/dev only: drop the memoized paths so a test that mutates Rails.root or Bundler can re-derive.
112 113 114 |
# File 'lib/errsight/backtrace.rb', line 112 def reset_defaults! remove_instance_variable(:@default_gem_paths) if defined?(@default_gem_paths) end |