[do not merge] feat: Span streaming & new span API #1545
Triggered via pull request
March 10, 2026 11:32
sentrivana
synchronize
#5551
Status
Success
Total duration
19s
Artifacts
–
changelog-preview.yml
on: pull_request_target
changelog-preview
/
preview
15s
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.
|