Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 91 additions & 1 deletion sentry_sdk/traces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -228,6 +231,14 @@
f"active={self._active})>"
)

def __enter__(self) -> "StreamedSpan":
return self

def __exit__(
self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]"
) -> None:
pass

Check failure on line 240 in sentry_sdk/traces.py

View workflow job for this annotation

GitHub Actions / warden: code-review

StreamedSpan context manager doesn't set scope.span, breaking automatic span parenting

The `__enter__` method returns `self` without setting `scope.span = self`, and `__exit__` does nothing. This breaks automatic span parenting: when using `with start_span(name='A'):` followed by nested `start_span(name='B')`, span B won't detect span A as its parent because `scope.span` is never updated. Compare with `tracing.py:Span.__enter__` (lines 389-394) which correctly sets `scope.span = self` and `__exit__` (lines 396-406) which restores the old span.

Check warning on line 240 in sentry_sdk/traces.py

View workflow job for this annotation

GitHub Actions / warden: find-bugs

StreamedSpan.__exit__ is no-op, contradicting documented auto-finish behavior

The `__exit__` method added to `StreamedSpan` is a no-op (`pass`), but the documentation at line 88 states 'The span automatically finishes once the `with` block is exited' and the `@trace` decorator docstring (line 377) says it 'finishes the span when the function returns or raises an exception'. Additionally, unlike the existing `Span.__exit__` in tracing.py (lines 396-406), this implementation doesn't set error status on exceptions, doesn't call a finish/end method, and doesn't restore scope state. Users relying on context manager semantics will have spans that never complete.

def get_attributes(self) -> "Attributes":
return self._attributes

Expand Down Expand Up @@ -299,6 +310,14 @@
def __repr__(self) -> str:
return f"<{self.__class__.__name__}(sampled={self.sampled})>"

def __enter__(self) -> "NoOpStreamedSpan":
return self

def __exit__(
self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]"
) -> None:
pass

def get_attributes(self) -> "Attributes":
return {}

Expand Down Expand Up @@ -342,3 +361,74 @@
@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

Check warning on line 434 in sentry_sdk/traces.py

View workflow job for this annotation

GitHub Actions / warden: code-review

New streaming trace decorator lacks test coverage

The new `sentry_sdk.traces.trace` decorator has no dedicated test coverage. The existing tests in `tests/tracing/test_decorator.py` only test the old `sentry_sdk.tracing.trace` decorator. The new streaming decorator should have functional tests covering: basic usage, usage with parameters (name, attributes, active), decorator behavior with sync/async functions, and edge cases like exception handling.
56 changes: 56 additions & 0 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,57 @@
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.
"""

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

Check warning on line 993 in sentry_sdk/tracing_utils.py

View workflow job for this annotation

GitHub Actions / warden: code-review

[EW2-W2A] New streaming trace decorator lacks test coverage (additional location)

The new `sentry_sdk.traces.trace` decorator has no dedicated test coverage. The existing tests in `tests/tracing/test_decorator.py` only test the old `sentry_sdk.tracing.trace` decorator. The new streaming decorator should have functional tests covering: basic usage, usage with parameters (name, attributes, active), decorator behavior with sync/async functions, and edge cases like exception handling.


def get_current_span(scope: "Optional[sentry_sdk.Scope]" = None) -> "Optional[Span]":
"""
Returns the currently active span if there is one running, otherwise `None`
Expand Down Expand Up @@ -1317,6 +1368,11 @@
LOW_QUALITY_TRANSACTION_SOURCES,
SENTRY_TRACE_HEADER_NAME,
)
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
Loading