Skip to content

Commit c282536

Browse files
committed
WIP
1 parent 9302ad9 commit c282536

File tree

5 files changed

+68
-76
lines changed

5 files changed

+68
-76
lines changed

sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb

Lines changed: 6 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,21 @@ class ActiveRecordSubscriber < AbstractSubscriber
1414
SUPPORT_SOURCE_LOCATION = ActiveSupport::BacktraceCleaner.method_defined?(:clean_frame)
1515

1616
if SUPPORT_SOURCE_LOCATION
17-
class_attribute :backtrace_cleaner, default: (ActiveSupport::BacktraceCleaner.new.tap do |cleaner|
17+
backtrace_cleaner = ActiveSupport::BacktraceCleaner.new.tap do |cleaner|
1818
cleaner.add_silencer { |line| line.include?("sentry-ruby/lib") || line.include?("sentry-rails/lib") }
19-
end)
20-
21-
CLEAN_FRAME_CACHE_SIZE = 1000
19+
end
2220

23-
@clean_frame_cache = {}
24-
@backtrace_cache = {}
21+
class_attribute :backtrace_cleaner, default: backtrace_cleaner.freeze
2522
end
2623

2724
class << self
2825
def subscribe!
2926
subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload|
30-
next if EXCLUDED_EVENTS.include? payload[:name]
31-
3227
record_query_source = SUPPORT_SOURCE_LOCATION && Sentry.configuration.rails.enable_db_query_source
3328
query_source_threshold = Sentry.configuration.rails.db_query_source_threshold_ms
3429

30+
next if EXCLUDED_EVENTS.include? payload[:name]
31+
3532
record_on_current_span(
3633
op: SPAN_PREFIX + event_name,
3734
origin: SPAN_ORIGIN,
@@ -69,7 +66,7 @@ def subscribe!
6966

7067
# both duration and query_source_threshold are in ms
7168
if record_query_source && duration >= query_source_threshold
72-
backtrace_line = query_source_location
69+
backtrace_line = Backtrace.source_location(backtrace_cleaner)
7370

7471
if backtrace_line
7572
span.set_data(Span::DataConventions::FILEPATH, backtrace_line.file) if backtrace_line.file
@@ -82,48 +79,6 @@ def subscribe!
8279
end
8380
end
8481
end
85-
86-
# Thread.each_caller_location is an API added in Ruby 3.2 that doesn't always collect the entire stack like
87-
# Kernel#caller or #caller_locations do. See https://github.com/rails/rails/pull/49095 for more context.
88-
if SUPPORT_SOURCE_LOCATION && Thread.respond_to?(:each_caller_location)
89-
def query_source_location
90-
Thread.each_caller_location do |location|
91-
if should_include_location?(location)
92-
return cached_backtrace_line(location)
93-
end
94-
end
95-
nil
96-
end
97-
98-
def should_include_location?(location)
99-
cache_key = [location.absolute_path, location.lineno]
100-
101-
cached_result = @clean_frame_cache[cache_key]
102-
return cached_result unless cached_result.nil?
103-
104-
# Check if backtrace_cleaner would include this frame
105-
frame = backtrace_cleaner.clean_frame(location)
106-
should_include = !frame.nil?
107-
108-
if @clean_frame_cache.size < CLEAN_FRAME_CACHE_SIZE
109-
@clean_frame_cache[cache_key] = should_include
110-
end
111-
112-
should_include
113-
end
114-
115-
def cached_backtrace_line(location)
116-
cache_key = [location.absolute_path, location.lineno]
117-
118-
@backtrace_cache[cache_key] ||= Sentry::Backtrace::Line.from_source_location(location)
119-
end
120-
else
121-
# Since Sentry is mostly used in production, we don't want to fallback to the slower implementation
122-
# and adds potentially big overhead to the application.
123-
def query_source_location
124-
nil
125-
end
126-
end
12782
end
12883
end
12984
end

sentry-ruby/lib/sentry/backtrace.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "rubygems"
4+
require "concurrent/map"
45
require "sentry/backtrace/line"
56

67
module Sentry
@@ -25,6 +26,52 @@ def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_cal
2526
new(lines)
2627
end
2728

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+
cache = line_cache.fetch_or_store(backtrace_cleaner.__id__) { Concurrent::Map.new }
36+
37+
Thread.each_caller_location do |location|
38+
frame_key = [location.absolute_path, location.lineno].freeze
39+
cached_value = cache[frame_key]
40+
41+
next if cached_value == :skip
42+
43+
if cached_value
44+
return cached_value
45+
else
46+
cleaned_frame = backtrace_cleaner.clean_frame(location)
47+
48+
if cleaned_frame
49+
line = Line.from_source_location(location)
50+
cache[frame_key] = line
51+
52+
return line
53+
else
54+
cache[frame_key] = :skip
55+
56+
next
57+
end
58+
end
59+
end
60+
end
61+
62+
def self.line_cache
63+
@line_cache ||= Concurrent::Map.new
64+
end
65+
else
66+
# Since Sentry is mostly used in production, we don't want to fallback
67+
# to the slower implementation and adds potentially big overhead to the
68+
# application.
69+
def self.source_location(*)
70+
nil
71+
end
72+
end
73+
74+
2875
def initialize(lines)
2976
@lines = lines
3077
end

sentry-ruby/lib/sentry/backtrace/line.rb

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,23 +53,13 @@ def self.parse(unparsed_line, in_app_pattern = nil)
5353
# @param [Regexp, nil] in_app_pattern Optional pattern to determine if the line is in-app
5454
# @return [Line] The backtrace line
5555
def self.from_source_location(location, in_app_pattern = nil)
56-
# Directly extract information from Thread::Backtrace::Location object
57-
# instead of converting to string and parsing with regex
58-
file = location.absolute_path || location.path
56+
file = location.absolute_path
5957
number = location.lineno
58+
method = location.base_label
6059

61-
# In Ruby 3.4+, label includes class name like "ClassName#method_name"
62-
# base_label is just the method name without the class
63-
method = location.base_label || location.label
64-
65-
# Extract module/class name from label if present (Ruby 3.4+)
66-
# Format: "ClassName#method_name" or "ClassName.method_name"
6760
label = location.label
68-
module_name = if label && label.include?("#")
69-
label.split("#", 2).first
70-
elsif label && label.include?(".")
71-
label.split(".", 2).first
72-
end
61+
index = label.index("#") || label.index(".")
62+
module_name = label[0, index] if index
7363

7464
new(file, number, method, module_name, in_app_pattern)
7565
end

sentry-ruby/lib/sentry/benchmarks/benchmark_transport.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,3 @@ def send_envelope(envelope)
1313
end
1414
end
1515
end
16-

sentry-ruby/spec/sentry/backtrace/lines_spec.rb

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,9 @@
4444

4545
describe ".from_source_location", skip: !Thread.respond_to?(:each_caller_location) do
4646
it "creates a Line from Thread::Backtrace::Location" do
47-
# Get a location from the current call stack
4847
location = caller_locations.first
4948
line = described_class.from_source_location(location, in_app_pattern)
5049

51-
# Verify the line was created with correct types
5250
expect(line).to be_a(described_class)
5351
expect(line.file).to be_a(String)
5452
expect(line.number).to be_a(Integer)
@@ -60,20 +58,23 @@
6058
location = caller_locations.first
6159
line = described_class.from_source_location(location)
6260

63-
# Verify that the extracted values match the location object
64-
expect(line.file).to eq(location.absolute_path || location.path)
61+
expect(line.file).to eq(location.absolute_path)
6562
expect(line.number).to eq(location.lineno)
66-
expect(line.method).to eq(location.base_label || location.label)
63+
expect(line.method).to eq(location.base_label)
6764
end
6865

69-
it "extracts module name from label when present" do
66+
it "extracts module name from label when present", when: { ruby_version?: [:>=, "3.4"] } do
7067
location = caller_locations.first
7168
line = described_class.from_source_location(location)
7269

73-
# Module name extraction depends on Ruby version and context
74-
# In Ruby 3.4+, label includes class name for methods defined in classes
75-
# The module_name should be a string if present, or nil
76-
expect(line.module_name).to be_a(String).or be_nil
70+
expect(line.module_name).to be_a(String)
71+
end
72+
73+
it "skips module name from label when present", when: { ruby_version?: [:<, "3.4"] } do
74+
location = caller_locations.first
75+
line = described_class.from_source_location(location)
76+
77+
expect(line.module_name).to be(nil)
7778
end
7879
end
7980
end

0 commit comments

Comments
 (0)