diff --git a/mypy.ini b/mypy.ini index a16903768b..1b5abb4ff7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -48,3 +48,5 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-asgiref.*] ignore_missing_imports = True +[mypy-executing.*] +ignore_missing_imports = True diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 3c14a314c5..dfdde1ce80 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -157,7 +157,7 @@ def process_django_templates(event, hint): for i in reversed(range(len(frames))): f = frames[i] if ( - f.get("function") in ("parse", "render") + f.get("function") in ("Parser.parse", "parse", "render") and f.get("module") == "django.template.base" ): i += 1 diff --git a/sentry_sdk/integrations/executing.py b/sentry_sdk/integrations/executing.py new file mode 100644 index 0000000000..4fbf729bb1 --- /dev/null +++ b/sentry_sdk/integrations/executing.py @@ -0,0 +1,68 @@ +from __future__ import absolute_import + +from sentry_sdk import Hub +from sentry_sdk._types import MYPY +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import walk_exception_chain, iter_stacks + +if MYPY: + from typing import Optional + + from sentry_sdk._types import Event, Hint + +try: + import executing +except ImportError: + raise DidNotEnable("executing is not installed") + + +class ExecutingIntegration(Integration): + identifier = "executing" + + @staticmethod + def setup_once(): + # type: () -> None + + @add_global_event_processor + def add_executing_info(event, hint): + # type: (Event, Optional[Hint]) -> Optional[Event] + if Hub.current.get_integration(ExecutingIntegration) is None: + return event + + if hint is None: + return event + + exc_info = hint.get("exc_info", None) + + if exc_info is None: + return event + + exception = event.get("exception", None) + + if exception is None: + return event + + values = exception.get("values", None) + + if values is None: + return event + + for exception, (_exc_type, _exc_value, exc_tb) in zip( + reversed(values), walk_exception_chain(exc_info) + ): + sentry_frames = [ + frame + for frame in exception.get("stacktrace", {}).get("frames", []) + if frame.get("function") + ] + tbs = list(iter_stacks(exc_tb)) + if len(sentry_frames) != len(tbs): + continue + + for sentry_frame, tb in zip(sentry_frames, tbs): + frame = tb.tb_frame + source = executing.Source.for_frame(frame) + sentry_frame["function"] = source.code_qualname(frame.f_code) + + return event diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 548796399c..105fbaf8fa 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -28,6 +28,7 @@ from sentry_sdk._types import ExcInfo, EndpointType + epoch = datetime(1970, 1, 1) diff --git a/test-requirements.txt b/test-requirements.txt index be051169ad..5a2e527154 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,3 +7,4 @@ pytest-cov==2.8.1 gevent eventlet newrelic +executing diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index 3c26b426f5..9830d2ae5f 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -9,6 +9,7 @@ from django.core.management import execute_from_command_line from django.db.utils import OperationalError, ProgrammingError, DataError +from sentry_sdk.integrations.executing import ExecutingIntegration try: from django.urls import reverse @@ -408,8 +409,11 @@ def test_read_request(sentry_init, client, capture_events): assert "data" not in event["request"] -def test_template_exception(sentry_init, client, capture_events): - sentry_init(integrations=[DjangoIntegration()]) +@pytest.mark.parametrize("with_executing_integration", [[], [ExecutingIntegration()]]) +def test_template_exception( + sentry_init, client, capture_events, with_executing_integration +): + sentry_init(integrations=[DjangoIntegration()] + with_executing_integration) events = capture_events() content, status, headers = client.get(reverse("template_exc")) @@ -437,11 +441,19 @@ def test_template_exception(sentry_init, client, capture_events): filenames = [ (f.get("function"), f.get("module")) for f in exception["stacktrace"]["frames"] ] - assert filenames[-3:] == [ - (u"parse", u"django.template.base"), - (None, None), - (u"invalid_block_tag", u"django.template.base"), - ] + + if with_executing_integration: + assert filenames[-3:] == [ + (u"Parser.parse", u"django.template.base"), + (None, None), + (u"Parser.invalid_block_tag", u"django.template.base"), + ] + else: + assert filenames[-3:] == [ + (u"parse", u"django.template.base"), + (None, None), + (u"invalid_block_tag", u"django.template.base"), + ] @pytest.mark.parametrize( diff --git a/tests/test_client.py b/tests/test_client.py index a1c6b90a24..d9a13157e4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -15,6 +15,7 @@ capture_exception, capture_event, ) +from sentry_sdk.integrations.executing import ExecutingIntegration from sentry_sdk.transport import Transport from sentry_sdk._compat import reraise, text_type, PY2 from sentry_sdk.utils import HAS_CHAINED_EXCEPTIONS @@ -216,6 +217,35 @@ def test_with_locals_disabled(sentry_init, capture_events): ) +@pytest.mark.parametrize("integrations", [[], [ExecutingIntegration()]]) +def test_function_names(sentry_init, capture_events, integrations): + sentry_init(integrations=integrations) + events = capture_events() + + def foo(): + try: + bar() + except Exception: + capture_exception() + + def bar(): + 1 / 0 + + foo() + + (event,) = events + (thread,) = event["exception"]["values"] + functions = [x["function"] for x in thread["stacktrace"]["frames"]] + + if integrations: + assert functions == [ + "test_function_names..foo", + "test_function_names..bar", + ] + else: + assert functions == ["foo", "bar"] + + def test_attach_stacktrace_enabled(sentry_init, capture_events): sentry_init(attach_stacktrace=True) events = capture_events() @@ -231,6 +261,7 @@ def bar(): (event,) = events (thread,) = event["threads"]["values"] functions = [x["function"] for x in thread["stacktrace"]["frames"]] + assert functions[-2:] == ["foo", "bar"]