Skip to content

Commit bc1ef4a

Browse files
ericapisaniclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 7787e6d commit bc1ef4a

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

sentry_sdk/integrations/httpx2.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sentry_sdk
44
from sentry_sdk.consts import OP, SPANDATA
55
from sentry_sdk.integrations import DidNotEnable, Integration
6+
from sentry_sdk.scope import should_send_default_pii
67
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME
78
from sentry_sdk.tracing_utils import (
89
add_http_request_source,
@@ -73,7 +74,7 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response":
7374
) as streamed_span:
7475
attributes: "Attributes" = {}
7576

76-
if parsed_url is not None:
77+
if parsed_url is not None and should_send_default_pii():
7778
attributes["url.full"] = parsed_url.url
7879
if parsed_url.query:
7980
attributes["url.query"] = parsed_url.query
@@ -183,7 +184,7 @@ async def send(
183184
) as streamed_span:
184185
attributes: "Attributes" = {}
185186

186-
if parsed_url is not None:
187+
if parsed_url is not None and should_send_default_pii():
187188
attributes["url.full"] = parsed_url.url
188189
if parsed_url.query:
189190
attributes["url.query"] = parsed_url.query

tests/integrations/httpx2/test_httpx2.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,7 @@ def test_http_url_attributes_span_streaming(
11231123
sentry_init(
11241124
integrations=[Httpx2Integration()],
11251125
traces_sample_rate=1.0,
1126+
send_default_pii=True,
11261127
_experiments={"trace_lifecycle": "stream"},
11271128
)
11281129

@@ -1158,6 +1159,7 @@ def test_http_url_attributes_no_query_or_fragment_span_streaming(
11581159
sentry_init(
11591160
integrations=[Httpx2Integration()],
11601161
traces_sample_rate=1.0,
1162+
send_default_pii=True,
11611163
_experiments={"trace_lifecycle": "stream"},
11621164
)
11631165

@@ -1179,3 +1181,38 @@ def test_http_url_attributes_no_query_or_fragment_span_streaming(
11791181
assert "url.query" not in http_span["attributes"]
11801182
assert "url.fragment" not in http_span["attributes"]
11811183
assert http_span["attributes"]["http.response.status_code"] == 200
1184+
1185+
1186+
@pytest.mark.parametrize(
1187+
"httpx2_client",
1188+
(httpx2.Client(), httpx2.AsyncClient()),
1189+
)
1190+
def test_http_url_attributes_pii_disabled_span_streaming(
1191+
sentry_init, capture_items, httpx2_client, httpx2_mock
1192+
):
1193+
httpx2_mock.add_response()
1194+
1195+
sentry_init(
1196+
integrations=[Httpx2Integration()],
1197+
traces_sample_rate=1.0,
1198+
_experiments={"trace_lifecycle": "stream"},
1199+
)
1200+
1201+
items = capture_items("span")
1202+
1203+
url = "http://example.com/?foo=bar#frag"
1204+
1205+
if asyncio.iscoroutinefunction(httpx2_client.get):
1206+
asyncio.run(httpx2_client.get(url))
1207+
else:
1208+
httpx2_client.get(url)
1209+
1210+
sentry_sdk.flush()
1211+
1212+
http_span = _get_http_client_span(items)
1213+
1214+
assert http_span["attributes"]["http.request.method"] == "GET"
1215+
assert "url.full" not in http_span["attributes"]
1216+
assert "url.query" not in http_span["attributes"]
1217+
assert "url.fragment" not in http_span["attributes"]
1218+
assert http_span["attributes"]["http.response.status_code"] == 200

0 commit comments

Comments
 (0)