diff --git a/sentry-rails/examples/rails-6.0/config/initializers/sentry.rb b/sentry-rails/examples/rails-6.0/config/initializers/sentry.rb index a620d6c76..571083b5f 100644 --- a/sentry-rails/examples/rails-6.0/config/initializers/sentry.rb +++ b/sentry-rails/examples/rails-6.0/config/initializers/sentry.rb @@ -4,7 +4,9 @@ config.traces_sample_rate = 1.0 # set a float between 0.0 and 1.0 to enable performance monitoring config.dsn = 'https://2fb45f003d054a7ea47feb45898f7649@o447951.ingest.sentry.io/5434472' config.release = `git branch --show-current` - config.async = lambda do |event, hint| - Sentry::SendEventJob.perform_later(event, hint) - end + # you can use the pre-defined job for the async callback + # + # config.async = lambda do |event, hint| + # Sentry::SendEventJob.perform_later(event, hint) + # end end diff --git a/sentry-ruby/CHANGELOG.md b/sentry-ruby/CHANGELOG.md index 09f6f41ec..6c8a75c3d 100644 --- a/sentry-ruby/CHANGELOG.md +++ b/sentry-ruby/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Refactor interface construction [#1296](https://github.com/getsentry/sentry-ruby/pull/1296) + ## 4.2.2 - Add thread_id to Exception interface [#1291](https://github.com/getsentry/sentry-ruby/pull/1291) diff --git a/sentry-ruby/lib/sentry/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index 7ada6e687..0e833ffaa 100644 --- a/sentry-ruby/lib/sentry/configuration.rb +++ b/sentry-ruby/lib/sentry/configuration.rb @@ -4,6 +4,7 @@ require "sentry/dsn" require "sentry/transport/configuration" require "sentry/linecache" +require "sentry/interfaces/stacktrace_builder" module Sentry class Configuration @@ -195,6 +196,7 @@ def initialize @transport = Transport::Configuration.new @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map) + run_post_initialization_callbacks end @@ -290,6 +292,16 @@ def tracing_enabled? !!((@traces_sample_rate && @traces_sample_rate > 0.0) || @traces_sampler) end + def stacktrace_builder + @stacktrace_builder ||= StacktraceBuilder.new( + project_root: @project_root.to_s, + app_dirs_pattern: @app_dirs_pattern, + linecache: @linecache, + context_lines: @context_lines, + backtrace_cleanup_callback: @backtrace_cleanup_callback + ) + end + private def detect_release diff --git a/sentry-ruby/lib/sentry/event.rb b/sentry-ruby/lib/sentry/event.rb index f05038496..20cf6a4a3 100644 --- a/sentry-ruby/lib/sentry/event.rb +++ b/sentry-ruby/lib/sentry/event.rb @@ -20,7 +20,7 @@ class Event MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8 attr_accessor(*ATTRIBUTES) - attr_reader :configuration, :request, :exception, :stacktrace, :threads + attr_reader :configuration, :request, :exception, :threads def initialize(configuration:, integration_meta: nil, message: nil) # this needs to go first because some setters rely on configuration @@ -99,7 +99,6 @@ def type def to_hash data = serialize_attributes data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs - data[:stacktrace] = stacktrace.to_hash if stacktrace data[:request] = request.to_hash if request data[:exception] = exception.to_hash if exception data[:threads] = threads.to_hash if threads @@ -112,48 +111,23 @@ def to_json_compatible end def add_request_interface(env) - @request = Sentry::RequestInterface.from_rack(env) + @request = Sentry::RequestInterface.build(env: env) end def add_threads_interface(backtrace: nil, **options) - @threads = ThreadsInterface.new(**options) - @threads.stacktrace = initialize_stacktrace_interface(backtrace) if backtrace + @threads = ThreadsInterface.build( + backtrace: backtrace, + stacktrace_builder: configuration.stacktrace_builder, + **options + ) end - def add_exception_interface(exc) - if exc.respond_to?(:sentry_context) - @extra.merge!(exc.sentry_context) - end - - @exception = Sentry::ExceptionInterface.new.tap do |exc_int| - exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exc).reverse - backtraces = Set.new - exc_int.values = exceptions.map do |e| - SingleExceptionInterface.new.tap do |int| - int.type = e.class.to_s - int.value = e.message.byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES) - int.module = e.class.to_s.split('::')[0...-1].join('::') - int.thread_id = Thread.current.object_id - - int.stacktrace = - if e.backtrace && !backtraces.include?(e.backtrace.object_id) - backtraces << e.backtrace.object_id - initialize_stacktrace_interface(e.backtrace) - end - end - end + def add_exception_interface(exception) + if exception.respond_to?(:sentry_context) + @extra.merge!(exception.sentry_context) end - end - def initialize_stacktrace_interface(backtrace) - StacktraceInterface.new( - backtrace: backtrace, - project_root: configuration.project_root.to_s, - app_dirs_pattern: configuration.app_dirs_pattern, - linecache: configuration.linecache, - context_lines: configuration.context_lines, - backtrace_cleanup_callback: configuration.backtrace_cleanup_callback - ) + @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: configuration.stacktrace_builder) end private diff --git a/sentry-ruby/lib/sentry/interfaces/exception.rb b/sentry-ruby/lib/sentry/interfaces/exception.rb index c498f10df..057a5e32b 100644 --- a/sentry-ruby/lib/sentry/interfaces/exception.rb +++ b/sentry-ruby/lib/sentry/interfaces/exception.rb @@ -1,11 +1,29 @@ module Sentry class ExceptionInterface < Interface - attr_accessor :values + def initialize(values:) + @values = values + end def to_hash data = super data[:values] = data[:values].map(&:to_hash) if data[:values] data end + + def self.build(exception:, stacktrace_builder:) + exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exception).reverse + processed_backtrace_ids = Set.new + + exceptions = exceptions.map do |e| + if e.backtrace && !processed_backtrace_ids.include?(e.backtrace.object_id) + processed_backtrace_ids << e.backtrace.object_id + SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder) + else + SingleExceptionInterface.new(exception: exception) + end + end + + new(values: exceptions) + end end end diff --git a/sentry-ruby/lib/sentry/interfaces/request.rb b/sentry-ruby/lib/sentry/interfaces/request.rb index fd412d4f6..d3f94baa3 100644 --- a/sentry-ruby/lib/sentry/interfaces/request.rb +++ b/sentry-ruby/lib/sentry/interfaces/request.rb @@ -17,10 +17,10 @@ class RequestInterface < Interface attr_accessor :url, :method, :data, :query_string, :cookies, :headers, :env - def self.from_rack(env) + def self.build(env:) env = clean_env(env) - req = ::Rack::Request.new(env) - self.new(req) + request = ::Rack::Request.new(env) + self.new(request: request) end def self.clean_env(env) @@ -34,17 +34,17 @@ def self.clean_env(env) env end - def initialize(req) - env = req.env + def initialize(request:) + env = request.env if Sentry.configuration.send_default_pii - self.data = read_data_from(req) - self.cookies = req.cookies + self.data = read_data_from(request) + self.cookies = request.cookies end - self.url = req.scheme && req.url.split('?').first - self.method = req.request_method - self.query_string = req.query_string + self.url = request.scheme && request.url.split('?').first + self.method = request.request_method + self.query_string = request.query_string self.headers = filter_and_format_headers(env) self.env = filter_and_format_env(env) diff --git a/sentry-ruby/lib/sentry/interfaces/single_exception.rb b/sentry-ruby/lib/sentry/interfaces/single_exception.rb index 6a2c796d2..4ff68dd17 100644 --- a/sentry-ruby/lib/sentry/interfaces/single_exception.rb +++ b/sentry-ruby/lib/sentry/interfaces/single_exception.rb @@ -1,15 +1,26 @@ module Sentry class SingleExceptionInterface < Interface - attr_accessor :type - attr_accessor :value - attr_accessor :module - attr_accessor :thread_id - attr_accessor :stacktrace + attr_reader :type, :value, :module, :thread_id, :stacktrace + + def initialize(exception:, stacktrace: nil) + @type = exception.class.to_s + @value = exception.message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES) + @module = exception.class.to_s.split('::')[0...-1].join('::') + @thread_id = Thread.current.object_id + @stacktrace = stacktrace + end def to_hash data = super data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace] data end + + # patch this method if you want to change an exception's stacktrace frames + # also see `StacktraceBuilder.build`. + def self.build_with_stacktrace(exception:, stacktrace_builder:) + stacktrace = stacktrace_builder.build(backtrace: exception.backtrace) + new(exception: exception, stacktrace: stacktrace) + end end end diff --git a/sentry-ruby/lib/sentry/interfaces/stacktrace.rb b/sentry-ruby/lib/sentry/interfaces/stacktrace.rb index d4983a358..480f47847 100644 --- a/sentry-ruby/lib/sentry/interfaces/stacktrace.rb +++ b/sentry-ruby/lib/sentry/interfaces/stacktrace.rb @@ -2,18 +2,8 @@ module Sentry class StacktraceInterface attr_reader :frames - def initialize(backtrace:, project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil) - @project_root = project_root - @frames = [] - - parsed_backtrace_lines = Backtrace.parse( - backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback - ).lines - - parsed_backtrace_lines.reverse.each_with_object(@frames) do |line, frames| - frame = convert_parsed_line_into_frame(line, project_root, linecache, context_lines) - frames << frame if frame.filename - end + def initialize(frames:) + @frames = frames end def to_hash @@ -22,30 +12,24 @@ def to_hash private - def convert_parsed_line_into_frame(line, project_root, linecache, context_lines) - frame = StacktraceInterface::Frame.new(@project_root, line) - frame.set_context(linecache, context_lines) if context_lines - frame - end - # Not actually an interface, but I want to use the same style class Frame < Interface - attr_accessor :abs_path, :context_line, :function, :in_app, - :lineno, :module, :pre_context, :post_context, :vars + attr_accessor :abs_path, :context_line, :function, :in_app, :filename, + :lineno, :module, :pre_context, :post_context, :vars def initialize(project_root, line) @project_root = project_root - @abs_path = line.file if line.file + @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 end - def filename + def compute_filename return if abs_path.nil? - return @filename if instance_variable_defined?(:@filename) prefix = if under_project_root? && in_app @@ -56,19 +40,18 @@ def filename longest_load_path end - @filename = prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path + prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path end def set_context(linecache, context_lines) return unless abs_path - self.pre_context, self.context_line, self.post_context = \ + @pre_context, @context_line, @post_context = \ linecache.get_file_context(abs_path, lineno, context_lines) end def to_hash(*args) data = super(*args) - data[:filename] = filename data.delete(:vars) unless vars && !vars.empty? data.delete(:pre_context) unless pre_context && !pre_context.empty? data.delete(:post_context) unless post_context && !post_context.empty? diff --git a/sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb b/sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb new file mode 100644 index 000000000..fe0b63664 --- /dev/null +++ b/sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb @@ -0,0 +1,50 @@ +module Sentry + class StacktraceBuilder + attr_reader :project_root, :app_dirs_pattern, :linecache, :context_lines, :backtrace_cleanup_callback + + def initialize(project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil) + @project_root = project_root + @app_dirs_pattern = app_dirs_pattern + @linecache = linecache + @context_lines = context_lines + @backtrace_cleanup_callback = backtrace_cleanup_callback + end + + # you can pass a block to customize/exclude frames: + # + # ```ruby + # builder.build(backtrace) do |frame| + # if frame.module.match?(/a_gem/) + # nil + # else + # frame + # end + # end + # ``` + def build(backtrace:, &frame_callback) + parsed_lines = parse_backtrace_lines(backtrace).select(&:file) + + frames = parsed_lines.reverse.map do |line| + frame = convert_parsed_line_into_frame(line) + frame = frame_callback.call(frame) if frame_callback + frame + end.compact + + StacktraceInterface.new(frames: frames) + end + + private + + def convert_parsed_line_into_frame(line) + frame = StacktraceInterface::Frame.new(project_root, line) + frame.set_context(linecache, context_lines) if context_lines + frame + end + + def parse_backtrace_lines(backtrace) + Backtrace.parse( + backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback + ).lines + end + end +end diff --git a/sentry-ruby/lib/sentry/interfaces/threads.rb b/sentry-ruby/lib/sentry/interfaces/threads.rb index 09297a2ef..8c5ee5f69 100644 --- a/sentry-ruby/lib/sentry/interfaces/threads.rb +++ b/sentry-ruby/lib/sentry/interfaces/threads.rb @@ -1,12 +1,11 @@ module Sentry class ThreadsInterface - attr_accessor :stacktrace - - def initialize(crashed: false) + def initialize(crashed: false, stacktrace: nil) @id = Thread.current.object_id @name = Thread.current.name @current = true @crashed = crashed + @stacktrace = stacktrace end def to_hash @@ -22,5 +21,12 @@ def to_hash ] } end + + # patch this method if you want to change a threads interface's stacktrace frames + # also see `StacktraceBuilder.build`. + def self.build(backtrace:, stacktrace_builder:, **options) + stacktrace = stacktrace_builder.build(backtrace: backtrace) if backtrace + new(**options, stacktrace: stacktrace) + end end end diff --git a/sentry-ruby/spec/sentry/event_spec.rb b/sentry-ruby/spec/sentry/event_spec.rb index 919b2f50e..db2bd8b1a 100644 --- a/sentry-ruby/spec/sentry/event_spec.rb +++ b/sentry-ruby/spec/sentry/event_spec.rb @@ -136,52 +136,6 @@ end end - describe "#initialize_stacktrace_interface" do - let(:fixture_root) { File.join(Dir.pwd, "spec", "support") } - let(:fixture_file) { File.join(fixture_root, "stacktrace_test_fixture.rb") } - let(:configuration) do - Sentry::Configuration.new.tap do |config| - config.project_root = fixture_root - end - end - - let(:backtrace) do - [ - "#{fixture_file}:6:in `bar'", - "#{fixture_file}:2:in `foo'" - ] - end - - subject do - described_class.new(configuration: configuration) - end - - it "returns an array of StacktraceInterface::Frames with correct information" do - interface = subject.initialize_stacktrace_interface(backtrace) - expect(interface).to be_a(Sentry::StacktraceInterface) - - frames = interface.frames - - first_frame = frames.first - - expect(first_frame.filename).to match(/stacktrace_test_fixture.rb/) - expect(first_frame.function).to eq("foo") - expect(first_frame.lineno).to eq(2) - expect(first_frame.pre_context).to eq([nil, nil, "def foo\n"]) - expect(first_frame.context_line).to eq(" bar\n") - expect(first_frame.post_context).to eq(["end\n", "\n", "def bar\n"]) - - second_frame = frames.last - - expect(second_frame.filename).to match(/stacktrace_test_fixture.rb/) - expect(second_frame.function).to eq("bar") - expect(second_frame.lineno).to eq(6) - expect(second_frame.pre_context).to eq(["end\n", "\n", "def bar\n"]) - expect(second_frame.context_line).to eq(" baz\n") - expect(second_frame.post_context).to eq(["end\n", nil, nil]) - end - end - describe '#to_json_compatible' do subject do Sentry::Event.new(configuration: configuration).tap do |event| diff --git a/sentry-ruby/spec/sentry/interfaces/request_interface_spec.rb b/sentry-ruby/spec/sentry/interfaces/request_interface_spec.rb index a1992c03b..cce836ebb 100644 --- a/sentry-ruby/spec/sentry/interfaces/request_interface_spec.rb +++ b/sentry-ruby/spec/sentry/interfaces/request_interface_spec.rb @@ -6,7 +6,7 @@ let(:exception) { ZeroDivisionError.new("divided by 0") } let(:additional_headers) { {} } let(:env) { Rack::MockRequest.env_for("/test", additional_headers) } - let(:interface) { described_class.from_rack(env) } + let(:interface) { described_class.build(env: env) } before do Sentry.init do |config| @@ -18,7 +18,7 @@ it 'excludes non whitelisted params from rack env' do additional_env = { "random_param" => "text", "query_string" => "test" } new_env = env.merge(additional_env) - interface = described_class.from_rack(new_env) + interface = described_class.build(env: new_env) expect(interface.env).to_not include(additional_env) end @@ -27,14 +27,14 @@ Sentry.configuration.rack_env_whitelist = %w(random_param query_string) additional_env = { "random_param" => "text", "query_string" => "test" } new_env = env.merge(additional_env) - interface = described_class.from_rack(new_env) + interface = described_class.build(env: new_env) expect(interface.env).to eq(additional_env) end it 'keeps the original env intact when an empty whitelist is provided' do Sentry.configuration.rack_env_whitelist = [] - interface = described_class.from_rack(env) + interface = described_class.build(env: env) expect(interface.env).to eq(env) end @@ -44,7 +44,7 @@ let(:additional_headers) { { "HTTP_VERSION" => "HTTP/1.1", "HTTP_COOKIE" => "test", "HTTP_X_REQUEST_ID" => "12345678" } } it 'transforms headers to conform with the interface' do - interface = described_class.from_rack(env) + interface = described_class.build(env: env) expect(interface.headers).to eq("Content-Length" => "0", "Version" => "HTTP/1.1", "X-Request-Id" => "12345678") end @@ -53,7 +53,7 @@ let(:additional_headers) { { "action_dispatch.request_id" => "12345678" } } it 'transforms headers to conform with the interface' do - interface = described_class.from_rack(env) + interface = described_class.build(env: env) expect(interface.headers).to eq("Content-Length" => "0", "X-Request-Id" => "12345678") end @@ -66,7 +66,7 @@ it 'does not call #to_s for unnecessary env variables' do expect(mock).not_to receive(:to_s) - interface = described_class.from_rack(env) + interface = described_class.build(env: env) end end end @@ -76,7 +76,7 @@ ::Rack::RACK_REQUEST_COOKIE_HASH => "cookies!" ) - interface = described_class.from_rack(new_env) + interface = described_class.build(env: new_env) expect(interface.cookies).to eq(nil) expect(interface.env["COOKIE"]).to eq(nil) @@ -88,7 +88,7 @@ "HTTP_COOKIE" => "cookies!" ) - interface = described_class.from_rack(new_env) + interface = described_class.build(env: new_env) expect(interface.headers["Cookie"]).to eq(nil) end @@ -103,7 +103,7 @@ "CONTENT_TYPE" => "text/html" ) - interface = described_class.from_rack(new_env) + interface = described_class.build(env: new_env) expect(interface.headers["Content-Length"]).to eq("10") expect(interface.headers["Content-Type"]).to eq("text/html") @@ -112,14 +112,14 @@ it 'does not ignore version headers which do not match SERVER_PROTOCOL' do new_env = env.merge("SERVER_PROTOCOL" => "HTTP/1.1", "HTTP_VERSION" => "HTTP/2.0") - interface = described_class.from_rack(new_env) + interface = described_class.build(env: new_env) expect(interface.headers["Version"]).to eq("HTTP/2.0") end it 'retains any literal "HTTP-" in the actual header name' do new_env = env.merge("HTTP_HTTP_CUSTOM_HTTP_HEADER" => "test") - interface = described_class.from_rack(new_env) + interface = described_class.build(env: new_env) expect(interface.headers).to include("Http-Custom-Http-Header" => "test") end @@ -133,7 +133,7 @@ def to_s new_env = env.merge("HTTP_FOO" => "BAR", "rails_object" => obj) - expect { interface = described_class.from_rack(new_env) }.to_not raise_error + expect { interface = described_class.build(env: new_env) }.to_not raise_error end end @@ -144,7 +144,7 @@ def to_s ::Rack::RACK_INPUT => StringIO.new("data=ignore me") ) - interface = described_class.from_rack(new_env) + interface = described_class.build(env: new_env) expect(interface.data).to eq(nil) end @@ -154,7 +154,7 @@ def to_s it "doesn't store request body by default" do new_env = env.merge(::Rack::RACK_INPUT => StringIO.new("ignore me")) - interface = described_class.from_rack(new_env) + interface = described_class.build(env: new_env) expect(interface.data).to eq(nil) end @@ -170,7 +170,7 @@ def to_s ::Rack::RACK_REQUEST_COOKIE_HASH => "cookies!" ) - interface = described_class.from_rack(new_env) + interface = described_class.build(env: new_env) expect(interface.cookies).to eq("cookies!") end @@ -181,7 +181,7 @@ def to_s ::Rack::RACK_INPUT => StringIO.new("data=catch me") ) - interface = described_class.from_rack(new_env) + interface = described_class.build(env: new_env) expect(interface.data).to eq({ "data" => "catch me" }) end @@ -189,7 +189,7 @@ def to_s it "stores request body" do new_env = env.merge(::Rack::RACK_INPUT => StringIO.new("catch me")) - interface = described_class.from_rack(new_env) + interface = described_class.build(env: new_env) expect(interface.data).to eq("catch me") end @@ -204,7 +204,7 @@ def to_s "HTTP_X_FORWARDED_FOR" => ip ) - interface = described_class.from_rack(env) + interface = described_class.build(env: env) expect(interface.env).to include("REMOTE_ADDR") expect(interface.headers.keys).to include("Client-Ip") diff --git a/sentry-ruby/spec/sentry/interfaces/stacktrace_builder_spec.rb b/sentry-ruby/spec/sentry/interfaces/stacktrace_builder_spec.rb new file mode 100644 index 000000000..d1cc5257a --- /dev/null +++ b/sentry-ruby/spec/sentry/interfaces/stacktrace_builder_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +RSpec.describe Sentry::StacktraceBuilder do + describe "#build" do + let(:fixture_root) { File.join(Dir.pwd, "spec", "support") } + let(:fixture_file) { File.join(fixture_root, "stacktrace_test_fixture.rb") } + let(:configuration) do + Sentry::Configuration.new.tap do |config| + config.project_root = fixture_root + end + end + + let(:backtrace) do + [ + "#{fixture_file}:6:in `bar'", + "#{fixture_file}:2:in `foo'" + ] + end + + subject do + configuration.stacktrace_builder + end + + it "ignores frames without filename" do + interface = subject.build(backtrace: [":6:in `foo'"]) + expect(interface.frames).to be_empty + end + + it "returns an array of StacktraceInterface::Frames with correct information" do + interface = subject.build(backtrace: backtrace) + expect(interface).to be_a(Sentry::StacktraceInterface) + + frames = interface.frames + + first_frame = frames.first + + expect(first_frame.filename).to match(/stacktrace_test_fixture.rb/) + expect(first_frame.function).to eq("foo") + expect(first_frame.lineno).to eq(2) + expect(first_frame.pre_context).to eq([nil, nil, "def foo\n"]) + expect(first_frame.context_line).to eq(" bar\n") + expect(first_frame.post_context).to eq(["end\n", "\n", "def bar\n"]) + + second_frame = frames.last + + expect(second_frame.filename).to match(/stacktrace_test_fixture.rb/) + expect(second_frame.function).to eq("bar") + expect(second_frame.lineno).to eq(6) + expect(second_frame.pre_context).to eq(["end\n", "\n", "def bar\n"]) + expect(second_frame.context_line).to eq(" baz\n") + expect(second_frame.post_context).to eq(["end\n", nil, nil]) + end + + context "with block argument" do + it "removes the frame if it's evaluated as nil" do + interface = subject.build(backtrace: backtrace) do |frame| + nil + end + + expect(interface.frames).to be_empty + end + it "yields frame to the block" do + interface = subject.build(backtrace: backtrace) do |frame| + frame.vars = { foo: "bar" } + frame + end + + frames = interface.frames + + first_frame = frames.first + + expect(first_frame.filename).to match(/stacktrace_test_fixture.rb/) + expect(first_frame.function).to eq("foo") + expect(first_frame.vars).to eq({ foo: "bar" }) + + second_frame = frames.last + + expect(second_frame.filename).to match(/stacktrace_test_fixture.rb/) + expect(second_frame.function).to eq("bar") + expect(second_frame.lineno).to eq(6) + expect(second_frame.vars).to eq({ foo: "bar" }) + end + end + end +end