From 941863e2f692f067deaf23e9b21452679f513f53 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Mar 2026 13:57:08 +0100 Subject: [PATCH 1/6] ref: Add streaming trace decorator --- sentry_sdk/traces.py | 76 ++++++++++++++++++++++++++++++++++++- sentry_sdk/tracing_utils.py | 59 ++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index f6156e77ac..09ba250e23 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -14,9 +14,12 @@ from sentry_sdk.utils import format_attribute, logger if TYPE_CHECKING: - from typing import Any, Optional, Union + from typing import Any, Callable, Optional, ParamSpec, TypeVar, Union from sentry_sdk._types import Attributes, AttributeValue + P = ParamSpec("P") + R = TypeVar("R") + class SpanStatus(str, Enum): OK = "ok" @@ -342,3 +345,74 @@ def trace_id(self) -> str: @property def sampled(self) -> "Optional[bool]": return False + + +def trace( + func: "Optional[Callable[P, R]]" = None, + *, + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, +) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": + """ + Decorator to start a span around a function call. + + This decorator automatically creates a new span when the decorated function + is called, and finishes the span when the function returns or raises an exception. + + :param func: The function to trace. When used as a decorator without parentheses, + this is the function being decorated. When used with parameters (e.g., + ``@trace(op="custom")``, this should be None. + :type func: Callable or None + + :param name: The human-readable name/description for the span. If not provided, + defaults to the function name. This provides more specific details about + what the span represents (e.g., "GET /api/users", "process_user_data"). + :type name: str or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes provide additional context about the span's execution. + :type attributes: dict[str, Any] or None + + :param active: Controls whether spans started while this span is running + will automatically become its children. That's the default behavior. If + you want to create a span that shouldn't have any children (unless + provided explicitly via the `parent_span` argument), set this to False. + :type active: bool + + :returns: When used as ``@trace``, returns the decorated function. When used as + ``@trace(...)`` with parameters, returns a decorator function. + :rtype: Callable or decorator function + + Example:: + + import sentry_sdk + + # Simple usage with default values + @sentry_sdk.trace + def process_data(): + # Function implementation + pass + + # With custom parameters + @sentry_sdk.trace( + name="Get user data", + attributes={"postgres": True} + ) + def make_db_query(sql): + # Function implementation + pass + """ + from sentry_sdk.tracing_utils import create_streaming_span_decorator + + decorator = create_streaming_span_decorator( + name=name, + attributes=attributes, + active=active, + ) + + if func: + return decorator(func) + else: + return decorator diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index c1d6c44535..8fe31bb33d 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -942,6 +942,58 @@ def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": return span_decorator +def create_streaming_span_decorator( + name: "Optional[str]" = None, + attributes: "Optional[dict[str, Any]]" = None, + active: bool = True, +) -> "Any": + """ + Create a span creating decorator that can wrap both sync and async functions. + """ + from sentry_sdk.scope import should_send_default_pii + + def span_decorator(f: "Any") -> "Any": + """ + Decorator to create a span for the given function. + """ + + @functools.wraps(f) + async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + span_name = name or qualname_from_function(f) or "" + + with start_streaming_span( + name=span_name, attributes=attributes, active=active + ): + result = await f(*args, **kwargs) + return result + + try: + async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + @functools.wraps(f) + def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": + span_name = name or qualname_from_function(f) or "" + + with start_streaming_span( + name=span_name, attributes=attributes, active=active + ): + return f(*args, **kwargs) + + try: + sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + if inspect.iscoroutinefunction(f): + return async_wrapper + else: + return sync_wrapper + + return span_decorator + + def get_current_span(scope: "Optional[sentry_sdk.Scope]" = None) -> "Optional[Span]": """ Returns the currently active span if there is one running, otherwise `None` @@ -1320,3 +1372,10 @@ def add_sentry_baggage_to_headers( if TYPE_CHECKING: from sentry_sdk.tracing import Span + + +from sentry_sdk.traces import ( + LOW_QUALITY_SEGMENT_SOURCES, + start_span as start_streaming_span, + StreamedSpan, +) From f2738ff53a93069b1eadc0d920befedde9a970b8 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Mar 2026 14:58:52 +0100 Subject: [PATCH 2/6] reorder imports --- sentry_sdk/tracing_utils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 8fe31bb33d..9c6f811d85 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1369,13 +1369,11 @@ def add_sentry_baggage_to_headers( LOW_QUALITY_TRANSACTION_SOURCES, SENTRY_TRACE_HEADER_NAME, ) - -if TYPE_CHECKING: - from sentry_sdk.tracing import Span - - from sentry_sdk.traces import ( LOW_QUALITY_SEGMENT_SOURCES, start_span as start_streaming_span, StreamedSpan, ) + +if TYPE_CHECKING: + from sentry_sdk.tracing import Span From c974d3edca3a39d33a4cd9a1869a7521026c3237 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Mar 2026 15:15:15 +0100 Subject: [PATCH 3/6] add dummy __enter__, __exit__ --- sentry_sdk/traces.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index bb924563d5..f02954c949 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -231,6 +231,14 @@ def __repr__(self) -> str: f"active={self._active})>" ) + def __enter__(self) -> "StreamedSpan": + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + pass + def get_attributes(self) -> "Attributes": return self._attributes @@ -302,6 +310,14 @@ def __init__(self) -> None: def __repr__(self) -> str: return f"<{self.__class__.__name__}(sampled={self.sampled})>" + def __enter__(self) -> "StreamedSpan": + return self + + def __exit__( + self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" + ) -> None: + pass + def get_attributes(self) -> "Attributes": return {} From 831adae305487de271d9113776a08db111174fe3 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Mar 2026 15:20:26 +0100 Subject: [PATCH 4/6] type hint --- sentry_sdk/traces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index f02954c949..bede851e0c 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -310,7 +310,7 @@ def __init__(self) -> None: def __repr__(self) -> str: return f"<{self.__class__.__name__}(sampled={self.sampled})>" - def __enter__(self) -> "StreamedSpan": + def __enter__(self) -> "NoOpStreamedSpan": return self def __exit__( From 1dcf176a90df631b1002eb0b601d5c80861a9962 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 5 Mar 2026 16:22:49 +0100 Subject: [PATCH 5/6] remove unused import --- sentry_sdk/tracing_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 9c6f811d85..80b4628153 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -950,7 +950,6 @@ def create_streaming_span_decorator( """ Create a span creating decorator that can wrap both sync and async functions. """ - from sentry_sdk.scope import should_send_default_pii def span_decorator(f: "Any") -> "Any": """ From 1006e7b30294e54445a4bc9b996cad895b1f009b Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 6 Mar 2026 14:02:47 +0100 Subject: [PATCH 6/6] remove unused imports --- sentry_sdk/tracing_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 80b4628153..54c3dcc6f5 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1369,9 +1369,7 @@ def add_sentry_baggage_to_headers( SENTRY_TRACE_HEADER_NAME, ) from sentry_sdk.traces import ( - LOW_QUALITY_SEGMENT_SOURCES, start_span as start_streaming_span, - StreamedSpan, ) if TYPE_CHECKING: