summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Cramer <dcramer@gmail.com>2016-07-25 10:13:33 -0700
committerGitHub <noreply@github.com>2016-07-25 10:13:33 -0700
commit2369418a50de098f074cbe5ccffab0227eb77b2d (patch)
tree6c23ff9271e986cdab002d258587127eb8a68e5a
parent574418413804cb4a203fce3e986d1cadba637d7d (diff)
parent63e77b9f077c1119a25ced32ec6710e0a70965b0 (diff)
downloadraven-2369418a50de098f074cbe5ccffab0227eb77b2d.tar.gz
Merge pull request #811 from cjerdonek/issue-550-exception-chains
Issue 550 exception chains
-rw-r--r--raven/events.py90
-rw-r--r--tests/events/tests.py69
2 files changed, 129 insertions, 30 deletions
diff --git a/raven/events.py b/raven/events.py
index 5398f83..67402be 100644
--- a/raven/events.py
+++ b/raven/events.py
@@ -32,6 +32,35 @@ class BaseEvent(object):
return self.client.transform(value)
+# The __suppress_context__ attribute was added in Python 3.3.
+# See PEP 415 for details:
+# https://www.python.org/dev/peps/pep-0415/
+if hasattr(Exception, '__suppress_context__'):
+ def _chained_exceptions(exc_info):
+ """
+ Return a generator iterator over an exception's chain.
+
+ The exceptions are yielded from outermost to innermost (i.e. last to
+ first when viewing a stack trace).
+ """
+ yield exc_info
+ exc_type, exc, exc_traceback = exc_info
+
+ while True:
+ if exc.__suppress_context__:
+ # Then __cause__ should be used instead.
+ exc = exc.__cause__
+ else:
+ exc = exc.__context__
+ if exc is None:
+ break
+ yield type(exc), exc, exc.__traceback__
+else:
+ # Then we do not support reporting exception chains.
+ def _chained_exceptions(exc_info):
+ yield exc_info
+
+
class Exception(BaseEvent):
"""
Exceptions store the following metadata:
@@ -49,6 +78,28 @@ class Exception(BaseEvent):
return '%s: %s' % (exc['type'], exc['value'])
return exc['type']
+ def _get_value(self, exc_type, exc_value, exc_traceback):
+ """
+ Convert exception info to a value for the values list.
+ """
+ stack_info = get_stack_info(
+ iter_traceback_frames(exc_traceback),
+ transformer=self.transform,
+ capture_locals=self.client.capture_locals,
+ )
+
+ exc_module = getattr(exc_type, '__module__', None)
+ if exc_module:
+ exc_module = str(exc_module)
+ exc_type = getattr(exc_type, '__name__', '<unknown>')
+
+ return {
+ 'value': to_unicode(exc_value),
+ 'type': str(exc_type),
+ 'module': to_unicode(exc_module),
+ 'stacktrace': stack_info,
+ }
+
def capture(self, exc_info=None, **kwargs):
if not exc_info or exc_info is True:
exc_info = sys.exc_info()
@@ -56,36 +107,15 @@ class Exception(BaseEvent):
if not exc_info:
raise ValueError('No exception found')
- exc_type, exc_value, exc_traceback = exc_info
-
- try:
- stack_info = get_stack_info(
- iter_traceback_frames(exc_traceback),
- transformer=self.transform,
- capture_locals=self.client.capture_locals,
- )
-
- exc_module = getattr(exc_type, '__module__', None)
- if exc_module:
- exc_module = str(exc_module)
- exc_type = getattr(exc_type, '__name__', '<unknown>')
-
- return {
- 'level': kwargs.get('level', logging.ERROR),
- self.name: {
- 'values': [{
- 'value': to_unicode(exc_value),
- 'type': str(exc_type),
- 'module': to_unicode(exc_module),
- 'stacktrace': stack_info,
- }],
- },
- }
- finally:
- try:
- del exc_type, exc_value, exc_traceback
- except Exception as e:
- self.logger.exception(e)
+ values = []
+ for exc_info in _chained_exceptions(exc_info):
+ value = self._get_value(*exc_info)
+ values.append(value)
+
+ return {
+ 'level': kwargs.get('level', logging.ERROR),
+ self.name: {'values': values},
+ }
class Message(BaseEvent):
diff --git a/tests/events/tests.py b/tests/events/tests.py
new file mode 100644
index 0000000..a75e326
--- /dev/null
+++ b/tests/events/tests.py
@@ -0,0 +1,69 @@
+import six
+
+from raven.base import Client
+from raven.events import Exception as ExceptionEvent
+from raven.utils.testutils import TestCase
+
+
+class ExceptionTest(TestCase):
+
+ # Handle compatibility.
+ if hasattr(Exception, '__suppress_context__'):
+ # Then exception chains are supported.
+ def transform_expected(self, expected):
+ return expected
+ else:
+ # Otherwise, we only report the first element.
+ def transform_expected(self, expected):
+ return expected[:1]
+
+ def check_capture(self, expected):
+ """
+ Check the return value of capture().
+
+ Args:
+ expected: the expected "type" values.
+ """
+ c = Client()
+ event = ExceptionEvent(c)
+ result = event.capture()
+ info = result['exception']
+ values = info['values']
+
+ type_names = [value['type'] for value in values]
+ expected = self.transform_expected(expected)
+
+ self.assertEqual(type_names, expected)
+
+ def test_simple(self):
+ try:
+ raise ValueError()
+ except Exception:
+ self.check_capture(['ValueError'])
+
+ def test_nested(self):
+ try:
+ raise ValueError()
+ except Exception:
+ try:
+ raise KeyError()
+ except Exception:
+ self.check_capture(['KeyError', 'ValueError'])
+
+ def test_raise_from(self):
+ try:
+ raise ValueError()
+ except Exception as exc:
+ try:
+ six.raise_from(KeyError(), exc)
+ except Exception:
+ self.check_capture(['KeyError', 'ValueError'])
+
+ def test_raise_from_different(self):
+ try:
+ raise ValueError()
+ except Exception as exc:
+ try:
+ six.raise_from(KeyError(), TypeError())
+ except Exception:
+ self.check_capture(['KeyError', 'TypeError'])