From c2d5065289d065bac9191cbf224283e51e52049a Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 14 Sep 2018 11:29:41 +0200 Subject: [PATCH] feat: Add mechanism data to event --- sentry_sdk/integrations/_wsgi.py | 33 ++++++++++++++---------- sentry_sdk/integrations/celery.py | 15 ++++++++--- sentry_sdk/integrations/django.py | 14 +++++++--- sentry_sdk/integrations/excepthook.py | 12 ++++++--- sentry_sdk/integrations/flask.py | 13 +++++++--- sentry_sdk/integrations/logging.py | 17 ++++++------ sentry_sdk/utils.py | 27 +++++++++++++++---- tests/integrations/celery/test_celery.py | 4 ++- tests/integrations/conftest.py | 20 ++++++-------- tests/integrations/django/test_basic.py | 6 ++++- tests/integrations/flask/test_flask.py | 14 ++++++++-- 11 files changed, 121 insertions(+), 54 deletions(-) diff --git a/sentry_sdk/integrations/_wsgi.py b/sentry_sdk/integrations/_wsgi.py index c95955f86c..2cad37e0eb 100644 --- a/sentry_sdk/integrations/_wsgi.py +++ b/sentry_sdk/integrations/_wsgi.py @@ -1,9 +1,12 @@ import json import sys -from sentry_sdk import capture_exception from sentry_sdk.hub import Hub, _should_send_default_pii, _get_client_options -from sentry_sdk.utils import AnnotatedValue, capture_internal_exceptions +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + event_from_exception, +) from sentry_sdk._compat import reraise, implements_iterator @@ -155,14 +158,24 @@ def run_wsgi_app(app, environ, start_response): try: rv = app(environ, start_response) except Exception: - einfo = sys.exc_info() - capture_exception(einfo) + einfo = _capture_exception(hub) hub.pop_scope_unsafe() reraise(*einfo) return _ScopePoppingResponse(hub, rv) +def _capture_exception(hub): + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + with_locals=hub.client.options["with_locals"], + mechanism={"type": "wsgi", "handled": False}, + ) + hub.capture_event(event, hint=hint) + return exc_info + + @implements_iterator class _ScopePoppingResponse(object): __slots__ = ("_response", "_hub") @@ -175,9 +188,7 @@ def __iter__(self): try: self._response = iter(self._response) except Exception: - einfo = sys.exc_info() - capture_exception(einfo) - reraise(*einfo) + reraise(*_capture_exception(self.hub)) return self def __next__(self): @@ -186,9 +197,7 @@ def __next__(self): except StopIteration: raise except Exception: - einfo = sys.exc_info() - capture_exception(einfo) - reraise(*einfo) + reraise(*_capture_exception(self.hub)) def close(self): self._hub.pop_scope_unsafe() @@ -196,9 +205,7 @@ def close(self): try: self._response.close() except Exception: - einfo = sys.exc_info() - capture_exception(einfo) - reraise(*einfo) + reraise(*_capture_exception(self.hub)) def _make_wsgi_event_processor(environ): diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index fca387823a..8f22ea7190 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -4,7 +4,7 @@ from celery.exceptions import SoftTimeLimitExceeded from sentry_sdk import Hub -from sentry_sdk.utils import capture_internal_exceptions +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception from sentry_sdk.integrations import Integration @@ -33,9 +33,18 @@ def _process_failure_signal(self, sender, task_id, einfo, **kw): getattr(sender, "name", sender), ] - hub.capture_exception(einfo.exc_info) + self._capture_event(hub, einfo.exc_info) else: - hub.capture_exception(einfo.exc_info) + self._capture_event(hub, einfo.exc_info) + + def _capture_event(self, hub, exc_info): + event, hint = event_from_exception( + exc_info, + with_locals=hub.client.options["with_locals"], + mechanism={"type": "celery", "handled": False}, + ) + + hub.capture_event(event, hint=hint) def _handle_task_prerun(self, sender, task, **kw): with capture_internal_exceptions(): diff --git a/sentry_sdk/integrations/django.py b/sentry_sdk/integrations/django.py index e3099b5550..5c4e5ab01c 100644 --- a/sentry_sdk/integrations/django.py +++ b/sentry_sdk/integrations/django.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import sys import weakref from django import VERSION as DJANGO_VERSION @@ -10,9 +11,9 @@ except ImportError: from django.core.urlresolvers import resolve -from sentry_sdk import capture_exception, configure_scope +from sentry_sdk import Hub, configure_scope from sentry_sdk.hub import _should_send_default_pii -from sentry_sdk.utils import capture_internal_exceptions +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception from sentry_sdk.integrations import Integration from sentry_sdk.integrations._wsgi import RequestExtractor, run_wsgi_app @@ -90,7 +91,14 @@ def event_processor(event, hint): def _got_request_exception(request=None, **kwargs): - capture_exception() + hub = Hub.current + event, hint = event_from_exception( + sys.exc_info(), + with_locals=hub.client.options["with_locals"], + mechanism={"type": "django", "handled": False}, + ) + + hub.capture_event(event, hint=hint) class DjangoRequestExtractor(RequestExtractor): diff --git a/sentry_sdk/integrations/excepthook.py b/sentry_sdk/integrations/excepthook.py index d3c27e2aeb..07b694c23e 100644 --- a/sentry_sdk/integrations/excepthook.py +++ b/sentry_sdk/integrations/excepthook.py @@ -1,7 +1,7 @@ import sys -from sentry_sdk import capture_exception -from sentry_sdk.utils import capture_internal_exceptions +from sentry_sdk import Hub +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception from sentry_sdk.integrations import Integration @@ -20,7 +20,13 @@ def install(self): def _make_excepthook(old_excepthook): def sentry_sdk_excepthook(exctype, value, traceback): with capture_internal_exceptions(): - capture_exception((exctype, value, traceback)) + hub = Hub.current + event, hint = event_from_exception( + (exctype, value, traceback), + with_locals=hub.client.options["with_locals"], + mechanism={"type": "excepthook", "handled": False}, + ) + hub.capture_event(event, hint=hint) return old_excepthook(exctype, value, traceback) diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index e0de727206..9726736634 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -2,9 +2,9 @@ import weakref -from sentry_sdk import Hub, capture_exception, configure_scope +from sentry_sdk import Hub, configure_scope from sentry_sdk.hub import _should_send_default_pii -from sentry_sdk.utils import capture_internal_exceptions +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception from sentry_sdk.integrations import Integration from sentry_sdk.integrations._wsgi import RequestExtractor, run_wsgi_app @@ -105,7 +105,14 @@ def size_of_file(self, file): def _capture_exception(sender, exception, **kwargs): - capture_exception(exception) + hub = Hub.current + event, hint = event_from_exception( + exception, + with_locals=hub.client.options["with_locals"], + mechanism={"type": "flask", "handled": False}, + ) + + hub.capture_event(event, hint=hint) def _make_request_event_processor(app, weak_request): diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index b7e1e0dda5..303ed7d148 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -4,7 +4,6 @@ import logging import datetime -from sentry_sdk import capture_event, add_breadcrumb from sentry_sdk.utils import ( to_string, event_from_exception, @@ -72,17 +71,19 @@ def _emit(self, record): if not self.can_record(record): return - if self._should_create_event(record): - hub = Hub.current - if hub.client is None: - return + hub = Hub.current + if hub.client is None: + return + if self._should_create_event(record): with capture_internal_exceptions(): hint = None # exc_info might be None or (None, None, None) if record.exc_info is not None and record.exc_info[0] is not None: event, hint = event_from_exception( - record.exc_info, with_locals=hub.client.options["with_locals"] + record.exc_info, + with_locals=hub.client.options["with_locals"], + mechanism={"type": "logging", "handled": True}, ) else: event = {} @@ -94,10 +95,10 @@ def _emit(self, record): "params": record.args, } - capture_event(event, hint=hint) + hub.capture_event(event, hint=hint) with capture_internal_exceptions(): - add_breadcrumb( + hub.add_breadcrumb( self._breadcrumb_from_record(record), hint={"log_record": record} ) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index f0af25dcde..d5bd10addd 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -322,21 +322,36 @@ def stacktrace_from_traceback(tb, with_locals=True): return {"frames": [frame_from_traceback(tb, with_locals) for tb in iter_stacks(tb)]} -def single_exception_from_error_tuple(exc_type, exc_value, tb, with_locals=True): +def get_errno(exc_value): + return getattr(exc_value, "errno", None) + + +def single_exception_from_error_tuple( + exc_type, exc_value, tb, with_locals=True, mechanism=None +): + errno = get_errno(exc_value) + if errno is not None: + mechanism = mechanism or {} + mechanism_meta = mechanism.setdefault("meta", {}) + mechanism_meta.setdefault("errno", {"code": errno}) + return { "module": get_type_module(exc_type), "type": get_type_name(exc_type), "value": safe_str(exc_value), "stacktrace": stacktrace_from_traceback(tb, with_locals), + "mechanism": mechanism, } -def exceptions_from_error_tuple(exc_info, with_locals=True): +def exceptions_from_error_tuple(exc_info, with_locals=True, mechanism=None): exc_type, exc_value, tb = exc_info rv = [] while exc_type is not None: rv.append( - single_exception_from_error_tuple(exc_type, exc_value, tb, with_locals) + single_exception_from_error_tuple( + exc_type, exc_value, tb, with_locals, mechanism + ) ) cause = getattr(exc_value, "__cause__", None) if cause is None: @@ -414,13 +429,15 @@ def exc_info_from_error(error): return exc_type, exc_value, tb -def event_from_exception(exc_info, with_locals=False, processors=None): +def event_from_exception(exc_info, with_locals=False, processors=None, mechanism=None): exc_info = exc_info_from_error(exc_info) hint = event_hint_with_exc_info(exc_info) return ( { "level": "error", - "exception": {"values": exceptions_from_error_tuple(exc_info, with_locals)}, + "exception": { + "values": exceptions_from_error_tuple(exc_info, with_locals, mechanism) + }, }, hint, ) diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index 01c92853cc..b6a3bfe593 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -16,7 +16,6 @@ def celery(sentry_init): def test_simple(capture_events, celery): - events = capture_events() @celery.task(name="dummy_task") @@ -31,6 +30,9 @@ def dummy_task(x, y): exception, = event["exception"]["values"] assert exception["type"] == "ZeroDivisionError" + event, = events + assert event["exception"]["values"][0]["mechanism"]["type"] == "celery" + def test_ignore_expected(capture_events, celery): events = capture_events() diff --git a/tests/integrations/conftest.py b/tests/integrations/conftest.py index 418701e186..6a4b5a69fd 100644 --- a/tests/integrations/conftest.py +++ b/tests/integrations/conftest.py @@ -1,4 +1,3 @@ -import sys import pytest import sentry_sdk @@ -7,19 +6,16 @@ def capture_exceptions(monkeypatch): def inner(): errors = set() - old_capture_exception = sentry_sdk.Hub.current.capture_exception + old_capture_event = sentry_sdk.Hub.current.capture_event - def capture_exception(error=None): - given_error = error - error = error or sys.exc_info()[1] - if isinstance(error, tuple): - error = error[1] - errors.add(error) - return old_capture_exception(given_error) + def capture_event(event, hint=None): + if hint: + if "exc_info" in hint: + error = hint["exc_info"][1] + errors.add(error) + return old_capture_event(event, hint=hint) - monkeypatch.setattr( - sentry_sdk.Hub.current, "capture_exception", capture_exception - ) + monkeypatch.setattr(sentry_sdk.Hub.current, "capture_event", capture_event) return errors return inner diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index d78b50137f..d018a8631f 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -23,13 +23,17 @@ def client(monkeypatch_test_transport): return Client(application) -def test_view_exceptions(client, capture_exceptions): +def test_view_exceptions(client, capture_exceptions, capture_events): exceptions = capture_exceptions() + events = capture_events() client.get(reverse("view_exc")) error, = exceptions assert isinstance(error, ZeroDivisionError) + event, = events + assert event["exception"]["values"][0]["mechanism"]["type"] == "django" + def test_middleware_exceptions(client, capture_exceptions): exceptions = capture_exceptions() diff --git a/tests/integrations/flask/test_flask.py b/tests/integrations/flask/test_flask.py index fbcaddc70c..561258dada 100644 --- a/tests/integrations/flask/test_flask.py +++ b/tests/integrations/flask/test_flask.py @@ -49,7 +49,7 @@ def test_has_context(sentry_init, app, capture_events): @pytest.mark.parametrize("debug", (True, False)) @pytest.mark.parametrize("testing", (True, False)) -def test_errors(sentry_init, capture_exceptions, app, debug, testing): +def test_errors(sentry_init, capture_exceptions, capture_events, app, debug, testing): sentry_init(integrations=[flask_sentry.FlaskIntegration()]) app.debug = debug @@ -60,6 +60,7 @@ def index(): 1 / 0 exceptions = capture_exceptions() + events = capture_events() client = app.test_client() try: @@ -70,6 +71,9 @@ def index(): exc, = exceptions assert isinstance(exc, ZeroDivisionError) + event, = events + assert event["exception"]["values"][0]["mechanism"]["type"] == "flask" + def test_flask_login_not_installed(sentry_init, app, capture_events, monkeypatch): sentry_init(integrations=[flask_sentry.FlaskIntegration()]) @@ -343,7 +347,9 @@ def foo(): ) -def test_wsgi_level_error_is_caught(app, capture_exceptions, sentry_init): +def test_wsgi_level_error_is_caught( + app, capture_exceptions, capture_events, sentry_init +): sentry_init(integrations=[flask_sentry.FlaskIntegration()]) def wsgi_app(environ, start_response): @@ -354,6 +360,7 @@ def wsgi_app(environ, start_response): client = app.test_client() exceptions = capture_exceptions() + events = capture_events() with pytest.raises(ZeroDivisionError) as exc: client.get("/") @@ -362,6 +369,9 @@ def wsgi_app(environ, start_response): assert error is exc.value + event, = events + assert event["exception"]["values"][0]["mechanism"]["type"] == "wsgi" + def test_500(sentry_init, capture_events, app): sentry_init(integrations=[flask_sentry.FlaskIntegration()])