Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 7 additions & 31 deletions sentry-ruby/lib/sentry/interfaces/stacktrace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,44 +27,24 @@ class Frame < Interface
attr_accessor :abs_path, :context_line, :function, :in_app, :filename,
:lineno, :module, :pre_context, :post_context, :vars

def initialize(project_root, line, strip_backtrace_load_path = true)
def initialize(project_root, line, strip_backtrace_load_path = true, filename_cache: nil)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead project_root parameter in Frame constructor

Low Severity

The project_root positional parameter in Frame.initialize is accepted but never stored or referenced. All filename computation now delegates to @filename_cache, which carries its own project_root. This dead parameter is misleading — callers passing different values for project_root and the cache's project_root would get silent inconsistencies.

Fix in Cursor Fix in Web

@strip_backtrace_load_path = strip_backtrace_load_path
@filename_cache = filename_cache

@abs_path = line.file
@function = line.method if line.method
@lineno = line.number
@in_app = line.in_app
@module = line.module_name if line.module_name
@filename = compute_filename(project_root, strip_backtrace_load_path)
@filename = compute_filename
end

def to_s
"#{@filename}:#{@lineno}"
end

def compute_filename(project_root, strip_backtrace_load_path)
return if abs_path.nil?
return abs_path unless strip_backtrace_load_path

under_root = project_root && abs_path.start_with?(project_root)
prefix =
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frame silently produces nil filename without cache

Medium Severity

compute_filename uses the safe navigation operator @filename_cache&.compute_filename(...), which returns nil when filename_cache is nil (the default). The old code always computed a proper filename from project_root and abs_path. Now any Frame created without a filename_cache gets @filename = nil instead. Since project_root is still accepted as a constructor parameter, callers have no indication that it's ignored and a filename_cache is mandatory.

Additional Locations (1)
Fix in Cursor Fix in Web

if under_root && in_app
project_root
elsif under_root
longest_load_path || project_root
else
longest_load_path
end

if prefix
prefix_str = prefix.to_s
offset = if prefix_str.end_with?(File::SEPARATOR)
prefix_str.bytesize
else
prefix_str.bytesize + 1
end
abs_path.byteslice(offset, abs_path.bytesize - offset)
else
abs_path
end
def compute_filename
@filename_cache&.compute_filename(abs_path, in_app, @strip_backtrace_load_path)
end

def set_context(linecache, context_lines)
Expand All @@ -84,10 +64,6 @@ def to_h(*args)
end

private

def longest_load_path
$LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
end
end
end
end
5 changes: 4 additions & 1 deletion sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "sentry/utils/filename_cache"

module Sentry
class StacktraceBuilder
# @return [String]
Expand Down Expand Up @@ -47,6 +49,7 @@ def initialize(
@backtrace_cleanup_callback = backtrace_cleanup_callback
@strip_backtrace_load_path = strip_backtrace_load_path
@in_app_pattern = Regexp.new("^(#{project_root}/)?#{app_dirs_pattern}") if app_dirs_pattern
@filename_cache = FilenameCache.new(project_root)
end

# Generates a StacktraceInterface with the given backtrace.
Expand Down Expand Up @@ -87,7 +90,7 @@ def build(backtrace:, &frame_callback)
private

def convert_parsed_line_into_frame(line)
frame = StacktraceInterface::Frame.new(project_root, line, strip_backtrace_load_path)
frame = StacktraceInterface::Frame.new(project_root, line, strip_backtrace_load_path, filename_cache: @filename_cache)
frame.set_context(linecache, context_lines) if context_lines
frame
end
Expand Down
45 changes: 45 additions & 0 deletions sentry-ruby/lib/sentry/utils/filename_cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module Sentry
class FilenameCache
def initialize(project_root)
@project_root = project_root
@load_paths = $LOAD_PATH.map(&:to_s).sort_by(&:size).reverse.freeze
@cache = {}
end

def compute_filename(abs_path, in_app, strip_backtrace_load_path)
return unless abs_path
return abs_path unless strip_backtrace_load_path

@cache.fetch(abs_path) do
under_root = @project_root && abs_path.start_with?(@project_root)
prefix =
if under_root && in_app
@project_root
elsif under_root
longest_load_path(abs_path) || @project_root
else
longest_load_path(abs_path)
end

@cache[abs_path] = if prefix
offset = if prefix.end_with?(File::SEPARATOR)
prefix.bytesize
else
prefix.bytesize + 1
end
abs_path.byteslice(offset, abs_path.bytesize - offset)
else
abs_path
end
end
end

private

def longest_load_path(abs_path)
@load_paths.find { |path| abs_path.start_with?(path) }
end
end
end
9 changes: 5 additions & 4 deletions sentry-ruby/spec/sentry/interfaces/stacktrace_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@
let(:lines) do
Sentry::Backtrace.parse(raw_lines, configuration.project_root, configuration.app_dirs_pattern).lines
end
let(:filename_cache) { Sentry::FilenameCache.new(configuration.project_root) }

it "initializes a Frame with the correct info from the given Backtrace::Line object" do
first_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.first)
first_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.first, true, filename_cache: filename_cache)

expect(first_frame.filename).to match(/base.rb/)
expect(first_frame.in_app).to eq(false)
expect(first_frame.function).to eq("save")
expect(first_frame.lineno).to eq(10)

second_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.last)
second_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.last, true, filename_cache: filename_cache)

expect(second_frame.filename).to match(/post.rb/)
expect(second_frame.in_app).to eq(true)
Expand All @@ -30,11 +31,11 @@
end

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

second_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.last, false)
second_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.last, false, filename_cache: filename_cache)
expect(second_frame.filename).to eq(second_frame.abs_path)
expect(second_frame.filename).to eq(raw_lines.last.split(':').first)
end
Expand Down
4 changes: 3 additions & 1 deletion sentry-ruby/spec/sentry/transport_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,9 @@
frames: frame_list_size.times.map do |zero_based_index|
Sentry::StacktraceInterface::Frame.new(
"/fake/path",
Sentry::Backtrace::Line.parse("app.rb:#{zero_based_index + 1}:in `/'", in_app_pattern)
Sentry::Backtrace::Line.parse("app.rb:#{zero_based_index + 1}:in `/'", in_app_pattern),
true,
filename_cache: Sentry::FilenameCache.new("/fake/path")
)
end,
)
Expand Down
Loading