Skip to content

Commit

Permalink
Add mechanism interface and default to handled false in integrations
Browse files Browse the repository at this point in the history
  • Loading branch information
sl0thentr0py committed Mar 26, 2024
1 parent ffffce9 commit 776afb7
Show file tree
Hide file tree
Showing 12 changed files with 55 additions and 15 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,9 @@
## Unreleased

### Features

- Add `Mechanism` interface and default to unhandled for integration exceptions [#2280](https://github.com/getsentry/sentry-ruby/pull/2280)

### Bug Fixes

- Don't instantiate connection in `ActiveRecordSubscriber` ([#2278](https://github.com/getsentry/sentry-ruby/pull/2278))
Expand Down
4 changes: 2 additions & 2 deletions sentry-ruby/lib/sentry/client.rb
Expand Up @@ -81,7 +81,7 @@ def capture_event(event, scope, hint = {})
# @param exception [Exception] the exception to be reported.
# @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors.
# @return [Event, nil]
def event_from_exception(exception, hint = {})
def event_from_exception(exception, hint = {}, mechanism = nil)
return unless @configuration.sending_allowed?

ignore_exclusions = hint.delete(:ignore_exclusions) { false }
Expand All @@ -90,7 +90,7 @@ def event_from_exception(exception, hint = {})
integration_meta = Sentry.integrations[hint[:integration]]

ErrorEvent.new(configuration: configuration, integration_meta: integration_meta).tap do |event|
event.add_exception_interface(exception)
event.add_exception_interface(exception, mechanism: mechanism)
event.add_threads_interface(crashed: true)
event.level = :error
end
Expand Down
4 changes: 2 additions & 2 deletions sentry-ruby/lib/sentry/error_event.rb
Expand Up @@ -27,12 +27,12 @@ def add_threads_interface(backtrace: nil, **options)
end

# @!visibility private
def add_exception_interface(exception)
def add_exception_interface(exception, mechanism: nil)
if exception.respond_to?(:sentry_context)
@extra.merge!(exception.sentry_context)
end

@exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder)
@exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder, mechanism: mechanism)
end
end
end
3 changes: 2 additions & 1 deletion sentry-ruby/lib/sentry/hub.rb
Expand Up @@ -128,8 +128,9 @@ def capture_exception(exception, **options, &block)

options[:hint] ||= {}
options[:hint][:exception] = exception
mechanism = options.delete(:mechanism)

event = current_client.event_from_exception(exception, options[:hint])
event = current_client.event_from_exception(exception, options[:hint], mechanism)

return unless event

Expand Down
4 changes: 4 additions & 0 deletions sentry-ruby/lib/sentry/integrable.rb
Expand Up @@ -14,6 +14,10 @@ def integration_name
def capture_exception(exception, **options, &block)
options[:hint] ||= {}
options[:hint][:integration] = integration_name

# within an integration, we usually intercept uncaught exceptions so we set handled to false.
options[:mechanism] ||= Sentry::Mechanism.new(type: integration_name, handled: false)

Sentry.capture_exception(exception, **options, &block)
end

Expand Down
1 change: 1 addition & 0 deletions sentry-ruby/lib/sentry/interface.rb
Expand Up @@ -14,3 +14,4 @@ def to_hash
require "sentry/interfaces/single_exception"
require "sentry/interfaces/stacktrace"
require "sentry/interfaces/threads"
require "sentry/interfaces/mechanism"
7 changes: 4 additions & 3 deletions sentry-ruby/lib/sentry/interfaces/exception.rb
Expand Up @@ -24,17 +24,18 @@ def to_hash
# @param stacktrace_builder [StacktraceBuilder]
# @see SingleExceptionInterface#build_with_stacktrace
# @see SingleExceptionInterface#initialize
# @param mechanism [Mechanism]
# @return [ExceptionInterface]
def self.build(exception:, stacktrace_builder:)
def self.build(exception:, stacktrace_builder:, mechanism: nil)
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)
SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder, mechanism: mechanism)
else
SingleExceptionInterface.new(exception: exception)
SingleExceptionInterface.new(exception: exception, mechanism: mechanism)
end
end

Expand Down
20 changes: 20 additions & 0 deletions sentry-ruby/lib/sentry/interfaces/mechanism.rb
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Sentry
class Mechanism < Interface
# Generic identifier, mostly the source integration for this exception.
# @return [String]
attr_accessor :type

# A manually captured exception has handled set to true,
# false if coming from an integration where we intercept an uncaught exception.
# Defaults to true here and will be set to false explicitly in integrations.
# @return [Boolean]
attr_accessor :handled

def initialize(type: 'generic', handled: true)
@type = type
@handled = handled
end
end
end
10 changes: 6 additions & 4 deletions sentry-ruby/lib/sentry/interfaces/single_exception.rb
Expand Up @@ -11,10 +11,10 @@ class SingleExceptionInterface < Interface
OMISSION_MARK = "...".freeze
MAX_LOCAL_BYTES = 1024

attr_reader :type, :module, :thread_id, :stacktrace
attr_reader :type, :module, :thread_id, :stacktrace, :mechanism
attr_accessor :value

def initialize(exception:, stacktrace: nil)
def initialize(exception:, stacktrace: nil, mechanism: nil)
@type = exception.class.to_s
exception_message =
if exception.respond_to?(:detailed_message)
Expand All @@ -29,17 +29,19 @@ def initialize(exception:, stacktrace: nil)
@module = exception.class.to_s.split('::')[0...-1].join('::')
@thread_id = Thread.current.object_id
@stacktrace = stacktrace
@mechanism = mechanism || Mechanism.new
end

def to_hash
data = super
data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace]
data[:mechanism] = data[:mechanism].to_hash if data[:mechanism]
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:)
def self.build_with_stacktrace(exception:, stacktrace_builder:, mechanism: nil)
stacktrace = stacktrace_builder.build(backtrace: exception.backtrace)

if locals = exception.instance_variable_get(:@sentry_locals)
Expand All @@ -61,7 +63,7 @@ def self.build_with_stacktrace(exception:, stacktrace_builder:)
stacktrace.frames.last.vars = locals
end

new(exception: exception, stacktrace: stacktrace)
new(exception: exception, stacktrace: stacktrace, mechanism: mechanism)
end
end
end
7 changes: 6 additions & 1 deletion sentry-ruby/lib/sentry/rack/capture_exceptions.rb
Expand Up @@ -4,6 +4,7 @@ module Sentry
module Rack
class CaptureExceptions
ERROR_EVENT_ID_KEY = "sentry.error_event_id"
MECHANISM_TYPE = "rack"

def initialize(app)
@app = app
Expand Down Expand Up @@ -56,7 +57,7 @@ def transaction_op
end

def capture_exception(exception, env)
Sentry.capture_exception(exception).tap do |event|
Sentry.capture_exception(exception, mechanism: mechanism).tap do |event|
env[ERROR_EVENT_ID_KEY] = event.event_id if event
end
end
Expand All @@ -74,6 +75,10 @@ def finish_transaction(transaction, status_code)
transaction.set_http_status(status_code)
transaction.finish
end

def mechanism
Sentry::Mechanism.new(type: MECHANISM_TYPE, handled: false)
end
end
end
end
4 changes: 3 additions & 1 deletion sentry-ruby/lib/sentry/rake.rb
Expand Up @@ -8,7 +8,9 @@ module Rake
module Application
# @api private
def display_error_message(ex)
Sentry.capture_exception(ex) do |scope|
mechanism = Sentry::Mechanism.new(type: 'rake', handled: false)

Check warning on line 11 in sentry-ruby/lib/sentry/rake.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/rake.rb#L11

Added line #L11 was not covered by tests

Sentry.capture_exception(ex, mechanism: mechanism) do |scope|
task_name = top_level_tasks.join(' ')
scope.set_transaction_name(task_name, source: :task)
scope.set_tag("rake_task", task_name)
Expand Down
2 changes: 1 addition & 1 deletion sentry-ruby/spec/sentry/client_spec.rb
Expand Up @@ -395,7 +395,7 @@ module ExcTag; end
context "when exclusions overridden with :ignore_exclusions" do
it 'returns Sentry::ErrorEvent' do
config.excluded_exceptions << Sentry::Test::BaseExc
expect(subject.event_from_exception(Sentry::Test::BaseExc.new, ignore_exclusions: true)).to be_a(Sentry::ErrorEvent)
expect(subject.event_from_exception(Sentry::Test::BaseExc.new, { ignore_exclusions: true })).to be_a(Sentry::ErrorEvent)
end
end
end
Expand Down

0 comments on commit 776afb7

Please sign in to comment.