From 6f56578c0f96cd73ce005514ce57a6bf1a6f3834 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 8 Sep 2026 14:26:58 +0200 Subject: [PATCH 1/7] feat(django): Add http.route attribute for WSGI handlers --- sentry_sdk/integrations/django/__init__.py | 46 +++++++++++++++++----- tests/integrations/django/test_basic.py | 20 ++++++++++ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index b8b1bc8f57..37b0c7a972 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 @@ -460,12 +463,7 @@ def _set_transaction_name_and_source( 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) + 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,21 @@ def _before_get_response(request: "WSGIRequest") -> None: _patch_drf() scope = sentry_sdk.get_current_scope() + + route_path = None + if hasattr(request, "urlconf"): + route_path = LEGACY_RESOLVER.resolve(request.path_info, urlconf=request.urlconf) + else: + route_path = LEGACY_RESOLVER.resolve(request.path_info) + + 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 +536,19 @@ 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"): + route_path = LEGACY_RESOLVER.resolve(request.path_info, urlconf=request.urlconf) + else: + route_path = LEGACY_RESOLVER.resolve(request.path_info) + + 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/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, From c7f18325fc9426bd71291367d0f7bc05076a0501 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 8 Sep 2026 14:32:25 +0200 Subject: [PATCH 2/7] add asgi test --- tests/integrations/django/asgi/test_asgi.py | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index c389b39889..8e5ddd78ca 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", "/projects/1") + 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] == "/projects/{project_id}" + + @pytest.mark.parametrize("application", APPS) @pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) @pytest.mark.parametrize("middleware_spans", [False, True]) From 485b779c1598028c804a03c4aa6c310bf8de4a3a Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 8 Sep 2026 14:34:00 +0200 Subject: [PATCH 3/7] add try...except --- sentry_sdk/integrations/django/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 37b0c7a972..ad0486da0c 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -538,9 +538,17 @@ def _attempt_resolve_again( route_path = None if hasattr(request, "urlconf"): - route_path = LEGACY_RESOLVER.resolve(request.path_info, urlconf=request.urlconf) + try: + route_path = LEGACY_RESOLVER.resolve( + request.path_info, urlconf=request.urlconf + ) + except Exception: + pass else: - route_path = LEGACY_RESOLVER.resolve(request.path_info) + 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: From fccaca77e685f18d9ac560448cc7dd751a9e7c95 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 8 Sep 2026 14:41:37 +0200 Subject: [PATCH 4/7] add missing try...except --- sentry_sdk/integrations/django/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index ad0486da0c..53a5caf5ec 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -507,9 +507,17 @@ def _before_get_response(request: "WSGIRequest") -> None: route_path = None if hasattr(request, "urlconf"): - route_path = LEGACY_RESOLVER.resolve(request.path_info, urlconf=request.urlconf) + try: + route_path = LEGACY_RESOLVER.resolve( + request.path_info, urlconf=request.urlconf + ) + except Exception: + pass else: - route_path = LEGACY_RESOLVER.resolve(request.path_info) + 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: From 4b1ae32c7bad3f9b197749c27fe0ef9f79def306 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 8 Sep 2026 14:43:08 +0200 Subject: [PATCH 5/7] only set transaction name if there is no Exception --- sentry_sdk/integrations/django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 53a5caf5ec..bff2982d35 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -462,7 +462,7 @@ 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": + elif transaction_style == "url" and route_path is not None: transaction_name = route_path if transaction_name is None: From 2a08da29dcdf9fcb2b647a5483b7afe05b21d577 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 8 Sep 2026 14:44:01 +0200 Subject: [PATCH 6/7] use real route --- tests/integrations/django/asgi/test_asgi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 8e5ddd78ca..38f479ff87 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -215,13 +215,13 @@ async def test_http_route( items = capture_items("span") - comm = HttpCommunicator(application, "GET", "/projects/1") + 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] == "/projects/{project_id}" + assert segment["attributes"][SPANDATA.HTTP_ROUTE] == "async_message" @pytest.mark.parametrize("application", APPS) From 952da11bc597b7f0931d4459457cc80d61fe89fb Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 8 Sep 2026 14:49:35 +0200 Subject: [PATCH 7/7] fix test url --- tests/integrations/django/asgi/test_asgi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 38f479ff87..5556dc7638 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -215,13 +215,13 @@ async def test_http_route( items = capture_items("span") - comm = HttpCommunicator(application, "GET", "async_message") + 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" + assert segment["attributes"][SPANDATA.HTTP_ROUTE] == "/async_message" @pytest.mark.parametrize("application", APPS)