Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
07a3ea3
ref(subprocess): Create breadcrumbs directly in integration
sentrivana Aug 7, 2026
2f91234
.
sentrivana Aug 7, 2026
c3aea86
ref: Move Redis breadcrumbs to integration
sentrivana Aug 7, 2026
15d2e2f
.
sentrivana Aug 7, 2026
53ab7f0
really mypy?
sentrivana Aug 7, 2026
0ea13df
.
sentrivana Aug 7, 2026
2f54487
move even earlier
sentrivana Aug 7, 2026
8e6087f
.
sentrivana Aug 7, 2026
91fe6a4
.
sentrivana Aug 7, 2026
5858c87
.
sentrivana Aug 7, 2026
906838b
Merge branch 'master' into ivana/move-breadcrumbs-to-integrations
sentrivana Aug 7, 2026
4e093aa
.
sentrivana Aug 7, 2026
040f223
Merge branch 'ivana/move-breadcrumbs-to-integrations' into ivana/move…
sentrivana Aug 7, 2026
7472af9
remove extra guards
sentrivana Aug 7, 2026
8ace993
Merge branch 'master' into ivana/move-redis-breadcrumbs-to-integration
sentrivana Aug 7, 2026
f492ba4
ref(aiohttp): Move breadcrumb capture to integration
sentrivana Aug 7, 2026
8e3adaf
exclude the spans
sentrivana Aug 7, 2026
2a172c1
.
sentrivana Aug 7, 2026
1239d84
.
sentrivana Aug 7, 2026
9adbede
Merge branch 'master' into ivana/move-http-crumbs-1
sentrivana Aug 7, 2026
abdc32b
.
sentrivana Aug 7, 2026
d03072b
.
sentrivana Aug 7, 2026
689d9d1
fix type annotation
sentrivana Aug 10, 2026
8bfb879
defensive access
sentrivana Aug 10, 2026
405fb12
Merge branch 'master' into ivana/move-http-crumbs-1
sentrivana Aug 10, 2026
31486cf
ref(pyreqwest): Move crumbs to integration
sentrivana Aug 10, 2026
ccf1de9
make it work in async
sentrivana Aug 10, 2026
0de51ad
fix sphinx
sentrivana Aug 10, 2026
1bdf216
gate no response
sentrivana Aug 10, 2026
62ca144
ref(httpx,httpx2): Move crumbs to integrations
sentrivana Aug 10, 2026
86c8163
.
sentrivana Aug 10, 2026
579d224
simplify
sentrivana Aug 10, 2026
3e992fb
Merge branch 'ivana/move-http-crumbs-2' into ivana/move-http-crumbs-3
sentrivana Aug 10, 2026
8ca14b3
.
sentrivana Aug 10, 2026
720071a
.
sentrivana Aug 10, 2026
f1312f7
.
sentrivana Aug 10, 2026
58c435a
Merge branch 'master' into ivana/move-http-crumbs-1
sentrivana Aug 10, 2026
e31a3aa
Merge branch 'ivana/move-http-crumbs-1' into ivana/move-http-crumbs-2
sentrivana Aug 10, 2026
e32ac38
Merge branch 'ivana/move-http-crumbs-2' into ivana/move-http-crumbs-3
sentrivana Aug 10, 2026
9c2b924
tests
sentrivana Aug 10, 2026
0d8e4ca
ref(stdlib): Move crumbs to integration
sentrivana Aug 11, 2026
a640d16
remove util completely
sentrivana Aug 11, 2026
d68e635
fix mypy
sentrivana Aug 11, 2026
a793322
tests
sentrivana Aug 11, 2026
0db0743
.
sentrivana Aug 11, 2026
be9520a
requests tests
sentrivana Aug 11, 2026
a44e3e1
simplify
sentrivana Aug 11, 2026
1e27316
.
sentrivana Aug 11, 2026
5df68a9
smaller diff
sentrivana Aug 11, 2026
2af3694
36
sentrivana Aug 11, 2026
2e0281a
.
sentrivana Aug 11, 2026
60870a2
return maybe_create
sentrivana Aug 11, 2026
db40334
.
sentrivana Aug 11, 2026
33a8c0c
.
sentrivana Aug 11, 2026
9c63035
Merge branch 'master' into ivana/move-http-crumbs-4
sentrivana Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 69 additions & 16 deletions sentry_sdk/integrations/stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from sentry_sdk.tracing import Span
from sentry_sdk.tracing_utils import (
EnvironHeaders,
add_http_breadcrumb,
add_http_request_source,
has_span_streaming_enabled,
should_propagate_trace,
Expand Down Expand Up @@ -112,11 +113,21 @@
parsed_url = parse_url(real_url, sanitize=False)

span_streaming = has_span_streaming_enabled(client.options)
span: "Union[Span, StreamedSpan, None]"
span: "Union[Span, StreamedSpan, None]" = None
breadcrumb: "dict[str, Any]" = {}

if span_streaming:
if sentry_sdk.traces.get_current_span() is None:
span = None
else:
breadcrumb[SPANDATA.HTTP_REQUEST_METHOD] = method
if parsed_url is not None and should_send_default_pii():
breadcrumb.update(
{
SPANDATA.URL_FRAGMENT: parsed_url.fragment,
SPANDATA.URL_FULL: parsed_url.url,
SPANDATA.URL_QUERY: parsed_url.query,
}
)

if sentry_sdk.traces.get_current_span() is not None:
span = sentry_sdk.traces.start_span(
name="%s %s"
% (
Expand All @@ -136,6 +147,7 @@
span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query)

set_on_span = span.set_attribute

else:
span = sentry_sdk.start_span(
op=OP.HTTP_CLIENT,
Expand All @@ -145,17 +157,35 @@
)

span.set_data(SPANDATA.HTTP_METHOD, method)
breadcrumb[SPANDATA.HTTP_METHOD] = method

if parsed_url is not None:
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
span.set_data("url", parsed_url.url)
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)

breadcrumb.update(
{
SPANDATA.HTTP_FRAGMENT: parsed_url.fragment,
"url": parsed_url.url,
SPANDATA.HTTP_QUERY: parsed_url.query,

Check failure on line 171 in sentry_sdk/integrations/stdlib.py

View check run for this annotation

@sentry/warden / warden: skill-scanner

Sensitive URL data added to breadcrumbs without PII check

The non-span_streaming path adds URL fragment, full URL, and query to breadcrumbs without checking should_send_default_pii(), while the span_streaming path gates the same fields behind that check.
Comment on lines 165 to +171

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sensitive URL data added to breadcrumbs without PII check

The non-span_streaming path adds URL fragment, full URL, and query to breadcrumbs without checking should_send_default_pii(), while the span_streaming path gates the same fields behind that check.

Evidence
  • The span_streaming branch checks should_send_default_pii() before adding parsed_url.fragment, parsed_url.url, and parsed_url.query to the breadcrumb dict.
  • The else branch at lines 165-171 updates breadcrumb with SPANDATA.HTTP_FRAGMENT, "url", and SPANDATA.HTTP_QUERY without any PII guard.
  • The unguarded breadcrumb is later passed to add_http_breadcrumb, emitting potentially sensitive query parameters and URL fragments even when default PII collection is disabled.

Identified by Warden · skill-scanner · DBV-UKK

}
)

set_on_span = span.set_data

# for proxies, these point to the proxy host/port
if span and tunnel_host:
set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host)
set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port)
if tunnel_host:
if span:
set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host)
set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port)

breadcrumb.update(
{
SPANDATA.NETWORK_PEER_ADDRESS: self.host,
SPANDATA.NETWORK_PEER_PORT: self.port,
}
)

rv = real_putrequest(self, method, url, *args, **kwargs)

Expand All @@ -174,28 +204,46 @@
self.putheader(key, value)

self._sentrysdk_span = span # type: ignore[attr-defined]
self._sentrysdk_breadcrumb = breadcrumb # type: ignore[attr-defined]

return rv

def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any":
span = getattr(self, "_sentrysdk_span", None)

if span is None:
return real_getresponse(self, *args, **kwargs)
breadcrumb = getattr(self, "_sentrysdk_breadcrumb", None)

try:
rv = real_getresponse(self, *args, **kwargs)
except BaseException:
_complete_span(span)
except BaseException as ex:
if span:
_complete_span(span)
if (
breadcrumb
and "getresponse() got an unexpected keyword argument 'buffering'"

@sentrivana sentrivana Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exception is basically used for control flow/compatibility in old urllib3. getresponse() will be called again afterwards without the extra arg, which is when we'll emit the breadcrumb. If this extra check were not here, we'd emit one crumb too many.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Also, we can get rid of this once we drop 3.6, it's only really a problem in super old requests on Python 3.6.)

not in str(ex)
):
# the exception msg check is needed for Python 3.6/requests compat
add_http_breadcrumb(None, breadcrumb)
raise

status_code = int(rv.status)

if breadcrumb:
breadcrumb[SPANDATA.HTTP_STATUS_CODE] = status_code

if span is None:
if breadcrumb:
add_http_breadcrumb(status_code, breadcrumb)
return rv
Comment thread
sentrivana marked this conversation as resolved.

if isinstance(span, StreamedSpan):
status_code = int(rv.status)
span.status = "error" if status_code >= 400 else "ok"
span.set_attribute("http.response.status_code", status_code)
else:
span.set_http_status(int(rv.status))
span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_code)
elif isinstance(span, Span):
span.set_http_status(status_code)
span.set_data("reason", rv.reason)
if breadcrumb:
breadcrumb["reason"] = rv.reason

# getresponse doesn't include actually reading the response body. This
# is done in read(). So if the metadata/headers suggest there's a body to
Expand All @@ -206,6 +254,11 @@
else:
_complete_span(span)

if breadcrumb:
# Regardless of whether the response itself has been fully read or not,
# the breadcrumb can now be emitted since we now have the status code.
add_http_breadcrumb(status_code, breadcrumb)

return rv

def read(self: "HTTPResponse", *args: "Any", **kwargs: "Any") -> "Any":
Expand Down
1 change: 1 addition & 0 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ def maybe_create_breadcrumbs_from_span(
"auto.http.pyreqwest",
"auto.http.httpx",
"auto.http.httpx2",
"auto.http.stdlib.httplib",
):
level = None
status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE)
Expand Down
132 changes: 132 additions & 0 deletions tests/integrations/requests/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,43 @@ def test_crumb_capture(sentry_init, capture_events):
)


@pytest.mark.parametrize("send_default_pii", [True, False])
def test_crumb_capture_span_streaming(sentry_init, capture_events, send_default_pii):
sentry_init(
integrations=[StdlibIntegration()],
send_default_pii=send_default_pii,
trace_lifecycle="stream",
)
events = capture_events()

url = f"http://localhost:{PORT}/hello-world" # noqa:E231
response = requests.get(url)
capture_message("Testing!")

(event,) = events
(crumb,) = event["breadcrumbs"]["values"]
assert crumb["type"] == "http"
assert crumb["category"] == "httplib"

if send_default_pii:
assert crumb["data"] == ApproxDict(
{
SPANDATA.URL_FULL: url,
SPANDATA.HTTP_REQUEST_METHOD: "GET",
SPANDATA.URL_FRAGMENT: "",
SPANDATA.URL_QUERY: "",
SPANDATA.HTTP_STATUS_CODE: response.status_code,
}
)
else:
assert crumb["data"] == ApproxDict(
{
SPANDATA.HTTP_REQUEST_METHOD: "GET",
SPANDATA.HTTP_STATUS_CODE: response.status_code,
}
)


@pytest.mark.skipif(
sys.version_info < (3, 7),
reason="The response status is not set on the span early enough in 3.6",
Expand Down Expand Up @@ -84,6 +121,68 @@ def test_crumb_capture_client_error(sentry_init, capture_events, status_code, le
)


@pytest.mark.skipif(
sys.version_info < (3, 7),
reason="The response status is not set on the span early enough in 3.6",
)
@pytest.mark.parametrize(
"status_code,level",
[
(200, None),
(301, None),
(403, "warning"),
(405, "warning"),
(500, "error"),
],
)
@pytest.mark.parametrize("send_default_pii", [True, False])
def test_crumb_capture_client_error_span_streaming(
sentry_init, capture_events, status_code, level, send_default_pii
):
sentry_init(
integrations=[StdlibIntegration()],
send_default_pii=send_default_pii,
trace_lifecycle="stream",
)

events = capture_events()

url = f"http://localhost:{PORT}/status/{status_code}" # noqa:E231
response = requests.get(url)

assert response.status_code == status_code

capture_message("Testing!")

(event,) = events
(crumb,) = event["breadcrumbs"]["values"]
assert crumb["type"] == "http"
assert crumb["category"] == "httplib"

if level is None:
assert "level" not in crumb
else:
assert crumb["level"] == level

if send_default_pii:
assert crumb["data"] == ApproxDict(
{
SPANDATA.URL_FULL: url,
SPANDATA.HTTP_REQUEST_METHOD: "GET",
SPANDATA.URL_FRAGMENT: "",
SPANDATA.URL_QUERY: "",
SPANDATA.HTTP_STATUS_CODE: response.status_code,
}
)
else:
assert crumb["data"] == ApproxDict(
{
SPANDATA.HTTP_REQUEST_METHOD: "GET",
SPANDATA.HTTP_STATUS_CODE: response.status_code,
}
)


@pytest.mark.tests_internal_exceptions
def test_omit_url_data_if_parsing_fails(sentry_init, capture_events):
sentry_init(integrations=[StdlibIntegration()])
Expand Down Expand Up @@ -112,3 +211,36 @@ def test_omit_url_data_if_parsing_fails(sentry_init, capture_events):
assert "url" not in event["breadcrumbs"]["values"][0]["data"]
assert SPANDATA.HTTP_FRAGMENT not in event["breadcrumbs"]["values"][0]["data"]
assert SPANDATA.HTTP_QUERY not in event["breadcrumbs"]["values"][0]["data"]


@pytest.mark.tests_internal_exceptions
def test_omit_url_data_if_parsing_fails_span_streaming(sentry_init, capture_events):
sentry_init(
integrations=[StdlibIntegration()],
trace_lifecycle="stream",
send_default_pii=True,
)

events = capture_events()

url = f"http://localhost:{PORT}/ok" # noqa:E231

with mock.patch(
"sentry_sdk.integrations.stdlib.parse_url",
side_effect=ValueError,
):
response = requests.get(url)

capture_message("Testing!")

(event,) = events
assert event["breadcrumbs"]["values"][0]["data"] == ApproxDict(
{
SPANDATA.HTTP_REQUEST_METHOD: "GET",
SPANDATA.HTTP_STATUS_CODE: response.status_code,
# no url related data
}
)
assert SPANDATA.URL_FULL not in event["breadcrumbs"]["values"][0]["data"]
assert SPANDATA.URL_FRAGMENT not in event["breadcrumbs"]["values"][0]["data"]
assert SPANDATA.URL_QUERY not in event["breadcrumbs"]["values"][0]["data"]
Loading
Loading