From 10e96aa708f6589be872a97695384775f4aa8999 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 1 May 2026 12:05:57 -0400 Subject: [PATCH 1/3] ref: Introduce `_is_sampled_streamed_span()` helper to replace repeated check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `isinstance(span, StreamedSpan) and not isinstance(span, NoOpStreamedSpan)` pattern was scattered across the codebase and easy to get wrong — forgetting the `NoOpStreamedSpan` guard is a recurring code review catch. Extract it into a single `_is_sampled_streamed_span()` helper in `traces.py` with a `TypeGuard[StreamedSpan]` return type for proper type narrowing, and replace all instances of the combined check in `scope.py`, `starlette.py`, and `fastapi.py`. --- sentry_sdk/integrations/fastapi.py | 6 ++---- sentry_sdk/integrations/starlette.py | 13 +++---------- sentry_sdk/scope.py | 25 +++++++++---------------- sentry_sdk/traces.py | 16 +++++++++++++++- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 10d4fd81ac..713c6d9efa 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -5,7 +5,7 @@ import sentry_sdk from sentry_sdk.integrations import DidNotEnable from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import NoOpStreamedSpan, StreamedSpan +from sentry_sdk.traces import _is_sampled_streamed_span from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import transaction_from_function @@ -95,9 +95,7 @@ def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": if has_span_streaming_enabled(client.options): current_span = current_scope.streamed_span - if isinstance(current_span, StreamedSpan) and not isinstance( - current_span, NoOpStreamedSpan - ): + if _is_sampled_streamed_span(current_span): segment = current_span._segment segment._update_active_thread() diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 68ff98cc02..b1134a436a 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -21,7 +21,7 @@ ) from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import NoOpStreamedSpan, StreamedSpan, _get_current_streamed_span +from sentry_sdk.traces import _get_current_streamed_span, _is_sampled_streamed_span from sentry_sdk.tracing import ( SOURCE_FOR_STYLE, TransactionSource, @@ -255,12 +255,7 @@ def _set_request_body_data_on_streaming_segment( info: "Optional[Dict[str, Any]]", ) -> None: current_span = _get_current_streamed_span() - if ( - info - and "data" in info - and isinstance(current_span, StreamedSpan) - and not isinstance(current_span, NoOpStreamedSpan) - ): + if info and "data" in info and _is_sampled_streamed_span(current_span): with capture_internal_exceptions(): current_span._segment.set_attribute( "http.request.body.data", @@ -557,9 +552,7 @@ def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": if span_streaming: current_span = current_scope.streamed_span - if isinstance(current_span, StreamedSpan) and not isinstance( - current_span, NoOpStreamedSpan - ): + if _is_sampled_streamed_span(current_span): current_span._segment._update_active_thread() elif current_scope.transaction is not None: current_scope.transaction.update_active_thread() diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 656fabc564..2da13158e3 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -34,7 +34,12 @@ _make_sampling_decision, PropagationContext, ) -from sentry_sdk.traces import _DEFAULT_PARENT_SPAN, StreamedSpan, NoOpStreamedSpan +from sentry_sdk.traces import ( + _DEFAULT_PARENT_SPAN, + StreamedSpan, + NoOpStreamedSpan, + _is_sampled_streamed_span, +) from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, @@ -585,11 +590,7 @@ def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": span_streaming = has_span_streaming_enabled(client.options) # If we have an active span, return traceparent from there - if ( - span_streaming - and self.streamed_span is not None - and not isinstance(self.streamed_span, NoOpStreamedSpan) - ): + if span_streaming and _is_sampled_streamed_span(self.streamed_span): return self.streamed_span._to_traceparent() elif not span_streaming and self.span is not None: return self.span._to_traceparent() @@ -609,11 +610,7 @@ def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": span_streaming = has_span_streaming_enabled(client.options) # If we have an active span, return baggage from there - if ( - span_streaming - and self.streamed_span is not None - and not isinstance(self.streamed_span, NoOpStreamedSpan) - ): + if span_streaming and _is_sampled_streamed_span(self.streamed_span): return self.streamed_span._to_baggage() elif not span_streaming and self.span is not None: return self.span._to_baggage() @@ -918,11 +915,7 @@ def streamed_span(self, span: "Optional[StreamedSpan]") -> None: # Also set _transaction and _transaction_info in streaming mode as this # is used for populating events and linking them to segments - if ( - isinstance(span, StreamedSpan) - and not isinstance(span, NoOpStreamedSpan) - and span._is_segment() - ): + if _is_sampled_streamed_span(span) and span._is_segment(): self._transaction = span.name if span._attributes.get("sentry.span.source"): self._transaction_info["source"] = str( diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 47c532c523..f31b8960af 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -31,7 +31,16 @@ ) if TYPE_CHECKING: - from typing import Any, Callable, Iterator, Optional, ParamSpec, TypeVar, Union + from typing import ( + Any, + Callable, + Iterator, + Optional, + ParamSpec, + TypeGuard, + TypeVar, + Union, + ) from sentry_sdk._types import Attributes, AttributeValue from sentry_sdk.profiler.continuous_profiler import ContinuousProfile @@ -757,6 +766,11 @@ def make_db_query(sql): return decorator +def _is_sampled_streamed_span(span: "Any") -> "TypeGuard[StreamedSpan]": + """Returns True if span is a StreamedSpan that was sampled (not a NoOpStreamedSpan).""" + return isinstance(span, StreamedSpan) and not isinstance(span, NoOpStreamedSpan) + + def _get_current_streamed_span( scope: "Optional[sentry_sdk.Scope]" = None, ) -> "Optional[StreamedSpan]": From 203c816c66856a89d483fb77de71a1b55e2d7e85 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Tue, 5 May 2026 07:56:12 -0400 Subject: [PATCH 2/3] use type method instead of isinstance --- sentry_sdk/integrations/fastapi.py | 8 ++-- sentry_sdk/integrations/starlette.py | 16 +++---- sentry_sdk/scope.py | 70 ++++++++++++++-------------- sentry_sdk/traces.py | 7 +-- 4 files changed, 50 insertions(+), 51 deletions(-) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 713c6d9efa..f80052cd63 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -1,19 +1,19 @@ import sys from copy import deepcopy from functools import wraps +from typing import TYPE_CHECKING import sentry_sdk from sentry_sdk.integrations import DidNotEnable from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import _is_sampled_streamed_span +from sentry_sdk.traces import _is_streamed_span from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import transaction_from_function -from typing import TYPE_CHECKING - if TYPE_CHECKING: from typing import Any, Callable, Dict + from sentry_sdk._types import Event try: @@ -95,7 +95,7 @@ def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": if has_span_streaming_enabled(client.options): current_span = current_scope.streamed_span - if _is_sampled_streamed_span(current_span): + if _is_streamed_span(current_span): segment = current_span._segment segment._update_active_thread() diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index b1134a436a..d9367fa495 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -1,17 +1,18 @@ import functools import json -import warnings import sys +import warnings from collections.abc import Set from copy import deepcopy from json import JSONDecodeError +from typing import TYPE_CHECKING import sentry_sdk from sentry_sdk.consts import OP from sentry_sdk.integrations import ( + _DEFAULT_FAILED_REQUEST_STATUS_CODES, DidNotEnable, Integration, - _DEFAULT_FAILED_REQUEST_STATUS_CODES, ) from sentry_sdk.integrations._wsgi_common import ( DEFAULT_HTTP_METHODS_TO_CAPTURE, @@ -21,7 +22,7 @@ ) from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import _get_current_streamed_span, _is_sampled_streamed_span +from sentry_sdk.traces import _get_current_streamed_span, _is_streamed_span from sentry_sdk.tracing import ( SOURCE_FOR_STYLE, TransactionSource, @@ -36,8 +37,6 @@ transaction_from_function, ) -from typing import TYPE_CHECKING - if TYPE_CHECKING: from typing import Any, Awaitable, Callable, Container, Dict, Optional, Tuple, Union @@ -54,7 +53,8 @@ ) from starlette.requests import Request # type: ignore from starlette.routing import Match # type: ignore - from starlette.types import ASGIApp, Receive, Scope as StarletteScope, Send # type: ignore + from starlette.types import ASGIApp, Receive, Send # type: ignore + from starlette.types import Scope as StarletteScope except ImportError: raise DidNotEnable("Starlette is not installed") @@ -255,7 +255,7 @@ def _set_request_body_data_on_streaming_segment( info: "Optional[Dict[str, Any]]", ) -> None: current_span = _get_current_streamed_span() - if info and "data" in info and _is_sampled_streamed_span(current_span): + if info and "data" in info and _is_streamed_span(current_span): with capture_internal_exceptions(): current_span._segment.set_attribute( "http.request.body.data", @@ -552,7 +552,7 @@ def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": if span_streaming: current_span = current_scope.streamed_span - if _is_sampled_streamed_span(current_span): + if _is_streamed_span(current_span): current_span._segment._update_active_thread() elif current_scope.transaction is not None: current_scope.transaction.update_active_thread() diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 2da13158e3..fdb868274b 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1,13 +1,14 @@ import os import sys import warnings -from copy import copy, deepcopy from collections import deque from contextlib import contextmanager -from enum import Enum +from copy import copy, deepcopy from datetime import datetime, timezone +from enum import Enum from functools import wraps from itertools import chain +from typing import TYPE_CHECKING, cast import sentry_sdk from sentry_sdk._types import AnnotatedValue @@ -18,7 +19,7 @@ INSTRUMENTER, SPANDATA, ) -from sentry_sdk.feature_flags import FlagBuffer, DEFAULT_FLAG_CAPACITY +from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer from sentry_sdk.profiler.continuous_profiler import ( get_profiler_id, try_autostart_continuous_profiler, @@ -26,19 +27,11 @@ ) from sentry_sdk.profiler.transaction_profiler import Profile from sentry_sdk.session import Session -from sentry_sdk.tracing_utils import ( - Baggage, - has_tracing_enabled, - has_span_streaming_enabled, - is_ignored_span, - _make_sampling_decision, - PropagationContext, -) from sentry_sdk.traces import ( _DEFAULT_PARENT_SPAN, - StreamedSpan, NoOpStreamedSpan, - _is_sampled_streamed_span, + StreamedSpan, + _is_streamed_span, ) from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, @@ -47,40 +40,48 @@ Span, Transaction, ) +from sentry_sdk.tracing_utils import ( + Baggage, + PropagationContext, + _make_sampling_decision, + has_span_streaming_enabled, + has_tracing_enabled, + is_ignored_span, +) from sentry_sdk.utils import ( + ContextVar, capture_internal_exception, capture_internal_exceptions, - ContextVar, datetime_from_isoformat, disable_capture_event, event_from_exception, exc_info_from_error, format_attribute, - logger, has_logs_enabled, has_metrics_enabled, + logger, ) -from typing import TYPE_CHECKING, cast - if TYPE_CHECKING: from collections.abc import Mapping - - from typing import Any - from typing import Callable - from typing import Deque - from typing import Dict - from typing import Generator - from typing import Iterator - from typing import List - from typing import Optional - from typing import ParamSpec - from typing import Tuple - from typing import TypeVar - from typing import Union + from typing import ( + Any, + Callable, + Deque, + Dict, + Generator, + Iterator, + List, + Optional, + ParamSpec, + Tuple, + TypeVar, + Union, + ) from typing_extensions import Unpack + import sentry_sdk from sentry_sdk._types import ( Attributes, AttributeValue, @@ -97,11 +98,8 @@ SamplingContext, Type, ) - from sentry_sdk.tracing import TransactionKwargs - import sentry_sdk - P = ParamSpec("P") R = TypeVar("R") @@ -590,7 +588,7 @@ def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": span_streaming = has_span_streaming_enabled(client.options) # If we have an active span, return traceparent from there - if span_streaming and _is_sampled_streamed_span(self.streamed_span): + if span_streaming and _is_streamed_span(self.streamed_span): return self.streamed_span._to_traceparent() elif not span_streaming and self.span is not None: return self.span._to_traceparent() @@ -610,7 +608,7 @@ def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": span_streaming = has_span_streaming_enabled(client.options) # If we have an active span, return baggage from there - if span_streaming and _is_sampled_streamed_span(self.streamed_span): + if span_streaming and _is_streamed_span(self.streamed_span): return self.streamed_span._to_baggage() elif not span_streaming and self.span is not None: return self.span._to_baggage() @@ -915,7 +913,7 @@ def streamed_span(self, span: "Optional[StreamedSpan]") -> None: # Also set _transaction and _transaction_info in streaming mode as this # is used for populating events and linking them to segments - if _is_sampled_streamed_span(span) and span._is_segment(): + if _is_streamed_span(span) and span._is_segment(): self._transaction = span.name if span._attributes.get("sentry.span.source"): self._transaction_info["source"] = str( diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index f31b8960af..3a5414f75c 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -41,6 +41,7 @@ TypeVar, Union, ) + from sentry_sdk._types import Attributes, AttributeValue from sentry_sdk.profiler.continuous_profiler import ContinuousProfile @@ -766,9 +767,9 @@ def make_db_query(sql): return decorator -def _is_sampled_streamed_span(span: "Any") -> "TypeGuard[StreamedSpan]": - """Returns True if span is a StreamedSpan that was sampled (not a NoOpStreamedSpan).""" - return isinstance(span, StreamedSpan) and not isinstance(span, NoOpStreamedSpan) +def _is_streamed_span(span: "Any") -> "TypeGuard[StreamedSpan]": + """Returns True if span is a StreamedSpan (not a NoOpStreamedSpan).""" + return type(span) is StreamedSpan def _get_current_streamed_span( From 701bc00b3d8bb0ec999ff54b4bdda80d683b9247 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Tue, 5 May 2026 11:58:25 -0400 Subject: [PATCH 3/3] inline all the things --- sentry_sdk/integrations/fastapi.py | 4 ++-- sentry_sdk/integrations/starlette.py | 7 ++++--- sentry_sdk/scope.py | 7 +++---- sentry_sdk/traces.py | 6 ------ 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index f80052cd63..5833e5f290 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -6,7 +6,7 @@ import sentry_sdk from sentry_sdk.integrations import DidNotEnable from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import _is_streamed_span +from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import transaction_from_function @@ -95,7 +95,7 @@ def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": if has_span_streaming_enabled(client.options): current_span = current_scope.streamed_span - if _is_streamed_span(current_span): + if type(current_span) is StreamedSpan: segment = current_span._segment segment._update_active_thread() diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index d9367fa495..0018d87b09 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -22,7 +22,8 @@ ) from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import _get_current_streamed_span, _is_streamed_span +from sentry_sdk.traces import _get_current_streamed_span +from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import ( SOURCE_FOR_STYLE, TransactionSource, @@ -255,7 +256,7 @@ def _set_request_body_data_on_streaming_segment( info: "Optional[Dict[str, Any]]", ) -> None: current_span = _get_current_streamed_span() - if info and "data" in info and _is_streamed_span(current_span): + if info and "data" in info and type(current_span) is StreamedSpan: with capture_internal_exceptions(): current_span._segment.set_attribute( "http.request.body.data", @@ -552,7 +553,7 @@ def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": if span_streaming: current_span = current_scope.streamed_span - if _is_streamed_span(current_span): + if type(current_span) is StreamedSpan: current_span._segment._update_active_thread() elif current_scope.transaction is not None: current_scope.transaction.update_active_thread() diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index fdb868274b..878d61c0a1 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -31,7 +31,6 @@ _DEFAULT_PARENT_SPAN, NoOpStreamedSpan, StreamedSpan, - _is_streamed_span, ) from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, @@ -588,7 +587,7 @@ def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": span_streaming = has_span_streaming_enabled(client.options) # If we have an active span, return traceparent from there - if span_streaming and _is_streamed_span(self.streamed_span): + if span_streaming and type(self.streamed_span) is StreamedSpan: return self.streamed_span._to_traceparent() elif not span_streaming and self.span is not None: return self.span._to_traceparent() @@ -608,7 +607,7 @@ def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": span_streaming = has_span_streaming_enabled(client.options) # If we have an active span, return baggage from there - if span_streaming and _is_streamed_span(self.streamed_span): + if span_streaming and type(self.streamed_span) is StreamedSpan: return self.streamed_span._to_baggage() elif not span_streaming and self.span is not None: return self.span._to_baggage() @@ -913,7 +912,7 @@ def streamed_span(self, span: "Optional[StreamedSpan]") -> None: # Also set _transaction and _transaction_info in streaming mode as this # is used for populating events and linking them to segments - if _is_streamed_span(span) and span._is_segment(): + if type(span) is StreamedSpan and span._is_segment(): self._transaction = span.name if span._attributes.get("sentry.span.source"): self._transaction_info["source"] = str( diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 3a5414f75c..56fab54fd2 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -37,7 +37,6 @@ Iterator, Optional, ParamSpec, - TypeGuard, TypeVar, Union, ) @@ -767,11 +766,6 @@ def make_db_query(sql): return decorator -def _is_streamed_span(span: "Any") -> "TypeGuard[StreamedSpan]": - """Returns True if span is a StreamedSpan (not a NoOpStreamedSpan).""" - return type(span) is StreamedSpan - - def _get_current_streamed_span( scope: "Optional[sentry_sdk.Scope]" = None, ) -> "Optional[StreamedSpan]":