From 255fcee874189e21bd95e75100bf8d51503c0fb2 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 16 Jun 2026 10:03:44 +0200 Subject: [PATCH 01/11] fix(asgi): Stop duplicating root_path in URLs --- sentry_sdk/integrations/_asgi_common.py | 7 +++++- sentry_sdk/integrations/asgi.py | 31 +++++++++++++++++++++---- sentry_sdk/integrations/starlette.py | 6 +++-- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index bb44896a04..8a8f471501 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -32,6 +32,7 @@ def _get_url( asgi_scope: "Dict[str, Any]", default_scheme: "Literal['ws', 'http']", host: "Optional[Union[AnnotatedValue, str]]", + path_includes_root_path: "bool" = True, ) -> str: """ Extract URL from the ASGI scope, without also including the querystring. @@ -39,7 +40,11 @@ def _get_url( scheme = asgi_scope.get("scheme", default_scheme) server = asgi_scope.get("server", None) - path = asgi_scope.get("root_path", "") + asgi_scope.get("path", "") + path = ( + asgi_scope.get("path", "") + if path_includes_root_path + else asgi_scope.get("root_path", "") + asgi_scope.get("path", "") + ) if host: return "%s://%s%s" % (scheme, host, path) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index f0470e33fc..baf9a1afbd 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -105,6 +105,7 @@ class SentryAsgiMiddleware: "mechanism_type", "span_origin", "http_methods_to_capture", + "path_includes_root_path", ) def __init__( @@ -116,6 +117,7 @@ def __init__( span_origin: str = "manual", http_methods_to_capture: "Tuple[str, ...]" = DEFAULT_HTTP_METHODS_TO_CAPTURE, asgi_version: "Optional[int]" = None, + path_includes_root_path: bool = True, ) -> None: """ Instrument an ASGI application with Sentry. Provides HTTP/websocket @@ -152,6 +154,7 @@ def __init__( self.span_origin = span_origin self.app = app self.http_methods_to_capture = http_methods_to_capture + self.path_includes_root_path = path_includes_root_path if asgi_version is None: if _looks_like_asgi3(app): @@ -447,7 +450,12 @@ def _get_transaction_name_and_source( if endpoint: name = transaction_from_function(endpoint) or "" else: - name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None) + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path_includes_root_path=self.path_includes_root_path, + ) source = TransactionSource.URL elif transaction_style == "url": @@ -459,7 +467,12 @@ def _get_transaction_name_and_source( if path is not None: name = path else: - name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None) + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path_includes_root_path=self.path_includes_root_path, + ) source = TransactionSource.URL if name is None: @@ -484,7 +497,12 @@ def _get_segment_name_and_source( if endpoint: name = qualname_from_function(endpoint) or "" else: - name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None) + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path_includes_root_path=self.path_includes_root_path, + ) source = SegmentSource.URL.value elif segment_style == "url": @@ -496,7 +514,12 @@ def _get_segment_name_and_source( if path is not None: name = path else: - name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None) + name = _get_url( + asgi_scope, + "http" if ty == "http" else "ws", + host=None, + path_includes_root_path=self.path_includes_root_path, + ) source = SegmentSource.URL.value if name is None: diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 1482efc25b..526c0abd0f 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -143,7 +143,8 @@ def setup_once() -> None: ) patch_middlewares() - patch_asgi_app() + path_includes_root_path = version >= (0, 33) + patch_asgi_app(path_includes_root_path=path_includes_root_path) patch_request_response() if version >= (0, 24): @@ -427,7 +428,7 @@ def _sentry_middleware_init( Middleware.__init__ = _sentry_middleware_init -def patch_asgi_app() -> None: +def patch_asgi_app(path_includes_root_path: "bool") -> None: """ Instrument Starlette ASGI app using the SentryAsgiMiddleware. """ @@ -451,6 +452,7 @@ async def _sentry_patched_asgi_app( else DEFAULT_HTTP_METHODS_TO_CAPTURE ), asgi_version=3, + path_includes_root_path=path_includes_root_path, ) return await middleware(scope, receive, send) From 89023f4f72482adc79ac75d10774f6349c9bc721 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 19 Jun 2026 10:10:42 +0200 Subject: [PATCH 02/11] starlette and fastapi tests --- sentry_sdk/integrations/_asgi_common.py | 18 ++++++-- sentry_sdk/integrations/asgi.py | 9 +++- sentry_sdk/integrations/starlette.py | 1 + tests/integrations/fastapi/test_fastapi.py | 42 +++++++++++++++++++ .../integrations/starlette/test_starlette.py | 42 +++++++++++++++++++ 5 files changed, 106 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index 8a8f471501..9f0b64216a 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -86,7 +86,9 @@ def _get_ip(asgi_scope: "Any") -> str: return asgi_scope.get("client")[0] -def _get_request_data(asgi_scope: "Any") -> "Dict[str, Any]": +def _get_request_data( + asgi_scope: "Any", path_includes_root_path: "bool" = True +) -> "Dict[str, Any]": """ Returns data related to the HTTP request from the ASGI scope. """ @@ -101,7 +103,10 @@ def _get_request_data(asgi_scope: "Any") -> "Dict[str, Any]": request_data["query_string"] = _get_query(asgi_scope) request_data["url"] = _get_url( - asgi_scope, "http" if ty == "http" else "ws", headers.get("host") + asgi_scope, + "http" if ty == "http" else "ws", + headers.get("host"), + path_includes_root_path=path_includes_root_path, ) client = asgi_scope.get("client") @@ -111,7 +116,9 @@ def _get_request_data(asgi_scope: "Any") -> "Dict[str, Any]": return request_data -def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]": +def _get_request_attributes( + asgi_scope: "Any", path_includes_root_path: "bool" = True +) -> "dict[str, Any]": """ Return attributes related to the HTTP request from the ASGI scope. """ @@ -132,7 +139,10 @@ def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]": attributes["http.query"] = query url_without_query_string = _get_url( - asgi_scope, "http" if ty == "http" else "ws", headers.get("host") + asgi_scope, + "http" if ty == "http" else "ws", + headers.get("host"), + path_includes_root_path=path_includes_root_path, ) query_string = _get_query(asgi_scope) attributes["url.full"] = ( diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index baf9a1afbd..55e024f92a 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -322,7 +322,8 @@ async def _run_app( with span_ctx as span: if isinstance(span, StreamedSpan): for attribute, value in _get_request_attributes( - scope + scope, + path_includes_root_path=self.path_includes_root_path, ).items(): span.set_attribute(attribute, value) @@ -404,7 +405,11 @@ def event_processor( self, event: "Event", hint: "Hint", asgi_scope: "Any" ) -> "Optional[Event]": request_data = event.get("request", {}) - request_data.update(_get_request_data(asgi_scope)) + request_data.update( + _get_request_data( + asgi_scope, path_includes_root_path=self.path_includes_root_path + ) + ) event["request"] = deepcopy(request_data) # Only set transaction name if not already set by Starlette or FastAPI (or other frameworks) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 526c0abd0f..892de37df0 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -143,6 +143,7 @@ def setup_once() -> None: ) patch_middlewares() + # See https://github.com/Kludex/starlette/commit/e8f0dcd54e4ceec47e02c45f5275374e292339ad path_includes_root_path = version >= (0, 33) patch_asgi_app(path_includes_root_path=path_includes_root_path) patch_request_response() diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index dc6bde89c8..67c8bf8008 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -1043,6 +1043,48 @@ def test_transaction_http_method_custom(sentry_init, capture_events): assert event2["request"]["method"] == "HEAD" +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_request_url(sentry_init, capture_events, capture_items, span_streaming): + sentry_init( + traces_sample_rate=1.0, + send_default_pii=True, + integrations=[ + StarletteIntegration(), + ], + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) + + starlette_app = fastapi_app_factory() + + client = TestClient(starlette_app, root_path="/root") + + if span_streaming: + items = capture_items("span") + + client.get("/root/nomessage") + sentry_sdk.flush() + spans = [item.payload for item in items] + + (server_span,) = ( + span + for span in spans + if span["attributes"].get("sentry.op") == "http.server" + ) + assert server_span["attributes"]["url.full"] == ( + "http://testserver/root/nomessage" + ) + else: + events = capture_events() + + client.get("/root/nomessage") + + assert len(events) == 1 + (event,) = events + assert event["request"]["url"] == "http://testserver/root/nomessage" + + @parametrize_test_configurable_status_codes def test_configurable_status_codes( sentry_init, diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index 22ae7c55a4..4bea39cb95 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -1477,6 +1477,48 @@ def test_transaction_http_method_default(sentry_init, capture_events): assert event["request"]["method"] == "GET" +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_request_url(sentry_init, capture_events, capture_items, span_streaming): + sentry_init( + traces_sample_rate=1.0, + send_default_pii=True, + integrations=[ + StarletteIntegration(), + ], + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) + + starlette_app = starlette_app_factory() + + client = TestClient(starlette_app, root_path="/root") + + if span_streaming: + items = capture_items("span") + + client.get("/root/nomessage") + sentry_sdk.flush() + spans = [item.payload for item in items] + + (server_span,) = ( + span + for span in spans + if span["attributes"].get("sentry.op") == "http.server" + ) + assert server_span["attributes"]["url.full"] == ( + "http://testserver/root/nomessage" + ) + else: + events = capture_events() + + client.get("/root/nomessage") + + assert len(events) == 1 + (event,) = events + assert event["request"]["url"] == "http://testserver/root/nomessage" + + @pytest.mark.skipif( STARLETTE_VERSION < (0, 21), reason="Requires Starlette >= 0.21, because earlier versions do not support HTTP 'HEAD' requests", From a58e42dee8d2d6ec7f464024f8d1bdde8a329a19 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 19 Jun 2026 11:02:50 +0200 Subject: [PATCH 03/11] remanining tests --- tests/integrations/django/asgi/test_asgi.py | 51 +++++++++++++++++++ tests/integrations/fastapi/test_fastapi.py | 7 ++- tests/integrations/litestar/test_litestar.py | 43 ++++++++++++++++ tests/integrations/quart/test_quart.py | 45 ++++++++++++++++ .../integrations/starlette/test_starlette.py | 10 +++- tests/integrations/starlite/test_starlite.py | 43 ++++++++++++++++ 6 files changed, 195 insertions(+), 4 deletions(-) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 4e9eb95556..2c5a18bd30 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -1049,3 +1049,54 @@ async def test_transaction_http_method_custom( (event1, event2) = events assert event1["request"]["method"] == "OPTIONS" assert event2["request"]["method"] == "HEAD" + + +@pytest.mark.parametrize("application", APPS) +@pytest.mark.asyncio +@pytest.mark.skipif( + django.VERSION < (3, 0), reason="Django ASGI support shipped in 3.0" +) +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_request_url( + sentry_init, + capture_events, + capture_items, + application, + span_streaming, +): + sentry_init( + integrations=[DjangoIntegration()], + traces_sample_rate=1.0, + send_default_pii=True, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + comm = HttpCommunicator( + application, + "GET", + "/root/nomessage", + ) + + if span_streaming: + items = capture_items("span") + await comm.get_response() + await comm.wait() + + sentry_sdk.flush() + spans = [item.payload for item in items] + + (server_span,) = ( + span + for span in spans + if span["attributes"].get("sentry.op") == "http.server" + ) + assert server_span["attributes"]["url.full"] == ( + "http://testserver/root/nomessage" + ) + else: + events = capture_events() + + await comm.get_response() + await comm.wait() + + (event,) = events + assert event["request"]["url"] == "http://testserver/root/nomessage" diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 67c8bf8008..583d95fd7f 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -62,6 +62,7 @@ def fastapi_app_factory(): app = FastAPI() + mounted_app = FastAPI() @app.get("/error") async def _error(): @@ -74,6 +75,7 @@ async def _message(): capture_message("Hi") return {"message": "Hi"} + @mounted_app.get("/nomessage") @app.delete("/nomessage") @app.get("/nomessage") @app.head("/nomessage") @@ -118,6 +120,8 @@ async def body_form( capture_message("hi") return {"status": "ok"} + app.mount("/root", mounted_app) + return app @@ -1058,7 +1062,7 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) starlette_app = fastapi_app_factory() - client = TestClient(starlette_app, root_path="/root") + client = TestClient(starlette_app) if span_streaming: items = capture_items("span") @@ -1080,7 +1084,6 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) client.get("/root/nomessage") - assert len(events) == 1 (event,) = events assert event["request"]["url"] == "http://testserver/root/nomessage" diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index 4abb037e36..89ecaa596c 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -47,6 +47,10 @@ async def message_with_id() -> "dict[str, Any]": capture_message("hi") return {"status": "ok"} + @get("/nomessage") + async def nomessage() -> "dict[str, Any]": + return {"status": "ok"} + logging_config = LoggingConfig() app = Litestar( @@ -55,6 +59,7 @@ async def message_with_id() -> "dict[str, Any]": custom_error, message, message_with_id, + nomessage, MyController, ], debug=debug, @@ -818,3 +823,41 @@ async def error() -> None: ... event_exception = events[0]["exception"]["values"][0] assert event_exception["type"] == "RuntimeError" assert event_exception["value"] == "Too Hot" + + +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_request_url(sentry_init, capture_events, capture_items, span_streaming): + sentry_init( + traces_sample_rate=1.0, + integrations=[LitestarIntegration()], + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) + + litestar_app = litestar_app_factory() + client = TestClient(litestar_app, root_path="/root") + + if span_streaming: + items = capture_items("span") + + client.get("/root/nomessage") + + sentry_sdk.flush() + spans = [item.payload for item in items] + + (server_span,) = ( + span + for span in spans + if span["attributes"].get("sentry.op") == "http.server" + ) + assert server_span["attributes"]["url.full"] == ( + "http://testserver/root/nomessage" + ) + else: + events = capture_events() + + client.get("/root/nomessage") + + (event,) = events + assert event["request"]["url"] == "http://testserver/root/nomessage" diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index 7c7579501b..afc65004a7 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -44,6 +44,10 @@ async def hi(): capture_message("hi") return "ok" + @app.route("/nomessage") + async def nomessage(): + return "ok" + @app.route("/message/") async def hi_with_id(message_id): capture_message("hi with id") @@ -682,6 +686,22 @@ async def test_span_origin(sentry_init, capture_events): assert event["contexts"]["trace"]["origin"] == "auto.http.quart" +@pytest.mark.asyncio +async def test_request_url(sentry_init, capture_events): + sentry_init( + traces_sample_rate=1.0, + integrations=[quart_sentry.QuartIntegration()], + ) + app = quart_app_factory() + client = app.test_client() + + events = capture_events() + await client.get("/root/nomessage", root_path="/root") + + (event,) = events + assert event["request"]["url"] == "http://localhost/root/nomessage" + + @pytest.mark.asyncio async def test_span_streaming_basic(sentry_init, capture_items): sentry_init( @@ -966,3 +986,28 @@ async def test_span_streaming_sensitive_header_passthrough_with_pii( segment["attributes"]["http.request.header.authorization"] == "Bearer secret-token" ) + + +@pytest.mark.asyncio +async def test_span_streaming_request_url(sentry_init, capture_items): + sentry_init( + traces_sample_rate=1.0, + send_default_pii=True, + integrations=[quart_sentry.QuartIntegration()], + _experiments={ + "trace_lifecycle": "stream", + }, + ) + app = quart_app_factory() + client = app.test_client() + + items = capture_items("span") + await client.get("/root/nomessage", root_path="/root") + + sentry_sdk.flush() + spans = [item.payload for item in items] + + (server_span,) = ( + span for span in spans if span["attributes"].get("sentry.op") == "http.server" + ) + assert server_span["attributes"]["url.full"] == "http://localhost/root/nomessage" diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index 4bea39cb95..5287911be6 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -146,6 +146,12 @@ async def _body_raw(request): "TRACE", ] + mounted_app = starlette.applications.Starlette( + routes=[ + starlette.routing.Route("/nomessage", _nomessage, methods=all_methods), + ], + ) + app = starlette.applications.Starlette( debug=debug, routes=[ @@ -160,6 +166,7 @@ async def _body_raw(request): starlette.routing.Route("/body/json", _body_json, methods=["POST"]), starlette.routing.Route("/body/form", _body_form, methods=["POST"]), starlette.routing.Route("/body/raw", _body_raw, methods=["POST"]), + starlette.routing.Mount("/root", app=mounted_app), ], middleware=middleware, ) @@ -1492,7 +1499,7 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) starlette_app = starlette_app_factory() - client = TestClient(starlette_app, root_path="/root") + client = TestClient(starlette_app) if span_streaming: items = capture_items("span") @@ -1514,7 +1521,6 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) client.get("/root/nomessage") - assert len(events) == 1 (event,) = events assert event["request"]["url"] == "http://testserver/root/nomessage" diff --git a/tests/integrations/starlite/test_starlite.py b/tests/integrations/starlite/test_starlite.py index 3b6dc44131..eb7478b89b 100644 --- a/tests/integrations/starlite/test_starlite.py +++ b/tests/integrations/starlite/test_starlite.py @@ -41,6 +41,10 @@ async def message_with_id() -> "Dict[str, Any]": capture_message("hi") return {"status": "ok"} + @get("/nomessage") + async def nomessage() -> "Dict[str, Any]": + return {"status": "ok"} + logging_config = LoggingConfig() app = Starlite( @@ -49,6 +53,7 @@ async def message_with_id() -> "Dict[str, Any]": custom_error, message, message_with_id, + nomessage, MyController, ], debug=debug, @@ -574,3 +579,41 @@ async def __call__(self, scope, receive, send): } else: assert "user" not in event + + +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_request_url(sentry_init, capture_events, capture_items, span_streaming): + sentry_init( + traces_sample_rate=1.0, + integrations=[StarliteIntegration()], + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) + + starlite_app = starlite_app_factory() + client = TestClient(starlite_app, root_path="/root") + + if span_streaming: + items = capture_items("span") + + client.get("/nomessage") + + sentry_sdk.flush() + spans = [item.payload for item in items] + + (server_span,) = ( + span + for span in spans + if span["attributes"].get("sentry.op") == "http.server" + ) + assert server_span["attributes"]["url.full"] == ( + "http://testserver/root/nomessage" + ) + else: + events = capture_events() + + client.get("/nomessage") + + (event,) = events + assert event["request"]["url"] == "http://testserver/root/nomessage" From 42aca2143cdb4d8a12d1b7ef50f790cb69e475d8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 19 Jun 2026 11:39:04 +0200 Subject: [PATCH 04/11] update --- sentry_sdk/integrations/litestar.py | 2 ++ sentry_sdk/integrations/starlite.py | 2 ++ tests/integrations/litestar/test_litestar.py | 9 ++++++--- tests/integrations/starlite/test_starlite.py | 9 ++++++--- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index f0c90a7921..ad0b1fa70d 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -92,6 +92,8 @@ def __init__( mechanism_type="asgi", span_origin=span_origin, asgi_version=3, + # https://github.com/litestar-org/litestar/issues/2077 + path_includes_root_path=False, ) def _capture_request_exception(self, exc: Exception) -> None: diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 1c9328a09d..71d2c19ece 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -77,6 +77,8 @@ def __init__( mechanism_type="asgi", span_origin=span_origin, asgi_version=3, + # https://github.com/litestar-org/litestar/issues/2077 + path_includes_root_path=False, ) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index 89ecaa596c..73a62f2e28 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -830,13 +830,16 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) sentry_init( traces_sample_rate=1.0, integrations=[LitestarIntegration()], + send_default_pii=True, _experiments={ "trace_lifecycle": "stream" if span_streaming else "static", }, ) litestar_app = litestar_app_factory() - client = TestClient(litestar_app, root_path="/root") + client = TestClient( + litestar_app, base_url="http://testserver.local", root_path="/root" + ) if span_streaming: items = capture_items("span") @@ -852,7 +855,7 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) if span["attributes"].get("sentry.op") == "http.server" ) assert server_span["attributes"]["url.full"] == ( - "http://testserver/root/nomessage" + "http://testserver.local/root/nomessage" ) else: events = capture_events() @@ -860,4 +863,4 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) client.get("/root/nomessage") (event,) = events - assert event["request"]["url"] == "http://testserver/root/nomessage" + assert event["request"]["url"] == "http://testserver.local/root/nomessage" diff --git a/tests/integrations/starlite/test_starlite.py b/tests/integrations/starlite/test_starlite.py index eb7478b89b..3ea483f39f 100644 --- a/tests/integrations/starlite/test_starlite.py +++ b/tests/integrations/starlite/test_starlite.py @@ -586,13 +586,16 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) sentry_init( traces_sample_rate=1.0, integrations=[StarliteIntegration()], + send_default_pii=True, _experiments={ "trace_lifecycle": "stream" if span_streaming else "static", }, ) starlite_app = starlite_app_factory() - client = TestClient(starlite_app, root_path="/root") + client = TestClient( + starlite_app, base_url="http://testserver.local", root_path="/root" + ) if span_streaming: items = capture_items("span") @@ -608,7 +611,7 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) if span["attributes"].get("sentry.op") == "http.server" ) assert server_span["attributes"]["url.full"] == ( - "http://testserver/root/nomessage" + "http://testserver.local/root/nomessage" ) else: events = capture_events() @@ -616,4 +619,4 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) client.get("/nomessage") (event,) = events - assert event["request"]["url"] == "http://testserver/root/nomessage" + assert event["request"]["url"] == "http://testserver.local/root/nomessage" From ad1d00f262cf66257fa0f0af6d39f5e1ad3d4511 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 19 Jun 2026 11:49:39 +0200 Subject: [PATCH 05/11] update --- tests/integrations/litestar/test_litestar.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index 73a62f2e28..3ea00c7be0 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -16,9 +16,12 @@ import sentry_sdk from sentry_sdk import capture_message from sentry_sdk.integrations.litestar import LitestarIntegration +from sentry_sdk.utils import package_version from tests.conftest import ApproxDict from tests.integrations.conftest import parametrize_test_configurable_status_codes +LITESTAR_VERSION = package_version("litestar") + def litestar_app_factory(middleware=None, debug=True, exception_handlers=None): class MyController(Controller): @@ -844,7 +847,9 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) if span_streaming: items = capture_items("span") - client.get("/root/nomessage") + # https://github.com/litestar-org/litestar/commit/72dda171768bd470adc065c47c1ecf1d80b5e749 + url = "/root/nomessage" if LITESTAR_VERSION > (2, 5, 3) else "/nomessage" + client.get(url) sentry_sdk.flush() spans = [item.payload for item in items] @@ -860,7 +865,9 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) else: events = capture_events() - client.get("/root/nomessage") + # https://github.com/litestar-org/litestar/commit/72dda171768bd470adc065c47c1ecf1d80b5e749 + url = "/root/nomessage" if LITESTAR_VERSION > (2, 5, 3) else "/nomessage" + client.get(url) (event,) = events assert event["request"]["url"] == "http://testserver.local/root/nomessage" From 539b1cdf15c5c376d9c545528d168223b6b246e8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 19 Jun 2026 11:49:56 +0200 Subject: [PATCH 06/11] logic error --- tests/integrations/litestar/test_litestar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index 3ea00c7be0..97770932ea 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -848,7 +848,7 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) items = capture_items("span") # https://github.com/litestar-org/litestar/commit/72dda171768bd470adc065c47c1ecf1d80b5e749 - url = "/root/nomessage" if LITESTAR_VERSION > (2, 5, 3) else "/nomessage" + url = "/root/nomessage" if LITESTAR_VERSION >= (2, 5, 3) else "/nomessage" client.get(url) sentry_sdk.flush() @@ -866,7 +866,7 @@ def test_request_url(sentry_init, capture_events, capture_items, span_streaming) events = capture_events() # https://github.com/litestar-org/litestar/commit/72dda171768bd470adc065c47c1ecf1d80b5e749 - url = "/root/nomessage" if LITESTAR_VERSION > (2, 5, 3) else "/nomessage" + url = "/root/nomessage" if LITESTAR_VERSION >= (2, 5, 3) else "/nomessage" client.get(url) (event,) = events From a7112ba2b4c602c526fa2d8ff25d8e6c677d21ab Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 19 Jun 2026 12:02:45 +0200 Subject: [PATCH 07/11] fix quart tests --- tests/integrations/quart/test_quart.py | 30 +++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index afc65004a7..c2ef37964b 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -14,7 +14,9 @@ set_tag, ) from sentry_sdk.integrations.logging import LoggingIntegration -from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE +from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE, package_version + +QUART_VERSION = package_version("quart") def quart_app_factory(): @@ -696,7 +698,18 @@ async def test_request_url(sentry_init, capture_events): client = app.test_client() events = capture_events() - await client.get("/root/nomessage", root_path="/root") + + # https://github.com/pallets/quart/commit/7be545c + url = ( + "/root/nomessage" + if QUART_VERSION + >= ( + 0, + 19, + ) + else "/nomessage" + ) + await client.get(url, root_path="/root") (event,) = events assert event["request"]["url"] == "http://localhost/root/nomessage" @@ -1002,7 +1015,18 @@ async def test_span_streaming_request_url(sentry_init, capture_items): client = app.test_client() items = capture_items("span") - await client.get("/root/nomessage", root_path="/root") + + # https://github.com/pallets/quart/commit/7be545c + url = ( + "/root/nomessage" + if QUART_VERSION + >= ( + 0, + 19, + ) + else "/nomessage" + ) + await client.get(url, root_path="/root") sentry_sdk.flush() spans = [item.payload for item in items] From 574b5e57227dc7dbf9cbd4f06b8fbfb348919cef Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 19 Jun 2026 13:08:16 +0200 Subject: [PATCH 08/11] fix django tests --- tests/integrations/django/asgi/test_asgi.py | 6 ++---- tests/integrations/quart/test_quart.py | 20 ++------------------ 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 2c5a18bd30..7a33289ffb 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -1089,9 +1089,7 @@ async def test_request_url( for span in spans if span["attributes"].get("sentry.op") == "http.server" ) - assert server_span["attributes"]["url.full"] == ( - "http://testserver/root/nomessage" - ) + assert server_span["attributes"]["url.full"] == ("/root/nomessage") else: events = capture_events() @@ -1099,4 +1097,4 @@ async def test_request_url( await comm.wait() (event,) = events - assert event["request"]["url"] == "http://testserver/root/nomessage" + assert event["request"]["url"] == "/root/nomessage" diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index c2ef37964b..6d3529d157 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -700,15 +700,7 @@ async def test_request_url(sentry_init, capture_events): events = capture_events() # https://github.com/pallets/quart/commit/7be545c - url = ( - "/root/nomessage" - if QUART_VERSION - >= ( - 0, - 19, - ) - else "/nomessage" - ) + url = "/root/nomessage" if QUART_VERSION >= (0, 19) else "/nomessage" await client.get(url, root_path="/root") (event,) = events @@ -1017,15 +1009,7 @@ async def test_span_streaming_request_url(sentry_init, capture_items): items = capture_items("span") # https://github.com/pallets/quart/commit/7be545c - url = ( - "/root/nomessage" - if QUART_VERSION - >= ( - 0, - 19, - ) - else "/nomessage" - ) + url = "/root/nomessage" if QUART_VERSION >= (0, 19) else "/nomessage" await client.get(url, root_path="/root") sentry_sdk.flush() From e7c7fab43350be7c1bbef7944dbc3e2023b2a1dc Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 19 Jun 2026 13:21:25 +0200 Subject: [PATCH 09/11] fix django tests --- tests/integrations/django/asgi/test_asgi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 7a33289ffb..37afb0b30f 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -1075,6 +1075,7 @@ async def test_request_url( "GET", "/root/nomessage", ) + comm.scope["root_path"] = "/root" if span_streaming: items = capture_items("span") From 518acbef5b91c3ddca86f54df54ebdb97bec22fb Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 19 Jun 2026 13:43:15 +0200 Subject: [PATCH 10/11] fix quart url --- sentry_sdk/integrations/quart.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/quart.py b/sentry_sdk/integrations/quart.py index 6a5603d825..d111425a9f 100644 --- a/sentry_sdk/integrations/quart.py +++ b/sentry_sdk/integrations/quart.py @@ -17,6 +17,7 @@ capture_internal_exceptions, ensure_integration_enabled, event_from_exception, + package_version, ) if TYPE_CHECKING: @@ -86,16 +87,23 @@ def setup_once() -> None: def patch_asgi_app() -> None: old_app = Quart.__call__ + version = package_version("quart") + async def sentry_patched_asgi_app( self: "Any", scope: "Any", receive: "Any", send: "Any" ) -> "Any": - if sentry_sdk.get_client().get_integration(QuartIntegration) is None: + if ( + sentry_sdk.get_client().get_integration(QuartIntegration) is None + or version is None + ): return await old_app(self, scope, receive, send) middleware = SentryAsgiMiddleware( lambda *a, **kw: old_app(self, *a, **kw), span_origin=QuartIntegration.origin, asgi_version=3, + # https://github.com/pallets/quart/commit/7be545c + path_includes_root_path=version >= (0, 19), ) return await middleware(scope, receive, send) From b4205873f1a5f61c8b4f188c4213cfcef5456208 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 19 Jun 2026 14:47:48 +0200 Subject: [PATCH 11/11] update docstrings --- sentry_sdk/integrations/litestar.py | 5 ++++- sentry_sdk/integrations/quart.py | 2 ++ sentry_sdk/integrations/starlette.py | 3 ++- sentry_sdk/integrations/starlite.py | 1 - 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index ad0b1fa70d..70a0fec430 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -92,7 +92,10 @@ def __init__( mechanism_type="asgi", span_origin=span_origin, asgi_version=3, - # https://github.com/litestar-org/litestar/issues/2077 + # Unlike Starlette, LiteStar does not extend scope["root_path"] with the mount path. + # Since LiteStar handles servers that include and do not include scope["root_path"] in scope["path"] + # with the commit below, keep the existing behavior for compatibility. + # https://github.com/litestar-org/litestar/commit/72dda171768bd470adc065c47c1ecf1d80b5e749 path_includes_root_path=False, ) diff --git a/sentry_sdk/integrations/quart.py b/sentry_sdk/integrations/quart.py index d111425a9f..4578706fe3 100644 --- a/sentry_sdk/integrations/quart.py +++ b/sentry_sdk/integrations/quart.py @@ -102,6 +102,8 @@ async def sentry_patched_asgi_app( lambda *a, **kw: old_app(self, *a, **kw), span_origin=QuartIntegration.origin, asgi_version=3, + # Starting with the commit below, Quart treats any scope["path"] + # that does not include scope["root_path"] as invalid. # https://github.com/pallets/quart/commit/7be545c path_includes_root_path=version >= (0, 19), ) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 892de37df0..dc2b7d00e1 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -143,7 +143,8 @@ def setup_once() -> None: ) patch_middlewares() - # See https://github.com/Kludex/starlette/commit/e8f0dcd54e4ceec47e02c45f5275374e292339ad + # Starlette's Mount includes scope["root_path"] in scope["path"] starting with: + # https://github.com/Kludex/starlette/commit/e8f0dcd54e4ceec47e02c45f5275374e292339ad. path_includes_root_path = version >= (0, 33) patch_asgi_app(path_includes_root_path=path_includes_root_path) patch_request_response() diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 71d2c19ece..e23efaa612 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -77,7 +77,6 @@ def __init__( mechanism_type="asgi", span_origin=span_origin, asgi_version=3, - # https://github.com/litestar-org/litestar/issues/2077 path_includes_root_path=False, )