From bc1ef4a2e805088497dc60e92d5c3e5f59e8f9ea Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 25 Jun 2026 14:51:29 -0400 Subject: [PATCH] fix(httpx2): Gate url.full, url.query on send_default_pii url.full and url.query can contain sensitive data (query strings, credentials in URLs). Gate these attributes behind send_default_pii to match the behavior of the aiohttp and wsgi integrations. Co-Authored-By: Claude Sonnet 4.6 --- sentry_sdk/integrations/httpx2.py | 5 ++-- tests/integrations/httpx2/test_httpx2.py | 37 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/httpx2.py b/sentry_sdk/integrations/httpx2.py index 19babb7bcf..25062aaa11 100644 --- a/sentry_sdk/integrations/httpx2.py +++ b/sentry_sdk/integrations/httpx2.py @@ -3,6 +3,7 @@ import sentry_sdk from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.scope import should_send_default_pii from sentry_sdk.tracing import BAGGAGE_HEADER_NAME from sentry_sdk.tracing_utils import ( add_http_request_source, @@ -73,7 +74,7 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": ) as streamed_span: attributes: "Attributes" = {} - if parsed_url is not None: + if parsed_url is not None and should_send_default_pii(): attributes["url.full"] = parsed_url.url if parsed_url.query: attributes["url.query"] = parsed_url.query @@ -183,7 +184,7 @@ async def send( ) as streamed_span: attributes: "Attributes" = {} - if parsed_url is not None: + if parsed_url is not None and should_send_default_pii(): attributes["url.full"] = parsed_url.url if parsed_url.query: attributes["url.query"] = parsed_url.query diff --git a/tests/integrations/httpx2/test_httpx2.py b/tests/integrations/httpx2/test_httpx2.py index c8fb3b970e..15d50513ae 100644 --- a/tests/integrations/httpx2/test_httpx2.py +++ b/tests/integrations/httpx2/test_httpx2.py @@ -1123,6 +1123,7 @@ def test_http_url_attributes_span_streaming( sentry_init( integrations=[Httpx2Integration()], traces_sample_rate=1.0, + send_default_pii=True, _experiments={"trace_lifecycle": "stream"}, ) @@ -1158,6 +1159,7 @@ def test_http_url_attributes_no_query_or_fragment_span_streaming( sentry_init( integrations=[Httpx2Integration()], traces_sample_rate=1.0, + send_default_pii=True, _experiments={"trace_lifecycle": "stream"}, ) @@ -1179,3 +1181,38 @@ def test_http_url_attributes_no_query_or_fragment_span_streaming( assert "url.query" not in http_span["attributes"] assert "url.fragment" not in http_span["attributes"] assert http_span["attributes"]["http.response.status_code"] == 200 + + +@pytest.mark.parametrize( + "httpx2_client", + (httpx2.Client(), httpx2.AsyncClient()), +) +def test_http_url_attributes_pii_disabled_span_streaming( + sentry_init, capture_items, httpx2_client, httpx2_mock +): + httpx2_mock.add_response() + + sentry_init( + integrations=[Httpx2Integration()], + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream"}, + ) + + items = capture_items("span") + + url = "http://example.com/?foo=bar#frag" + + if asyncio.iscoroutinefunction(httpx2_client.get): + asyncio.run(httpx2_client.get(url)) + else: + httpx2_client.get(url) + + sentry_sdk.flush() + + http_span = _get_http_client_span(items) + + assert http_span["attributes"]["http.request.method"] == "GET" + assert "url.full" not in http_span["attributes"] + assert "url.query" not in http_span["attributes"] + assert "url.fragment" not in http_span["attributes"] + assert http_span["attributes"]["http.response.status_code"] == 200