Skip to content

feat(httpx): Migrate to span first#6084

Open
ericapisani wants to merge 6 commits intomasterfrom
ep/span-first-httpx-c35
Open

feat(httpx): Migrate to span first#6084
ericapisani wants to merge 6 commits intomasterfrom
ep/span-first-httpx-c35

Conversation

@ericapisani
Copy link
Copy Markdown
Member

@ericapisani ericapisani commented Apr 16, 2026

Migrates the httpx integration to the span-first (streaming span) architecture.

When _experiments={"trace_lifecycle": "stream"} is enabled, the integration
now uses StreamedSpan via sentry_sdk.traces.start_span instead of the legacy
Span-based (transactions) path. The legacy path remains unchanged for backwards compatibility.

Supporting changes:

  • add_http_request_source_for_streamed_span() added to tracing_utils — a
    StreamedSpan-compatible variant that must be called inside the span context
    manager so source data is attached before the span is flushed
  • add_source() updated to dispatch between set_data/set_attribute depending
    on span type
  • start_timestamp_monotonic_ns property added to StreamedSpan and
    NoOpStreamedSpan to support the request source duration threshold check

Part of the broader span-first integration migration.

Fixes PY-2330
Fixes #6028

Adds span streaming support to the httpx integration by adding a new
code path that uses `start_span` from `sentry_sdk.traces` when
`_experiments={"trace_lifecycle": "stream"}` is enabled. The existing
legacy path is preserved unchanged.

Test coverage is updated to:
- Rename span-data-asserting tests with a `_legacy` suffix
- Fix broken mock targets in duration threshold tests (`start_span` →
  `legacy_start_span` after the rename introduced two names)
- Add `_span_streaming` variants for all tests that assert on span data,
  using `capture_items("span")` and `span["attributes"]` instead of
  `event["spans"][*]["data"]`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 16, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (ci) Cancel in-progress PR workflows on new commit push by joshuarli in #5994
  • (httpx) Migrate to span first by ericapisani in #6084
  • Add db.driver.name spans to database integrations by ericapisani in #6082

Bug Fixes 🐛

  • (google_genai) Redact binary data in inline_data and fix multi-part message extraction by ericapisani in #5977
  • (grpc) Add isolation_scope to async server interceptor by robinvd in #5940
  • (profiler) Stop nulling buffer on teardown by ericapisani in #6075

Internal Changes 🔧

  • (celery) Remove unused NoOpMgr from utils by sentrivana in #6078
  • (ci) Update outdated pinned action version comments by JoshuaMoelans in #6088
  • (pydantic-ai) Remove dead Model.request patch by alexander-alderman-webb in #5956
  • (tests) Replace deprecated enable_tracingwith traces_sample_rate by sentrivana in #6077
  • Set explicit base-branch for codecov action by ericapisani in #5992

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 16, 2026

Codecov Results 📊

13 passed | Total: 13 | Pass Rate: 100% | Execution Time: 8.00s

All tests are passing successfully.

❌ Patch coverage is 6.03%. Project has 15098 uncovered lines.

Files with missing lines (3)
File Patch % Lines
tracing_utils.py 36.99% ⚠️ 477 Missing and 28 partials
traces.py 36.77% ⚠️ 196 Missing
httpx.py 9.32% ⚠️ 107 Missing

Generated by Codecov Action

@github-actions
Copy link
Copy Markdown
Contributor

Codecov Results 📊


Generated by Codecov Action

@ericapisani
Copy link
Copy Markdown
Member Author

bugbot run

Comment thread sentry_sdk/traces.py
Comment thread sentry_sdk/integrations/httpx.py Outdated
@ericapisani ericapisani marked this pull request as ready for review April 16, 2026 20:35
@ericapisani ericapisani requested a review from a team as a code owner April 16, 2026 20:35
Copy link
Copy Markdown
Contributor

@sentrivana sentrivana left a comment

Choose a reason for hiding this comment

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

TY! Left some comments, the gist:

  • Any attributes we set on the span in streaming mode need to be in Sentry Conventions. I've tried to point out some that need changing but please go through the rest too and double check 🙏🏻
  • I'd remove all of the start_timestamp_monotonic_ms changes. I don't really see the benefit over just using the start_timestamp and removing it makes the new and old add_http_request_source similar enough that we can unify them.

Comment thread sentry_sdk/traces.py
Comment on lines +467 to +469
@property
def start_timestamp_monotonic_ns(self) -> "Optional[int]":
return self._start_timestamp_monotonic_ns
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.

Let's not expose new properties on the span -- the idea is to keep the API surface minimal. We can still use the attribute directly in our code if we need to, but I think we don't (see other comments).

Comment thread sentry_sdk/traces.py
Comment on lines +281 to +283
# profiling depends on this value and requires that
# it is measured in nanoseconds
self._start_timestamp_monotonic_ns = nanosecond_time()
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.

For context, this code including the try..except was ported as-is from the existing tracing implementation. I also don't know why we're guarding against AttributeErrors here (and later in _end), but it seems deliberate (this was before LLMs 😆). I wouldn't change it, or at least not in this PR unless we're 100% sure the exception handling is not necessary.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I also don't know why we're guarding against AttributeErrors here (and later in _end), but it seems deliberate (this was before LLMs 😆).

From my commit splunking, it looks like this try-except was introduced a while ago in the existing tracing implementation when Python 2 was still supported, and the perf_counter didn't exist on the time module just yet as it was first made available in Python 3.3..

Since we've dropped support for Python 2 within the SDK, this is safe to remove 🔥

Comment thread sentry_sdk/integrations/httpx.py Outdated
Comment thread sentry_sdk/integrations/httpx.py Outdated
Comment thread sentry_sdk/integrations/httpx.py Outdated
Comment thread sentry_sdk/tracing_utils.py Outdated
Comment thread sentry_sdk/tracing_utils.py Outdated
Comment thread sentry_sdk/integrations/httpx.py Outdated
Comment thread sentry_sdk/tracing_utils.py Outdated
Comment on lines +370 to +374
if span.start_timestamp_monotonic_ns is not None:
elapsed = nanosecond_time() - span.start_timestamp_monotonic_ns
end_timestamp = span.start_timestamp + timedelta(microseconds=elapsed / 1000)
else:
end_timestamp = datetime.now(timezone.utc)
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.

Why work with the monotonic start timestamp here at all? The original add_http_request_source just uses start_timestamp, so I believe that should be good enough?

Suggested change
if span.start_timestamp_monotonic_ns is not None:
elapsed = nanosecond_time() - span.start_timestamp_monotonic_ns
end_timestamp = span.start_timestamp + timedelta(microseconds=elapsed / 1000)
else:
end_timestamp = datetime.now(timezone.utc)
end_timestamp = datetime.now(timezone.utc)

Copy link
Copy Markdown
Contributor

@sentrivana sentrivana Apr 17, 2026

Choose a reason for hiding this comment

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

If we make the change I proposed above, then as far as I can see the only difference to the original add_http_request_source is just the source of end_timestamp and then I believe we don't need a separate function for the streaming case at all. We just need something like this in the original function:

if span.timestamp is not None:
    end_timestamp = span.timestamp
else:
    end_timestamp = datetime.now(timezone.utc)

Copy link
Copy Markdown
Member Author

@ericapisani ericapisani Apr 17, 2026

Choose a reason for hiding this comment

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

Why work with the monotonic start timestamp here at all? The original add_http_request_source just uses start_timestamp, so I believe that should be good enough?

I opted to use it in this way because the span.timestamp is set within _end using the start_timestamp_monotonic_ns (snippet below from within StreamedSpan but here's the link on main as well):

if self._timestamp is None:
            if self._start_timestamp_monotonic_ns is not None:
                elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns
                self._timestamp = self._start_timestamp + timedelta(
                    microseconds=elapsed / 1000
                )
            else:
                self._timestamp = datetime.now(timezone.utc)

and I was trying to emulate the behaviour as closely as possible (which is also why I exposed it as a property on the class).

If we want to specifically avoid exposing start_timestamp_monotonic_ns as a property, we could also explore adding a method that does this calculation for us to retain the behaviour.

I'm also realizing that, since the changes that I introduced unconditionally set the self._start_timestamp_monotonic_ns within the __init__, this can never be None and so the else falling back to a datetime stamp is dead code and needs to be removed.
-- edit: this is wrong, I forgot about the NoOpStreamedSpan class which would leave this uninitialized

Also - I think you're right that this can be consolidated into the single function (add_http_request_source), so will look to make those changes regardless.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've consolidated into a single function in 25a953e , but have left the start_timestamp_monotonic_ns changes in there for now

Comment thread sentry_sdk/integrations/httpx.py Outdated
Comment thread sentry_sdk/integrations/httpx.py Outdated
Comment thread sentry_sdk/integrations/httpx.py Outdated
Comment thread tests/integrations/httpx/test_httpx.py Outdated
@ericapisani ericapisani requested a review from sentrivana April 17, 2026 18:27
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9d2546f. Configure here.

Comment thread sentry_sdk/tracing_utils.py
@linear-code
Copy link
Copy Markdown

linear-code bot commented Apr 17, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate httpx to span first

2 participants