From 65c1a6de273782ca1a75b20a2e3a7787312d7be5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 22 Jul 2026 13:23:03 +0200 Subject: [PATCH 1/4] fix: Include query string and fragment in `url.full` span attribute The `url.full` attribute was only being set to the base URL (scheme + host + path), omitting query string and fragment. This affected aiohttp (server and client), httpx, httpx2, and WSGI integrations. ASGI and Quart were already correct. Co-Authored-By: Claude Opus 4.6 --- sentry_sdk/integrations/aiohttp.py | 14 ++++++++++++-- sentry_sdk/integrations/httpx.py | 16 ++++++++++++++-- sentry_sdk/integrations/httpx2.py | 16 ++++++++++++++-- sentry_sdk/integrations/wsgi.py | 5 ++++- tests/integrations/aiohttp/test_aiohttp.py | 4 +--- tests/integrations/httpx/test_httpx.py | 2 +- tests/integrations/httpx2/test_httpx2.py | 2 +- tests/integrations/wsgi/test_wsgi.py | 5 ++++- 8 files changed, 51 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index c27821d953..4de0244cef 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -161,11 +161,15 @@ async def sentry_app_handle( url_attributes = {} if should_send_default_pii(): - url_attributes["url.full"] = "%s://%s%s" % ( + url_full = "%s://%s%s" % ( request.scheme, request.host, request.path, ) + if request.query_string: + url_full += "?" + request.query_string + + url_attributes["url.full"] = url_full url_attributes["url.path"] = request.path if request.query_string: @@ -362,7 +366,13 @@ async def on_request_start( "http.request.method": method, } if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url + url_full = parsed_url.url + if parsed_url.query: + url_full += "?" + parsed_url.query + if parsed_url.fragment: + url_full += "#" + parsed_url.fragment + + attributes["url.full"] = url_full attributes["url.path"] = params.url.path if parsed_url.query: diff --git a/sentry_sdk/integrations/httpx.py b/sentry_sdk/integrations/httpx.py index 779302a238..333754e209 100644 --- a/sentry_sdk/integrations/httpx.py +++ b/sentry_sdk/integrations/httpx.py @@ -76,7 +76,13 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": attributes: "Attributes" = {} if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url + url_full = parsed_url.url + if parsed_url.query: + url_full += "?" + parsed_url.query + if parsed_url.fragment: + url_full += "#" + parsed_url.fragment + + attributes["url.full"] = url_full if parsed_url.query: attributes["url.query"] = parsed_url.query if parsed_url.fragment: @@ -162,7 +168,13 @@ async def send( attributes: "Attributes" = {} if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url + url_full = parsed_url.url + if parsed_url.query: + url_full += "?" + parsed_url.query + if parsed_url.fragment: + url_full += "#" + parsed_url.fragment + + attributes["url.full"] = url_full if parsed_url.query: attributes["url.query"] = parsed_url.query if parsed_url.fragment: diff --git a/sentry_sdk/integrations/httpx2.py b/sentry_sdk/integrations/httpx2.py index 1ce3e93524..b658ce8ba4 100644 --- a/sentry_sdk/integrations/httpx2.py +++ b/sentry_sdk/integrations/httpx2.py @@ -77,7 +77,13 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": attributes: "Attributes" = {} if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url + url_full = parsed_url.url + if parsed_url.query: + url_full += "?" + parsed_url.query + if parsed_url.fragment: + url_full += "#" + parsed_url.fragment + + attributes["url.full"] = url_full if parsed_url.query: attributes["url.query"] = parsed_url.query if parsed_url.fragment: @@ -164,7 +170,13 @@ async def send( attributes: "Attributes" = {} if parsed_url is not None and should_send_default_pii(): - attributes["url.full"] = parsed_url.url + url_full = parsed_url.url + if parsed_url.query: + url_full += "?" + parsed_url.query + if parsed_url.fragment: + url_full += "#" + parsed_url.fragment + + attributes["url.full"] = url_full if parsed_url.query: attributes["url.query"] = parsed_url.query if parsed_url.fragment: diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index d3b91ff1ba..b40e6a4b5b 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -422,6 +422,9 @@ def _get_request_attributes( if path: attributes["url.path"] = path - attributes["url.full"] = get_request_url(environ, use_x_forwarded_for) + url_full = get_request_url(environ, use_x_forwarded_for) + if query_string: + url_full += "?" + query_string + attributes["url.full"] = url_full return attributes diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 5a4433e62b..1af04e79c3 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -1540,10 +1540,8 @@ async def hello(request): url_full = inner_client_span["attributes"]["url.full"] - # parse_url() splits the URL — url.full is the base URL only, with the - # query string captured separately on url.query. assert url_full.startswith("http://127.0.0.1:") - assert url_full.endswith("/") + assert "?foo=bar" in url_full assert inner_client_span["attributes"]["url.path"] == "/" diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py index 1470059137..7b82c553bc 100644 --- a/tests/integrations/httpx/test_httpx.py +++ b/tests/integrations/httpx/test_httpx.py @@ -1246,7 +1246,7 @@ def test_http_url_attributes_span_streaming( assert http_span["attributes"]["http.response.status_code"] == 200 if send_default_pii: - assert http_span["attributes"]["url.full"] == "http://example.com/" + assert http_span["attributes"]["url.full"] == "http://example.com/?foo=bar#frag" assert http_span["attributes"]["url.query"] == "foo=bar" assert http_span["attributes"]["url.fragment"] == "frag" else: diff --git a/tests/integrations/httpx2/test_httpx2.py b/tests/integrations/httpx2/test_httpx2.py index e8e8da54a3..cad039e108 100644 --- a/tests/integrations/httpx2/test_httpx2.py +++ b/tests/integrations/httpx2/test_httpx2.py @@ -1234,7 +1234,7 @@ def test_http_url_attributes_span_streaming( http_span = _get_http_client_span(items) assert http_span["attributes"]["http.request.method"] == "GET" - assert http_span["attributes"]["url.full"] == "http://example.com/" + assert http_span["attributes"]["url.full"] == "http://example.com/?foo=bar#frag" assert http_span["attributes"]["url.query"] == "foo=bar" assert http_span["attributes"]["url.fragment"] == "frag" assert http_span["attributes"]["http.response.status_code"] == 200 diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index 89c106e31b..09ffe23826 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -254,7 +254,10 @@ def dogpark(environ, start_response): assert span["status"] == "ok" if send_pii: - assert span["attributes"]["url.full"] == "http://localhost/dogs/are/great" + assert ( + span["attributes"]["url.full"] + == "http://localhost/dogs/are/great?toy=tennisball" + ) assert span["attributes"]["url.path"] == "/dogs/are/great" assert span["attributes"]["http.query"] == "toy=tennisball" else: From 5dd4a0ac7cfebcdf3f9628376773f532f052fb5b Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 23 Jul 2026 09:19:01 +0200 Subject: [PATCH 2/4] merge --- sentry_sdk/integrations/wsgi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index da0d8e62ca..b0156a9829 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -444,6 +444,7 @@ def _get_request_attributes( if has_data_collection_enabled(client_options): query_string = environ.get("QUERY_STRING") + filtered_qs = None if query_string: filtered_qs = _apply_data_collection_filtering_to_query_string( query_string=query_string, @@ -458,6 +459,8 @@ def _get_request_attributes( attributes["url.path"] = path attributes["url.full"] = get_request_url(environ, use_x_forwarded_for) + if filtered_qs is not None: + attributes["url.full"] += f"?{filtered_qs}" if client_options["data_collection"]["user_info"]: client_ip = get_client_ip(environ) From a8040895bb0e3a0aeb3456c495e360bb21bed0ca Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 23 Jul 2026 09:26:26 +0200 Subject: [PATCH 3/4] fix(aiohttp): Resolve merge conflict and fix url.full to include query/fragment Resolved merge conflict in the client streaming path to combine query/fragment appending with the data_collection gate. Fixed a bug in the server-side should_send_default_pii path where url_full was referenced before assignment. Co-Authored-By: Claude Opus 4.6 --- sentry_sdk/integrations/aiohttp.py | 36 +++++++++++++----------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index ccb2dfbb1a..d520da2136 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -187,20 +187,18 @@ async def sentry_app_handle( url_attributes["url.full"] += "?" + filtered_query_string elif should_send_default_pii(): - url_attributes["url.full"] = "%s://%s%s" % ( + url_full = "%s://%s%s" % ( request.scheme, request.host, request.path, ) if request.query_string: url_full += "?" + request.query_string + url_attributes["url.query"] = request.query_string url_attributes["url.full"] = url_full url_attributes["url.path"] = request.path - if request.query_string: - url_attributes["url.query"] = request.query_string - client_address_attributes = {} if should_send_default_pii() and request.remote: client_address_attributes["client.address"] = request.remote @@ -391,25 +389,10 @@ async def on_request_start( "sentry.origin": AioHttpIntegration.origin, "http.request.method": method, } -<<<<<<< HEAD - if parsed_url is not None and should_send_default_pii(): - url_full = parsed_url.url - if parsed_url.query: - url_full += "?" + parsed_url.query - if parsed_url.fragment: - url_full += "#" + parsed_url.fragment - - attributes["url.full"] = url_full - attributes["url.path"] = params.url.path -======= if parsed_url is not None: if has_data_collection_enabled(client.options): - attributes["url.full"] = parsed_url.url + url_full = parsed_url.url attributes["url.path"] = params.url.path ->>>>>>> master - - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment if parsed_url.query: filtered_query = ( @@ -422,15 +405,26 @@ async def on_request_start( ) if filtered_query: attributes["url.query"] = filtered_query + url_full += "?" + filtered_query + + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + url_full += "#" + parsed_url.fragment + + attributes["url.full"] = url_full elif should_send_default_pii(): - attributes["url.full"] = parsed_url.url + url_full = parsed_url.url attributes["url.path"] = params.url.path if parsed_url.query: + url_full += "?" + parsed_url.query attributes["url.query"] = parsed_url.query if parsed_url.fragment: + url_full += "#" + parsed_url.fragment attributes["url.fragment"] = parsed_url.fragment + attributes["url.full"] = url_full + span = sentry_sdk.traces.start_span( name=span_name, attributes=attributes ) From 5f9a3a0f8d27ce1417996a69f8cfa658366876ff Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 23 Jul 2026 09:28:49 +0200 Subject: [PATCH 4/4] . --- sentry_sdk/integrations/aiohttp.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index d520da2136..9c21952119 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -184,7 +184,9 @@ async def sentry_app_handle( ) if filtered_query_string: url_attributes["url.query"] = filtered_query_string - url_attributes["url.full"] += "?" + filtered_query_string + url_attributes["url.full"] += ( + "?" + filtered_query_string + ) elif should_send_default_pii(): url_full = "%s://%s%s" % (