Skip to content

Commit 07451d6

Browse files
authored
fix(aiohttp): Gate url.full, url.path, url.query on send_default_pii (#6650)
URL attributes (`url.full`, `url.path`, `url.query`) contain potentially sensitive data — full request URLs, path segments, and query strings — that should not be captured unless the user has explicitly opted in to PII collection. Previously these were always recorded on both server and outgoing client spans. This gates all three attributes behind `should_send_default_pii()` for both span types, matching the behavior of `client.address` and `user.ip_address` that were already gated. Tests for the two streaming span cases are parametrized to cover both PII-on and PII-off paths. Fixes PY-2545 Fixes #6631
1 parent 68e5ea7 commit 07451d6

2 files changed

Lines changed: 60 additions & 26 deletions

File tree

sentry_sdk/integrations/aiohttp.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,17 @@ async def sentry_app_handle(
159159
header_value
160160
)
161161

162-
url_query_attribute = (
163-
{"url.query": request.query_string}
164-
if request.query_string
165-
else {}
166-
)
162+
url_attributes = {}
163+
if should_send_default_pii():
164+
url_attributes["url.full"] = "%s://%s%s" % (
165+
request.scheme,
166+
request.host,
167+
request.path,
168+
)
169+
url_attributes["url.path"] = request.path
170+
171+
if request.query_string:
172+
url_attributes["url.query"] = request.query_string
167173

168174
client_address_attributes = {}
169175
if should_send_default_pii() and request.remote:
@@ -180,10 +186,8 @@ async def sentry_app_handle(
180186
"sentry.op": OP.HTTP_SERVER,
181187
"sentry.origin": AioHttpIntegration.origin,
182188
"sentry.span.source": SegmentSource.ROUTE.value,
183-
"url.full": "%s://%s%s"
184-
% (request.scheme, request.host, request.path),
185189
"http.request.method": request.method,
186-
**url_query_attribute,
190+
**url_attributes,
187191
**client_address_attributes,
188192
**header_attributes,
189193
},
@@ -354,8 +358,10 @@ async def on_request_start(
354358
"sentry.origin": AioHttpIntegration.origin,
355359
"http.request.method": method,
356360
}
357-
if parsed_url is not None:
361+
if parsed_url is not None and should_send_default_pii():
358362
attributes["url.full"] = parsed_url.url
363+
attributes["url.path"] = params.url.path
364+
359365
if parsed_url.query:
360366
attributes["url.query"] = parsed_url.query
361367
if parsed_url.fragment:

tests/integrations/aiohttp/test_aiohttp.py

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,10 +1137,14 @@ async def handle(_):
11371137

11381138

11391139
@pytest.mark.asyncio
1140-
async def test_tracing_span_streaming(sentry_init, aiohttp_client, capture_items):
1140+
@pytest.mark.parametrize("send_pii", [True, False])
1141+
async def test_tracing_span_streaming(
1142+
sentry_init, aiohttp_client, capture_items, send_pii
1143+
):
11411144
sentry_init(
11421145
integrations=[AioHttpIntegration()],
11431146
traces_sample_rate=1.0,
1147+
send_default_pii=send_pii,
11441148
_experiments={"trace_lifecycle": "stream"},
11451149
)
11461150

@@ -1184,13 +1188,25 @@ async def hello(request):
11841188

11851189
# Request attributes derived directly from the aiohttp request.
11861190
assert server_span["attributes"]["http.request.method"] == "GET"
1187-
# client.address and user.ip_address is gated on send_default_pii (default False), so it must
1188-
# not be captured here.
1189-
assert "client.address" not in server_span["attributes"]
1190-
assert "user.ip_address" not in server_span["attributes"]
1191-
url_full = server_span["attributes"]["url.full"]
1192-
assert url_full.startswith("http://127.0.0.1:")
1193-
assert url_full.endswith("/")
1191+
1192+
if send_pii:
1193+
assert "client.address" in server_span["attributes"]
1194+
assert "user.ip_address" in server_span["attributes"]
1195+
1196+
url_full = server_span["attributes"]["url.full"]
1197+
assert url_full.startswith("http://127.0.0.1:")
1198+
assert url_full.endswith("/")
1199+
1200+
url_path = server_span["attributes"]["url.path"]
1201+
assert url_path == "/"
1202+
else:
1203+
assert "url.full" not in server_span["attributes"]
1204+
assert "url.path" not in server_span["attributes"]
1205+
assert "url.query" not in server_span["attributes"]
1206+
1207+
assert "client.address" not in server_span["attributes"]
1208+
assert "user.ip_address" not in server_span["attributes"]
1209+
11941210
# aiohttp's test client always sends a Host header; we assert it propagates
11951211
# into the span attributes via _filter_headers.
11961212
assert "http.request.header.host" in server_span["attributes"]
@@ -1280,12 +1296,14 @@ async def hello(request):
12801296

12811297

12821298
@pytest.mark.asyncio
1299+
@pytest.mark.parametrize("send_pii", [True, False])
12831300
async def test_url_query_attribute_span_streaming(
1284-
sentry_init, aiohttp_client, capture_items
1301+
sentry_init, aiohttp_client, capture_items, send_pii
12851302
):
12861303
sentry_init(
12871304
integrations=[AioHttpIntegration()],
12881305
traces_sample_rate=1.0,
1306+
send_default_pii=send_pii,
12891307
_experiments={"trace_lifecycle": "stream"},
12901308
)
12911309

@@ -1306,7 +1324,10 @@ async def hello(request):
13061324
assert len(items) == 2
13071325
server_segment, client_segment = [item.payload for item in items]
13081326

1309-
assert server_segment["attributes"]["url.query"] == "foo=bar&baz=qux"
1327+
if send_pii:
1328+
assert server_segment["attributes"]["url.query"] == "foo=bar&baz=qux"
1329+
else:
1330+
assert "url.query" not in server_segment["attributes"]
13101331

13111332

13121333
@pytest.mark.asyncio
@@ -1486,12 +1507,14 @@ async def hello(request):
14861507

14871508

14881509
@pytest.mark.asyncio
1510+
@pytest.mark.parametrize("send_pii", [True, False])
14891511
async def test_outgoing_client_span_span_streaming(
1490-
sentry_init, aiohttp_raw_server, aiohttp_client, capture_items
1512+
sentry_init, aiohttp_raw_server, aiohttp_client, capture_items, send_pii
14911513
):
14921514
sentry_init(
14931515
integrations=[AioHttpIntegration()],
14941516
traces_sample_rate=1.0,
1517+
send_default_pii=send_pii,
14951518
_experiments={"trace_lifecycle": "stream"},
14961519
)
14971520

@@ -1536,14 +1559,19 @@ async def hello(request):
15361559
assert inner_client_span["attributes"]["sentry.origin"] == "auto.http.aiohttp"
15371560
assert inner_client_span["attributes"]["http.request.method"] == "GET"
15381561
assert inner_client_span["attributes"]["http.response.status_code"] == 200
1539-
assert inner_client_span["attributes"]["url.query"] == "foo=bar"
15401562
assert inner_client_span["status"] == "ok"
15411563

1542-
url_full = inner_client_span["attributes"]["url.full"]
1543-
# parse_url() splits the URL — url.full is the base URL only, with the
1544-
# query string captured separately on url.query.
1545-
assert url_full.startswith("http://127.0.0.1:")
1546-
assert url_full.endswith("/")
1564+
if send_pii:
1565+
assert inner_client_span["attributes"]["url.query"] == "foo=bar"
1566+
1567+
url_full = inner_client_span["attributes"]["url.full"]
1568+
1569+
# parse_url() splits the URL — url.full is the base URL only, with the
1570+
# query string captured separately on url.query.
1571+
assert url_full.startswith("http://127.0.0.1:")
1572+
assert url_full.endswith("/")
1573+
1574+
assert inner_client_span["attributes"]["url.path"] == "/"
15471575

15481576

15491577
@pytest.mark.asyncio

0 commit comments

Comments
 (0)