Skip to content

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

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

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

Triggered via pull request March 4, 2026 15:54
Status Success
Total duration 2m 46s
Artifacts 2
Matrix: Network
All Network tests passed
4s
All Network tests passed
Fit to window
Zoom out
Zoom in

Annotations

7 errors and 45 warnings
UnboundLocalError when redis command raises exception: sentry_sdk/integrations/redis/_sync_common.py#L158
When `old_execute_command` raises an exception, the `value` variable is never assigned. The `finally` block then attempts to use `value` in `_set_cache_data(cache_span, self, cache_properties, value)`, which will raise an `UnboundLocalError`. This causes the actual Redis exception to be suppressed and replaced with a confusing internal error.
StreamedSpan lacks set_status() method causing AttributeError: sentry_sdk/integrations/sqlalchemy.py#L102
The code calls `span.set_status(SpanStatus.ERROR)` on a `StreamedSpan` instance, but `StreamedSpan` does not have a `set_status()` method - it only has a `status` property setter. This will raise an `AttributeError` at runtime when a SQLAlchemy query errors in streaming mode. Other integrations (e.g., Celery at line 105) correctly use `span.status = SpanStatus.ERROR` instead.
TypeError when comparing None sample_rand to sample_rate: sentry_sdk/tracing_utils.py#L1551
When `propagation_context.baggage` exists but doesn't contain a valid `sample_rand` value, `propagation_context.baggage._sample_rand()` returns `None`. This `None` value is then used in the comparison `sampled = sample_rand < sample_rate` at line 1551, which will raise a `TypeError: '<' not supported between instances of 'NoneType' and 'float'`. This occurs when baggage is present but malformed or missing the sample_rand field.
StreamedSpan lacks set_status() method causing AttributeError on SQL errors: sentry_sdk/integrations/sqlalchemy.py#L102
The code calls `span.set_status(SpanStatus.ERROR)` on StreamedSpan instances at line 102, but StreamedSpan class in traces.py does not define a `set_status()` method. It only has a `status` property setter. When a SQL error occurs and the span is a StreamedSpan, this will raise an AttributeError, causing the error handler to fail.
[KE9-DC2] StreamedSpan lacks set_status() method causing AttributeError on SQL errors (additional location): sentry_sdk/integrations/anthropic.py#L572
The code calls `span.set_status(SpanStatus.ERROR)` on StreamedSpan instances at line 102, but StreamedSpan class in traces.py does not define a `set_status()` method. It only has a `status` property setter. When a SQL error occurs and the span is a StreamedSpan, this will raise an AttributeError, causing the error handler to fail.
[KE9-DC2] StreamedSpan lacks set_status() method causing AttributeError on SQL errors (additional location): sentry_sdk/tracing_utils.py#L1124
The code calls `span.set_status(SpanStatus.ERROR)` on StreamedSpan instances at line 102, but StreamedSpan class in traces.py does not define a `set_status()` method. It only has a `status` property setter. When a SQL error occurs and the span is a StreamedSpan, this will raise an AttributeError, causing the error handler to fail.
TypeError when sample_rand is None in make_sampling_decision: sentry_sdk/tracing_utils.py#L1551
In `make_sampling_decision()`, when `propagation_context.baggage` exists but `baggage._sample_rand()` returns `None` (which happens when the incoming trace has inconsistent sample_rate and parent_sampled values that cause `_fill_sample_rand()` to fail), the code proceeds to line 1551 where `sampled = sample_rand < sample_rate` raises `TypeError: '<' not supported between instances of 'NoneType' and 'float'`. This can crash the application when handling malformed incoming trace headers from upstream services.
Network (3.11, ubuntu-22.04)
❌ Patch coverage check failed: 17.19% < target 80%
Network (3.6, ubuntu-22.04)
❌ Patch coverage check failed: 18.66% < target 80%
Network (3.6, ubuntu-22.04)
Failed to upload coverage artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.6, ubuntu-22.04)
Failed to upload test artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.8, ubuntu-22.04)
❌ Patch coverage check failed: 17.19% < target 80%
Network (3.8, ubuntu-22.04)
Failed to upload coverage artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.8, ubuntu-22.04)
Failed to upload test artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.7, ubuntu-22.04)
❌ Patch coverage check failed: 17.19% < target 80%
Network (3.7, ubuntu-22.04)
Failed to upload coverage artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.7, ubuntu-22.04)
Failed to upload test artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.10, ubuntu-22.04)
❌ Patch coverage check failed: 17.19% < target 80%
Network (3.10, ubuntu-22.04)
Failed to upload coverage artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.10, ubuntu-22.04)
Failed to upload test artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.12, ubuntu-22.04)
❌ Patch coverage check failed: 17.19% < target 80%
Network (3.12, ubuntu-22.04)
Failed to upload coverage artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.12, ubuntu-22.04)
Failed to upload test artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.14, ubuntu-22.04)
❌ Patch coverage check failed: 14.83% < target 80%
Network (3.14, ubuntu-22.04)
Failed to upload coverage artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.14, ubuntu-22.04)
Failed to upload test artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.14t, ubuntu-22.04)
❌ Patch coverage check failed: 14.83% < target 80%
Network (3.14t, ubuntu-22.04)
Failed to upload coverage artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.14t, ubuntu-22.04)
Failed to upload test artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.9, ubuntu-22.04)
❌ Patch coverage check failed: 18.60% < target 80%
Network (3.9, ubuntu-22.04)
Failed to upload coverage artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.9, ubuntu-22.04)
Failed to upload test artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.13, ubuntu-22.04)
❌ Patch coverage check failed: 14.83% < target 80%
Network (3.13, ubuntu-22.04)
Failed to upload coverage artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
Network (3.13, ubuntu-22.04)
Failed to upload test artifact: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
StreamedSpan not closed on error in streaming mode: sentry_sdk/integrations/anthropic.py#L610
When using streaming mode (`_experiments={"trace_lifecycle": "stream"}`), if an exception occurs during the Anthropic API call, the `StreamedSpan` will not have its `__exit__()` method called. The change from `span is not None` to `isinstance(span, Span)` excludes `StreamedSpan` instances from the error cleanup path. Since `_sentry_patched_create_common` uses `get_start_span_function()` which returns `StreamedSpan` in streaming mode, and `span.__enter__()` is called on line 413, the span's cleanup (`_end()`) will never run on error, leaving spans unclosed and scope state corrupted.
Span streaming mode ignores http_methods_to_capture filter: sentry_sdk/integrations/asgi.py#L238
In span streaming mode, when the request type is 'http' but the method is not in `http_methods_to_capture`, the code still creates a span (lines 238-240). In contrast, the non-streaming mode correctly sets `transaction = None` and uses `nullcontext()`, creating no span. This causes streaming mode to trace HTTP methods (e.g., OPTIONS, HEAD) that should be filtered out according to the middleware configuration.
_set_status ignores status parameter for StreamedSpan, always sets ERROR: sentry_sdk/integrations/celery/__init__.py#L104
The `_set_status` function receives a `status` string parameter ('aborted' or 'internal_error'), but for `StreamedSpan` instances, it ignores this parameter and always sets `SpanStatus.ERROR`. This means that when Celery control flow exceptions (Retry, Ignore, Reject) trigger `_set_status('aborted')`, the span is incorrectly marked as ERROR instead of being handled appropriately. This could result in misleading error metrics in Sentry where task retries are counted as errors.
HTTP status code attribute not set for StreamedSpan: sentry_sdk/integrations/httpx.py#L117
The StreamedSpan path only sets `span.status` and `reason` attribute but does not set `SPANDATA.HTTP_STATUS_CODE` (http.response.status_code). The old API via `set_http_status()` sets this attribute, which is used by downstream processing like `http_status_code_to_event_level()` in tracing_utils.py. This results in data parity loss between streaming and non-streaming modes.
[VXN-676] HTTP status code attribute not set for StreamedSpan (additional location): sentry_sdk/integrations/httpx.py#L200
The StreamedSpan path only sets `span.status` and `reason` attribute but does not set `SPANDATA.HTTP_STATUS_CODE` (http.response.status_code). The old API via `set_http_status()` sets this attribute, which is used by downstream processing like `http_status_code_to_event_level()` in tracing_utils.py. This results in data parity loss between streaming and non-streaming modes.
[VXN-676] HTTP status code attribute not set for StreamedSpan (additional location): sentry_sdk/integrations/stdlib.py#L175
The StreamedSpan path only sets `span.status` and `reason` attribute but does not set `SPANDATA.HTTP_STATUS_CODE` (http.response.status_code). The old API via `set_http_status()` sets this attribute, which is used by downstream processing like `http_status_code_to_event_level()` in tracing_utils.py. This results in data parity loss between streaming and non-streaming modes.
_end() returns early without setting _finished flag when client is inactive: sentry_sdk/traces.py#L326
When `client.is_active()` returns False at line 326, the method returns early without setting `self._finished = True`. However, the scope detachment (lines 317-321) and profiler stop (line 314) have already occurred. If `_end()` is called again later when the client becomes active, the method will: (1) try to access the deleted `_context_manager_state` attribute causing a silently-swallowed AttributeError, and (2) attempt to stop an already-stopped profiler. While `capture_internal_exceptions()` prevents crashes, this leads to inconsistent state and harder-to-debug issues.
NoOpStreamedSpan missing attribute initialization causes AttributeError: sentry_sdk/traces.py#L539
NoOpStreamedSpan inherits from StreamedSpan but its `__init__` doesn't initialize many attributes that inherited methods/properties access. Properties like `active`, `timestamp`, `start_timestamp`, `status`, and methods like `get_baggage()`, `get_trace_context()`, `dynamic_sampling_context()` will raise `AttributeError` when called because attributes like `_active`, `_timestamp`, `_start_timestamp`, `_status`, `_baggage`, `_parent_span_id` are never set. Any code path using these NoOpStreamedSpan instances that accesses these inherited members will fail at runtime.
http_methods_to_capture filter bypassed in span streaming mode: sentry_sdk/integrations/asgi.py#L238
In span streaming mode, when `ty in ("http", "websocket")` but the method is NOT in `http_methods_to_capture`, the code still proceeds to call `sentry_sdk.traces.start_span()` at line 238. In contrast, the legacy path correctly sets `transaction = None` and uses `nullcontext()` to skip span creation. This causes spans to be created for HTTP methods that should be filtered out (e.g., OPTIONS, HEAD when only GET/POST are configured), leading to unwanted telemetry data.
HTTP status code attribute missing for StreamedSpan in httpx integration: sentry_sdk/integrations/httpx.py#L116
When span streaming is enabled, the HTTP status code is not recorded as an attribute on the span. The legacy `Span.set_http_status()` method sets both the span status and `SPANDATA.HTTP_STATUS_CODE` (http.response.status_code), but the StreamedSpan path only sets the status and 'reason' attribute. This means spans created in streaming mode will be missing the http.response.status_code attribute, which may affect observability, querying, and dashboards in Sentry that rely on this attribute.
[RQZ-FEP] HTTP status code attribute missing for StreamedSpan in httpx integration (additional location): sentry_sdk/integrations/stdlib.py#L175
When span streaming is enabled, the HTTP status code is not recorded as an attribute on the span. The legacy `Span.set_http_status()` method sets both the span status and `SPANDATA.HTTP_STATUS_CODE` (http.response.status_code), but the StreamedSpan path only sets the status and 'reason' attribute. This means spans created in streaming mode will be missing the http.response.status_code attribute, which may affect observability, querying, and dashboards in Sentry that rely on this attribute.
Async Redis spans leak when command raises exception: sentry_sdk/integrations/redis/_async_common.py#L145
In `_sentry_execute_command`, both `db_span` and `cache_span` are entered via `__enter__()` but if `await old_execute_command()` raises an exception, the `__exit__()` calls at lines 152-156 (in context after) are never executed. This causes span resource leaks and corrupts the span tree because active spans remain attached to the scope. The synchronous version in `_sync_common.py` correctly uses a `try/finally` block (lines 151-160) to ensure spans are always closed.
[YBD-DXZ] Async Redis spans leak when command raises exception (additional location): sentry_sdk/integrations/redis/_sync_common.py#L154
In `_sentry_execute_command`, both `db_span` and `cache_span` are entered via `__enter__()` but if `await old_execute_command()` raises an exception, the `__exit__()` calls at lines 152-156 (in context after) are never executed. This causes span resource leaks and corrupts the span tree because active spans remain attached to the scope. The synchronous version in `_sync_common.py` correctly uses a `try/finally` block (lines 151-160) to ensure spans are always closed.
[YBD-DXZ] Async Redis spans leak when command raises exception (additional location): sentry_sdk/_span_batcher.py#L69
In `_sentry_execute_command`, both `db_span` and `cache_span` are entered via `__enter__()` but if `await old_execute_command()` raises an exception, the `__exit__()` calls at lines 152-156 (in context after) are never executed. This causes span resource leaks and corrupts the span tree because active spans remain attached to the scope. The synchronous version in `_sync_common.py` correctly uses a `try/finally` block (lines 151-160) to ensure spans are always closed.
[YBD-DXZ] Async Redis spans leak when command raises exception (additional location): sentry_sdk/integrations/redis/_sync_common.py#L158
In `_sentry_execute_command`, both `db_span` and `cache_span` are entered via `__enter__()` but if `await old_execute_command()` raises an exception, the `__exit__()` calls at lines 152-156 (in context after) are never executed. This causes span resource leaks and corrupts the span tree because active spans remain attached to the scope. The synchronous version in `_sync_common.py` correctly uses a `try/finally` block (lines 151-160) to ensure spans are always closed.
AttributeError when legacy Span is used as parent in start_streamed_span: sentry_sdk/scope.py#L1297
At line 1240, `parent_span` is assigned from `self.span or self.get_current_scope().span`, which can be a legacy `Span` object (from `sentry_sdk.tracing.Span`). The code at line 1297 accesses `parent_span._segment`, but legacy `Span` does not have a `_segment` attribute. This will raise an `AttributeError` if a legacy span exists on the scope when `start_streamed_span` is called. While the PR states the old and new APIs should not be mixed, there's no runtime guard to provide a helpful error message or graceful degradation.
NoOpStreamedSpan._end can record duplicate lost events and fail on second call: sentry_sdk/traces.py#L587
Unlike `StreamedSpan._end()` which has a `_finished` guard, `NoOpStreamedSpan._end()` has no protection against being called multiple times. Each call will record a lost event (lines 596-600), inflating lost event counts. If `_scope` is set, the second call will also try to access `_context_manager_state` which was deleted, causing an AttributeError (caught by `capture_internal_exceptions`).

Artifacts

Produced during runtime
Name Size Digest
codecov-coverage-results-feat-span-first-test-network Expired
109 KB
sha256:3addfe4b3450f10c29ea475fa6249c26c5d5aa63a3c7232735263d62370ec295
codecov-test-results-feat-span-first-test-network Expired
232 Bytes
sha256:bf149dd6e1d9847cb73cc217abd3454b4eef5fa3674acfed0db6aed1b91075ca