Skip to content

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

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

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

Triggered via pull request March 10, 2026 11:32
@sentrivanasentrivana
synchronize #5551
Status Success
Total duration 19s
Artifacts

changelog-preview.yml

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

Annotations

9 errors and 15 warnings
StreamedSpan.set_status() method does not exist - will raise AttributeError: sentry_sdk/integrations/sqlalchemy.py#L102
The code calls `span.set_status(SpanStatus.ERROR)` on a `StreamedSpan` instance at line 102, but `StreamedSpan` does not have a `set_status()` method. It only has a `status` property setter. This will cause an `AttributeError` at runtime when a SQL error occurs in streaming mode. Other integrations (e.g., tracing_utils.py:1140, celery/__init__.py:105) correctly use `span.status = SpanStatus.ERROR` for `StreamedSpan`.
[3LQ-X5U] StreamedSpan.set_status() method does not exist - will raise AttributeError (additional location): sentry_sdk/integrations/celery/__init__.py#L105
The code calls `span.set_status(SpanStatus.ERROR)` on a `StreamedSpan` instance at line 102, but `StreamedSpan` does not have a `set_status()` method. It only has a `status` property setter. This will cause an `AttributeError` at runtime when a SQL error occurs in streaming mode. Other integrations (e.g., tracing_utils.py:1140, celery/__init__.py:105) correctly use `span.status = SpanStatus.ERROR` for `StreamedSpan`.
AttributeError when using legacy Span with new scope methods: sentry_sdk/scope.py#L586
The code now calls `_to_traceparent()`, `_to_baggage()`, and `_get_trace_context()` on `self.span`, but `self.span` can be either a `StreamedSpan` (which has these private methods) or a legacy `Span` class (which only has `to_traceparent()`, `to_baggage()`, `get_trace_context()` without underscore prefix). When using the legacy non-streaming mode, calling these methods will raise `AttributeError: 'Span' object has no attribute '_to_traceparent'`.
UnboundLocalError when Redis command raises exception: sentry_sdk/integrations/redis/_sync_common.py#L158
When `old_execute_command` raises an exception, the `finally` block executes but `value` is never assigned. Line 158 calls `_set_cache_data(cache_span, self, cache_properties, value)` which will raise `UnboundLocalError: local variable 'value' referenced before assignment`. This masks the original Redis exception and breaks exception propagation.
[KJS-X9R] UnboundLocalError when Redis command raises exception (additional location): sentry_sdk/integrations/redis/_async_common.py#L145
When `old_execute_command` raises an exception, the `finally` block executes but `value` is never assigned. Line 158 calls `_set_cache_data(cache_span, self, cache_properties, value)` which will raise `UnboundLocalError: local variable 'value' referenced before assignment`. This masks the original Redis exception and breaks exception propagation.
StreamedSpan.set_status() does not exist - will raise AttributeError at runtime: sentry_sdk/integrations/sqlalchemy.py#L102
The code calls `span.set_status(SpanStatus.ERROR)` on line 102 when `span` is a `StreamedSpan` instance. However, `StreamedSpan` does not have a `set_status()` method - it only has a `status` property with a setter. This will raise an `AttributeError` at runtime when a SQL error occurs in span streaming mode. The fix should use `span.status = SpanStatus.ERROR` instead of `span.set_status(SpanStatus.ERROR)`.
[2U6-5QX] StreamedSpan.set_status() does not exist - will raise AttributeError at runtime (additional location): sentry_sdk/integrations/celery/__init__.py#L104
The code calls `span.set_status(SpanStatus.ERROR)` on line 102 when `span` is a `StreamedSpan` instance. However, `StreamedSpan` does not have a `set_status()` method - it only has a `status` property with a setter. This will raise an `AttributeError` at runtime when a SQL error occurs in span streaming mode. The fix should use `span.status = SpanStatus.ERROR` instead of `span.set_status(SpanStatus.ERROR)`.
[2U6-5QX] StreamedSpan.set_status() does not exist - will raise AttributeError at runtime (additional location): sentry_sdk/api.py#L532
The code calls `span.set_status(SpanStatus.ERROR)` on line 102 when `span` is a `StreamedSpan` instance. However, `StreamedSpan` does not have a `set_status()` method - it only has a `status` property with a setter. This will raise an `AttributeError` at runtime when a SQL error occurs in span streaming mode. The fix should use `span.status = SpanStatus.ERROR` instead of `span.set_status(SpanStatus.ERROR)`.
AttributeError when accessing _segment.set_attribute on NoOpStreamedSpan: sentry_sdk/integrations/strawberry.py#L225
When span streaming is enabled but a span is unsampled or ignored, `start_streamed_span` returns a `NoOpStreamedSpan`, which inherits from `StreamedSpan`. However, `NoOpStreamedSpan._segment` is set to `None`. At line 226, the code checks `isinstance(self.graphql_span, StreamedSpan)` which matches `NoOpStreamedSpan`, then calls `self.graphql_span._segment.set_attribute(...)`. This will raise `AttributeError: 'NoneType' object has no attribute 'set_attribute'` when the span is not sampled.
StreamedSpan not closed on error in Anthropic integration: sentry_sdk/integrations/anthropic.py#L572
When using span streaming mode, if an exception occurs during the Anthropic API call, the StreamedSpan is never closed. The `isinstance(span, Span)` check on line 572 only handles the legacy `Span` type. For `StreamedSpan`, `_capture_exception()` sets `span.status = SpanStatus.ERROR`, but since the type check fails, `span.__exit__()` is never called. This results in resource leaks and lost span data when errors occur in streaming mode.
[93W-HU4] StreamedSpan not closed on error in Anthropic integration (additional location): sentry_sdk/integrations/anthropic.py#L610
When using span streaming mode, if an exception occurs during the Anthropic API call, the StreamedSpan is never closed. The `isinstance(span, Span)` check on line 572 only handles the legacy `Span` type. For `StreamedSpan`, `_capture_exception()` sets `span.status = SpanStatus.ERROR`, but since the type check fails, `span.__exit__()` is never called. This results in resource leaks and lost span data when errors occur in streaming mode.
HTTP status code not recorded in span streaming mode: sentry_sdk/integrations/asgi.py#L289
In the span streaming path, when a StreamedSpan receives an HTTP response, only a simplified 'error'/'ok' status is set (lines 290-294). The actual HTTP status code is never recorded as an attribute. In contrast, the legacy Span path calls `set_http_status()` which records the status code as `http.response.status_code`. This results in loss of HTTP status code telemetry data when using the new span streaming mode.
[7W8-KHM] HTTP status code not recorded in span streaming mode (additional location): sentry_sdk/integrations/httpx.py#L116
In the span streaming path, when a StreamedSpan receives an HTTP response, only a simplified 'error'/'ok' status is set (lines 290-294). The actual HTTP status code is never recorded as an attribute. In contrast, the legacy Span path calls `set_http_status()` which records the status code as `http.response.status_code`. This results in loss of HTTP status code telemetry data when using the new span streaming mode.
[7W8-KHM] HTTP status code not recorded in span streaming mode (additional location): sentry_sdk/integrations/stdlib.py#L175
In the span streaming path, when a StreamedSpan receives an HTTP response, only a simplified 'error'/'ok' status is set (lines 290-294). The actual HTTP status code is never recorded as an attribute. In contrast, the legacy Span path calls `set_http_status()` which records the status code as `http.response.status_code`. This results in loss of HTTP status code telemetry data when using the new span streaming mode.
Missing exception handling causes spans to never close on Redis errors: sentry_sdk/integrations/redis/_async_common.py#L145
In `_sentry_execute_command`, `db_span.__enter__()` and `cache_span.__enter__()` are called, but there's no try/finally block wrapping `await old_execute_command(...)`. If the Redis command throws an exception, the spans' `__exit__` methods will never be called. This causes spans to leak on the scope (corrupting parent-child relationships for subsequent spans) and prevents the spans from being sent to Sentry. The sync version correctly uses try/finally (see `_sync_common.py` lines 151-160).
[BXJ-HEW] Missing exception handling causes spans to never close on Redis errors (additional location): sentry_sdk/integrations/redis/_sync_common.py#L158
In `_sentry_execute_command`, `db_span.__enter__()` and `cache_span.__enter__()` are called, but there's no try/finally block wrapping `await old_execute_command(...)`. If the Redis command throws an exception, the spans' `__exit__` methods will never be called. This causes spans to leak on the scope (corrupting parent-child relationships for subsequent spans) and prevents the spans from being sent to Sentry. The sync version correctly uses try/finally (see `_sync_common.py` lines 151-160).
AttributeError when NoOpStreamedSpan._segment is accessed: sentry_sdk/integrations/strawberry.py#L225
When span streaming is enabled but the span is either ignored or not sampled, `sentry_sdk.traces.start_span()` returns a `NoOpStreamedSpan` which sets `_segment = None`. The code at line 226 does `isinstance(self.graphql_span, StreamedSpan)` check which passes for `NoOpStreamedSpan` (it's a subclass), then accesses `self.graphql_span._segment.set_attribute(...)`. Since `_segment` is `None` for `NoOpStreamedSpan`, this raises `AttributeError: 'NoneType' object has no attribute 'set_attribute'`. This will crash the GraphQL request handling when a span is unsampled or ignored.
[TB2-LGN] AttributeError when NoOpStreamedSpan._segment is accessed (additional location): sentry_sdk/scope.py#L1286
When span streaming is enabled but the span is either ignored or not sampled, `sentry_sdk.traces.start_span()` returns a `NoOpStreamedSpan` which sets `_segment = None`. The code at line 226 does `isinstance(self.graphql_span, StreamedSpan)` check which passes for `NoOpStreamedSpan` (it's a subclass), then accesses `self.graphql_span._segment.set_attribute(...)`. Since `_segment` is `None` for `NoOpStreamedSpan`, this raises `AttributeError: 'NoneType' object has no attribute 'set_attribute'`. This will crash the GraphQL request handling when a span is unsampled or ignored.
Duplicate timestamp initialization causes inconsistent timing: sentry_sdk/traces.py#L290
The `__init__` method initializes `_start_timestamp`, `_timestamp`, and `_start_timestamp_monotonic_ns` twice (lines 280-288 and 290-298 are identical). This is a copy-paste bug that causes `_start_timestamp` and `_start_timestamp_monotonic_ns` to be set twice with slightly different values, as time passes between the two calls. The second assignment overwrites the first, making span start times slightly later than they should be.
NoOpStreamedSpan records lost events multiple times when end() is called more than once: sentry_sdk/traces.py#L623
The `NoOpStreamedSpan._end()` method calls `record_lost_event()` unconditionally without any guard to prevent duplicate execution. Unlike `StreamedSpan._end()` which checks `if self._timestamp is not None: return`, `NoOpStreamedSpan` has no such protection. This causes inaccurate lost event counting when: (1) `end()` is called explicitly and the span is also used as a context manager (both `end()` and `__exit__` trigger `_end()`), or (2) `end()` is called multiple times.
Span streaming mode creates spans for HTTP methods that should be skipped: sentry_sdk/integrations/asgi.py#L238
In the span streaming branch, when `ty == "http"` but the HTTP method is not in `http_methods_to_capture`, neither `continue_trace()` nor `new_trace()` is called (because the inner condition at line 223-226 fails), yet `start_span()` is still called unconditionally at line 238. In the legacy path, `transaction` would remain `None` and no span would be created. This behavioral difference means span streaming mode will create spans for HTTP requests that should be filtered out, potentially causing unexpected telemetry data.
[63E-3HA] Span streaming mode creates spans for HTTP methods that should be skipped (additional location): sentry_sdk/integrations/httpx.py#L116
In the span streaming branch, when `ty == "http"` but the HTTP method is not in `http_methods_to_capture`, neither `continue_trace()` nor `new_trace()` is called (because the inner condition at line 223-226 fails), yet `start_span()` is still called unconditionally at line 238. In the legacy path, `transaction` would remain `None` and no span would be created. This behavioral difference means span streaming mode will create spans for HTTP requests that should be filtered out, potentially causing unexpected telemetry data.
Duplicate timestamp initialization code in __init__: sentry_sdk/traces.py#L280
The `__init__` method contains two identical code blocks that initialize `_start_timestamp`, `_timestamp`, and `_start_timestamp_monotonic_ns`. Lines 280-288 set these values, then lines 290-298 set them again with identical code. This appears to be a merge/rebase error. While this doesn't cause runtime errors, it unnecessarily calls `datetime.now()` and `nanosecond_time()` twice, wasting CPU cycles and potentially causing slight timing inaccuracies if there's measurable time between the two calls.
Duplicate code recording lost event and scope restoration can both execute: sentry_sdk/traces.py#L623
In `NoOpStreamedSpan._end()`, `record_lost_event()` is called every time `_end()` is invoked, regardless of whether the span has a scope or was previously ended. This means if `_end()` is called multiple times (e.g., once explicitly and once via context manager `__exit__`), multiple lost events will be recorded for the same span. The scope detachment logic correctly checks for `_previous_span_on_scope` existence, but the lost event recording has no such guard.