Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a3f7747
feat(stdlib): Support span streaming
alexander-alderman-webb Apr 28, 2026
fb549ba
test(stdlib): Remove mocks in outgoing trace header tests
alexander-alderman-webb Apr 28, 2026
945394d
merge
alexander-alderman-webb Apr 28, 2026
694114f
wip tests
alexander-alderman-webb Apr 28, 2026
42d8c73
.
alexander-alderman-webb Apr 28, 2026
518e3c9
merge
alexander-alderman-webb Apr 28, 2026
be7e413
remove print
alexander-alderman-webb Apr 28, 2026
1c9f953
test(stdlib): Overwrite timestamps in getresponse instead of putrequest
alexander-alderman-webb Apr 28, 2026
e889d0e
merge
alexander-alderman-webb Apr 28, 2026
22b2886
update tests
alexander-alderman-webb Apr 28, 2026
b732d86
test iteration
alexander-alderman-webb Apr 28, 2026
1c549fb
remove reason attribute
alexander-alderman-webb Apr 28, 2026
5b3406a
parameterize tests
alexander-alderman-webb Apr 28, 2026
debbdda
cleanup
alexander-alderman-webb Apr 28, 2026
b15b7b0
adjust outgoing trace test
alexander-alderman-webb Apr 28, 2026
72b7c62
subprocess tests
alexander-alderman-webb Apr 28, 2026
c39c549
stdlib integration
alexander-alderman-webb Apr 28, 2026
a0c8bc7
use documented url attributes
alexander-alderman-webb Apr 28, 2026
d688f01
add type hint
alexander-alderman-webb Apr 28, 2026
50fe59b
use http.request.method
alexander-alderman-webb Apr 28, 2026
1cbd5e1
update
alexander-alderman-webb Apr 28, 2026
ef94687
flip order in if-elif
alexander-alderman-webb Apr 29, 2026
346d124
add tests again
alexander-alderman-webb Apr 29, 2026
9a54013
add len assertion
alexander-alderman-webb Apr 29, 2026
224d4a4
update span name
alexander-alderman-webb Apr 29, 2026
1106fe4
use non-deprecated function
alexander-alderman-webb Apr 29, 2026
5c0a875
set request source attributes before finishing span
alexander-alderman-webb Apr 29, 2026
771e846
remove double init
alexander-alderman-webb Apr 29, 2026
f64437c
update description
alexander-alderman-webb Apr 29, 2026
347101f
fix code source tests
alexander-alderman-webb Apr 29, 2026
bc1f32d
merge master
alexander-alderman-webb Apr 29, 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
78 changes: 59 additions & 19 deletions sentry_sdk/integrations/stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.integrations import Integration
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.tracing import Span
from sentry_sdk.traces import StreamedSpan
from sentry_sdk.tracing_utils import (
EnvironHeaders,
should_propagate_trace,
add_http_request_source,
has_span_streaming_enabled,
)
from sentry_sdk.utils import (
SENSITIVE_DATA_SUBSTITUTE,
Expand All @@ -31,6 +34,7 @@
from typing import Dict
from typing import Optional
from typing import List
from typing import Union

from sentry_sdk._types import Event, Hint

Expand Down Expand Up @@ -99,22 +103,38 @@
with capture_internal_exceptions():
parsed_url = parse_url(real_url, sanitize=False)

span = sentry_sdk.start_span(
op=OP.HTTP_CLIENT,
name="%s %s"
% (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
origin="auto.http.stdlib.httplib",
)
span.set_data(SPANDATA.HTTP_METHOD, method)
span_streaming = has_span_streaming_enabled(client.options)
span: "Union[Span, StreamedSpan]"
if span_streaming:
span = sentry_sdk.traces.start_span(
name="%s %s"
% (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
attributes={
"sentry.origin": "auto.http.stdlib.httplib",
"sentry.op": OP.HTTP_CLIENT,
},
)
set_on_span = span.set_attribute

else:
span = sentry_sdk.start_span(
op=OP.HTTP_CLIENT,
name="%s %s"
% (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
origin="auto.http.stdlib.httplib",
)
set_on_span = span.set_data

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

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

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

Expand All @@ -139,14 +159,23 @@
def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any":
span = getattr(self, "_sentrysdk_span", None)

print("span is here")

Check warning on line 162 in sentry_sdk/integrations/stdlib.py

View check run for this annotation

@sentry/warden / warden: code-review

Stray debug print statement left in production code

A `print("span is here")` statement was added to the `getresponse` wrapper. This is clearly leftover debug output that will execute on every HTTP response handled by the stdlib integration, polluting stdout for all users of the SDK and potentially leaking into production logs. This is a behavioral side effect that affects every consumer of the integration.
Comment thread
alexander-alderman-webb marked this conversation as resolved.
Outdated

if span is None:
return real_getresponse(self, *args, **kwargs)

try:
rv = real_getresponse(self, *args, **kwargs)

span.set_http_status(int(rv.status))
span.set_data("reason", rv.reason)
if isinstance(span, StreamedSpan):
span.set_attribute("reason", rv.reason)

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_data("reason", rv.reason)
finally:
Comment thread
sentry[bot] marked this conversation as resolved.
span.finish()

Expand Down Expand Up @@ -226,11 +255,22 @@

env = None

with sentry_sdk.start_span(
op=OP.SUBPROCESS,
name=description,
origin="auto.subprocess.stdlib.subprocess",
) as span:
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
span: "Union[Span, StreamedSpan]"
if span_streaming:
span = sentry_sdk.start_span(
op=OP.SUBPROCESS,
name=description,
origin="auto.subprocess.stdlib.subprocess",
)
else:
span = sentry_sdk.start_span(
op=OP.SUBPROCESS,
name=description,
origin="auto.subprocess.stdlib.subprocess",
Comment thread
sentry[bot] marked this conversation as resolved.
)

Check warning on line 271 in sentry_sdk/integrations/stdlib.py

View check run for this annotation

@sentry/warden / warden: find-bugs

span_streaming branch in subprocess instrumentation is identical to non-streaming branch, making feature a no-op

Both arms of `if span_streaming:` call `sentry_sdk.start_span(op=..., name=..., origin=...)` with identical arguments. Compare to the httplib instrumentation in the same file (lines 108-126), where the streaming branch invokes `sentry_sdk.traces.start_span(name=..., attributes={'sentry.origin': ..., 'sentry.op': ...})` to obtain a `StreamedSpan`. As written, the type annotation `Union[Span, StreamedSpan]` is misleading because `span` will never actually be a `StreamedSpan`, and span streaming silently does nothing for subprocess spans. Downstream `isinstance(span, StreamedSpan)` checks (used elsewhere in this file) will never trigger here, so subprocess spans will not be emitted via the streaming pipeline when the feature is enabled.
Comment thread
alexander-alderman-webb marked this conversation as resolved.

with span:
for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers(
span=span
):
Expand Down
80 changes: 43 additions & 37 deletions tests/integrations/stdlib/test_httplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,15 @@
)


def test_outgoing_trace_headers(sentry_init, monkeypatch):
def test_outgoing_trace_headers(sentry_init, capture_events, monkeypatch):
# HTTPSConnection.send is passed a string containing (among other things)
# the headers on the request. Mock it so we can check the headers, and also
# so it doesn't try to actually talk to the internet.
mock_send = mock.Mock()
monkeypatch.setattr(HTTPSConnection, "send", mock_send)

sentry_init(traces_sample_rate=1.0)
events = capture_events()

headers = {
"sentry-trace": "771a43a4192642f0b136d5159a501700-1234567890abcdef-1",
Expand Down Expand Up @@ -237,38 +238,42 @@
key, val = line.split(": ")
request_headers[key] = val

request_span = transaction._span_recorder.spans[-1]
expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format(
trace_id=transaction.trace_id,
parent_span_id=request_span.span_id,
sampled=1,
)
assert request_headers["sentry-trace"] == expected_sentry_trace

expected_outgoing_baggage = (
"sentry-trace_id=771a43a4192642f0b136d5159a501700,"
"sentry-public_key=49d0f7386ad645858ae85020e393bef3,"
"sentry-sample_rate=1.0,"
"sentry-user_id=Am%C3%A9lie,"
"sentry-sample_rand=0.132521102938283"
)
(event,) = events
request_span = event["spans"][-1]
expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format(
trace_id=transaction.trace_id,
parent_span_id=request_span.span_id,

Check failure on line 245 in tests/integrations/stdlib/test_httplib.py

View check run for this annotation

@sentry/warden / warden: code-review

Test accesses dict span via attribute syntax, causing AttributeError

After the refactor, `request_span` is taken from `event["spans"][-1]`, which is a dict (serialized span from the captured event), not a Span object. Accessing `request_span.span_id` on a dict will raise `AttributeError: 'dict' object has no attribute 'span_id'`, causing the test to fail at runtime. The code should use `request_span["span_id"]` instead.

Check failure on line 245 in tests/integrations/stdlib/test_httplib.py

View check run for this annotation

@sentry/warden / warden: find-bugs

request_span.span_id raises AttributeError on serialized span dict

After switching from `transaction._span_recorder.spans[-1]` (a Span object) to `event["spans"][-1]` (a serialized dict in the captured event), the code still uses attribute access `request_span.span_id`. Captured event spans are dictionaries and must be accessed with `request_span["span_id"]` (as is done elsewhere in the test suite, e.g. tests/integrations/asyncio/test_asyncio.py and tests/integrations/celery/test_celery.py). The test will fail with AttributeError before reaching the assertion.
Comment thread
alexander-alderman-webb marked this conversation as resolved.
Outdated
sampled=1,
)
assert request_headers["sentry-trace"] == expected_sentry_trace

expected_outgoing_baggage = (
"sentry-trace_id=771a43a4192642f0b136d5159a501700,"
"sentry-public_key=49d0f7386ad645858ae85020e393bef3,"
"sentry-sample_rate=1.0,"
"sentry-user_id=Am%C3%A9lie,"
"sentry-sample_rand=0.132521102938283"
)

assert request_headers["baggage"] == expected_outgoing_baggage
assert request_headers["baggage"] == expected_outgoing_baggage


def test_outgoing_trace_headers_head_sdk(sentry_init, monkeypatch):
def test_outgoing_trace_headers_head_sdk(sentry_init, capture_events, monkeypatch):
# HTTPSConnection.send is passed a string containing (among other things)
# the headers on the request. Mock it so we can check the headers, and also
# so it doesn't try to actually talk to the internet.
mock_send = mock.Mock()
monkeypatch.setattr(HTTPSConnection, "send", mock_send)

sentry_init(traces_sample_rate=0.5, release="foo")
events = capture_events()

with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=250000):
transaction = continue_trace({})

with start_transaction(transaction=transaction, name="Head SDK tx") as transaction:
HTTPSConnection("www.squirrelchasers.com").request("GET", "/top-chasers")
connection = HTTPSConnection("www.squirrelchasers.com")
connection.request("GET", "/top-chasers")

(request_str,) = mock_send.call_args[0]
request_headers = {}
Expand All @@ -277,24 +282,25 @@
key, val = line.split(": ")
request_headers[key] = val

request_span = transaction._span_recorder.spans[-1]
expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format(
trace_id=transaction.trace_id,
parent_span_id=request_span.span_id,
sampled=1,
)
assert request_headers["sentry-trace"] == expected_sentry_trace

expected_outgoing_baggage = (
"sentry-trace_id=%s,"
"sentry-sample_rand=0.250000,"
"sentry-environment=production,"
"sentry-release=foo,"
"sentry-sample_rate=0.5,"
"sentry-sampled=%s"
) % (transaction.trace_id, "true" if transaction.sampled else "false")

assert request_headers["baggage"] == expected_outgoing_baggage
(event,) = events
request_span = event["spans"][0]
expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format(
trace_id=transaction.trace_id,
parent_span_id=request_span.span_id,

Check failure on line 289 in tests/integrations/stdlib/test_httplib.py

View check run for this annotation

@sentry/warden / warden: code-review

[LJX-ZXL] Test accesses dict span via attribute syntax, causing AttributeError (additional location)

After the refactor, `request_span` is taken from `event["spans"][-1]`, which is a dict (serialized span from the captured event), not a Span object. Accessing `request_span.span_id` on a dict will raise `AttributeError: 'dict' object has no attribute 'span_id'`, causing the test to fail at runtime. The code should use `request_span["span_id"]` instead.

Check failure on line 289 in tests/integrations/stdlib/test_httplib.py

View check run for this annotation

@sentry/warden / warden: find-bugs

[MUA-25M] request_span.span_id raises AttributeError on serialized span dict (additional location)

After switching from `transaction._span_recorder.spans[-1]` (a Span object) to `event["spans"][-1]` (a serialized dict in the captured event), the code still uses attribute access `request_span.span_id`. Captured event spans are dictionaries and must be accessed with `request_span["span_id"]` (as is done elsewhere in the test suite, e.g. tests/integrations/asyncio/test_asyncio.py and tests/integrations/celery/test_celery.py). The test will fail with AttributeError before reaching the assertion.
sampled=1,
)
assert request_headers["sentry-trace"] == expected_sentry_trace

expected_outgoing_baggage = (
"sentry-trace_id=%s,"
"sentry-sample_rand=0.250000,"
"sentry-environment=production,"
"sentry-release=foo,"
"sentry-sample_rate=0.5,"
"sentry-sampled=%s"
) % (transaction.trace_id, "true" if transaction.sampled else "false")

assert request_headers["baggage"] == expected_outgoing_baggage


@pytest.mark.parametrize(
Expand Down
Loading