From a07b0be4b80f976e6fc9e3a53dfa966d6f464d26 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Wed, 24 Jun 2026 09:39:59 -0400 Subject: [PATCH 1/2] fix(aiohttp): Gate url.full, url.path, url.query on send_default_pii URL attributes (url.full, url.path, url.query) contain potentially sensitive data (full request URLs, paths, query strings) that should not be captured unless the user has opted in to sending PII. Gate these attributes behind send_default_pii() for both server and outgoing client spans to match the behavior of other integrations. Refs PY-2545 Co-Authored-By: Claude Sonnet 4.6 --- sentry_sdk/integrations/aiohttp.py | 18 ++++++-- tests/integrations/aiohttp/test_aiohttp.py | 53 ++++++++++++++++------ 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index f905420d87..a1b5012133 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -159,12 +159,20 @@ async def sentry_app_handle( header_value ) - url_query_attribute = ( + url_attributes = ( {"url.query": request.query_string} if request.query_string else {} ) + if should_send_default_pii(): + url_attributes["url.full"] = "%s://%s%s" % ( + request.scheme, + request.host, + request.path, + ) + url_attributes["url.path"] = request.path + client_address_attributes = {} if should_send_default_pii() and request.remote: client_address_attributes["client.address"] = request.remote @@ -180,10 +188,8 @@ async def sentry_app_handle( "sentry.op": OP.HTTP_SERVER, "sentry.origin": AioHttpIntegration.origin, "sentry.span.source": SegmentSource.ROUTE.value, - "url.full": "%s://%s%s" - % (request.scheme, request.host, request.path), "http.request.method": request.method, - **url_query_attribute, + **url_attributes, **client_address_attributes, **header_attributes, }, @@ -354,8 +360,10 @@ async def on_request_start( "sentry.origin": AioHttpIntegration.origin, "http.request.method": method, } - if parsed_url is not None: + if parsed_url is not None and should_send_default_pii(): attributes["url.full"] = parsed_url.url + attributes["url.path"] = params.url.path + if parsed_url.query: attributes["url.query"] = parsed_url.query if parsed_url.fragment: diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 70f76204cc..c222996344 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -1137,10 +1137,14 @@ async def handle(_): @pytest.mark.asyncio -async def test_tracing_span_streaming(sentry_init, aiohttp_client, capture_items): +@pytest.mark.parametrize("send_pii", [True, False]) +async def test_tracing_span_streaming( + sentry_init, aiohttp_client, capture_items, send_pii +): sentry_init( integrations=[AioHttpIntegration()], traces_sample_rate=1.0, + send_default_pii=send_pii, _experiments={"trace_lifecycle": "stream"}, ) @@ -1184,13 +1188,25 @@ async def hello(request): # Request attributes derived directly from the aiohttp request. assert server_span["attributes"]["http.request.method"] == "GET" - # client.address and user.ip_address is gated on send_default_pii (default False), so it must - # not be captured here. - assert "client.address" not in server_span["attributes"] - assert "user.ip_address" not in server_span["attributes"] - url_full = server_span["attributes"]["url.full"] - assert url_full.startswith("http://127.0.0.1:") - assert url_full.endswith("/") + + if send_pii: + assert "client.address" in server_span["attributes"] + assert "user.ip_address" in server_span["attributes"] + + url_full = server_span["attributes"]["url.full"] + assert url_full.startswith("http://127.0.0.1:") + assert url_full.endswith("/") + + url_path = server_span["attributes"]["url.path"] + assert url_path == "/" + else: + assert "url.full" not in server_span["attributes"] + assert "url.path" not in server_span["attributes"] + assert "url.query" not in server_span["attributes"] + + assert "client.address" not in server_span["attributes"] + assert "user.ip_address" not in server_span["attributes"] + # aiohttp's test client always sends a Host header; we assert it propagates # into the span attributes via _filter_headers. assert "http.request.header.host" in server_span["attributes"] @@ -1486,12 +1502,14 @@ async def hello(request): @pytest.mark.asyncio +@pytest.mark.parametrize("send_pii", [True, False]) async def test_outgoing_client_span_span_streaming( - sentry_init, aiohttp_raw_server, aiohttp_client, capture_items + sentry_init, aiohttp_raw_server, aiohttp_client, capture_items, send_pii ): sentry_init( integrations=[AioHttpIntegration()], traces_sample_rate=1.0, + send_default_pii=send_pii, _experiments={"trace_lifecycle": "stream"}, ) @@ -1536,14 +1554,19 @@ async def hello(request): assert inner_client_span["attributes"]["sentry.origin"] == "auto.http.aiohttp" assert inner_client_span["attributes"]["http.request.method"] == "GET" assert inner_client_span["attributes"]["http.response.status_code"] == 200 - assert inner_client_span["attributes"]["url.query"] == "foo=bar" assert inner_client_span["status"] == "ok" - 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("/") + if send_pii: + assert inner_client_span["attributes"]["url.query"] == "foo=bar" + + 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 inner_client_span["attributes"]["url.path"] == "/" @pytest.mark.asyncio From ae3ad3059eb738fcd986d23cb7aa96c4c3315c06 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Wed, 24 Jun 2026 10:17:00 -0400 Subject: [PATCH 2/2] forgot to move setting of a url.query behind the pii check --- sentry_sdk/integrations/aiohttp.py | 10 ++++------ tests/integrations/aiohttp/test_aiohttp.py | 9 +++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index a1b5012133..d22f3a745b 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -159,12 +159,7 @@ async def sentry_app_handle( header_value ) - url_attributes = ( - {"url.query": request.query_string} - if request.query_string - else {} - ) - + url_attributes = {} if should_send_default_pii(): url_attributes["url.full"] = "%s://%s%s" % ( request.scheme, @@ -173,6 +168,9 @@ async def sentry_app_handle( ) 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 diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index c222996344..583e7a50b6 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -1296,12 +1296,14 @@ async def hello(request): @pytest.mark.asyncio +@pytest.mark.parametrize("send_pii", [True, False]) async def test_url_query_attribute_span_streaming( - sentry_init, aiohttp_client, capture_items + sentry_init, aiohttp_client, capture_items, send_pii ): sentry_init( integrations=[AioHttpIntegration()], traces_sample_rate=1.0, + send_default_pii=send_pii, _experiments={"trace_lifecycle": "stream"}, ) @@ -1322,7 +1324,10 @@ async def hello(request): assert len(items) == 2 server_segment, client_segment = [item.payload for item in items] - assert server_segment["attributes"]["url.query"] == "foo=bar&baz=qux" + if send_pii: + assert server_segment["attributes"]["url.query"] == "foo=bar&baz=qux" + else: + assert "url.query" not in server_segment["attributes"] @pytest.mark.asyncio