diff --git a/CHANGELOG.md b/CHANGELOG.md index b5c9b9c86..f80fa03c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - Add `Mechanism` interface and default to unhandled for integration exceptions [#2280](https://github.com/getsentry/sentry-ruby/pull/2280) +- Use `mechanism` to track crashed sessions for unhandled exceptions [#2281](https://github.com/getsentry/sentry-ruby/pull/2281) ### Bug Fixes diff --git a/sentry-ruby/lib/sentry/hub.rb b/sentry-ruby/lib/sentry/hub.rb index 74e82167d..90cbbdaa9 100644 --- a/sentry-ruby/lib/sentry/hub.rb +++ b/sentry-ruby/lib/sentry/hub.rb @@ -134,7 +134,7 @@ def capture_exception(exception, **options, &block) return unless event - current_scope.session&.update_from_exception(event.exception) + current_scope.session&.update_from_error_event(event) capture_event(event, **options, &block).tap do # mark the exception as captured so we can use this information to avoid duplicated capturing diff --git a/sentry-ruby/lib/sentry/session.rb b/sentry-ruby/lib/sentry/session.rb index 52646c93b..4cc72880f 100644 --- a/sentry-ruby/lib/sentry/session.rb +++ b/sentry-ruby/lib/sentry/session.rb @@ -4,9 +4,8 @@ module Sentry class Session attr_reader :started, :status, :aggregation_key - # TODO-neel add :crashed after adding handled mechanism - STATUSES = %i[ok errored exited] - AGGREGATE_STATUSES = %i[errored exited] + STATUSES = %i[ok errored crashed exited] + AGGREGATE_STATUSES = %i[errored crashed exited] def initialize @started = Sentry.utc_now @@ -17,9 +16,11 @@ def initialize @aggregation_key = Time.utc(@started.year, @started.month, @started.day, @started.hour, @started.min) end - # TODO-neel add :crashed after adding handled mechanism - def update_from_exception(_exception = nil) - @status = :errored + def update_from_error_event(event) + return unless event.is_a?(ErrorEvent) && event.exception + + crashed = event.exception.values.any? { |e| e.mechanism.handled == false } + @status = crashed ? :crashed : :errored end def close diff --git a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb index c5751bba0..fb1a2f2db 100644 --- a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb +++ b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb @@ -640,8 +640,16 @@ def will_be_sampled_by_sdk case req.path_info when /success/ [200, {}, ['ok']] - when /error/ + when /crash/ 1 / 0 + when /error/ + begin + 1 / 0 + rescue => e + Sentry.capture_exception(e) + end + + [200, {}, ['error']] end end @@ -658,12 +666,17 @@ def will_be_sampled_by_sdk stack.call(env) end + 3.times do + env = Rack::MockRequest.env_for('/crash') + expect { stack.call(env) }.to raise_error(ZeroDivisionError) + end + 2.times do env = Rack::MockRequest.env_for('/error') - expect { stack.call(env) }.to raise_error(ZeroDivisionError) + stack.call(env) end - expect(sentry_events.count).to eq(2) + expect(sentry_events.count).to eq(5) Sentry.session_flusher.flush @@ -674,7 +687,7 @@ def will_be_sampled_by_sdk item = envelope.items.first expect(item.type).to eq('sessions') expect(item.payload[:attrs]).to eq({ release: 'test-release', environment: 'test' }) - expect(item.payload[:aggregates].first).to eq({ exited: 10, errored: 2, started: now_bucket.iso8601 }) + expect(item.payload[:aggregates].first).to eq({ exited: 10, errored: 2, crashed: 3, started: now_bucket.iso8601 }) end end end diff --git a/sentry-ruby/spec/sentry/session_flusher_spec.rb b/sentry-ruby/spec/sentry/session_flusher_spec.rb index b2bf91f82..05f033fdd 100644 --- a/sentry-ruby/spec/sentry/session_flusher_spec.rb +++ b/sentry-ruby/spec/sentry/session_flusher_spec.rb @@ -5,6 +5,7 @@ let(:configuration) do Sentry::Configuration.new.tap do |config| + config.dsn = Sentry::TestHelper::DUMMY_DSN config.release = 'test-release' config.environment = 'test' config.transport.transport_class = Sentry::DummyTransport @@ -50,6 +51,18 @@ Time.utc(time.year, time.month, time.day, time.hour, time.min) end + let(:handled_event) do + exception = Exception.new('test') + mech = Sentry::Mechanism.new(type: 'custom', handled: true) + client.event_from_exception(exception, {}, mech) + end + + let(:unhandled_event) do + exception = Exception.new('test') + mech = Sentry::Mechanism.new(type: 'custom', handled: false) + client.event_from_exception(exception, {}, mech) + end + before do Timecop.freeze(now) do 10.times do @@ -60,7 +73,14 @@ 5.times do session = Sentry::Session.new - session.update_from_exception + session.update_from_error_event(handled_event) + session.close + subject.add_session(session) + end + + 2.times do + session = Sentry::Session.new + session.update_from_error_event(unhandled_event) session.close subject.add_session(session) end @@ -77,7 +97,12 @@ item = envelope.items.first expect(item.type).to eq('sessions') expect(item.payload[:attrs]).to eq({ release: 'test-release', environment: 'test' }) - expect(item.payload[:aggregates].first).to eq({ exited: 10, errored: 5, started: now.iso8601 }) + expect(item.payload[:aggregates].first).to eq({ + started: now.iso8601, + exited: 10, + errored: 5, + crashed: 2 + }) end end end @@ -124,7 +149,7 @@ subject.add_session(session) pending_aggregates = subject.instance_variable_get(:@pending_aggregates) expect(pending_aggregates.keys.first).to be_a(Time) - expect(pending_aggregates.values.first).to include({ errored: 0, exited: 1 }) + expect(pending_aggregates.values.first).to include({ errored: 0, crashed: 0, exited: 1 }) end context "when thread creation fails" do