Skip to content

Commit 284e394

Browse files
effronHazAT
authored andcommitted
Add configuration option to inspect nested exceptions for exclusion (#872)
* add configuration option to inspect nested exceptions for exclusion, implement checking nested exceptions for exclusion * make specs compatible with ruby versions that do not support `Exception#cause`
1 parent 1b3553f commit 284e394

File tree

7 files changed

+145
-20
lines changed

7 files changed

+145
-20
lines changed

lib/raven/base.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
require 'raven/transports/http'
2424
require 'raven/utils/deep_merge'
2525
require 'raven/utils/real_ip'
26+
require 'raven/utils/exception_cause_chain'
2627
require 'raven/instance'
2728

2829
require 'forwardable'

lib/raven/configuration.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
require 'uri'
22

33
module Raven
4-
class Configuration
4+
class Configuration # rubocop:disable Metrics/ClassLength
55
# Directories to be recognized as part of your app. e.g. if you
66
# have an `engines` dir at the root of your project, you may want
77
# to set this to something like /(app|config|engines|lib)/
@@ -31,6 +31,10 @@ class Configuration
3131
# You should probably append to this rather than overwrite it.
3232
attr_accessor :excluded_exceptions
3333

34+
# Boolean to check nested exceptions when deciding if to exclude. Defaults to false
35+
attr_accessor :inspect_exception_causes_for_exclusion
36+
alias inspect_exception_causes_for_exclusion? inspect_exception_causes_for_exclusion
37+
3438
# DSN component - set automatically if DSN provided
3539
attr_accessor :host
3640

@@ -205,6 +209,7 @@ def initialize
205209
self.environments = []
206210
self.exclude_loggers = []
207211
self.excluded_exceptions = IGNORE_DEFAULT.dup
212+
self.inspect_exception_causes_for_exclusion = false
208213
self.linecache = ::Raven::LineCache.new
209214
self.logger = ::Raven::Logger.new(STDOUT)
210215
self.open_timeout = 1
@@ -349,14 +354,24 @@ def detect_release
349354
logger.error "Error detecting release: #{ex.message}"
350355
end
351356

352-
def excluded_exception?(exc)
353-
excluded_exceptions.any? { |x| get_exception_class(x) === exc }
357+
def excluded_exception?(incoming_exception)
358+
excluded_exceptions.any? do |excluded_exception|
359+
matches_exception?(get_exception_class(excluded_exception), incoming_exception)
360+
end
354361
end
355362

356363
def get_exception_class(x)
357364
x.is_a?(Module) ? x : qualified_const_get(x)
358365
end
359366

367+
def matches_exception?(excluded_exception_class, incoming_exception)
368+
if inspect_exception_causes_for_exclusion?
369+
Raven::Utils::ExceptionCauseChain.exception_to_array(incoming_exception).any? { |cause| excluded_exception_class === cause }
370+
else
371+
excluded_exception_class === incoming_exception
372+
end
373+
end
374+
360375
# In Ruby <2.0 const_get can't lookup "SomeModule::SomeClass" in one go
361376
def qualified_const_get(x)
362377
x = x.to_s

lib/raven/event.rb

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def to_json_compatible
139139

140140
def add_exception_interface(exc)
141141
interface(:exception) do |exc_int|
142-
exceptions = exception_chain_to_array(exc)
142+
exceptions = Raven::Utils::ExceptionCauseChain.exception_to_array(exc).reverse
143143
backtraces = Set.new
144144
exc_int.values = exceptions.map do |e|
145145
SingleExceptionInterface.new do |int|
@@ -237,20 +237,6 @@ def async_json_processors
237237
].map { |v| v.new(self) }
238238
end
239239

240-
def exception_chain_to_array(exc)
241-
if exc.respond_to?(:cause) && exc.cause
242-
exceptions = [exc]
243-
while exc.cause
244-
exc = exc.cause
245-
break if exceptions.any? { |e| e.object_id == exc.object_id }
246-
exceptions << exc
247-
end
248-
exceptions.reverse!
249-
else
250-
[exc]
251-
end
252-
end
253-
254240
def list_gem_specs
255241
# Older versions of Rubygems don't support iterating over all specs
256242
Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module Raven
2+
module Utils
3+
module ExceptionCauseChain
4+
def self.exception_to_array(exception)
5+
if exception.respond_to?(:cause) && exception.cause
6+
exceptions = [exception]
7+
while exception.cause
8+
exception = exception.cause
9+
break if exceptions.any? { |e| e.object_id == exception.object_id }
10+
exceptions << exception
11+
end
12+
exceptions
13+
else
14+
[exception]
15+
end
16+
end
17+
end
18+
end
19+
end

spec/raven/configuration_spec.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,66 @@
185185
expect(subject.capture_allowed?).to eq(true)
186186
end
187187
end
188+
189+
describe '#exception_class_allowed?' do
190+
class MyTestException < RuntimeError; end
191+
192+
context 'with custom excluded_exceptions' do
193+
before do
194+
subject.excluded_exceptions = ['MyTestException']
195+
end
196+
197+
context 'when the raised exception is a Raven::Error' do
198+
let(:incoming_exception) { Raven::Error.new }
199+
it 'returns false' do
200+
expect(subject.exception_class_allowed?(incoming_exception)).to eq false
201+
end
202+
end
203+
204+
context 'when the raised exception is not in excluded_exceptions' do
205+
let(:incoming_exception) { RuntimeError.new }
206+
it 'returns true' do
207+
expect(subject.exception_class_allowed?(incoming_exception)).to eq true
208+
end
209+
end
210+
211+
context 'when the raised exception has a cause that is in excluded_exceptions' do
212+
let(:incoming_exception) { build_exception_with_cause(MyTestException.new) }
213+
context 'when inspect_exception_causes_for_exclusion is false' do
214+
it 'returns true' do
215+
expect(subject.exception_class_allowed?(incoming_exception)).to eq true
216+
end
217+
end
218+
219+
# Only check causes when they're supported by the ruby version
220+
context 'when inspect_exception_causes_for_exclusion is true' do
221+
before do
222+
subject.inspect_exception_causes_for_exclusion = true
223+
end
224+
225+
if Exception.new.respond_to? :cause
226+
context 'when the language version supports exception causes' do
227+
it 'returns false' do
228+
expect(subject.exception_class_allowed?(incoming_exception)).to eq false
229+
end
230+
end
231+
else
232+
context 'when the language version does not support exception causes' do
233+
it 'returns true' do
234+
expect(subject.exception_class_allowed?(incoming_exception)).to eq true
235+
end
236+
end
237+
end
238+
end
239+
end
240+
241+
context 'when the raised exception is in excluded_exceptions' do
242+
let(:incoming_exception) { MyTestException.new }
243+
244+
it 'returns false' do
245+
expect(subject.exception_class_allowed?(incoming_exception)).to eq false
246+
end
247+
end
248+
end
249+
end
188250
end
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
require 'spec_helper'
2+
3+
RSpec.describe Raven::Utils::ExceptionCauseChain do
4+
describe '.exception_to_array' do
5+
# Only check causes when they're supported
6+
if Exception.new.respond_to? :cause
7+
context 'when the ruby version supports exception causes' do
8+
context 'when the exception has a cause' do
9+
let(:exception) { build_exception_with_cause }
10+
11+
it 'captures the cause' do
12+
expect(described_class.exception_to_array(exception).length).to eq(2)
13+
end
14+
end
15+
16+
context 'when the exception has nested causes' do
17+
let(:exception) { build_exception_with_two_causes }
18+
19+
it 'captures nested causes' do
20+
expect(described_class.exception_to_array(exception).length).to eq(3)
21+
end
22+
end
23+
24+
context 'when the exception has a recursive cause' do
25+
let(:exception) { build_exception_with_recursive_cause }
26+
27+
it 'should handle it gracefully' do
28+
expect(described_class.exception_to_array(exception).length).to eq(1)
29+
end
30+
end
31+
end
32+
else
33+
context 'when the ruby version does not support exception causes' do
34+
let(:exception) { build_exception_with_two_causes }
35+
36+
it 'returns the passed in exception' do
37+
expect(described_class.exception_to_array(exception)).to eq [exception]
38+
end
39+
end
40+
end
41+
end
42+
end

spec/spec_helper.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ def build_exception
4242
return exception
4343
end
4444

45-
def build_exception_with_cause
45+
def build_exception_with_cause(cause = "exception a")
4646
begin
47-
raise "exception a"
47+
raise cause
4848
rescue
4949
raise "exception b"
5050
end

0 commit comments

Comments
 (0)