diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index b8b1bc8f57..bff2982d35 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -451,7 +451,10 @@ def _patch_django_asgi_handler() -> None: def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "WSGIRequest" + scope: "sentry_sdk.Scope", + transaction_style: str, + request: "WSGIRequest", + route_path: "Optional[str]", ) -> None: try: transaction_name = None @@ -459,13 +462,8 @@ def _set_transaction_name_and_source( fn = resolve(request.path).func transaction_name = transaction_from_function(getattr(fn, "view_class", fn)) - elif transaction_style == "url": - if hasattr(request, "urlconf"): - transaction_name = LEGACY_RESOLVER.resolve( - request.path_info, urlconf=request.urlconf - ) - else: - transaction_name = LEGACY_RESOLVER.resolve(request.path_info) + elif transaction_style == "url" and route_path is not None: + transaction_name = route_path if transaction_name is None: transaction_name = request.path_info @@ -477,6 +475,11 @@ def _set_transaction_name_and_source( transaction_name, source=source, ) + + if transaction_style == "url" and route_path is not None: + server_span = scope._server_segment_span + if server_span is not None: + server_span.set_attribute(SPANDATA.HTTP_ROUTE, route_path) except Resolver404: urlconf = import_module(settings.ROOT_URLCONF) # This exception only gets thrown when transaction_style is `function_name` @@ -501,8 +504,29 @@ def _before_get_response(request: "WSGIRequest") -> None: _patch_drf() scope = sentry_sdk.get_current_scope() + + route_path = None + if hasattr(request, "urlconf"): + try: + route_path = LEGACY_RESOLVER.resolve( + request.path_info, urlconf=request.urlconf + ) + except Exception: + pass + else: + try: + route_path = LEGACY_RESOLVER.resolve(request.path_info) + except Exception: + pass + + server_span = sentry_sdk.get_current_scope()._server_segment_span + if server_span is not None and route_path is not None: + server_span.set_attribute(SPANDATA.HTTP_ROUTE, route_path) + # Rely on WSGI middleware to start a trace - _set_transaction_name_and_source(scope, integration.transaction_style, request) + _set_transaction_name_and_source( + scope, integration.transaction_style, request, route_path=route_path + ) scope.add_event_processor( _make_wsgi_request_event_processor(weakref.ref(request), integration) @@ -520,7 +544,27 @@ def _attempt_resolve_again( if not hasattr(request, "urlconf"): return - _set_transaction_name_and_source(scope, transaction_style, request) + route_path = None + if hasattr(request, "urlconf"): + try: + route_path = LEGACY_RESOLVER.resolve( + request.path_info, urlconf=request.urlconf + ) + except Exception: + pass + else: + try: + route_path = LEGACY_RESOLVER.resolve(request.path_info) + except Exception: + pass + + server_span = sentry_sdk.get_current_scope()._server_segment_span + if server_span is not None and route_path is not None: + server_span.set_attribute(SPANDATA.HTTP_ROUTE, route_path) + + _set_transaction_name_and_source( + scope, transaction_style, request, route_path=route_path + ) def _get_user_from_request_and_set_on_scope(request: "WSGIRequest") -> None: diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index c389b39889..5556dc7638 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -197,6 +197,33 @@ async def test_async_views( } +@pytest.mark.parametrize("application", APPS) +@pytest.mark.asyncio +@pytest.mark.skipif( + django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1" +) +async def test_http_route( + sentry_init, + capture_items, + application, +): + sentry_init( + integrations=[DjangoIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + ) + + items = capture_items("span") + + comm = HttpCommunicator(application, "GET", "/async_message") + await comm.get_response() + await comm.wait() + + sentry_sdk.flush() + (segment,) = (item.payload for item in items if item.payload.get("is_segment")) + assert segment["attributes"][SPANDATA.HTTP_ROUTE] == "/async_message" + + @pytest.mark.parametrize("application", APPS) @pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) @pytest.mark.parametrize("middleware_spans", [False, True]) diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index e94231beef..cc61635dec 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -1148,6 +1148,26 @@ def test_transaction_style_tracing_disabled( assert event["transaction"] == expected_transaction +def test_http_route( + sentry_init, + client, + capture_items, +): + sentry_init( + integrations=[DjangoIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + ) + + items = capture_items("span") + + unpack_werkzeug_response(client.get("/message")) + + sentry_sdk.flush() + (segment,) = (item.payload for item in items if item.payload.get("is_segment")) + assert segment["attributes"][SPANDATA.HTTP_ROUTE] == "/message" + + @pytest.mark.parametrize("span_streaming", [True, False]) def test_request_body( sentry_init,