Skip to content

[do not merge] feat: Span streaming & new span API #1075

[do not merge] feat: Span streaming & new span API

[do not merge] feat: Span streaming & new span API #1075

Triggered via pull request February 25, 2026 10:34
@sentrivanasentrivana
synchronize #5317
Status Success
Total duration 17s
Artifacts

changelog-preview.yml

on: pull_request_target
changelog-preview  /  preview
13s
changelog-preview / preview
Fit to window
Zoom out
Zoom in

Annotations

6 errors and 18 warnings
UnboundLocalError crash when exception occurs inside capture_internal_exceptions block: sentry_sdk/integrations/celery/__init__.py#L324
The variable `span_ctx` is declared at line 324 but only assigned inside the `capture_internal_exceptions()` block (lines 337/360). If an exception occurs after `transaction` is assigned (e.g., at line 333-335 or 349-359) but before `span_ctx` is assigned, the exception is suppressed but `span_ctx` remains unbound. The check at line 362 only verifies `transaction is not None`, then line 365 `with span_ctx:` will raise `UnboundLocalError`, crashing the Celery task execution.
StreamedSpan imported under TYPE_CHECKING but used at runtime with isinstance(): sentry_sdk/integrations/graphene.py#L30
StreamedSpan is imported inside the `if TYPE_CHECKING:` block (line 30), but it's used in an `isinstance()` check at runtime (line 167 in `graphql_span`). When span streaming is enabled and the `finally` block executes, a `NameError: name 'StreamedSpan' is not defined` will be raised because the import is not available at runtime.
[K2Y-Z8F] StreamedSpan imported under TYPE_CHECKING but used at runtime with isinstance() (additional location): sentry_sdk/integrations/stdlib.py#L127
StreamedSpan is imported inside the `if TYPE_CHECKING:` block (line 30), but it's used in an `isinstance()` check at runtime (line 167 in `graphql_span`). When span streaming is enabled and the `finally` block executes, a `NameError: name 'StreamedSpan' is not defined` will be raised because the import is not available at runtime.
AttributeError when calling set_transaction_name with NoOpStreamedSpan as current span: sentry_sdk/scope.py#L828
In `set_transaction_name`, the code checks `isinstance(self._span, StreamedSpan)` and then accesses `self._span.segment.set_name(name)`. However, `NoOpStreamedSpan` (a subclass of `StreamedSpan`) sets `segment = None` in its constructor. Since `isinstance()` returns True for subclasses, when the current span is a `NoOpStreamedSpan`, accessing `.segment.set_name()` will raise `AttributeError: 'NoneType' object has no attribute 'set_name'`. This can occur when spans are filtered out via `is_ignored_span()` or when a span has no name.
AttributeError when calling set_transaction_name with NoOpStreamedSpan on scope: sentry_sdk/scope.py#L829
The code calls `self._span.segment.set_name(name)` when `self._span` is an instance of `StreamedSpan`. However, `NoOpStreamedSpan` is a subclass of `StreamedSpan` that sets `self.segment = None` in its constructor. When `self._span` is a `NoOpStreamedSpan`, accessing `self._span.segment.set_name(name)` will raise `AttributeError: 'NoneType' object has no attribute 'set_name'`. The code should either check `isinstance(self._span, NoOpStreamedSpan)` first or use a null check on `segment` before calling methods on it.
[VTF-C8W] AttributeError when calling set_transaction_name with NoOpStreamedSpan on scope (additional location): sentry_sdk/traces.py#L527
The code calls `self._span.segment.set_name(name)` when `self._span` is an instance of `StreamedSpan`. However, `NoOpStreamedSpan` is a subclass of `StreamedSpan` that sets `self.segment = None` in its constructor. When `self._span` is a `NoOpStreamedSpan`, accessing `self._span.segment.set_name(name)` will raise `AttributeError: 'NoneType' object has no attribute 'set_name'`. The code should either check `isinstance(self._span, NoOpStreamedSpan)` first or use a null check on `segment` before calling methods on it.
Async httpx client doesn't strip existing Sentry baggage before appending, causing duplicate entries: sentry_sdk/integrations/httpx.py#L186
The async client (lines 186-192) handles baggage headers differently from the sync client. The sync client uses `add_sentry_baggage_to_headers()` which strips existing Sentry items before appending new ones. The async client simply appends the new baggage without stripping, potentially causing duplicate or conflicting Sentry baggage items in outgoing requests. This inconsistency could lead to trace propagation issues and confusing debugging scenarios.
Spans never closed when Redis command throws exception: sentry_sdk/integrations/redis/_async_common.py#L135
In `_sentry_execute_command`, `db_span.__enter__()` and optionally `cache_span.__enter__()` are called manually, but if `await old_execute_command()` raises an exception, `__exit__()` is never called on either span. This causes spans to leak (never be finished/sent to Sentry) and prevents exception status from being recorded on the spans. The proper context manager protocol requires a try/finally block when using manual `__enter__`/`__exit__` calls.
[5Y2-CP9] Spans never closed when Redis command throws exception (additional location): sentry_sdk/integrations/redis/_sync_common.py#L135
In `_sentry_execute_command`, `db_span.__enter__()` and optionally `cache_span.__enter__()` are called manually, but if `await old_execute_command()` raises an exception, `__exit__()` is never called on either span. This causes spans to leak (never be finished/sent to Sentry) and prevents exception status from being recorded on the spans. The proper context manager protocol requires a try/finally block when using manual `__enter__`/`__exit__` calls.
Missing parent_span in on_parse causes incorrect span parenting in streaming mode: sentry_sdk/integrations/strawberry.py#L261
In `on_parse()`, when span streaming is enabled, `sentry_sdk.traces.start_span()` is called without the `parent_span` argument (line 261-262). This is inconsistent with `on_validate()` which correctly passes `parent_span=self.graphql_span`. Without the explicit parent, the parsing span will be parented to whatever span is currently active on the scope, which may not be `self.graphql_span`, resulting in incorrect trace hierarchies.
Async resolve() missing set_op() and set_origin() for StreamedSpan: sentry_sdk/integrations/strawberry.py#L314
The async `SentryAsyncExtension.resolve()` method does not call `set_op(OP.GRAPHQL_RESOLVE)` or `set_origin(StrawberryIntegration.origin)` when creating a StreamedSpan, while the sync `SentrySyncExtension.resolve()` method does. This inconsistency means async GraphQL field resolution spans will lack operation type and origin metadata, making them harder to identify and filter in Sentry's UI.
Span data silently lost when end() called without start(): sentry_sdk/traces.py#L376
The `end()` method (line 376) calls `__exit__()` which accesses `self._context_manager_state` (line 347). This attribute is only set during `__enter__()` (called by `start()`). If a user creates a span and calls `end()` without first calling `start()`, an AttributeError is raised but silently swallowed by `capture_internal_exceptions()`. The span is never captured and no error is reported to the user, causing silent data loss.
NoOpStreamedSpan.dynamic_sampling_context() will crash with AttributeError: sentry_sdk/traces.py#L527
The `dynamic_sampling_context()` method is inherited from `StreamedSpan` but not overridden in `NoOpStreamedSpan`. When called, it executes `self.segment.get_baggage().dynamic_sampling_context()`. Since `NoOpStreamedSpan.__init__` sets `self.segment = None`, this will raise `AttributeError: 'NoneType' object has no attribute 'get_baggage'`. Any code that obtains a span and calls `dynamic_sampling_context()` without checking the span type will crash.
[EVH-QF4] NoOpStreamedSpan.dynamic_sampling_context() will crash with AttributeError (additional location): sentry_sdk/traces.py#L576
The `dynamic_sampling_context()` method is inherited from `StreamedSpan` but not overridden in `NoOpStreamedSpan`. When called, it executes `self.segment.get_baggage().dynamic_sampling_context()`. Since `NoOpStreamedSpan.__init__` sets `self.segment = None`, this will raise `AttributeError: 'NoneType' object has no attribute 'get_baggage'`. Any code that obtains a span and calls `dynamic_sampling_context()` without checking the span type will crash.
_running_size not updated when MAX_BEFORE_FLUSH triggers flush: sentry_sdk/_span_batcher.py#L68
When `size + 1 >= MAX_BEFORE_FLUSH` on line 68, the code returns early without updating `_running_size[span.trace_id]`. The span has already been added to `_span_buffer` (line 66), but its size is never tracked. If the flush event doesn't clear all buffers immediately (e.g., due to threading), subsequent byte-based flush decisions will underestimate the actual buffer size, potentially causing memory growth beyond the intended limit.
[ZA4-GL3] _running_size not updated when MAX_BEFORE_FLUSH triggers flush (additional location): sentry_sdk/integrations/redis/utils.py#L152
When `size + 1 >= MAX_BEFORE_FLUSH` on line 68, the code returns early without updating `_running_size[span.trace_id]`. The span has already been added to `_span_buffer` (line 66), but its size is never tracked. If the flush event doesn't clear all buffers immediately (e.g., due to threading), subsequent byte-based flush decisions will underestimate the actual buffer size, potentially causing memory growth beyond the intended limit.
StreamedSpan status hardcoded to ERROR, ignoring actual status parameter: sentry_sdk/integrations/celery/__init__.py#L104
When `scope.span` is a `StreamedSpan`, the code always sets `SpanStatus.ERROR` regardless of the actual `status` parameter passed to `_set_status`. This loses the distinction between "aborted" (for control flow exceptions like Retry, Ignore, Reject) and "internal_error" (for actual failures). Since `StreamedSpan.set_status()` accepts `Union[SpanStatus, str]` and handles both types correctly, the status parameter should be passed through.
[399-3R6] StreamedSpan status hardcoded to ERROR, ignoring actual status parameter (additional location): sentry_sdk/integrations/strawberry.py#L314
When `scope.span` is a `StreamedSpan`, the code always sets `SpanStatus.ERROR` regardless of the actual `status` parameter passed to `_set_status`. This loses the distinction between "aborted" (for control flow exceptions like Retry, Ignore, Reject) and "internal_error" (for actual failures). Since `StreamedSpan.set_status()` accepts `Union[SpanStatus, str]` and handles both types correctly, the status parameter should be passed through.
UnboundLocalError possible when span_streaming is enabled and span setup fails: sentry_sdk/integrations/celery/__init__.py#L324
In `_wrap_tracer`, the `span_ctx` variable is declared with a type annotation but not initialized. In the span_streaming branch, if `start_span()` succeeds but any subsequent setter method (`set_origin`, `set_source`, `set_op`) throws an exception, the exception is suppressed by `capture_internal_exceptions()` but `span_ctx` is never assigned. Since `transaction` is not None, the code proceeds to `with span_ctx:` (line 365), causing an `UnboundLocalError` that crashes the Celery task.
Missing test coverage for httpx integration with span streaming mode: sentry_sdk/integrations/httpx.py#L64
The httpx integration now supports both legacy tracing and the new span streaming mode (`trace_lifecycle: stream`), but the existing tests in `tests/integrations/httpx/test_httpx.py` only test the legacy mode using `start_transaction()`. The new StreamedSpan code paths (lines 64-67, 80-88, 145-148, 161-168) are not covered by any integration tests, meaning bugs in span attribute setting or the streaming workflow won't be caught.
Spans leak when Redis command raises exception: sentry_sdk/integrations/redis/_async_common.py#L135
In `_sentry_execute_command`, spans are opened via `__enter__()` but closed via `__exit__(None, None, None)` without a try/finally block. If `await old_execute_command()` raises an exception, both `db_span` and `cache_span` will never have `__exit__` called, leaving them unclosed. This can cause memory leaks and incorrect tracing data (spans that are never finished).
[DU7-VFT] Spans leak when Redis command raises exception (additional location): sentry_sdk/integrations/redis/_sync_common.py#L135
In `_sentry_execute_command`, spans are opened via `__enter__()` but closed via `__exit__(None, None, None)` without a try/finally block. If `await old_execute_command()` raises an exception, both `db_span` and `cache_span` will never have `__exit__` called, leaving them unclosed. This can cause memory leaks and incorrect tracing data (spans that are never finished).
Missing parent_span in on_parse causes orphaned span in streaming mode: sentry_sdk/integrations/strawberry.py#L261
In `on_parse()` for streaming mode (line 261-262), `sentry_sdk.traces.start_span()` is called without the `parent_span=self.graphql_span` argument, unlike `on_validate()` (line 239-240) which correctly passes it. This inconsistency means the parsing span will be parented to whatever span is currently active on the scope rather than explicitly to `self.graphql_span`, potentially causing incorrect span hierarchy in the trace.
Scope resolved but not used when capturing span: sentry_sdk/traces.py#L438
In `_end()`, the `scope` variable is carefully resolved on lines 397-399 to use the passed scope, `self._scope`, or the current scope as fallback. However, on line 438, `sentry_sdk.get_current_scope()._capture_span(self)` ignores this resolved scope and fetches a fresh current scope instead. If the current scope has changed between entering and ending the span (e.g., in async contexts), this could cause the span to be captured with incorrect scope data.