Skip to content

Commit 2309e5b

Browse files
committed
Don't let backtrace_cleanup_callback affect abs_path
At some point, rails added a [default gem filter to their `BacktraceCleaner`](https://github.com/rails/rails/blob/a8709e6ea26eca73a652af4fdd0a9f7db5352af4/activesupport/lib/active_support/backtrace_cleaner.rb#L118-L125) which messed up our linecache/context lines parsing logic since we get paths like ``` activesupport (7.1.2) lib/active_support/callbacks.rb ``` instead of raw paths. This PR cleans up handling this case by making a clear distinction between `abs_path` and `filename` as was intended in the original relay protocol and we were never populating these properly.
1 parent a9b3687 commit 2309e5b

File tree

6 files changed

+122
-27
lines changed

6 files changed

+122
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
- Fix `send_default_pii` handling in rails controller spans [#2443](https://github.com/getsentry/sentry-ruby/pull/2443)
1818
- Fixes [#2438](https://github.com/getsentry/sentry-ruby/issues/2438)
1919
- Fix `RescuedExceptionInterceptor` to handle an empty configuration [#2428](https://github.com/getsentry/sentry-ruby/pull/2428)
20+
- Don't let `backtrace_cleanup_callback` affect `abs_path` and separate filename handling ([#2474](https://github.com/getsentry/sentry-ruby/pull/2474))
21+
- Fixes [#2472](https://github.com/getsentry/sentry-ruby/issues/2472)
2022

2123
## 5.21.0
2224

sentry-rails/spec/sentry/rails_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,16 @@ def capture_in_separate_process(exit_code:)
242242
expect(traces.dig(-1, "function")).to be_nil
243243
end
244244

245+
it "makes sure BacktraceCleaner gem cleanup doesn't affect context lines population" do
246+
get "/view_exception"
247+
248+
traces = event.dig("exception", "values", 0, "stacktrace", "frames")
249+
gem_frame = traces.find { |t| t["abs_path"].match(/actionview/) }
250+
expect(gem_frame["pre_context"]).not_to be_empty
251+
expect(gem_frame["post_context"]).not_to be_empty
252+
expect(gem_frame["context_line"]).not_to be_empty
253+
end
254+
245255
it "doesn't filters exception backtrace if backtrace_cleanup_callback is overridden" do
246256
make_basic_app do |config|
247257
config.backtrace_cleanup_callback = lambda { |backtrace| backtrace }

sentry-ruby/lib/sentry/backtrace.rb

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ class Line
1818
# org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:170)
1919
JAVA_INPUT_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)$/
2020

21+
# The absolute path in the backtrace
22+
attr_reader :abs_path
23+
2124
# The file portion of the line (such as app/models/user.rb)
2225
attr_reader :file
2326

@@ -35,31 +38,45 @@ class Line
3538
# Parses a single line of a given backtrace
3639
# @param [String] unparsed_line The raw line from +caller+ or some backtrace
3740
# @return [Line] The parsed backtrace line
38-
def self.parse(unparsed_line, in_app_pattern = nil)
41+
def self.parse(unparsed_line, in_app_pattern = nil, cleaned_line = nil)
3942
ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT)
4043
if ruby_match
41-
_, file, number, _, method = ruby_match.to_a
42-
file.sub!(/\.class$/, RB_EXTENSION)
44+
_, abs_path, number, _, method = ruby_match.to_a
45+
abs_path.sub!(/\.class$/, RB_EXTENSION)
4346
module_name = nil
47+
48+
cleaned_match = cleaned_line.match(RUBY_INPUT_FORMAT) if cleaned_line
49+
if cleaned_match
50+
# prefer data from cleaned line
51+
_, file, number, _, method = cleaned_match.to_a
52+
file.sub!(/\.class$/, RB_EXTENSION)
53+
end
4454
else
4555
java_match = unparsed_line.match(JAVA_INPUT_FORMAT)
46-
_, module_name, method, file, number = java_match.to_a
56+
_, module_name, method, abs_path, number = java_match.to_a
57+
58+
59+
cleaned_match = cleaned_line.match(JAVA_INPUT_FORMAT) if cleaned_line
60+
# prefer data from cleaned line
61+
_, module_name, method, file, number = cleaned_match.to_a if cleaned_match
4762
end
48-
new(file, number, method, module_name, in_app_pattern)
63+
64+
new(abs_path, number, method, module_name, in_app_pattern, file || abs_path)
4965
end
5066

51-
def initialize(file, number, method, module_name, in_app_pattern)
52-
@file = file
67+
def initialize(abs_path, number, method, module_name, in_app_pattern, file = nil)
68+
@abs_path = abs_path
5369
@module_name = module_name
5470
@number = number.to_i
5571
@method = method
5672
@in_app_pattern = in_app_pattern
73+
@file = file
5774
end
5875

5976
def in_app
6077
return false unless in_app_pattern
6178

62-
if file =~ in_app_pattern
79+
if abs_path =~ in_app_pattern
6380
true
6481
else
6582
false
@@ -86,14 +103,14 @@ def inspect
86103
def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback)
87104
ruby_lines = backtrace.is_a?(Array) ? backtrace : backtrace.split(/\n\s*/)
88105

89-
ruby_lines = backtrace_cleanup_callback.call(ruby_lines) if backtrace_cleanup_callback
106+
cleaned_lines = backtrace_cleanup_callback ? backtrace_cleanup_callback.call(ruby_lines) : []
90107

91108
in_app_pattern ||= begin
92109
Regexp.new("^(#{project_root}/)?#{app_dirs_pattern}")
93110
end
94111

95-
lines = ruby_lines.to_a.map do |unparsed_line|
96-
Line.parse(unparsed_line, in_app_pattern)
112+
lines = ruby_lines.to_a.zip(cleaned_lines).map do |unparsed_line, cleaned_line|
113+
Line.parse(unparsed_line, in_app_pattern, cleaned_line)
97114
end
98115

99116
new(lines)

sentry-ruby/lib/sentry/interfaces/stacktrace.rb

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,33 @@ def initialize(project_root, line, strip_backtrace_load_path = true)
3131
@project_root = project_root
3232
@strip_backtrace_load_path = strip_backtrace_load_path
3333

34-
@abs_path = line.file
34+
@abs_path = line.abs_path
3535
@function = line.method if line.method
3636
@lineno = line.number
3737
@in_app = line.in_app
3838
@module = line.module_name if line.module_name
39-
@filename = compute_filename
39+
40+
@filename = compute_filename(line.file)
4041
end
4142

4243
def to_s
4344
"#{@filename}:#{@lineno}"
4445
end
4546

46-
def compute_filename
47-
return if abs_path.nil?
48-
return abs_path unless @strip_backtrace_load_path
47+
def compute_filename(file)
48+
return if file.nil?
49+
return file unless @strip_backtrace_load_path
4950

51+
under_project_root = under_project_root?(file)
5052
prefix =
51-
if under_project_root? && in_app
53+
if under_project_root && in_app
5254
@project_root
53-
elsif under_project_root?
54-
longest_load_path || @project_root
5555
else
56-
longest_load_path
56+
longest_load_path = longest_load_path(file)
57+
under_project_root ? longest_load_path || @project_root : longest_load_path
5758
end
5859

59-
prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
60+
prefix ? file[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : file
6061
end
6162

6263
def set_context(linecache, context_lines)
@@ -77,12 +78,12 @@ def to_hash(*args)
7778

7879
private
7980

80-
def under_project_root?
81-
@project_root && abs_path.start_with?(@project_root)
81+
def under_project_root?(file)
82+
@project_root && file.start_with?(@project_root)
8283
end
8384

84-
def longest_load_path
85-
$LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
85+
def longest_load_path(file)
86+
$LOAD_PATH.select { |path| file.start_with?(path.to_s) }.max_by(&:size)
8687
end
8788
end
8889
end

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
let(:unparsed_gem_line) do
1313
"/PATH_TO_RUBY/gems/2.7.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1675:in `call'"
1414
end
15+
let(:cleaned_gem_line) do
16+
"sinatra (2.1.0) lib/sinatra/base.rb:1675:in `call'"
17+
end
1518

1619
let(:in_app_pattern) do
1720
project_root = Sentry.configuration.project_root&.to_s
@@ -22,6 +25,7 @@
2225
it "parses app backtrace correctly" do
2326
line = described_class.parse(unparsed_app_line, in_app_pattern)
2427

28+
expect(line.abs_path).to eq("app.rb")
2529
expect(line.file).to eq("app.rb")
2630
expect(line.number).to eq(12)
2731
expect(line.method).to eq("/")
@@ -33,6 +37,9 @@
3337
it "parses gem backtrace correctly" do
3438
line = described_class.parse(unparsed_gem_line, in_app_pattern)
3539

40+
expect(line.abs_path).to eq(
41+
"/PATH_TO_RUBY/gems/2.7.0/gems/sinatra-2.1.0/lib/sinatra/base.rb"
42+
)
3643
expect(line.file).to eq(
3744
"/PATH_TO_RUBY/gems/2.7.0/gems/sinatra-2.1.0/lib/sinatra/base.rb"
3845
)
@@ -42,5 +49,21 @@
4249
expect(line.module_name).to eq(nil)
4350
expect(line.in_app).to eq(false)
4451
end
52+
53+
it "parses cleaned gem backtrace correctly" do
54+
line = described_class.parse(unparsed_gem_line, in_app_pattern, cleaned_gem_line)
55+
56+
expect(line.abs_path).to eq(
57+
"/PATH_TO_RUBY/gems/2.7.0/gems/sinatra-2.1.0/lib/sinatra/base.rb"
58+
)
59+
expect(line.file).to eq(
60+
"sinatra (2.1.0) lib/sinatra/base.rb"
61+
)
62+
expect(line.number).to eq(1675)
63+
expect(line.method).to eq("call")
64+
expect(line.in_app_pattern).to eq(in_app_pattern)
65+
expect(line.module_name).to eq(nil)
66+
expect(line.in_app).to eq(false)
67+
end
4568
end
4669
end

sentry-ruby/spec/sentry/interfaces/stacktrace_spec.rb

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,69 @@
1818
it "initializes a Frame with the correct info from the given Backtrace::Line object" do
1919
first_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.first)
2020

21-
expect(first_frame.filename).to match(/base.rb/)
21+
expect(first_frame.abs_path).to eq("#{Dir.home}/.rvm/gems/activerecord/base.rb")
22+
expect(first_frame.filename).to eq("#{Dir.home}/.rvm/gems/activerecord/base.rb")
2223
expect(first_frame.in_app).to eq(false)
2324
expect(first_frame.function).to eq("save")
2425
expect(first_frame.lineno).to eq(10)
2526

2627
second_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.last)
2728

28-
expect(second_frame.filename).to match(/post.rb/)
29+
expect(second_frame.abs_path).to match("#{configuration.project_root}/app/models/post.rb")
30+
expect(second_frame.filename).to match("app/models/post.rb")
2931
expect(second_frame.in_app).to eq(true)
3032
expect(second_frame.function).to eq("save_user")
3133
expect(second_frame.lineno).to eq(5)
3234
end
3335

3436
it "does not strip load path when strip_backtrace_load_path is false" do
3537
first_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.first, false)
38+
expect(first_frame.abs_path).to eq(first_frame.abs_path)
3639
expect(first_frame.filename).to eq(first_frame.abs_path)
3740
expect(first_frame.filename).to eq(raw_lines.first.split(':').first)
3841

3942
second_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.last, false)
43+
expect(second_frame.abs_path).to eq(second_frame.abs_path)
4044
expect(second_frame.filename).to eq(second_frame.abs_path)
4145
expect(second_frame.filename).to eq(raw_lines.last.split(':').first)
4246
end
47+
48+
context "with backtrace cleaner" do
49+
let(:backtrace_cleanup_callback) do
50+
proc do |backtrace|
51+
backtrace.map do |line|
52+
if line.match(/gems/)
53+
line.sub("#{Dir.home}/.rvm/gems/", "")
54+
elsif line.start_with?(configuration.project_root)
55+
line.sub(configuration.project_root, "")
56+
else
57+
line
58+
end
59+
end
60+
end
61+
end
62+
63+
let(:lines) do
64+
Sentry::Backtrace.parse(raw_lines, configuration.project_root, configuration.app_dirs_pattern, &backtrace_cleanup_callback).lines
65+
end
66+
67+
it "handles raw and cleaned lines properly" do
68+
first_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.first)
69+
70+
expect(first_frame.abs_path).to eq("#{Dir.home}/.rvm/gems/activerecord/base.rb")
71+
expect(first_frame.filename).to eq("activerecord/base.rb")
72+
expect(first_frame.in_app).to eq(false)
73+
expect(first_frame.function).to eq("save")
74+
expect(first_frame.lineno).to eq(10)
75+
76+
second_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.last)
77+
78+
expect(second_frame.abs_path).to match("#{configuration.project_root}/app/models/post.rb")
79+
expect(second_frame.filename).to match("app/models/post.rb")
80+
expect(second_frame.in_app).to eq(true)
81+
expect(second_frame.function).to eq("save_user")
82+
expect(second_frame.lineno).to eq(5)
83+
end
84+
end
4385
end
4486
end

0 commit comments

Comments
 (0)