11# frozen_string_literal: true
22
33require "rubygems"
4+ require "concurrent/map"
5+ require "sentry/backtrace/line"
46
57module Sentry
68 # @api private
79 class Backtrace
8- # Handles backtrace parsing line by line
9- class Line
10- RB_EXTENSION = ".rb"
11- # regexp (optional leading X: on windows, or JRuby9000 class-prefix)
12- RUBY_INPUT_FORMAT = /
13- ^ \s * (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>):
14- (\d +)
15- (?: :in\s ('|`)(?:([\w :]+)\# )?([^']+)')?$
16- /x
17-
18- # org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:170)
19- JAVA_INPUT_FORMAT = /^([\w $.]+)\. ([\w $]+)\( ([\w $.]+):(\d +)\) $/
20-
21- # The file portion of the line (such as app/models/user.rb)
22- attr_reader :file
23-
24- # The line number portion of the line
25- attr_reader :number
26-
27- # The method of the line (such as index)
28- attr_reader :method
29-
30- # The module name (JRuby)
31- attr_reader :module_name
32-
33- attr_reader :in_app_pattern
34-
35- # Parses a single line of a given backtrace
36- # @param [String] unparsed_line The raw line from +caller+ or some backtrace
37- # @return [Line] The parsed backtrace line
38- def self . parse ( unparsed_line , in_app_pattern = nil )
39- ruby_match = unparsed_line . match ( RUBY_INPUT_FORMAT )
40-
41- if ruby_match
42- _ , file , number , _ , module_name , method = ruby_match . to_a
43- file . sub! ( /\. class$/ , RB_EXTENSION )
44- module_name = module_name
45- else
46- java_match = unparsed_line . match ( JAVA_INPUT_FORMAT )
47- _ , module_name , method , file , number = java_match . to_a
48- end
49- new ( file , number , method , module_name , in_app_pattern )
50- end
51-
52- def initialize ( file , number , method , module_name , in_app_pattern )
53- @file = file
54- @module_name = module_name
55- @number = number . to_i
56- @method = method
57- @in_app_pattern = in_app_pattern
58- end
59-
60- def in_app
61- return false unless in_app_pattern
62-
63- if file =~ in_app_pattern
64- true
65- else
66- false
67- end
68- end
69-
70- # Reconstructs the line in a readable fashion
71- def to_s
72- "#{ file } :#{ number } :in `#{ method } '"
73- end
74-
75- def ==( other )
76- to_s == other . to_s
77- end
78-
79- def inspect
80- "<Line:#{ self } >"
81- end
82- end
83-
8410 # holder for an Array of Backtrace::Line instances
8511 attr_reader :lines
8612
@@ -100,6 +26,48 @@ def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_cal
10026 new ( lines )
10127 end
10228
29+ # Thread.each_caller_location is an API added in Ruby 3.2 that doesn't always collect
30+ # the entire stack like Kernel#caller or #caller_locations do.
31+ #
32+ # @see https://github.com/rails/rails/pull/49095 for more context.
33+ if Thread . respond_to? ( :each_caller_location )
34+ def self . source_location ( &backtrace_cleaner )
35+ Thread . each_caller_location do |location |
36+ frame_key = [ location . absolute_path , location . lineno ]
37+ cached_value = line_cache [ frame_key ]
38+
39+ next if cached_value == :skip
40+
41+ if cached_value
42+ return cached_value
43+ else
44+ if cleaned_frame = backtrace_cleaner . ( location )
45+ line = Line . from_source_location ( location )
46+ line_cache [ frame_key ] = line
47+
48+ return line
49+ else
50+ line_cache [ frame_key ] = :skip
51+
52+ next
53+ end
54+ end
55+ end
56+ end
57+
58+ def self . line_cache
59+ @line_cache ||= Concurrent ::Map . new
60+ end
61+ else
62+ # Since Sentry is mostly used in production, we don't want to fallback
63+ # to the slower implementation and adds potentially big overhead to the
64+ # application.
65+ def self . source_location ( *)
66+ nil
67+ end
68+ end
69+
70+
10371 def initialize ( lines )
10472 @lines = lines
10573 end
0 commit comments