Skip to content

feat(tracing): Improve @trace decorator. #4648

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 53 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
1162254
First version of helpers for better manual instrumentation
antonpirker Jul 30, 2025
4346db1
minor improvements
antonpirker Jul 30, 2025
bfc6626
A nicer implementation
antonpirker Jul 30, 2025
f193ecb
style
antonpirker Jul 30, 2025
8488a53
fixing setting of name
antonpirker Jul 30, 2025
5621f15
Some linting
antonpirker Jul 30, 2025
43ff0c6
Replaced original trace decorator with new_decorator
antonpirker Jul 30, 2025
6a5c88e
Cleanup
antonpirker Jul 30, 2025
74941d4
fix start child
antonpirker Jul 30, 2025
6d36b28
If no span, do nothing
antonpirker Jul 30, 2025
09d5646
change less code
antonpirker Jul 30, 2025
3020f92
comment
antonpirker Jul 30, 2025
acd5401
its description internally
antonpirker Jul 31, 2025
695dd06
move _set_span_attribuets into span
antonpirker Jul 31, 2025
5068c5b
typing
antonpirker Jul 31, 2025
98c8d42
.
antonpirker Jul 31, 2025
35e7809
Add enum for template
antonpirker Jul 31, 2025
a538d2a
Make some consts public
antonpirker Jul 31, 2025
241d5cf
Also make op part of public api
antonpirker Aug 1, 2025
33f79b8
sort
antonpirker Aug 1, 2025
dbbf3b4
Allow setting a key to None
antonpirker Aug 4, 2025
cfec8c0
Merge branch 'master' into antonpirker/manual-instrumentation
antonpirker Aug 4, 2025
c234dd4
typing
antonpirker Aug 4, 2025
cf5382f
Guess some attributes from input/response
antonpirker Aug 4, 2025
fdcdfdf
fix tool description
antonpirker Aug 4, 2025
9917808
Added more token names
antonpirker Aug 4, 2025
7c1076b
better span naming
antonpirker Aug 4, 2025
a6eed59
Tool input and output
antonpirker Aug 4, 2025
2011ab4
Check PII settings before adding tool input/output
antonpirker Aug 4, 2025
668a421
Better usage extraction
antonpirker Aug 4, 2025
d8b5838
cleanup
antonpirker Aug 4, 2025
94796c6
make attribute a string
antonpirker Aug 4, 2025
ee845b1
cleanup
antonpirker Aug 4, 2025
c2d47bc
renaming
antonpirker Aug 5, 2025
5e7d44b
Merge branch 'master' into antonpirker/manual-instrumentation
antonpirker Aug 5, 2025
4843674
update
antonpirker Aug 5, 2025
f0d3ab6
cleanup
antonpirker Aug 5, 2025
a75954e
Merge branch 'master' into antonpirker/manual-instrumentation
antonpirker Aug 5, 2025
58a1646
apidocs
antonpirker Aug 5, 2025
4002e86
Merge branch 'master' into antonpirker/manual-instrumentation
antonpirker Aug 5, 2025
989e0be
apidoc
antonpirker Aug 5, 2025
aa82162
apidocs
antonpirker Aug 5, 2025
0c3da75
typo
antonpirker Aug 5, 2025
55a1035
default value
antonpirker Aug 5, 2025
0b84d48
remove ai stuff
antonpirker Aug 5, 2025
35d8f12
remove template stuff
antonpirker Aug 5, 2025
10e82cd
remove
antonpirker Aug 5, 2025
c2d4e48
removed too much
antonpirker Aug 5, 2025
9a63d8c
cleanup
antonpirker Aug 5, 2025
c20429b
Apply suggestions from code review
antonpirker Aug 5, 2025
2d93a65
typing
antonpirker Aug 5, 2025
b0cc71a
typing
antonpirker Aug 5, 2025
42afdfa
moved up
antonpirker Aug 5, 2025
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
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Enriching Events
Performance Monitoring
======================

.. autofunction:: sentry_sdk.api.trace
.. autofunction:: sentry_sdk.api.continue_trace
.. autofunction:: sentry_sdk.api.get_current_span
.. autofunction:: sentry_sdk.api.start_span
Expand Down
77 changes: 59 additions & 18 deletions sentry_sdk/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sentry_sdk
from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA
from sentry_sdk.profiler.continuous_profiler import get_profiler_id
from sentry_sdk.tracing_utils import create_span_decorator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Circular Import Issue in Tracing Module

The new top-level import of create_span_decorator in sentry_sdk/tracing.py creates a circular import dependency with sentry_sdk/tracing_utils.py. tracing_utils.py imports from tracing.py at its bottom, while tracing.py now imports from tracing_utils.py at its top. This breaks the existing pattern for handling circular imports (which previously used a local import within the trace() function) and can cause module loading failures or import order issues.

Fix in Cursor Fix in Web

from sentry_sdk.utils import (
get_current_thread_meta,
is_valid_sample_rate,
Expand Down Expand Up @@ -1340,43 +1341,83 @@ def _set_initial_sampling_decision(self, sampling_context):
if TYPE_CHECKING:

@overload
def trace(func=None):
# type: (None) -> Callable[[Callable[P, R]], Callable[P, R]]
def trace(func=None, *, op=None, name=None, attributes=None):
# type: (None, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[[Callable[P, R]], Callable[P, R]]
# Handles: @trace() and @trace(op="custom")
pass

@overload
def trace(func):
# type: (Callable[P, R]) -> Callable[P, R]
# Handles: @trace
pass


def trace(func=None):
# type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]
def trace(func=None, *, op=None, name=None, attributes=None):
# type: (Optional[Callable[P, R]], Optional[str], Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]
"""
Decorator to start a child span under the existing current transaction.
If there is no current transaction, then nothing will be traced.
Decorator to start a child span around a function call.

.. code-block::
:caption: Usage
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 op: The operation name for the span. This is a high-level description
of what the span represents (e.g., "http.client", "db.query").
You can use predefined constants from :py:class:`sentry_sdk.consts.OP`
or provide your own string. If not provided, a default operation will
be assigned based on the template.
:type op: str 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

: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
from sentry_sdk.consts import OP

# Simple usage with default values
@sentry_sdk.trace
def my_function():
...
def process_data():
# Function implementation
pass

@sentry_sdk.trace
async def my_async_function():
...
# With custom parameters
@sentry_sdk.trace(
op=OP.DB_QUERY,
name="Get user data",
attributes={"postgres": True}
)
def make_db_query(sql):
# Function implementation
pass
"""
from sentry_sdk.tracing_utils import start_child_span_decorator
decorator = create_span_decorator(
op=op,
name=name,
attributes=attributes,
)

# This patterns allows usage of both @sentry_traced and @sentry_traced(...)
# See https://stackoverflow.com/questions/52126071/decorator-with-arguments-avoid-parenthesis-when-no-arguments/52126278
if func:
return start_child_span_decorator(func)
return decorator(func)
else:
return start_child_span_decorator
return decorator


# Circular imports
Expand Down
90 changes: 53 additions & 37 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import contextlib
import functools
import inspect
import os
import re
import sys
from collections.abc import Mapping
from datetime import timedelta
from decimal import ROUND_DOWN, Decimal, DefaultContext, localcontext
from functools import wraps
from random import Random
from urllib.parse import quote, unquote
import uuid
Expand Down Expand Up @@ -770,70 +770,86 @@ def normalize_incoming_data(incoming_data):
return data


def start_child_span_decorator(func):
# type: (Any) -> Any
def create_span_decorator(op=None, name=None, attributes=None):
# type: (Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any
"""
Decorator to add child spans for functions.
Create a span decorator that can wrap both sync and async functions.

See also ``sentry_sdk.tracing.trace()``.
:param op: The operation type for the span.
:param name: The name of the span.
:param attributes: Additional attributes to set on the span.
Comment on lines +778 to +780
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we intend to add more parameters in the future? How were these ones selected?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there will be a template parameter and thats it.

"""
# Asynchronous case
if inspect.iscoroutinefunction(func):

@wraps(func)
async def func_with_tracing(*args, **kwargs):
# type: (*Any, **Any) -> Any
def span_decorator(f):
# type: (Any) -> Any
"""
Decorator to create a span for the given function.
"""

span = get_current_span()
@functools.wraps(f)
async def async_wrapper(*args, **kwargs):
# type: (*Any, **Any) -> Any
current_span = get_current_span()

if span is None:
if current_span is None:
logger.debug(
"Cannot create a child span for %s. "
"Please start a Sentry transaction before calling this function.",
qualname_from_function(func),
qualname_from_function(f),
)
return await func(*args, **kwargs)
return await f(*args, **kwargs)

span_op = op or OP.FUNCTION
span_name = name or qualname_from_function(f) or ""

with span.start_child(
op=OP.FUNCTION,
name=qualname_from_function(func),
):
return await func(*args, **kwargs)
with current_span.start_child(
op=span_op,
name=span_name,
) as span:
span.update_data(attributes or {})
result = await f(*args, **kwargs)
return result

try:
func_with_tracing.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
except Exception:
pass

# Synchronous case
else:

@wraps(func)
def func_with_tracing(*args, **kwargs):
@functools.wraps(f)
def sync_wrapper(*args, **kwargs):
# type: (*Any, **Any) -> Any
current_span = get_current_span()

span = get_current_span()

if span is None:
if current_span is None:
logger.debug(
"Cannot create a child span for %s. "
"Please start a Sentry transaction before calling this function.",
qualname_from_function(func),
qualname_from_function(f),
)
return func(*args, **kwargs)
return f(*args, **kwargs)

span_op = op or OP.FUNCTION
span_name = name or qualname_from_function(f) or ""

with span.start_child(
op=OP.FUNCTION,
name=qualname_from_function(func),
):
return func(*args, **kwargs)
with current_span.start_child(
op=span_op,
name=span_name,
) as span:
span.update_data(attributes or {})
result = f(*args, **kwargs)
return result

try:
func_with_tracing.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
except Exception:
pass

return func_with_tracing
if inspect.iscoroutinefunction(f):
return async_wrapper
else:
return sync_wrapper

return span_decorator


def get_current_span(scope=None):
Expand Down
6 changes: 5 additions & 1 deletion tests/tracing/test_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest

from sentry_sdk.tracing import trace
from sentry_sdk.tracing_utils import start_child_span_decorator
from sentry_sdk.tracing_utils import create_span_decorator
from sentry_sdk.utils import logger
from tests.conftest import patch_start_tracing_child

Expand All @@ -24,6 +24,7 @@ def test_trace_decorator():
fake_start_child.assert_not_called()
assert result == "return_of_sync_function"

start_child_span_decorator = create_span_decorator()
result2 = start_child_span_decorator(my_example_function)()
fake_start_child.assert_called_once_with(
op="function", name="test_decorator.my_example_function"
Expand All @@ -38,6 +39,7 @@ def test_trace_decorator_no_trx():
fake_debug.assert_not_called()
assert result == "return_of_sync_function"

start_child_span_decorator = create_span_decorator()
result2 = start_child_span_decorator(my_example_function)()
fake_debug.assert_called_once_with(
"Cannot create a child span for %s. "
Expand All @@ -55,6 +57,7 @@ async def test_trace_decorator_async():
fake_start_child.assert_not_called()
assert result == "return_of_async_function"

start_child_span_decorator = create_span_decorator()
result2 = await start_child_span_decorator(my_async_example_function)()
fake_start_child.assert_called_once_with(
op="function",
Expand All @@ -71,6 +74,7 @@ async def test_trace_decorator_async_no_trx():
fake_debug.assert_not_called()
assert result == "return_of_async_function"

start_child_span_decorator = create_span_decorator()
result2 = await start_child_span_decorator(my_async_example_function)()
fake_debug.assert_called_once_with(
"Cannot create a child span for %s. "
Expand Down