From 1162254f56ef1100fb8f26e52badda411979c010 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Jul 2025 14:27:35 +0200 Subject: [PATCH 01/51] First version of helpers for better manual instrumentation --- sentry_sdk/__init__.py | 1 + sentry_sdk/api.py | 26 +++++++- sentry_sdk/tracing.py | 133 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 158 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 7b1eda172a..a37b52ff4e 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -50,6 +50,7 @@ "start_session", "end_session", "set_transaction_name", + "update_current_span", ] # Initialize the debug support after everything is loaded diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index a4fb95e9a1..f034f976f0 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -6,7 +6,7 @@ from sentry_sdk._init_implementation import init from sentry_sdk.consts import INSTRUMENTER from sentry_sdk.scope import Scope, _ScopeManager, new_scope, isolation_scope -from sentry_sdk.tracing import NoOpSpan, Transaction, trace +from sentry_sdk.tracing import NoOpSpan, Transaction, trace, new_trace from sentry_sdk.crons import monitor from typing import TYPE_CHECKING @@ -81,10 +81,12 @@ def overload(x): "start_span", "start_transaction", "trace", + "new_trace", "monitor", "start_session", "end_session", "set_transaction_name", + "update_current_span", ] @@ -473,3 +475,25 @@ def end_session(): def set_transaction_name(name, source=None): # type: (str, Optional[str]) -> None return get_current_scope().set_transaction_name(name, source) + + +def update_current_span(op=None, name=None, attributes=None): + # type: (Optional[str], Optional[str], Optional[dict[str, Union[str, int, float, bool]]]) -> None + """ + Update the current span with the given parameters. + + :param op: The operation of the span. + :param name: The name of the span. + :param attributes: The attributes of the span. + """ + current_span = get_current_span() + + if op is not None: + current_span.op = op + + if name is not None: + current_span.name = name + + if attributes is not None: + for key, value in attributes.items(): + current_span.set_data(key, value) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index fc40221b9f..571e3d3e33 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,11 +1,13 @@ from decimal import Decimal +import functools +import inspect import uuid import warnings from datetime import datetime, timedelta, timezone from enum import Enum import sentry_sdk -from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA +from sentry_sdk.consts import INSTRUMENTER, OP, SPANSTATUS, SPANDATA from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.utils import ( get_current_thread_meta, @@ -1371,6 +1373,135 @@ async def my_async_function(): return start_child_span_decorator +def set_span_attributes(span, attributes): + # type: (Span, dict[str, Any]) -> None + for key, value in attributes.items(): + span.set_data(key, value) + + +def set_input_attributes(span, template, args, kwargs): + # depending on `template` set some attributes on the span derived from args and kwargs + pass + + +def set_output_attributes(span, template, result): + # depending on `template` set some attributes on the span derived from result + pass + + +def new_trace(func=None, *, as_type="span", name=None): + """ + Decorator to start a child span. + + :param func: The function to trace. + :param as_type: The type of span to create. + :param name: The name of the span. + """ + + def span_decorator(f, *a, **kw): + @functools.wraps(f) + async def async_wrapper(*args, **kwargs): + # type: (*Any, **Any) -> Any + op = kw.get("op", OP.FUNCTION) + span_name = kw.get("name", f.__name__) + attributes = kw.get("attributes", {}) + + with sentry_sdk.start_span( + op=op, + name=span_name, + ) as span: + set_span_attributes(span, attributes) + set_input_attributes(span, as_type, args, kwargs) + + result = await f(*args, **kwargs) + + set_output_attributes(span, as_type, result) + + return result + + @functools.wraps(f) + def sync_wrapper(*args, **kwargs): + # type: (*Any, **Any) -> Any + op = kw.get("op", OP.FUNCTION) + span_name = kw.get("name", f.__name__) + attributes = kw.get("attributes", {}) + + with sentry_sdk.start_span( + op=op, + name=span_name, + ) as span: + set_span_attributes(span, attributes) + set_input_attributes(span, as_type, args, kwargs) + + result = f(*args, **kwargs) + + set_output_attributes(span, as_type, result) + + return result + + if inspect.iscoroutinefunction(f): + wrapper = async_wrapper + else: + wrapper = sync_wrapper + + return wrapper + + def ai_chat_decorator(f): + # type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] + attributes = { + "gen_ai.operation.name": "chat", + } + + return span_decorator( + f, + op=OP.GEN_AI_CHAT, + name="chat [MODEL]", + attributes=attributes, + ) + + def ai_agent_decorator(f): + # type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] + span_name = name or f.__name__ + attributes = { + "gen_ai.agent.name": span_name, + "gen_ai.operation.name": "invoke_agent", + } + + return span_decorator( + f, + op=OP.GEN_AI_INVOKE_AGENT, + name=f"invoke_agent {span_name}", + attributes=attributes, + ) + + def ai_tool_decorator(f): + # type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] + span_name = name or f.__name__ + attributes = { + "gen_ai.tool.name": span_name, + "gen_ai.operation.name": "execute_tool", + } + + return span_decorator( + f, + op=OP.GEN_AI_EXECUTE_TOOL, + name=f"execute_tool {span_name}", + attributes=attributes, + ) + + decorator_for_type = { + "ai_chat": ai_chat_decorator, + "ai_agent": ai_agent_decorator, + "ai_tool": ai_tool_decorator, + } + decorator = decorator_for_type.get(as_type, span_decorator) + + if func: + return decorator(func) + else: + return decorator + + # Circular imports from sentry_sdk.tracing_utils import ( From 4346db165e00fa34e9c2622ab33e68d52983f4e9 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Jul 2025 15:22:41 +0200 Subject: [PATCH 02/51] minor improvements --- sentry_sdk/tracing.py | 63 +++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 571e3d3e33..7821c05e9b 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -15,6 +15,7 @@ logger, nanosecond_time, should_be_treated_as_error, + qualname_from_function, ) from typing import TYPE_CHECKING @@ -1373,19 +1374,38 @@ async def my_async_function(): return start_child_span_decorator -def set_span_attributes(span, attributes): +def _set_span_attributes(span, attributes): # type: (Span, dict[str, Any]) -> None + """ + Set the given attributes on the given span. + + :param span: The span to set attributes on. + :param attributes: The attributes to set on the span. + """ for key, value in attributes.items(): span.set_data(key, value) -def set_input_attributes(span, template, args, kwargs): - # depending on `template` set some attributes on the span derived from args and kwargs +def _set_input_attributes(span, template, args, kwargs): + """ + Set LLM input attributes based on given information to the given span. + + :param span: The span to set attributes on. + :param template: The template to use to set attributes on the span. + :param args: The arguments to the LLM call. + :param kwargs: The keyword arguments to the LLM call. + """ pass -def set_output_attributes(span, template, result): - # depending on `template` set some attributes on the span derived from result +def _set_output_attributes(span, template, result): + """ + Set LLM output attributes based on given information to the given span. + + :param span: The span to set attributes on. + :param template: The template to use to set attributes on the span. + :param result: The result of the LLM call. + """ pass @@ -1403,19 +1423,17 @@ def span_decorator(f, *a, **kw): async def async_wrapper(*args, **kwargs): # type: (*Any, **Any) -> Any op = kw.get("op", OP.FUNCTION) - span_name = kw.get("name", f.__name__) + span_name = kw.get("name", qualname_from_function(f)) attributes = kw.get("attributes", {}) with sentry_sdk.start_span( op=op, name=span_name, ) as span: - set_span_attributes(span, attributes) - set_input_attributes(span, as_type, args, kwargs) - + _set_span_attributes(span, attributes) + _set_input_attributes(span, as_type, args, kwargs) result = await f(*args, **kwargs) - - set_output_attributes(span, as_type, result) + _set_output_attributes(span, as_type, result) return result @@ -1423,19 +1441,17 @@ async def async_wrapper(*args, **kwargs): def sync_wrapper(*args, **kwargs): # type: (*Any, **Any) -> Any op = kw.get("op", OP.FUNCTION) - span_name = kw.get("name", f.__name__) + span_name = kw.get("name", qualname_from_function(f)) attributes = kw.get("attributes", {}) with sentry_sdk.start_span( op=op, name=span_name, ) as span: - set_span_attributes(span, attributes) - set_input_attributes(span, as_type, args, kwargs) - + _set_span_attributes(span, attributes) + _set_input_attributes(span, as_type, args, kwargs) result = f(*args, **kwargs) - - set_output_attributes(span, as_type, result) + _set_output_attributes(span, as_type, result) return result @@ -1461,34 +1477,35 @@ def ai_chat_decorator(f): def ai_agent_decorator(f): # type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] - span_name = name or f.__name__ + agent_name = name or qualname_from_function(f) attributes = { - "gen_ai.agent.name": span_name, "gen_ai.operation.name": "invoke_agent", + "gen_ai.agent.name": agent_name, } return span_decorator( f, op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {span_name}", + name=f"invoke_agent {agent_name}", attributes=attributes, ) def ai_tool_decorator(f): # type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] - span_name = name or f.__name__ + tool_name = name or qualname_from_function(f) attributes = { - "gen_ai.tool.name": span_name, "gen_ai.operation.name": "execute_tool", + "gen_ai.tool.name": tool_name, } return span_decorator( f, op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {span_name}", + name=f"execute_tool {tool_name}", attributes=attributes, ) + # Select a type based decorator (with default fallback to span_decorator) decorator_for_type = { "ai_chat": ai_chat_decorator, "ai_agent": ai_agent_decorator, From bfc66268f26b928465fc4cb71a25456b6093f880 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Jul 2025 16:35:22 +0200 Subject: [PATCH 03/51] A nicer implementation --- sentry_sdk/tracing.py | 152 ++++------------------------------ sentry_sdk/tracing_utils.py | 159 ++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 137 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 7821c05e9b..3d6d84d9f9 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,21 +1,19 @@ from decimal import Decimal -import functools -import inspect import uuid import warnings from datetime import datetime, timedelta, timezone from enum import Enum import sentry_sdk -from sentry_sdk.consts import INSTRUMENTER, OP, SPANSTATUS, SPANDATA +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 from sentry_sdk.utils import ( get_current_thread_meta, is_valid_sample_rate, logger, nanosecond_time, should_be_treated_as_error, - qualname_from_function, ) from typing import TYPE_CHECKING @@ -281,7 +279,6 @@ class Span: "_local_aggregator", "scope", "origin", - "name", "_flags", "_flags_capacity", ) @@ -352,6 +349,11 @@ def __init__( self.update_active_thread() self.set_profiler_id(get_profiler_id()) + @property + def name(self): + # type: () -> str + return self.description + # TODO this should really live on the Transaction class rather than the Span # class def init_span_recorder(self, maxlen): @@ -1374,144 +1376,20 @@ async def my_async_function(): return start_child_span_decorator -def _set_span_attributes(span, attributes): - # type: (Span, dict[str, Any]) -> None - """ - Set the given attributes on the given span. - - :param span: The span to set attributes on. - :param attributes: The attributes to set on the span. - """ - for key, value in attributes.items(): - span.set_data(key, value) - - -def _set_input_attributes(span, template, args, kwargs): - """ - Set LLM input attributes based on given information to the given span. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param args: The arguments to the LLM call. - :param kwargs: The keyword arguments to the LLM call. - """ - pass - - -def _set_output_attributes(span, template, result): - """ - Set LLM output attributes based on given information to the given span. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param result: The result of the LLM call. - """ - pass - - -def new_trace(func=None, *, as_type="span", name=None): +def new_trace(func=None, *, as_type="span", name=None, attributes=None): """ Decorator to start a child span. :param func: The function to trace. :param as_type: The type of span to create. - :param name: The name of the span. + :param name: The name of the span. (defaults to the function name) + :param attributes: Additional attributes to set on the span. """ - - def span_decorator(f, *a, **kw): - @functools.wraps(f) - async def async_wrapper(*args, **kwargs): - # type: (*Any, **Any) -> Any - op = kw.get("op", OP.FUNCTION) - span_name = kw.get("name", qualname_from_function(f)) - attributes = kw.get("attributes", {}) - - with sentry_sdk.start_span( - op=op, - name=span_name, - ) as span: - _set_span_attributes(span, attributes) - _set_input_attributes(span, as_type, args, kwargs) - result = await f(*args, **kwargs) - _set_output_attributes(span, as_type, result) - - return result - - @functools.wraps(f) - def sync_wrapper(*args, **kwargs): - # type: (*Any, **Any) -> Any - op = kw.get("op", OP.FUNCTION) - span_name = kw.get("name", qualname_from_function(f)) - attributes = kw.get("attributes", {}) - - with sentry_sdk.start_span( - op=op, - name=span_name, - ) as span: - _set_span_attributes(span, attributes) - _set_input_attributes(span, as_type, args, kwargs) - result = f(*args, **kwargs) - _set_output_attributes(span, as_type, result) - - return result - - if inspect.iscoroutinefunction(f): - wrapper = async_wrapper - else: - wrapper = sync_wrapper - - return wrapper - - def ai_chat_decorator(f): - # type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] - attributes = { - "gen_ai.operation.name": "chat", - } - - return span_decorator( - f, - op=OP.GEN_AI_CHAT, - name="chat [MODEL]", - attributes=attributes, - ) - - def ai_agent_decorator(f): - # type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] - agent_name = name or qualname_from_function(f) - attributes = { - "gen_ai.operation.name": "invoke_agent", - "gen_ai.agent.name": agent_name, - } - - return span_decorator( - f, - op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {agent_name}", - attributes=attributes, - ) - - def ai_tool_decorator(f): - # type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] - tool_name = name or qualname_from_function(f) - attributes = { - "gen_ai.operation.name": "execute_tool", - "gen_ai.tool.name": tool_name, - } - - return span_decorator( - f, - op=OP.GEN_AI_EXECUTE_TOOL, - name=f"execute_tool {tool_name}", - attributes=attributes, - ) - - # Select a type based decorator (with default fallback to span_decorator) - decorator_for_type = { - "ai_chat": ai_chat_decorator, - "ai_agent": ai_agent_decorator, - "ai_tool": ai_tool_decorator, - } - decorator = decorator_for_type.get(as_type, span_decorator) + decorator = create_span_decorator( + template=as_type, + name=name, + attributes=attributes, + ) if func: return decorator(func) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 552f4fd59a..bbdf208fb5 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1,4 +1,5 @@ import contextlib +import functools import inspect import os import re @@ -896,6 +897,164 @@ def _sample_rand_range(parent_sampled, sample_rate): return sample_rate, 1.0 +def _get_span_name(template, name): + span_name = name + if template == "ai_chat": + span_name = "chat [MODEL]" + elif template == "ai_agent": + span_name = f"invoke_agent {name}" + elif template == "ai_tool": + span_name = f"execute_tool {name}" + return span_name + + +def _get_span_op(template): + op = OP.FUNCTION + + if template == "ai_chat": + op = OP.GEN_AI_CHAT + elif template == "ai_agent": + op = OP.GEN_AI_INVOKE_AGENT + elif template == "ai_tool": + op = OP.GEN_AI_EXECUTE_TOOL + + return op + + +def _set_span_attributes(span, attributes): + # type: (Any, dict[str, Any]) -> None + """ + Set the given attributes on the given span. + + :param span: The span to set attributes on. + :param attributes: The attributes to set on the span. + """ + attributes = attributes or {} + + for key, value in attributes.items(): + span.set_data(key, value) + + +def _set_input_attributes(span, template, args, kwargs): + """ + Set span input attributes based on the given template. + + :param span: The span to set attributes on. + :param template: The template to use to set attributes on the span. + :param args: The arguments to the wrapped function. + :param kwargs: The keyword arguments to the wrapped function. + """ + attributes = {} + + if template == "ai_agent": + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", + SPANDATA.GEN_AI_AGENT_NAME: span.name, + } + elif template == "ai_chat": + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "chat", + } + elif template == "ai_tool": + attributes = { + SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", + SPANDATA.GEN_AI_TOOL_NAME: span.name, + } + + _set_span_attributes(span, attributes) + + +def _set_output_attributes(span, template, result): + """ + Set span output attributes based on the given template. + + :param span: The span to set attributes on. + :param template: The template to use to set attributes on the span. + :param result: The result of the wrapped function. + """ + attributes = {} + + _set_span_attributes(span, attributes) + + +def create_span_decorator(template, op=None, name=None, attributes=None): + # type: (str, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any + """ + Create a span decorator that can wrap both sync and async functions. + + :param template: The type of span to create (used for input/output attributes). + :param op: The operation type for the span. + :param name: The name of the span. + :param attributes: Additional attributes to set on the span. + """ + + def span_decorator(f): + @functools.wraps(f) + async def async_wrapper(*args, **kwargs): + # type: (*Any, **Any) -> Any + if get_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(f), + ) + return await f(*args, **kwargs) + + with sentry_sdk.start_span( + op=_get_span_op(template), + name=_get_span_name(template, name or qualname_from_function(f)), + ) as span: + _set_input_attributes(span, template, args, kwargs) + + result = await f(*args, **kwargs) + + _set_output_attributes(span, template, result) + _set_span_attributes(span, attributes) + + return result + + try: + async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] + except Exception: + pass + + @functools.wraps(f) + def sync_wrapper(*args, **kwargs): + # type: (*Any, **Any) -> Any + if get_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(f), + ) + return f(*args, **kwargs) + + with sentry_sdk.start_span( + op=_get_span_op(template), + name=_get_span_name(template, name or qualname_from_function(f)), + ) as span: + _set_input_attributes(span, template, args, kwargs) + + result = f(*args, **kwargs) + + _set_output_attributes(span, template, result) + _set_span_attributes(span, attributes) + + return result + + 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 + + # Circular imports from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, From f193ecb45ba449519886b36f32e04dfdff0771ee Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Jul 2025 16:38:56 +0200 Subject: [PATCH 04/51] style --- sentry_sdk/tracing_utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index bbdf208fb5..a6b897f484 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -899,12 +899,14 @@ def _sample_rand_range(parent_sampled, sample_rate): def _get_span_name(template, name): span_name = name + if template == "ai_chat": span_name = "chat [MODEL]" elif template == "ai_agent": span_name = f"invoke_agent {name}" elif template == "ai_tool": span_name = f"execute_tool {name}" + return span_name @@ -974,6 +976,8 @@ def _set_output_attributes(span, template, result): """ attributes = {} + # TODO: Implement :) + _set_span_attributes(span, attributes) From 8488a536bd8796a0455aee6fd3e938a3311f0b0f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Jul 2025 16:42:35 +0200 Subject: [PATCH 05/51] fixing setting of name --- sentry_sdk/tracing.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 3d6d84d9f9..f64b3c16b7 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -354,6 +354,11 @@ def name(self): # type: () -> str return self.description + @name.setter + def name(self, value): + # type: (str) -> None + self.description = value + # TODO this should really live on the Transaction class rather than the Span # class def init_span_recorder(self, maxlen): From 5621f157613e9ed0c77601bde91b72b4f0d92b40 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Jul 2025 16:59:50 +0200 Subject: [PATCH 06/51] Some linting --- sentry_sdk/tracing.py | 3 ++- sentry_sdk/tracing_utils.py | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index f64b3c16b7..46991bf0da 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -351,7 +351,7 @@ def __init__( @property def name(self): - # type: () -> str + # type: () -> Optional[str] return self.description @name.setter @@ -1382,6 +1382,7 @@ async def my_async_function(): def new_trace(func=None, *, as_type="span", name=None, attributes=None): + # type: (Optional[Callable[P, R]], str, Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] """ Decorator to start a child span. diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index a6b897f484..3698158a5a 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -898,6 +898,10 @@ def _sample_rand_range(parent_sampled, sample_rate): def _get_span_name(template, name): + # type: (str, str) -> str + """ + Get the name of the span based on the template and the name. + """ span_name = name if template == "ai_chat": @@ -911,6 +915,10 @@ def _get_span_name(template, name): def _get_span_op(template): + # type: (str) -> str + """ + Get the operation of the span based on the template. + """ op = OP.FUNCTION if template == "ai_chat": @@ -924,7 +932,7 @@ def _get_span_op(template): def _set_span_attributes(span, attributes): - # type: (Any, dict[str, Any]) -> None + # type: (Any, Optional[dict[str, Any]]) -> None """ Set the given attributes on the given span. @@ -938,6 +946,7 @@ def _set_span_attributes(span, attributes): def _set_input_attributes(span, template, args, kwargs): + # type: (Span, str, tuple[Any, ...], dict[str, Any]) -> None """ Set span input attributes based on the given template. @@ -946,7 +955,7 @@ def _set_input_attributes(span, template, args, kwargs): :param args: The arguments to the wrapped function. :param kwargs: The keyword arguments to the wrapped function. """ - attributes = {} + attributes = {} # type: dict[str, Any] if template == "ai_agent": attributes = { @@ -967,6 +976,7 @@ def _set_input_attributes(span, template, args, kwargs): def _set_output_attributes(span, template, result): + # type: (Span, str, Any) -> None """ Set span output attributes based on the given template. @@ -974,7 +984,7 @@ def _set_output_attributes(span, template, result): :param template: The template to use to set attributes on the span. :param result: The result of the wrapped function. """ - attributes = {} + attributes = {} # type: dict[str, Any] # TODO: Implement :) @@ -993,6 +1003,11 @@ def create_span_decorator(template, op=None, name=None, attributes=None): """ def span_decorator(f): + # type: (Any) -> Any + """ + Decorator to create a span for the given function. + """ + @functools.wraps(f) async def async_wrapper(*args, **kwargs): # type: (*Any, **Any) -> Any From 43ff0c6560e00968a6af5ed58ccd8870a14a8a4f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Jul 2025 17:04:36 +0200 Subject: [PATCH 07/51] Replaced original trace decorator with new_decorator --- sentry_sdk/tracing.py | 31 +-------------- sentry_sdk/tracing_utils.py | 67 --------------------------------- tests/tracing/test_decorator.py | 6 ++- 3 files changed, 6 insertions(+), 98 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 46991bf0da..77344266ca 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1352,36 +1352,7 @@ def trace(func): pass -def trace(func=None): - # type: (Optional[Callable[P, R]]) -> 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. - - .. code-block:: - :caption: Usage - - import sentry_sdk - - @sentry_sdk.trace - def my_function(): - ... - - @sentry_sdk.trace - async def my_async_function(): - ... - """ - from sentry_sdk.tracing_utils import start_child_span_decorator - - # 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) - else: - return start_child_span_decorator - - -def new_trace(func=None, *, as_type="span", name=None, attributes=None): +def trace(func=None, *, as_type="span", name=None, attributes=None): # type: (Optional[Callable[P, R]], str, Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] """ Decorator to start a child span. diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 3698158a5a..5f78d98dba 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -7,7 +7,6 @@ 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 @@ -771,72 +770,6 @@ def normalize_incoming_data(incoming_data): return data -def start_child_span_decorator(func): - # type: (Any) -> Any - """ - Decorator to add child spans for functions. - - See also ``sentry_sdk.tracing.trace()``. - """ - # Asynchronous case - if inspect.iscoroutinefunction(func): - - @wraps(func) - async def func_with_tracing(*args, **kwargs): - # type: (*Any, **Any) -> Any - - span = get_current_span() - - if 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), - ) - return await func(*args, **kwargs) - - with span.start_child( - op=OP.FUNCTION, - name=qualname_from_function(func), - ): - return await func(*args, **kwargs) - - try: - func_with_tracing.__signature__ = inspect.signature(func) # type: ignore[attr-defined] - except Exception: - pass - - # Synchronous case - else: - - @wraps(func) - def func_with_tracing(*args, **kwargs): - # type: (*Any, **Any) -> Any - - span = get_current_span() - - if 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), - ) - return func(*args, **kwargs) - - with span.start_child( - op=OP.FUNCTION, - name=qualname_from_function(func), - ): - return func(*args, **kwargs) - - try: - func_with_tracing.__signature__ = inspect.signature(func) # type: ignore[attr-defined] - except Exception: - pass - - return func_with_tracing - - def get_current_span(scope=None): # type: (Optional[sentry_sdk.Scope]) -> Optional[Span] """ diff --git a/tests/tracing/test_decorator.py b/tests/tracing/test_decorator.py index 18a66bd43e..24000af915 100644 --- a/tests/tracing/test_decorator.py +++ b/tests/tracing/test_decorator.py @@ -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 @@ -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(template="span") result2 = start_child_span_decorator(my_example_function)() fake_start_child.assert_called_once_with( op="function", name="test_decorator.my_example_function" @@ -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(template="span") result2 = start_child_span_decorator(my_example_function)() fake_debug.assert_called_once_with( "Cannot create a child span for %s. " @@ -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(template="span") result2 = await start_child_span_decorator(my_async_example_function)() fake_start_child.assert_called_once_with( op="function", @@ -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(template="span") result2 = await start_child_span_decorator(my_async_example_function)() fake_debug.assert_called_once_with( "Cannot create a child span for %s. " From 6a5c88e153d8baa850e0450989cbddef4b7fdc3c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Jul 2025 17:05:54 +0200 Subject: [PATCH 08/51] Cleanup --- sentry_sdk/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index f034f976f0..69e566e78e 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -6,7 +6,7 @@ from sentry_sdk._init_implementation import init from sentry_sdk.consts import INSTRUMENTER from sentry_sdk.scope import Scope, _ScopeManager, new_scope, isolation_scope -from sentry_sdk.tracing import NoOpSpan, Transaction, trace, new_trace +from sentry_sdk.tracing import NoOpSpan, Transaction, trace from sentry_sdk.crons import monitor from typing import TYPE_CHECKING @@ -81,7 +81,6 @@ def overload(x): "start_span", "start_transaction", "trace", - "new_trace", "monitor", "start_session", "end_session", From 74941d45cdf13628c4ce4a74fc7b72682c9eabde Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Jul 2025 17:16:44 +0200 Subject: [PATCH 09/51] fix start child --- sentry_sdk/tracing_utils.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 5f78d98dba..9d70f6f478 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -944,7 +944,9 @@ def span_decorator(f): @functools.wraps(f) async def async_wrapper(*args, **kwargs): # type: (*Any, **Any) -> Any - if get_current_span() is None: + current_span = get_current_span() + + if current_span is None: logger.debug( "Cannot create a child span for %s. " "Please start a Sentry transaction before calling this function.", @@ -952,7 +954,11 @@ async def async_wrapper(*args, **kwargs): ) return await f(*args, **kwargs) - with sentry_sdk.start_span( + start_span_func = ( + current_span.start_child if current_span else sentry_sdk.start_span + ) + + with start_span_func( op=_get_span_op(template), name=_get_span_name(template, name or qualname_from_function(f)), ) as span: @@ -973,7 +979,9 @@ async def async_wrapper(*args, **kwargs): @functools.wraps(f) def sync_wrapper(*args, **kwargs): # type: (*Any, **Any) -> Any - if get_current_span() is None: + current_span = get_current_span() + + if current_span is None: logger.debug( "Cannot create a child span for %s. " "Please start a Sentry transaction before calling this function.", @@ -981,7 +989,11 @@ def sync_wrapper(*args, **kwargs): ) return f(*args, **kwargs) - with sentry_sdk.start_span( + start_span_func = ( + current_span.start_child if current_span else sentry_sdk.start_span + ) + + with start_span_func( op=_get_span_op(template), name=_get_span_name(template, name or qualname_from_function(f)), ) as span: From 6d36b28e7c4bde9dfbf51a3b532b2246f995e851 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Jul 2025 17:20:29 +0200 Subject: [PATCH 10/51] If no span, do nothing --- sentry_sdk/api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 69e566e78e..b2b8ad2172 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -487,6 +487,9 @@ def update_current_span(op=None, name=None, attributes=None): """ current_span = get_current_span() + if current_span is None: + return + if op is not None: current_span.op = op From 09d5646a2822cb2a22b0111c966b326798bab21a Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Jul 2025 17:26:27 +0200 Subject: [PATCH 11/51] change less code --- sentry_sdk/api.py | 3 ++- sentry_sdk/tracing.py | 11 +---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index b2b8ad2172..ea32124a69 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -494,7 +494,8 @@ def update_current_span(op=None, name=None, attributes=None): current_span.op = op if name is not None: - current_span.name = name + # internally it is still description + current_span.description = name if attributes is not None: for key, value in attributes.items(): diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 77344266ca..d61aba301f 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -279,6 +279,7 @@ class Span: "_local_aggregator", "scope", "origin", + "name", "_flags", "_flags_capacity", ) @@ -349,16 +350,6 @@ def __init__( self.update_active_thread() self.set_profiler_id(get_profiler_id()) - @property - def name(self): - # type: () -> Optional[str] - return self.description - - @name.setter - def name(self, value): - # type: (str) -> None - self.description = value - # TODO this should really live on the Transaction class rather than the Span # class def init_span_recorder(self, maxlen): From 3020f92fa10c908b2a5b13100072afa69b0af76b Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Jul 2025 17:28:20 +0200 Subject: [PATCH 12/51] comment --- sentry_sdk/tracing_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 9d70f6f478..5938c673a5 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -890,6 +890,8 @@ def _set_input_attributes(span, template, args, kwargs): """ attributes = {} # type: dict[str, Any] + # TODO: Implement actual input parameters for those templates :) + if template == "ai_agent": attributes = { SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", From acd540139b993847c944b4929782f01daedf073a Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 31 Jul 2025 10:14:08 +0200 Subject: [PATCH 13/51] its description internally --- sentry_sdk/tracing_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 5938c673a5..1527a40951 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -895,7 +895,7 @@ def _set_input_attributes(span, template, args, kwargs): if template == "ai_agent": attributes = { SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_AGENT_NAME: span.name, + SPANDATA.GEN_AI_AGENT_NAME: span.description, } elif template == "ai_chat": attributes = { @@ -904,7 +904,7 @@ def _set_input_attributes(span, template, args, kwargs): elif template == "ai_tool": attributes = { SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: span.name, + SPANDATA.GEN_AI_TOOL_NAME: span.description, } _set_span_attributes(span, attributes) From 695dd06aeeaddbd79c93624af5d75f56c1b0d313 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 31 Jul 2025 12:48:05 +0200 Subject: [PATCH 14/51] move _set_span_attribuets into span --- sentry_sdk/tracing.py | 35 ++++++++++++++++++++++++++--------- sentry_sdk/tracing_utils.py | 22 ++++------------------ 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index d61aba301f..3ae9916a33 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -599,9 +599,24 @@ def set_tag(self, key, value): # type: (str, Any) -> None self._tags[key] = value - def set_data(self, key, value): - # type: (str, Any) -> None - self._data[key] = value + def set_data(self, key, value=None): + # type: (Union[str, Dict[str, Any]], Any) -> None + """Set data on the span. + + Can be called in two ways: + - set_data(key, value) - sets a single key-value pair + - set_data({"key": "value"}) - sets multiple key-value pairs from a dict + """ + if isinstance(key, dict): + # Dictionary calling pattern: set_data({"key": "value"}) + for k, v in key.items(): + self._data[k] = v + else: + # Traditional calling pattern: set_data(key, value) + if value is None: + raise ValueError("Value must be provided when key is a string") + + self._data[key] = value def set_flag(self, flag, result): # type: (str, bool) -> None @@ -1272,8 +1287,8 @@ def set_tag(self, key, value): # type: (str, Any) -> None pass - def set_data(self, key, value): - # type: (str, Any) -> None + def set_data(self, key, value=None): + # type: (Union[str, Dict[str, Any]], Any) -> None pass def set_status(self, value): @@ -1343,18 +1358,20 @@ def trace(func): pass -def trace(func=None, *, as_type="span", name=None, attributes=None): - # type: (Optional[Callable[P, R]], str, Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] +def trace(func=None, *, template="span", op=None, name=None, attributes=None): + # type: (Optional[Callable[P, R]], str, 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. :param func: The function to trace. - :param as_type: The type of span to create. + :param template: The type of span to create. + :param op: The operation of the span. :param name: The name of the span. (defaults to the function name) :param attributes: Additional attributes to set on the span. """ decorator = create_span_decorator( - template=as_type, + template=template, + op=op, name=name, attributes=attributes, ) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 1527a40951..86e6e7b8ae 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -864,20 +864,6 @@ def _get_span_op(template): return op -def _set_span_attributes(span, attributes): - # type: (Any, Optional[dict[str, Any]]) -> None - """ - Set the given attributes on the given span. - - :param span: The span to set attributes on. - :param attributes: The attributes to set on the span. - """ - attributes = attributes or {} - - for key, value in attributes.items(): - span.set_data(key, value) - - def _set_input_attributes(span, template, args, kwargs): # type: (Span, str, tuple[Any, ...], dict[str, Any]) -> None """ @@ -907,7 +893,7 @@ def _set_input_attributes(span, template, args, kwargs): SPANDATA.GEN_AI_TOOL_NAME: span.description, } - _set_span_attributes(span, attributes) + span.set_data(attributes) def _set_output_attributes(span, template, result): @@ -923,7 +909,7 @@ def _set_output_attributes(span, template, result): # TODO: Implement :) - _set_span_attributes(span, attributes) + span.set_data(attributes) def create_span_decorator(template, op=None, name=None, attributes=None): @@ -969,7 +955,7 @@ async def async_wrapper(*args, **kwargs): result = await f(*args, **kwargs) _set_output_attributes(span, template, result) - _set_span_attributes(span, attributes) + span.set_data(attributes) return result @@ -1004,7 +990,7 @@ def sync_wrapper(*args, **kwargs): result = f(*args, **kwargs) _set_output_attributes(span, template, result) - _set_span_attributes(span, attributes) + span.set_data(attributes) return result From 5068c5b3480391da0815c5cad3239f6fdbb17aef Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 31 Jul 2025 12:50:08 +0200 Subject: [PATCH 15/51] typing --- sentry_sdk/tracing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 3ae9916a33..718a4c4124 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1348,13 +1348,13 @@ 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, *, template="span", op=None, name=None, attributes=None): + # type: (Optional[Callable[P, R]], str, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] pass @overload - def trace(func): - # type: (Callable[P, R]) -> Callable[P, R] + def trace(func, *, template="span", op=None, name=None, attributes=None): + # type: (Callable[P, R], str, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] pass From 98c8d427c8394e2bf29568e63391ffa44116fbaf Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 31 Jul 2025 14:00:04 +0200 Subject: [PATCH 16/51] . --- sentry_sdk/api.py | 3 +-- sentry_sdk/tracing.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index ea32124a69..2153aeae21 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -498,5 +498,4 @@ def update_current_span(op=None, name=None, attributes=None): current_span.description = name if attributes is not None: - for key, value in attributes.items(): - current_span.set_data(key, value) + current_span.set_data(attributes) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 718a4c4124..fd0e4c0658 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -611,7 +611,7 @@ def set_data(self, key, value=None): # Dictionary calling pattern: set_data({"key": "value"}) for k, v in key.items(): self._data[k] = v - else: + elif isinstance(key, str): # Traditional calling pattern: set_data(key, value) if value is None: raise ValueError("Value must be provided when key is a string") From 35e780969c3a57ee8f2a08c7cd97c22936bf19d6 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 31 Jul 2025 14:14:26 +0200 Subject: [PATCH 17/51] Add enum for template --- sentry_sdk/consts.py | 11 +++++++++++ sentry_sdk/tracing.py | 18 +++++++++++------- sentry_sdk/tracing_utils.py | 29 +++++++++++++++-------------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 3ae33b6a94..8bcf80dc08 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -100,6 +100,17 @@ class CompressionAlgo(Enum): ] +class SpanTemplate(str, Enum): + SPAN = "span" + AI_AGENT = "ai_agent" + AI_TOOL = "ai_tool" + AI_CHAT = "ai_chat" + + def __str__(self): + # type: () -> str + return self.value + + class INSTRUMENTER: SENTRY = "sentry" OTEL = "otel" diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index fd0e4c0658..710b7afba0 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -5,7 +5,7 @@ from enum import Enum import sentry_sdk -from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA +from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA, SpanTemplate from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.tracing_utils import create_span_decorator from sentry_sdk.utils import ( @@ -1348,18 +1348,22 @@ def _set_initial_sampling_decision(self, sampling_context): if TYPE_CHECKING: @overload - def trace(func=None, *, template="span", op=None, name=None, attributes=None): - # type: (Optional[Callable[P, R]], str, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] + def trace( + func=None, *, template=SpanTemplate.SPAN, 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]]] pass @overload - def trace(func, *, template="span", op=None, name=None, attributes=None): - # type: (Callable[P, R], str, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] + def trace(func, *, template=SpanTemplate.SPAN, op=None, name=None, attributes=None): + # type: (Callable[P, R], ..., Optional[str], Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] pass -def trace(func=None, *, template="span", op=None, name=None, attributes=None): - # type: (Optional[Callable[P, R]], str, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] +def trace( + func=None, *, template=SpanTemplate.SPAN, 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. diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 86e6e7b8ae..5d9237f2c6 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -831,41 +831,41 @@ def _sample_rand_range(parent_sampled, sample_rate): def _get_span_name(template, name): - # type: (str, str) -> str + # type: (Union[str, "SpanTemplate"], str) -> str """ Get the name of the span based on the template and the name. """ span_name = name - if template == "ai_chat": + if template == SpanTemplate.AI_CHAT: span_name = "chat [MODEL]" - elif template == "ai_agent": + elif template == SpanTemplate.AI_AGENT: span_name = f"invoke_agent {name}" - elif template == "ai_tool": + elif template == SpanTemplate.AI_TOOL: span_name = f"execute_tool {name}" return span_name def _get_span_op(template): - # type: (str) -> str + # type: (Union[str, "SpanTemplate"]) -> str """ Get the operation of the span based on the template. """ op = OP.FUNCTION - if template == "ai_chat": + if template == SpanTemplate.AI_CHAT: op = OP.GEN_AI_CHAT - elif template == "ai_agent": + elif template == SpanTemplate.AI_AGENT: op = OP.GEN_AI_INVOKE_AGENT - elif template == "ai_tool": + elif template == SpanTemplate.AI_TOOL: op = OP.GEN_AI_EXECUTE_TOOL return op def _set_input_attributes(span, template, args, kwargs): - # type: (Span, str, tuple[Any, ...], dict[str, Any]) -> None + # type: (Span, Union[str, "SpanTemplate"], tuple[Any, ...], dict[str, Any]) -> None """ Set span input attributes based on the given template. @@ -878,16 +878,16 @@ def _set_input_attributes(span, template, args, kwargs): # TODO: Implement actual input parameters for those templates :) - if template == "ai_agent": + if template == SpanTemplate.AI_AGENT: attributes = { SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", SPANDATA.GEN_AI_AGENT_NAME: span.description, } - elif template == "ai_chat": + elif template == SpanTemplate.AI_CHAT: attributes = { SPANDATA.GEN_AI_OPERATION_NAME: "chat", } - elif template == "ai_tool": + elif template == SpanTemplate.AI_TOOL: attributes = { SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", SPANDATA.GEN_AI_TOOL_NAME: span.description, @@ -897,7 +897,7 @@ def _set_input_attributes(span, template, args, kwargs): def _set_output_attributes(span, template, result): - # type: (Span, str, Any) -> None + # type: (Span, Union[str, "SpanTemplate"], Any) -> None """ Set span output attributes based on the given template. @@ -913,7 +913,7 @@ def _set_output_attributes(span, template, result): def create_span_decorator(template, op=None, name=None, attributes=None): - # type: (str, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any + # type: (Union[str, "SpanTemplate"], Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any """ Create a span decorator that can wrap both sync and async functions. @@ -1013,6 +1013,7 @@ def sync_wrapper(*args, **kwargs): LOW_QUALITY_TRANSACTION_SOURCES, SENTRY_TRACE_HEADER_NAME, ) +from sentry_sdk.consts import SpanTemplate if TYPE_CHECKING: from sentry_sdk.tracing import Span From a538d2a0e076d699f6ddb1bb59667064264fc5d1 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 31 Jul 2025 14:29:17 +0200 Subject: [PATCH 18/51] Make some consts public --- sentry_sdk/__init__.py | 2 ++ sentry_sdk/api.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index a37b52ff4e..f6a68b09d4 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -51,6 +51,8 @@ "end_session", "set_transaction_name", "update_current_span", + "SpanTemplate", + "SpanAttr", ] # Initialize the debug support after everything is loaded diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 2153aeae21..be6e333407 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -4,7 +4,11 @@ from sentry_sdk import tracing_utils, Client from sentry_sdk._init_implementation import init -from sentry_sdk.consts import INSTRUMENTER +from sentry_sdk.consts import ( # noqa: N811 + INSTRUMENTER, + SpanTemplate, + SPANDATA as SpanAttr, +) from sentry_sdk.scope import Scope, _ScopeManager, new_scope, isolation_scope from sentry_sdk.tracing import NoOpSpan, Transaction, trace from sentry_sdk.crons import monitor @@ -86,6 +90,8 @@ def overload(x): "end_session", "set_transaction_name", "update_current_span", + "SpanTemplate", + "SpanAttr", ] From 241d5cfd2ba811c458e32302e261ec0a339e6d7f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 1 Aug 2025 12:03:48 +0200 Subject: [PATCH 19/51] Also make op part of public api --- sentry_sdk/__init__.py | 1 + sentry_sdk/api.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index f6a68b09d4..9b9ce1903d 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -53,6 +53,7 @@ "update_current_span", "SpanTemplate", "SpanAttr", + "SpanOp", ] # Initialize the debug support after everything is loaded diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index be6e333407..c0d5fa1661 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -8,6 +8,7 @@ INSTRUMENTER, SpanTemplate, SPANDATA as SpanAttr, + OP as SpanOp, ) from sentry_sdk.scope import Scope, _ScopeManager, new_scope, isolation_scope from sentry_sdk.tracing import NoOpSpan, Transaction, trace @@ -92,6 +93,7 @@ def overload(x): "update_current_span", "SpanTemplate", "SpanAttr", + "SpanOp", ] From 33f79b8dab91205d4c09e13743986703de82584f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 1 Aug 2025 12:05:11 +0200 Subject: [PATCH 20/51] sort --- sentry_sdk/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index c0d5fa1661..a32c947a7f 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -6,9 +6,9 @@ from sentry_sdk._init_implementation import init from sentry_sdk.consts import ( # noqa: N811 INSTRUMENTER, - SpanTemplate, - SPANDATA as SpanAttr, OP as SpanOp, + SPANDATA as SpanAttr, + SpanTemplate, ) from sentry_sdk.scope import Scope, _ScopeManager, new_scope, isolation_scope from sentry_sdk.tracing import NoOpSpan, Transaction, trace From dbbf3b4aa5164ecd551d7e914de36b728eda7457 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 4 Aug 2025 09:57:20 +0200 Subject: [PATCH 21/51] Allow setting a key to None --- sentry_sdk/tracing.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 710b7afba0..cf961cb187 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -611,11 +611,9 @@ def set_data(self, key, value=None): # Dictionary calling pattern: set_data({"key": "value"}) for k, v in key.items(): self._data[k] = v + elif isinstance(key, str): # Traditional calling pattern: set_data(key, value) - if value is None: - raise ValueError("Value must be provided when key is a string") - self._data[key] = value def set_flag(self, flag, result): From c234dd4ec6c91b4f864a7549a9636fb893599f06 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 4 Aug 2025 10:41:42 +0200 Subject: [PATCH 22/51] typing --- sentry_sdk/tracing.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index cf961cb187..40fce74add 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -599,14 +599,17 @@ def set_tag(self, key, value): # type: (str, Any) -> None self._tags[key] = value - def set_data(self, key, value=None): - # type: (Union[str, Dict[str, Any]], Any) -> None + def set_data(self, key=None, value=None): + # type: (Optional[Union[str, Dict[str, Any]]], Optional[Any]) -> None """Set data on the span. Can be called in two ways: - set_data(key, value) - sets a single key-value pair - set_data({"key": "value"}) - sets multiple key-value pairs from a dict """ + if key is None: + return + if isinstance(key, dict): # Dictionary calling pattern: set_data({"key": "value"}) for k, v in key.items(): @@ -1285,8 +1288,8 @@ def set_tag(self, key, value): # type: (str, Any) -> None pass - def set_data(self, key, value=None): - # type: (Union[str, Dict[str, Any]], Any) -> None + def set_data(self, key=None, value=None): + # type: (Optional[Union[str, Dict[str, Any]]], Optional[Any]) -> None pass def set_status(self, value): @@ -1349,19 +1352,19 @@ def _set_initial_sampling_decision(self, sampling_context): def trace( func=None, *, template=SpanTemplate.SPAN, 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]]] + # type: (Optional[Callable[P, R]], SpanTemplate, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[[Callable[P, R]], Callable[P, R]] pass @overload def trace(func, *, template=SpanTemplate.SPAN, op=None, name=None, attributes=None): - # type: (Callable[P, R], ..., Optional[str], Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] + # type: (Callable[P, R], SpanTemplate, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[P, R] pass def trace( func=None, *, template=SpanTemplate.SPAN, 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]]] + # type: (Optional[Callable[P, R]], SpanTemplate, 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. From cf5382ff6ae1b227863e33b3fd1a87a471978d6c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 4 Aug 2025 14:43:46 +0200 Subject: [PATCH 23/51] Guess some attributes from input/response --- sentry_sdk/consts.py | 6 ++ sentry_sdk/tracing_utils.py | 138 +++++++++++++++++++++++++++++++++--- 2 files changed, 134 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 8c98c859fc..bb130697be 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -460,6 +460,12 @@ class SPANDATA: Example: 0.7 """ + GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k" + """ + For an AI model call, the top_k parameter. Top_k essentially controls how random the output will be. + Example: 35 + """ + GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p" """ The top_p parameter used to control diversity via nucleus sampling. diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 5d9237f2c6..da2e198e55 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -830,21 +830,31 @@ def _sample_rand_range(parent_sampled, sample_rate): return sample_rate, 1.0 -def _get_span_name(template, name): - # type: (Union[str, "SpanTemplate"], str) -> str +def _get_span_name(template, name=None, kwargs=None): + # type: (Union[str, "SpanTemplate"], Optional[str], Optional[dict[str, Any]]) -> str """ Get the name of the span based on the template and the name. """ span_name = name if template == SpanTemplate.AI_CHAT: - span_name = "chat [MODEL]" + model = None + if kwargs and "model" in kwargs and isinstance(kwargs["model"], str): + model = kwargs["model"] + elif ( + kwargs and "model_name" in kwargs and isinstance(kwargs["model_name"], str) + ): + model = kwargs["model_name"] + + span_name = f"chat {model}" if model else "chat" + elif template == SpanTemplate.AI_AGENT: span_name = f"invoke_agent {name}" + elif template == SpanTemplate.AI_TOOL: span_name = f"execute_tool {name}" - return span_name + return span_name or "" def _get_span_op(template): @@ -856,18 +866,119 @@ def _get_span_op(template): if template == SpanTemplate.AI_CHAT: op = OP.GEN_AI_CHAT + elif template == SpanTemplate.AI_AGENT: op = OP.GEN_AI_INVOKE_AGENT + elif template == SpanTemplate.AI_TOOL: op = OP.GEN_AI_EXECUTE_TOOL return op +def _get_input_attributes(template, kwargs): + # type: (Union[str, "SpanTemplate"], dict[str, Any]) -> dict[str, Any] + """ + Get input attributes for the given span template. + """ + attributes = {} # type: dict[str, Any] + + for key, value in list(kwargs.items()): + if template in [ + SpanTemplate.AI_AGENT, + SpanTemplate.AI_TOOL, + SpanTemplate.AI_CHAT, + ]: + if key == "model" and isinstance(value, str): + attributes[SPANDATA.GEN_AI_REQUEST_MODEL] = value + elif key == "model_name" and isinstance(value, str): + attributes[SPANDATA.GEN_AI_REQUEST_MODEL] = value + + elif key == "agent" and isinstance(value, str): + attributes[SPANDATA.GEN_AI_AGENT_NAME] = value + elif key == "agent_name" and isinstance(value, str): + attributes[SPANDATA.GEN_AI_AGENT_NAME] = value + + elif key == "prompt" and isinstance(value, str): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( + {"role": "user", "content": value} + ) + elif key == "system_prompt" and isinstance(value, str): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( + {"role": "system", "content": value} + ) + + elif key == "max_tokens" and isinstance(value, int): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, []).append( + value + ) + elif key == "frequency_penalty" and isinstance(value, float): + attributes.setdefault( + SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, [] + ).append(value) + elif key == "presence_penalty" and isinstance(value, float): + attributes.setdefault( + SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, [] + ).append(value) + elif key == "temperature" and isinstance(value, float): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, []).append( + value + ) + elif key == "top_p" and isinstance(value, float): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_TOP_P, []).append(value) + elif key == "top_k" and isinstance(value, int): + attributes.setdefault(SPANDATA.GEN_AI_REQUEST_TOP_K, []).append(value) + + return attributes + + +def _get_output_attributes(template, result): + # type: (Union[str, "SpanTemplate"], Any) -> dict[str, Any] + """ + Get output attributes for the given span template. + """ + attributes = {} # type: dict[str, Any] + + if template in [SpanTemplate.AI_AGENT, SpanTemplate.AI_TOOL, SpanTemplate.AI_CHAT]: + if hasattr(result, "usage"): + if hasattr(result.usage, "input_tokens") and isinstance( + result.usage.input_tokens, int + ): + attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = ( + result.usage.input_tokens + ) + elif hasattr(result.usage, "output_tokens") and isinstance( + result.usage.output_tokens, int + ): + attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = ( + result.usage.output_tokens + ) + elif hasattr(result.usage, "total_tokens") and isinstance( + result.usage.total_tokens, int + ): + attributes[SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] = ( + result.usage.total_tokens + ) + + elif hasattr(result, "input_tokens") and isinstance(result.input_tokens, int): + attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = result.input_tokens + elif hasattr(result, "output_tokens") and isinstance(result.output_tokens, int): + attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = result.output_tokens + elif hasattr(result, "total_tokens") and isinstance(result.total_tokens, int): + attributes[SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] = result.total_tokens + + elif hasattr(result, "model") and isinstance(result.model, str): + attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = result.model + elif hasattr(result, "model_name") and isinstance(result.model_name, str): + attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = result.model_name + + return attributes + + def _set_input_attributes(span, template, args, kwargs): # type: (Span, Union[str, "SpanTemplate"], tuple[Any, ...], dict[str, Any]) -> None """ - Set span input attributes based on the given template. + Set span input attributes based on the given span template. :param span: The span to set attributes on. :param template: The template to use to set attributes on the span. @@ -876,8 +987,6 @@ def _set_input_attributes(span, template, args, kwargs): """ attributes = {} # type: dict[str, Any] - # TODO: Implement actual input parameters for those templates :) - if template == SpanTemplate.AI_AGENT: attributes = { SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", @@ -893,13 +1002,14 @@ def _set_input_attributes(span, template, args, kwargs): SPANDATA.GEN_AI_TOOL_NAME: span.description, } + attributes.update(_get_input_attributes(template, kwargs)) span.set_data(attributes) def _set_output_attributes(span, template, result): # type: (Span, Union[str, "SpanTemplate"], Any) -> None """ - Set span output attributes based on the given template. + Set span output attributes based on the given span template. :param span: The span to set attributes on. :param template: The template to use to set attributes on the span. @@ -907,8 +1017,10 @@ def _set_output_attributes(span, template, result): """ attributes = {} # type: dict[str, Any] - # TODO: Implement :) + if template == SpanTemplate.AI_AGENT and isinstance(result, str): + attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = result + attributes.update(_get_output_attributes(template, result)) span.set_data(attributes) @@ -948,10 +1060,16 @@ async def async_wrapper(*args, **kwargs): with start_span_func( op=_get_span_op(template), - name=_get_span_name(template, name or qualname_from_function(f)), + name=_get_span_name( + template, name or qualname_from_function(f), kwargs + ), ) as span: _set_input_attributes(span, template, args, kwargs) + docstring = getattr(f, "__doc__", None) + if template == SpanTemplate.AI_TOOL and docstring is not None: + span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, docstring) + result = await f(*args, **kwargs) _set_output_attributes(span, template, result) From fdcdfdf8043b7726af8b9b6c1a353e91ff7cec83 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 4 Aug 2025 14:51:35 +0200 Subject: [PATCH 24/51] fix tool description --- sentry_sdk/tracing_utils.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index da2e198e55..6fc7d1e925 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -975,13 +975,14 @@ def _get_output_attributes(template, result): return attributes -def _set_input_attributes(span, template, args, kwargs): - # type: (Span, Union[str, "SpanTemplate"], tuple[Any, ...], dict[str, Any]) -> None +def _set_input_attributes(span, template, f, args, kwargs): + # type: (Span, Union[str, "SpanTemplate"], Any, tuple[Any, ...], dict[str, Any]) -> None """ Set span input attributes based on the given span template. :param span: The span to set attributes on. :param template: The template to use to set attributes on the span. + :param f: The wrapped function. :param args: The arguments to the wrapped function. :param kwargs: The keyword arguments to the wrapped function. """ @@ -1002,6 +1003,10 @@ def _set_input_attributes(span, template, args, kwargs): SPANDATA.GEN_AI_TOOL_NAME: span.description, } + docstring = f.__doc__ + if docstring is not None: + attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring + attributes.update(_get_input_attributes(template, kwargs)) span.set_data(attributes) @@ -1064,11 +1069,7 @@ async def async_wrapper(*args, **kwargs): template, name or qualname_from_function(f), kwargs ), ) as span: - _set_input_attributes(span, template, args, kwargs) - - docstring = getattr(f, "__doc__", None) - if template == SpanTemplate.AI_TOOL and docstring is not None: - span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, docstring) + _set_input_attributes(span, template, f, args, kwargs) result = await f(*args, **kwargs) @@ -1103,7 +1104,7 @@ def sync_wrapper(*args, **kwargs): op=_get_span_op(template), name=_get_span_name(template, name or qualname_from_function(f)), ) as span: - _set_input_attributes(span, template, args, kwargs) + _set_input_attributes(span, template, f, args, kwargs) result = f(*args, **kwargs) From 9917808bea4f2a7d3d752d2ef2012f6dc31b73e1 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 4 Aug 2025 15:05:55 +0200 Subject: [PATCH 25/51] Added more token names --- sentry_sdk/tracing_utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 6fc7d1e925..13f6334dad 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -947,12 +947,24 @@ def _get_output_attributes(template, result): attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = ( result.usage.input_tokens ) + elif hasattr(result.usage, "prompt_tokens") and isinstance( + result.usage.prompt_tokens, int + ): + attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = ( + result.usage.prompt_tokens + ) elif hasattr(result.usage, "output_tokens") and isinstance( result.usage.output_tokens, int ): attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = ( result.usage.output_tokens ) + elif hasattr(result.usage, "completion_tokens") and isinstance( + result.usage.completion_tokens, int + ): + attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = ( + result.usage.completion_tokens + ) elif hasattr(result.usage, "total_tokens") and isinstance( result.usage.total_tokens, int ): From 7c1076b5303971423e1893faaab37a5028588a02 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 4 Aug 2025 15:23:21 +0200 Subject: [PATCH 26/51] better span naming --- sentry_sdk/tracing_utils.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 13f6334dad..3cd969df7c 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -830,8 +830,8 @@ def _sample_rand_range(parent_sampled, sample_rate): return sample_rate, 1.0 -def _get_span_name(template, name=None, kwargs=None): - # type: (Union[str, "SpanTemplate"], Optional[str], Optional[dict[str, Any]]) -> str +def _get_span_name(template, name, kwargs=None): + # type: (Union[str, "SpanTemplate"], str, Optional[dict[str, Any]]) -> str """ Get the name of the span based on the template and the name. """ @@ -854,7 +854,7 @@ def _get_span_name(template, name=None, kwargs=None): elif template == SpanTemplate.AI_TOOL: span_name = f"execute_tool {name}" - return span_name or "" + return span_name def _get_span_op(template): @@ -987,8 +987,8 @@ def _get_output_attributes(template, result): return attributes -def _set_input_attributes(span, template, f, args, kwargs): - # type: (Span, Union[str, "SpanTemplate"], Any, tuple[Any, ...], dict[str, Any]) -> None +def _set_input_attributes(span, template, name, f, args, kwargs): + # type: (Span, Union[str, "SpanTemplate"], str, Any, tuple[Any, ...], dict[str, Any]) -> None """ Set span input attributes based on the given span template. @@ -1003,7 +1003,7 @@ def _set_input_attributes(span, template, f, args, kwargs): if template == SpanTemplate.AI_AGENT: attributes = { SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_AGENT_NAME: span.description, + SPANDATA.GEN_AI_AGENT_NAME: name, } elif template == SpanTemplate.AI_CHAT: attributes = { @@ -1012,7 +1012,7 @@ def _set_input_attributes(span, template, f, args, kwargs): elif template == SpanTemplate.AI_TOOL: attributes = { SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: span.description, + SPANDATA.GEN_AI_TOOL_NAME: name, } docstring = f.__doc__ @@ -1075,13 +1075,12 @@ async def async_wrapper(*args, **kwargs): current_span.start_child if current_span else sentry_sdk.start_span ) + function_name = name or qualname_from_function(f) or "" with start_span_func( op=_get_span_op(template), - name=_get_span_name( - template, name or qualname_from_function(f), kwargs - ), + name=_get_span_name(template, function_name, kwargs), ) as span: - _set_input_attributes(span, template, f, args, kwargs) + _set_input_attributes(span, template, function_name, f, args, kwargs) result = await f(*args, **kwargs) @@ -1112,11 +1111,12 @@ def sync_wrapper(*args, **kwargs): current_span.start_child if current_span else sentry_sdk.start_span ) + function_name = name or qualname_from_function(f) or "" with start_span_func( op=_get_span_op(template), - name=_get_span_name(template, name or qualname_from_function(f)), + name=_get_span_name(template, function_name), ) as span: - _set_input_attributes(span, template, f, args, kwargs) + _set_input_attributes(span, template, function_name, f, args, kwargs) result = f(*args, **kwargs) From a6eed59f402819ae0f0969f828cbf5de5dae3b5d Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 4 Aug 2025 15:43:55 +0200 Subject: [PATCH 27/51] Tool input and output --- sentry_sdk/tracing_utils.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 3cd969df7c..258905510a 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -20,6 +20,7 @@ logger, match_regex_list, qualname_from_function, + safe_repr, to_string, try_convert, is_sentry_url, @@ -876,19 +877,15 @@ def _get_span_op(template): return op -def _get_input_attributes(template, kwargs): - # type: (Union[str, "SpanTemplate"], dict[str, Any]) -> dict[str, Any] +def _get_input_attributes(template, args, kwargs): + # type: (Union[str, "SpanTemplate"], tuple[Any, ...], dict[str, Any]) -> dict[str, Any] """ Get input attributes for the given span template. """ attributes = {} # type: dict[str, Any] - for key, value in list(kwargs.items()): - if template in [ - SpanTemplate.AI_AGENT, - SpanTemplate.AI_TOOL, - SpanTemplate.AI_CHAT, - ]: + if template in [SpanTemplate.AI_AGENT, SpanTemplate.AI_TOOL, SpanTemplate.AI_CHAT]: + for key, value in list(kwargs.items()): if key == "model" and isinstance(value, str): attributes[SPANDATA.GEN_AI_REQUEST_MODEL] = value elif key == "model_name" and isinstance(value, str): @@ -929,6 +926,11 @@ def _get_input_attributes(template, kwargs): elif key == "top_k" and isinstance(value, int): attributes.setdefault(SPANDATA.GEN_AI_REQUEST_TOP_K, []).append(value) + if template == SpanTemplate.AI_TOOL: + attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( + {"args": args, "kwargs": kwargs} + ) + return attributes @@ -984,6 +986,9 @@ def _get_output_attributes(template, result): elif hasattr(result, "model_name") and isinstance(result.model_name, str): attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = result.model_name + if template == SpanTemplate.AI_TOOL: + attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) + return attributes @@ -1019,7 +1024,7 @@ def _set_input_attributes(span, template, name, f, args, kwargs): if docstring is not None: attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring - attributes.update(_get_input_attributes(template, kwargs)) + attributes.update(_get_input_attributes(template, args, kwargs)) span.set_data(attributes) @@ -1114,7 +1119,7 @@ def sync_wrapper(*args, **kwargs): function_name = name or qualname_from_function(f) or "" with start_span_func( op=_get_span_op(template), - name=_get_span_name(template, function_name), + name=_get_span_name(template, function_name, kwargs), ) as span: _set_input_attributes(span, template, function_name, f, args, kwargs) From 2011ab4d167647ca4e83c0b77d58f24b48542a77 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 4 Aug 2025 16:03:57 +0200 Subject: [PATCH 28/51] Check PII settings before adding tool input/output --- sentry_sdk/tracing_utils.py | 52 +++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 258905510a..8a67aff426 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -877,8 +877,8 @@ def _get_span_op(template): return op -def _get_input_attributes(template, args, kwargs): - # type: (Union[str, "SpanTemplate"], tuple[Any, ...], dict[str, Any]) -> dict[str, Any] +def _get_input_attributes(template, send_pii, args, kwargs): + # type: (Union[str, "SpanTemplate"], bool, tuple[Any, ...], dict[str, Any]) -> dict[str, Any] """ Get input attributes for the given span template. """ @@ -927,15 +927,16 @@ def _get_input_attributes(template, args, kwargs): attributes.setdefault(SPANDATA.GEN_AI_REQUEST_TOP_K, []).append(value) if template == SpanTemplate.AI_TOOL: - attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( - {"args": args, "kwargs": kwargs} - ) + if send_pii: + attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( + {"args": args, "kwargs": kwargs} + ) return attributes -def _get_output_attributes(template, result): - # type: (Union[str, "SpanTemplate"], Any) -> dict[str, Any] +def _get_output_attributes(template, send_pii, result): + # type: (Union[str, "SpanTemplate"], bool, Any) -> dict[str, Any] """ Get output attributes for the given span template. """ @@ -987,18 +988,20 @@ def _get_output_attributes(template, result): attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = result.model_name if template == SpanTemplate.AI_TOOL: - attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) + if send_pii: + attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) return attributes -def _set_input_attributes(span, template, name, f, args, kwargs): - # type: (Span, Union[str, "SpanTemplate"], str, Any, tuple[Any, ...], dict[str, Any]) -> None +def _set_input_attributes(span, template, send_pii, name, f, args, kwargs): + # type: (Span, Union[str, "SpanTemplate"], bool, str, Any, tuple[Any, ...], dict[str, Any]) -> None """ Set span input attributes based on the given span template. :param span: The span to set attributes on. :param template: The template to use to set attributes on the span. + :param send_pii: Whether to send PII data. :param f: The wrapped function. :param args: The arguments to the wrapped function. :param kwargs: The keyword arguments to the wrapped function. @@ -1024,17 +1027,18 @@ def _set_input_attributes(span, template, name, f, args, kwargs): if docstring is not None: attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring - attributes.update(_get_input_attributes(template, args, kwargs)) + attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) span.set_data(attributes) -def _set_output_attributes(span, template, result): - # type: (Span, Union[str, "SpanTemplate"], Any) -> None +def _set_output_attributes(span, template, send_pii, result): + # type: (Span, Union[str, "SpanTemplate"], bool, Any) -> None """ Set span output attributes based on the given span template. :param span: The span to set attributes on. :param template: The template to use to set attributes on the span. + :param send_pii: Whether to send PII data. :param result: The result of the wrapped function. """ attributes = {} # type: dict[str, Any] @@ -1042,7 +1046,7 @@ def _set_output_attributes(span, template, result): if template == SpanTemplate.AI_AGENT and isinstance(result, str): attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = result - attributes.update(_get_output_attributes(template, result)) + attributes.update(_get_output_attributes(template, send_pii, result)) span.set_data(attributes) @@ -1056,6 +1060,7 @@ def create_span_decorator(template, op=None, name=None, attributes=None): :param name: The name of the span. :param attributes: Additional attributes to set on the span. """ + from sentry_sdk.scope import should_send_default_pii def span_decorator(f): # type: (Any) -> Any @@ -1081,15 +1086,20 @@ async def async_wrapper(*args, **kwargs): ) function_name = name or qualname_from_function(f) or "" + send_pii = should_send_default_pii() + with start_span_func( op=_get_span_op(template), name=_get_span_name(template, function_name, kwargs), ) as span: - _set_input_attributes(span, template, function_name, f, args, kwargs) + + _set_input_attributes( + span, template, send_pii, function_name, f, args, kwargs + ) result = await f(*args, **kwargs) - _set_output_attributes(span, template, result) + _set_output_attributes(span, template, send_pii, result) span.set_data(attributes) return result @@ -1117,15 +1127,19 @@ def sync_wrapper(*args, **kwargs): ) function_name = name or qualname_from_function(f) or "" + send_pii = should_send_default_pii() + with start_span_func( op=_get_span_op(template), name=_get_span_name(template, function_name, kwargs), ) as span: - _set_input_attributes(span, template, function_name, f, args, kwargs) + _set_input_attributes( + span, template, send_pii, function_name, f, args, kwargs + ) result = f(*args, **kwargs) - _set_output_attributes(span, template, result) + _set_output_attributes(span, template, send_pii, result) span.set_data(attributes) return result @@ -1144,12 +1158,12 @@ def sync_wrapper(*args, **kwargs): # Circular imports +from sentry_sdk.consts import SpanTemplate from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, LOW_QUALITY_TRANSACTION_SOURCES, SENTRY_TRACE_HEADER_NAME, ) -from sentry_sdk.consts import SpanTemplate if TYPE_CHECKING: from sentry_sdk.tracing import Span From 668a421d8e740d58ba96eab2d9393778a4e9bb00 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 4 Aug 2025 16:12:53 +0200 Subject: [PATCH 29/51] Better usage extraction --- sentry_sdk/tracing_utils.py | 65 ++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 8a67aff426..47eb9f1872 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -935,6 +935,29 @@ def _get_input_attributes(template, send_pii, args, kwargs): return attributes +def _get_usage_attributes(usage): + # type: (Any) -> dict[str, Any] + """ + Get usage attributes. + """ + attributes = {} + + if hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): + attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = usage.input_tokens + elif hasattr(usage, "prompt_tokens") and isinstance(usage.prompt_tokens, int): + attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = usage.prompt_tokens + elif hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): + attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = usage.output_tokens + elif hasattr(usage, "completion_tokens") and isinstance( + usage.completion_tokens, int + ): + attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = usage.completion_tokens + elif hasattr(usage, "total_tokens") and isinstance(usage.total_tokens, int): + attributes[SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] = usage.total_tokens + + return attributes + + def _get_output_attributes(template, send_pii, result): # type: (Union[str, "SpanTemplate"], bool, Any) -> dict[str, Any] """ @@ -943,44 +966,12 @@ def _get_output_attributes(template, send_pii, result): attributes = {} # type: dict[str, Any] if template in [SpanTemplate.AI_AGENT, SpanTemplate.AI_TOOL, SpanTemplate.AI_CHAT]: + attributes.update(_get_usage_attributes(result)) if hasattr(result, "usage"): - if hasattr(result.usage, "input_tokens") and isinstance( - result.usage.input_tokens, int - ): - attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = ( - result.usage.input_tokens - ) - elif hasattr(result.usage, "prompt_tokens") and isinstance( - result.usage.prompt_tokens, int - ): - attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = ( - result.usage.prompt_tokens - ) - elif hasattr(result.usage, "output_tokens") and isinstance( - result.usage.output_tokens, int - ): - attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = ( - result.usage.output_tokens - ) - elif hasattr(result.usage, "completion_tokens") and isinstance( - result.usage.completion_tokens, int - ): - attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = ( - result.usage.completion_tokens - ) - elif hasattr(result.usage, "total_tokens") and isinstance( - result.usage.total_tokens, int - ): - attributes[SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] = ( - result.usage.total_tokens - ) - - elif hasattr(result, "input_tokens") and isinstance(result.input_tokens, int): - attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = result.input_tokens - elif hasattr(result, "output_tokens") and isinstance(result.output_tokens, int): - attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = result.output_tokens - elif hasattr(result, "total_tokens") and isinstance(result.total_tokens, int): - attributes[SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] = result.total_tokens + attributes.update(_get_usage_attributes(result.usage)) + elif hasattr(result, "metadata"): + if hasattr(result.metadata, "usage"): + attributes.update(_get_usage_attributes(result.metadata.usage)) elif hasattr(result, "model") and isinstance(result.model, str): attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = result.model From d8b5838d7ea5fc4326847b0df3d0d096a3917155 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 4 Aug 2025 16:17:41 +0200 Subject: [PATCH 30/51] cleanup --- sentry_sdk/tracing_utils.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 47eb9f1872..ee68a34457 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -906,25 +906,17 @@ def _get_input_attributes(template, send_pii, args, kwargs): ) elif key == "max_tokens" and isinstance(value, int): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, []).append( - value - ) + attributes[SPANDATA.GEN_AI_REQUEST_MAX_TOKENS] = value elif key == "frequency_penalty" and isinstance(value, float): - attributes.setdefault( - SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, [] - ).append(value) + attributes[SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY] = value elif key == "presence_penalty" and isinstance(value, float): - attributes.setdefault( - SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, [] - ).append(value) + attributes[SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY] = value elif key == "temperature" and isinstance(value, float): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, []).append( - value - ) + attributes[SPANDATA.GEN_AI_REQUEST_TEMPERATURE] = value elif key == "top_p" and isinstance(value, float): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_TOP_P, []).append(value) + attributes[SPANDATA.GEN_AI_REQUEST_TOP_P] = value elif key == "top_k" and isinstance(value, int): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_TOP_K, []).append(value) + attributes[SPANDATA.GEN_AI_REQUEST_TOP_K] = value if template == SpanTemplate.AI_TOOL: if send_pii: From 94796c6a381c8d315f94f21a549b563e6327f06f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 4 Aug 2025 16:21:19 +0200 Subject: [PATCH 31/51] make attribute a string --- sentry_sdk/tracing_utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index ee68a34457..399108819b 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -918,6 +918,11 @@ def _get_input_attributes(template, send_pii, args, kwargs): elif key == "top_k" and isinstance(value, int): attributes[SPANDATA.GEN_AI_REQUEST_TOP_K] = value + if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes: + attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr( + attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] + ) + if template == SpanTemplate.AI_TOOL: if send_pii: attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( From ee845b1aedf108a0d106b7f0bb99e3334d616525 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 4 Aug 2025 16:25:44 +0200 Subject: [PATCH 32/51] cleanup --- sentry_sdk/tracing_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 399108819b..c1813bbfe2 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -939,15 +939,15 @@ def _get_usage_attributes(usage): """ attributes = {} - if hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): - attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = usage.input_tokens - elif hasattr(usage, "prompt_tokens") and isinstance(usage.prompt_tokens, int): + if hasattr(usage, "prompt_tokens") and isinstance(usage.prompt_tokens, int): attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = usage.prompt_tokens - elif hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): - attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = usage.output_tokens + elif hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): + attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = usage.input_tokens elif hasattr(usage, "completion_tokens") and isinstance( usage.completion_tokens, int ): + attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = usage.output_tokens + elif hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = usage.completion_tokens elif hasattr(usage, "total_tokens") and isinstance(usage.total_tokens, int): attributes[SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] = usage.total_tokens From c2d47bcb2843102a0eb29a6539a26a42d580443e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 10:43:01 +0200 Subject: [PATCH 33/51] renaming --- sentry_sdk/__init__.py | 3 --- sentry_sdk/api.py | 10 +-------- sentry_sdk/consts.py | 2 +- sentry_sdk/tracing.py | 14 ++++++------ sentry_sdk/tracing_utils.py | 44 ++++++++++++++++++------------------- 5 files changed, 31 insertions(+), 42 deletions(-) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 9b9ce1903d..a37b52ff4e 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -51,9 +51,6 @@ "end_session", "set_transaction_name", "update_current_span", - "SpanTemplate", - "SpanAttr", - "SpanOp", ] # Initialize the debug support after everything is loaded diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index a32c947a7f..2153aeae21 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -4,12 +4,7 @@ from sentry_sdk import tracing_utils, Client from sentry_sdk._init_implementation import init -from sentry_sdk.consts import ( # noqa: N811 - INSTRUMENTER, - OP as SpanOp, - SPANDATA as SpanAttr, - SpanTemplate, -) +from sentry_sdk.consts import INSTRUMENTER from sentry_sdk.scope import Scope, _ScopeManager, new_scope, isolation_scope from sentry_sdk.tracing import NoOpSpan, Transaction, trace from sentry_sdk.crons import monitor @@ -91,9 +86,6 @@ def overload(x): "end_session", "set_transaction_name", "update_current_span", - "SpanTemplate", - "SpanAttr", - "SpanOp", ] diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index bb130697be..71ffe582f1 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -100,7 +100,7 @@ class CompressionAlgo(Enum): ] -class SpanTemplate(str, Enum): +class SPANTEMPLATE(str, Enum): SPAN = "span" AI_AGENT = "ai_agent" AI_TOOL = "ai_tool" diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 40fce74add..70e1a05b4e 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -5,7 +5,7 @@ from enum import Enum import sentry_sdk -from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA, SpanTemplate +from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA, SPANTEMPLATE from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.tracing_utils import create_span_decorator from sentry_sdk.utils import ( @@ -1350,21 +1350,21 @@ def _set_initial_sampling_decision(self, sampling_context): @overload def trace( - func=None, *, template=SpanTemplate.SPAN, op=None, name=None, attributes=None + func=None, *, template=SPANTEMPLATE.SPAN, op=None, name=None, attributes=None ): - # type: (Optional[Callable[P, R]], SpanTemplate, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[[Callable[P, R]], Callable[P, R]] + # type: (Optional[Callable[P, R]], SPANTEMPLATE, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[[Callable[P, R]], Callable[P, R]] pass @overload - def trace(func, *, template=SpanTemplate.SPAN, op=None, name=None, attributes=None): - # type: (Callable[P, R], SpanTemplate, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[P, R] + def trace(func, *, template=SPANTEMPLATE.SPAN, op=None, name=None, attributes=None): + # type: (Callable[P, R], SPANTEMPLATE, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[P, R] pass def trace( - func=None, *, template=SpanTemplate.SPAN, op=None, name=None, attributes=None + func=None, *, template=SPANTEMPLATE.SPAN, op=None, name=None, attributes=None ): - # type: (Optional[Callable[P, R]], SpanTemplate, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] + # type: (Optional[Callable[P, R]], SPANTEMPLATE, 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. diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index c1813bbfe2..10b9a6861f 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -832,13 +832,13 @@ def _sample_rand_range(parent_sampled, sample_rate): def _get_span_name(template, name, kwargs=None): - # type: (Union[str, "SpanTemplate"], str, Optional[dict[str, Any]]) -> str + # type: (Union[str, "SPANTEMPLATE"], str, Optional[dict[str, Any]]) -> str """ Get the name of the span based on the template and the name. """ span_name = name - if template == SpanTemplate.AI_CHAT: + if template == SPANTEMPLATE.AI_CHAT: model = None if kwargs and "model" in kwargs and isinstance(kwargs["model"], str): model = kwargs["model"] @@ -849,42 +849,42 @@ def _get_span_name(template, name, kwargs=None): span_name = f"chat {model}" if model else "chat" - elif template == SpanTemplate.AI_AGENT: + elif template == SPANTEMPLATE.AI_AGENT: span_name = f"invoke_agent {name}" - elif template == SpanTemplate.AI_TOOL: + elif template == SPANTEMPLATE.AI_TOOL: span_name = f"execute_tool {name}" return span_name def _get_span_op(template): - # type: (Union[str, "SpanTemplate"]) -> str + # type: (Union[str, "SPANTEMPLATE"]) -> str """ Get the operation of the span based on the template. """ op = OP.FUNCTION - if template == SpanTemplate.AI_CHAT: + if template == SPANTEMPLATE.AI_CHAT: op = OP.GEN_AI_CHAT - elif template == SpanTemplate.AI_AGENT: + elif template == SPANTEMPLATE.AI_AGENT: op = OP.GEN_AI_INVOKE_AGENT - elif template == SpanTemplate.AI_TOOL: + elif template == SPANTEMPLATE.AI_TOOL: op = OP.GEN_AI_EXECUTE_TOOL return op def _get_input_attributes(template, send_pii, args, kwargs): - # type: (Union[str, "SpanTemplate"], bool, tuple[Any, ...], dict[str, Any]) -> dict[str, Any] + # type: (Union[str, "SPANTEMPLATE"], bool, tuple[Any, ...], dict[str, Any]) -> dict[str, Any] """ Get input attributes for the given span template. """ attributes = {} # type: dict[str, Any] - if template in [SpanTemplate.AI_AGENT, SpanTemplate.AI_TOOL, SpanTemplate.AI_CHAT]: + if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: for key, value in list(kwargs.items()): if key == "model" and isinstance(value, str): attributes[SPANDATA.GEN_AI_REQUEST_MODEL] = value @@ -923,7 +923,7 @@ def _get_input_attributes(template, send_pii, args, kwargs): attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] ) - if template == SpanTemplate.AI_TOOL: + if template == SPANTEMPLATE.AI_TOOL: if send_pii: attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( {"args": args, "kwargs": kwargs} @@ -956,13 +956,13 @@ def _get_usage_attributes(usage): def _get_output_attributes(template, send_pii, result): - # type: (Union[str, "SpanTemplate"], bool, Any) -> dict[str, Any] + # type: (Union[str, "SPANTEMPLATE"], bool, Any) -> dict[str, Any] """ Get output attributes for the given span template. """ attributes = {} # type: dict[str, Any] - if template in [SpanTemplate.AI_AGENT, SpanTemplate.AI_TOOL, SpanTemplate.AI_CHAT]: + if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: attributes.update(_get_usage_attributes(result)) if hasattr(result, "usage"): attributes.update(_get_usage_attributes(result.usage)) @@ -975,7 +975,7 @@ def _get_output_attributes(template, send_pii, result): elif hasattr(result, "model_name") and isinstance(result.model_name, str): attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = result.model_name - if template == SpanTemplate.AI_TOOL: + if template == SPANTEMPLATE.AI_TOOL: if send_pii: attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) @@ -983,7 +983,7 @@ def _get_output_attributes(template, send_pii, result): def _set_input_attributes(span, template, send_pii, name, f, args, kwargs): - # type: (Span, Union[str, "SpanTemplate"], bool, str, Any, tuple[Any, ...], dict[str, Any]) -> None + # type: (Span, Union[str, "SPANTEMPLATE"], bool, str, Any, tuple[Any, ...], dict[str, Any]) -> None """ Set span input attributes based on the given span template. @@ -996,16 +996,16 @@ def _set_input_attributes(span, template, send_pii, name, f, args, kwargs): """ attributes = {} # type: dict[str, Any] - if template == SpanTemplate.AI_AGENT: + if template == SPANTEMPLATE.AI_AGENT: attributes = { SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", SPANDATA.GEN_AI_AGENT_NAME: name, } - elif template == SpanTemplate.AI_CHAT: + elif template == SPANTEMPLATE.AI_CHAT: attributes = { SPANDATA.GEN_AI_OPERATION_NAME: "chat", } - elif template == SpanTemplate.AI_TOOL: + elif template == SPANTEMPLATE.AI_TOOL: attributes = { SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", SPANDATA.GEN_AI_TOOL_NAME: name, @@ -1020,7 +1020,7 @@ def _set_input_attributes(span, template, send_pii, name, f, args, kwargs): def _set_output_attributes(span, template, send_pii, result): - # type: (Span, Union[str, "SpanTemplate"], bool, Any) -> None + # type: (Span, Union[str, "SPANTEMPLATE"], bool, Any) -> None """ Set span output attributes based on the given span template. @@ -1031,7 +1031,7 @@ def _set_output_attributes(span, template, send_pii, result): """ attributes = {} # type: dict[str, Any] - if template == SpanTemplate.AI_AGENT and isinstance(result, str): + if template == SPANTEMPLATE.AI_AGENT and isinstance(result, str): attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = result attributes.update(_get_output_attributes(template, send_pii, result)) @@ -1039,7 +1039,7 @@ def _set_output_attributes(span, template, send_pii, result): def create_span_decorator(template, op=None, name=None, attributes=None): - # type: (Union[str, "SpanTemplate"], Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any + # type: (Union[str, "SPANTEMPLATE"], Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any """ Create a span decorator that can wrap both sync and async functions. @@ -1146,7 +1146,7 @@ def sync_wrapper(*args, **kwargs): # Circular imports -from sentry_sdk.consts import SpanTemplate +from sentry_sdk.consts import SPANTEMPLATE from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, LOW_QUALITY_TRANSACTION_SOURCES, From 48436741ba897579e4d353c4877bebc74e677f75 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 11:04:03 +0200 Subject: [PATCH 34/51] update --- sentry_sdk/api.py | 2 +- sentry_sdk/tracing.py | 26 +++++--------------------- sentry_sdk/tracing_utils.py | 8 ++++---- 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 2153aeae21..fba5e9b3c3 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -498,4 +498,4 @@ def update_current_span(op=None, name=None, attributes=None): current_span.description = name if attributes is not None: - current_span.set_data(attributes) + current_span.update_data(attributes) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index c85b5d55cc..296540cf61 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -599,25 +599,9 @@ def set_tag(self, key, value): # type: (str, Any) -> None self._tags[key] = value - def set_data(self, key=None, value=None): - # type: (Optional[Union[str, Dict[str, Any]]], Optional[Any]) -> None - """Set data on the span. - - Can be called in two ways: - - set_data(key, value) - sets a single key-value pair - - set_data({"key": "value"}) - sets multiple key-value pairs from a dict - """ - if key is None: - return - - if isinstance(key, dict): - # Dictionary calling pattern: set_data({"key": "value"}) - for k, v in key.items(): - self._data[k] = v - - elif isinstance(key, str): - # Traditional calling pattern: set_data(key, value) - self._data[key] = value + def set_data(self, key, value): + # type: (str, Any) -> None + self._data[key] = value def update_data(self, data): # type: (Dict[str, Any]) -> None @@ -1292,8 +1276,8 @@ def set_tag(self, key, value): # type: (str, Any) -> None pass - def set_data(self, key=None, value=None): - # type: (Optional[Union[str, Dict[str, Any]]], Optional[Any]) -> None + def set_data(self, key, value): + # type: (str, Any) -> None pass def update_data(self, data): diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 10b9a6861f..86de33e8b9 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1016,7 +1016,7 @@ def _set_input_attributes(span, template, send_pii, name, f, args, kwargs): attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) - span.set_data(attributes) + span.udpate_data(attributes) def _set_output_attributes(span, template, send_pii, result): @@ -1035,7 +1035,7 @@ def _set_output_attributes(span, template, send_pii, result): attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = result attributes.update(_get_output_attributes(template, send_pii, result)) - span.set_data(attributes) + span.udpate_data(attributes) def create_span_decorator(template, op=None, name=None, attributes=None): @@ -1088,7 +1088,7 @@ async def async_wrapper(*args, **kwargs): result = await f(*args, **kwargs) _set_output_attributes(span, template, send_pii, result) - span.set_data(attributes) + span.udpate_data(attributes) return result @@ -1128,7 +1128,7 @@ def sync_wrapper(*args, **kwargs): result = f(*args, **kwargs) _set_output_attributes(span, template, send_pii, result) - span.set_data(attributes) + span.udpate_data(attributes) return result From f0d3ab6b57e570e726e8238656244d9a9d769aba Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 11:06:14 +0200 Subject: [PATCH 35/51] cleanup --- sentry_sdk/consts.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 71ffe582f1..f8e0a4764c 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -460,12 +460,6 @@ class SPANDATA: Example: 0.7 """ - GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k" - """ - For an AI model call, the top_k parameter. Top_k essentially controls how random the output will be. - Example: 35 - """ - GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p" """ The top_p parameter used to control diversity via nucleus sampling. From 58a1646ca9d318ceccf892575add56ee0769e0d6 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 12:05:27 +0200 Subject: [PATCH 36/51] apidocs --- sentry_sdk/tracing.py | 64 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 296540cf61..a1b15e97f6 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1358,13 +1358,65 @@ def trace( ): # type: (Optional[Callable[P, R]], SPANTEMPLATE, 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. + Decorator to start a child 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 template: The type of span to create. This determines what kind of + span instrumentation and data collection will be applied. Use predefined + constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. + The default is `SPANTEMPLATE.SPAN` which is the right choice for most + use cases. + :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` + + :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, SPANTEMPLATE + + # Simple usage with default template + @sentry_sdk.trace + def process_data(): + # Function implementation + pass - :param func: The function to trace. - :param template: The type of span to create. - :param op: The operation of the span. - :param name: The name of the span. (defaults to the function name) - :param attributes: Additional attributes to set on the span. + # With custom parameters + @sentry_sdk.trace( + template=SPANTEMPLATE.AI_CHAT, + op=OP.GEN_AI_CHAT, + name="user_chat_completion", + attributes={"model": "gpt-4", "temperature": 0.7} + ) + def generate_response(prompt): + # Function implementation + pass """ decorator = create_span_decorator( template=template, From 989e0be16d9890e44e6bbfeb24d5a94dca758a04 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 13:52:36 +0200 Subject: [PATCH 37/51] apidoc --- docs/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api.rst b/docs/api.rst index 7d59030033..802abee75d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -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 From aa82162b453767dc0f7d4df26a1257f6becf8bff Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 13:58:02 +0200 Subject: [PATCH 38/51] apidocs --- sentry_sdk/tracing.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index a1b15e97f6..abc2c3d695 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1401,7 +1401,7 @@ def trace( import sentry_sdk from sentry_sdk.consts import OP, SPANTEMPLATE - # Simple usage with default template + # Simple usage with default values @sentry_sdk.trace def process_data(): # Function implementation @@ -1409,12 +1409,17 @@ def process_data(): # With custom parameters @sentry_sdk.trace( - template=SPANTEMPLATE.AI_CHAT, - op=OP.GEN_AI_CHAT, - name="user_chat_completion", - attributes={"model": "gpt-4", "temperature": 0.7} + op=OP.DB_QUERY, + name="Get user data", + attributes={"postgres": True} ) - def generate_response(prompt): + def make_db_query(sql): + # Function implementation + pass + + # With a custom template + @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) + def calculate_interest_rate(amount, rate, years): # Function implementation pass """ From 0c3da755beaeefab998609f4fddca11c7d397cfb Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 14:03:05 +0200 Subject: [PATCH 39/51] typo --- sentry_sdk/tracing_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 86de33e8b9..e53bce4ff7 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1016,7 +1016,7 @@ def _set_input_attributes(span, template, send_pii, name, f, args, kwargs): attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) - span.udpate_data(attributes) + span.update_data(attributes) def _set_output_attributes(span, template, send_pii, result): @@ -1035,7 +1035,7 @@ def _set_output_attributes(span, template, send_pii, result): attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = result attributes.update(_get_output_attributes(template, send_pii, result)) - span.udpate_data(attributes) + span.update_data(attributes) def create_span_decorator(template, op=None, name=None, attributes=None): @@ -1088,7 +1088,7 @@ async def async_wrapper(*args, **kwargs): result = await f(*args, **kwargs) _set_output_attributes(span, template, send_pii, result) - span.udpate_data(attributes) + span.update_data(attributes) return result @@ -1128,7 +1128,7 @@ def sync_wrapper(*args, **kwargs): result = f(*args, **kwargs) _set_output_attributes(span, template, send_pii, result) - span.udpate_data(attributes) + span.update_data(attributes) return result From 55a10353906ee87935006c26066ee2f2f0f295b6 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 14:10:34 +0200 Subject: [PATCH 40/51] default value --- sentry_sdk/tracing_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index e53bce4ff7..8370b93ebf 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1016,7 +1016,7 @@ def _set_input_attributes(span, template, send_pii, name, f, args, kwargs): attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) - span.update_data(attributes) + span.update_data(attributes or {}) def _set_output_attributes(span, template, send_pii, result): @@ -1035,7 +1035,7 @@ def _set_output_attributes(span, template, send_pii, result): attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = result attributes.update(_get_output_attributes(template, send_pii, result)) - span.update_data(attributes) + span.update_data(attributes or {}) def create_span_decorator(template, op=None, name=None, attributes=None): @@ -1088,7 +1088,7 @@ async def async_wrapper(*args, **kwargs): result = await f(*args, **kwargs) _set_output_attributes(span, template, send_pii, result) - span.update_data(attributes) + span.update_data(attributes or {}) return result @@ -1128,7 +1128,7 @@ def sync_wrapper(*args, **kwargs): result = f(*args, **kwargs) _set_output_attributes(span, template, send_pii, result) - span.update_data(attributes) + span.update_data(attributes or {}) return result From 0b84d486341660f9fa6a14eff3454aab5295159a Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 14:30:23 +0200 Subject: [PATCH 41/51] remove ai stuff --- sentry_sdk/consts.py | 3 - sentry_sdk/tracing.py | 33 ++--- sentry_sdk/tracing_utils.py | 240 +----------------------------------- 3 files changed, 14 insertions(+), 262 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 4b53a3ec7e..4a7a821412 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -102,9 +102,6 @@ class CompressionAlgo(Enum): class SPANTEMPLATE(str, Enum): SPAN = "span" - AI_AGENT = "ai_agent" - AI_TOOL = "ai_tool" - AI_CHAT = "ai_chat" def __str__(self): # type: () -> str diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index abc2c3d695..6c53029c7e 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1341,22 +1341,18 @@ def _set_initial_sampling_decision(self, sampling_context): if TYPE_CHECKING: @overload - def trace( - func=None, *, template=SPANTEMPLATE.SPAN, op=None, name=None, attributes=None - ): - # type: (Optional[Callable[P, R]], SPANTEMPLATE, Optional[str], Optional[str], Optional[dict[str, Any]]) -> 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]]) -> Callable[[Callable[P, R]], Callable[P, R]] pass @overload - def trace(func, *, template=SPANTEMPLATE.SPAN, op=None, name=None, attributes=None): - # type: (Callable[P, R], SPANTEMPLATE, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[P, R] + def trace(func, *, op=None, name=None, attributes=None): + # type: (Callable[P, R], Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[P, R] pass -def trace( - func=None, *, template=SPANTEMPLATE.SPAN, op=None, name=None, attributes=None -): - # type: (Optional[Callable[P, R]], SPANTEMPLATE, Optional[str], Optional[str], Optional[dict[str, Any]]) -> 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 around a function call. @@ -1368,13 +1364,6 @@ def trace( ``@trace(op="custom")``, this should be None. :type func: Callable or None - :param template: The type of span to create. This determines what kind of - span instrumentation and data collection will be applied. Use predefined - constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. - The default is `SPANTEMPLATE.SPAN` which is the right choice for most - use cases. - :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` - :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` @@ -1399,7 +1388,7 @@ def trace( Example:: import sentry_sdk - from sentry_sdk.consts import OP, SPANTEMPLATE + from sentry_sdk.consts import OP # Simple usage with default values @sentry_sdk.trace @@ -1416,15 +1405,9 @@ def process_data(): def make_db_query(sql): # Function implementation pass - - # With a custom template - @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) - def calculate_interest_rate(amount, rate, years): - # Function implementation - pass """ decorator = create_span_decorator( - template=template, + template=SPANTEMPLATE.SPAN, op=op, name=name, attributes=attributes, diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 8370b93ebf..bffbed8925 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -20,7 +20,6 @@ logger, match_regex_list, qualname_from_function, - safe_repr, to_string, try_convert, is_sentry_url, @@ -831,213 +830,6 @@ def _sample_rand_range(parent_sampled, sample_rate): return sample_rate, 1.0 -def _get_span_name(template, name, kwargs=None): - # type: (Union[str, "SPANTEMPLATE"], str, Optional[dict[str, Any]]) -> str - """ - Get the name of the span based on the template and the name. - """ - span_name = name - - if template == SPANTEMPLATE.AI_CHAT: - model = None - if kwargs and "model" in kwargs and isinstance(kwargs["model"], str): - model = kwargs["model"] - elif ( - kwargs and "model_name" in kwargs and isinstance(kwargs["model_name"], str) - ): - model = kwargs["model_name"] - - span_name = f"chat {model}" if model else "chat" - - elif template == SPANTEMPLATE.AI_AGENT: - span_name = f"invoke_agent {name}" - - elif template == SPANTEMPLATE.AI_TOOL: - span_name = f"execute_tool {name}" - - return span_name - - -def _get_span_op(template): - # type: (Union[str, "SPANTEMPLATE"]) -> str - """ - Get the operation of the span based on the template. - """ - op = OP.FUNCTION - - if template == SPANTEMPLATE.AI_CHAT: - op = OP.GEN_AI_CHAT - - elif template == SPANTEMPLATE.AI_AGENT: - op = OP.GEN_AI_INVOKE_AGENT - - elif template == SPANTEMPLATE.AI_TOOL: - op = OP.GEN_AI_EXECUTE_TOOL - - return op - - -def _get_input_attributes(template, send_pii, args, kwargs): - # type: (Union[str, "SPANTEMPLATE"], bool, tuple[Any, ...], dict[str, Any]) -> dict[str, Any] - """ - Get input attributes for the given span template. - """ - attributes = {} # type: dict[str, Any] - - if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: - for key, value in list(kwargs.items()): - if key == "model" and isinstance(value, str): - attributes[SPANDATA.GEN_AI_REQUEST_MODEL] = value - elif key == "model_name" and isinstance(value, str): - attributes[SPANDATA.GEN_AI_REQUEST_MODEL] = value - - elif key == "agent" and isinstance(value, str): - attributes[SPANDATA.GEN_AI_AGENT_NAME] = value - elif key == "agent_name" and isinstance(value, str): - attributes[SPANDATA.GEN_AI_AGENT_NAME] = value - - elif key == "prompt" and isinstance(value, str): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( - {"role": "user", "content": value} - ) - elif key == "system_prompt" and isinstance(value, str): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( - {"role": "system", "content": value} - ) - - elif key == "max_tokens" and isinstance(value, int): - attributes[SPANDATA.GEN_AI_REQUEST_MAX_TOKENS] = value - elif key == "frequency_penalty" and isinstance(value, float): - attributes[SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY] = value - elif key == "presence_penalty" and isinstance(value, float): - attributes[SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY] = value - elif key == "temperature" and isinstance(value, float): - attributes[SPANDATA.GEN_AI_REQUEST_TEMPERATURE] = value - elif key == "top_p" and isinstance(value, float): - attributes[SPANDATA.GEN_AI_REQUEST_TOP_P] = value - elif key == "top_k" and isinstance(value, int): - attributes[SPANDATA.GEN_AI_REQUEST_TOP_K] = value - - if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes: - attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr( - attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] - ) - - if template == SPANTEMPLATE.AI_TOOL: - if send_pii: - attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( - {"args": args, "kwargs": kwargs} - ) - - return attributes - - -def _get_usage_attributes(usage): - # type: (Any) -> dict[str, Any] - """ - Get usage attributes. - """ - attributes = {} - - if hasattr(usage, "prompt_tokens") and isinstance(usage.prompt_tokens, int): - attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = usage.prompt_tokens - elif hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int): - attributes[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] = usage.input_tokens - elif hasattr(usage, "completion_tokens") and isinstance( - usage.completion_tokens, int - ): - attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = usage.output_tokens - elif hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): - attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] = usage.completion_tokens - elif hasattr(usage, "total_tokens") and isinstance(usage.total_tokens, int): - attributes[SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] = usage.total_tokens - - return attributes - - -def _get_output_attributes(template, send_pii, result): - # type: (Union[str, "SPANTEMPLATE"], bool, Any) -> dict[str, Any] - """ - Get output attributes for the given span template. - """ - attributes = {} # type: dict[str, Any] - - if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]: - attributes.update(_get_usage_attributes(result)) - if hasattr(result, "usage"): - attributes.update(_get_usage_attributes(result.usage)) - elif hasattr(result, "metadata"): - if hasattr(result.metadata, "usage"): - attributes.update(_get_usage_attributes(result.metadata.usage)) - - elif hasattr(result, "model") and isinstance(result.model, str): - attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = result.model - elif hasattr(result, "model_name") and isinstance(result.model_name, str): - attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = result.model_name - - if template == SPANTEMPLATE.AI_TOOL: - if send_pii: - attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) - - return attributes - - -def _set_input_attributes(span, template, send_pii, name, f, args, kwargs): - # type: (Span, Union[str, "SPANTEMPLATE"], bool, str, Any, tuple[Any, ...], dict[str, Any]) -> None - """ - Set span input attributes based on the given span template. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param send_pii: Whether to send PII data. - :param f: The wrapped function. - :param args: The arguments to the wrapped function. - :param kwargs: The keyword arguments to the wrapped function. - """ - attributes = {} # type: dict[str, Any] - - if template == SPANTEMPLATE.AI_AGENT: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_AGENT_NAME: name, - } - elif template == SPANTEMPLATE.AI_CHAT: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - } - elif template == SPANTEMPLATE.AI_TOOL: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: name, - } - - docstring = f.__doc__ - if docstring is not None: - attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring - - attributes.update(_get_input_attributes(template, send_pii, args, kwargs)) - span.update_data(attributes or {}) - - -def _set_output_attributes(span, template, send_pii, result): - # type: (Span, Union[str, "SPANTEMPLATE"], bool, Any) -> None - """ - Set span output attributes based on the given span template. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param send_pii: Whether to send PII data. - :param result: The result of the wrapped function. - """ - attributes = {} # type: dict[str, Any] - - if template == SPANTEMPLATE.AI_AGENT and isinstance(result, str): - attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = result - - attributes.update(_get_output_attributes(template, send_pii, result)) - span.update_data(attributes or {}) - - def create_span_decorator(template, op=None, name=None, attributes=None): # type: (Union[str, "SPANTEMPLATE"], Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any """ @@ -1048,7 +840,6 @@ def create_span_decorator(template, op=None, name=None, attributes=None): :param name: The name of the span. :param attributes: Additional attributes to set on the span. """ - from sentry_sdk.scope import should_send_default_pii def span_decorator(f): # type: (Any) -> Any @@ -1074,22 +865,12 @@ async def async_wrapper(*args, **kwargs): ) function_name = name or qualname_from_function(f) or "" - send_pii = should_send_default_pii() with start_span_func( - op=_get_span_op(template), - name=_get_span_name(template, function_name, kwargs), - ) as span: - - _set_input_attributes( - span, template, send_pii, function_name, f, args, kwargs - ) - + op=OP.FUNCTION, + name=function_name, + ): result = await f(*args, **kwargs) - - _set_output_attributes(span, template, send_pii, result) - span.update_data(attributes or {}) - return result try: @@ -1115,21 +896,12 @@ def sync_wrapper(*args, **kwargs): ) function_name = name or qualname_from_function(f) or "" - send_pii = should_send_default_pii() with start_span_func( - op=_get_span_op(template), - name=_get_span_name(template, function_name, kwargs), - ) as span: - _set_input_attributes( - span, template, send_pii, function_name, f, args, kwargs - ) - + op=OP.FUNCTION, + name=function_name, + ): result = f(*args, **kwargs) - - _set_output_attributes(span, template, send_pii, result) - span.update_data(attributes or {}) - return result try: From 35d8f12cb1b235e5384e21597f2bfa7c37b06570 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 14:35:10 +0200 Subject: [PATCH 42/51] remove template stuff --- sentry_sdk/tracing.py | 3 +-- sentry_sdk/tracing_utils.py | 6 ++---- tests/tracing/test_decorator.py | 8 ++++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 6c53029c7e..9344ab90f8 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -5,7 +5,7 @@ from enum import Enum import sentry_sdk -from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA, SPANTEMPLATE +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 from sentry_sdk.utils import ( @@ -1407,7 +1407,6 @@ def make_db_query(sql): pass """ decorator = create_span_decorator( - template=SPANTEMPLATE.SPAN, op=op, name=name, attributes=attributes, diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index bffbed8925..3d41d073e3 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -830,12 +830,11 @@ def _sample_rand_range(parent_sampled, sample_rate): return sample_rate, 1.0 -def create_span_decorator(template, op=None, name=None, attributes=None): - # type: (Union[str, "SPANTEMPLATE"], Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any +def create_span_decorator(op=None, name=None, attributes=None): + # type: (Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any """ Create a span decorator that can wrap both sync and async functions. - :param template: The type of span to create (used for input/output attributes). :param op: The operation type for the span. :param name: The name of the span. :param attributes: Additional attributes to set on the span. @@ -918,7 +917,6 @@ def sync_wrapper(*args, **kwargs): # Circular imports -from sentry_sdk.consts import SPANTEMPLATE from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, LOW_QUALITY_TRANSACTION_SOURCES, diff --git a/tests/tracing/test_decorator.py b/tests/tracing/test_decorator.py index 24000af915..9a7074c470 100644 --- a/tests/tracing/test_decorator.py +++ b/tests/tracing/test_decorator.py @@ -24,7 +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(template="span") + 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" @@ -39,7 +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(template="span") + 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. " @@ -57,7 +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(template="span") + 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", @@ -74,7 +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(template="span") + 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. " From 10e82cdf69ad36f94dd03c68c880eafb7937c829 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 14:35:43 +0200 Subject: [PATCH 43/51] remove --- sentry_sdk/consts.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 4a7a821412..d402467e5e 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -100,14 +100,6 @@ class CompressionAlgo(Enum): ] -class SPANTEMPLATE(str, Enum): - SPAN = "span" - - def __str__(self): - # type: () -> str - return self.value - - class INSTRUMENTER: SENTRY = "sentry" OTEL = "otel" From c2d4e4822c2bb55a35185eeb4979431dab870cc3 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 14:51:39 +0200 Subject: [PATCH 44/51] removed too much --- sentry_sdk/tracing_utils.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 3d41d073e3..b9e67564e7 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -863,12 +863,14 @@ async def async_wrapper(*args, **kwargs): current_span.start_child if current_span else sentry_sdk.start_span ) - function_name = name or qualname_from_function(f) or "" + span_op = op or OP.FUNCTION + span_name = name or qualname_from_function(f) or "" with start_span_func( - op=OP.FUNCTION, - name=function_name, - ): + op=span_op, + name=span_name, + ) as span: + span.update_data(attributes or {}) result = await f(*args, **kwargs) return result @@ -894,12 +896,14 @@ def sync_wrapper(*args, **kwargs): current_span.start_child if current_span else sentry_sdk.start_span ) - function_name = name or qualname_from_function(f) or "" + span_op = op or OP.FUNCTION + span_name = name or qualname_from_function(f) or "" with start_span_func( - op=OP.FUNCTION, - name=function_name, - ): + op=span_op, + name=span_name, + ) as span: + span.update_data(attributes or {}) result = f(*args, **kwargs) return result From 9a63d8c67ba4b8d523f263d2f012201a17eaa81f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 15:02:49 +0200 Subject: [PATCH 45/51] cleanup --- sentry_sdk/tracing_utils.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index b9e67564e7..a7fe1dc754 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -859,14 +859,10 @@ async def async_wrapper(*args, **kwargs): ) return await f(*args, **kwargs) - start_span_func = ( - current_span.start_child if current_span else sentry_sdk.start_span - ) - span_op = op or OP.FUNCTION span_name = name or qualname_from_function(f) or "" - with start_span_func( + with current_span.start_child( op=span_op, name=span_name, ) as span: @@ -892,14 +888,10 @@ def sync_wrapper(*args, **kwargs): ) return f(*args, **kwargs) - start_span_func = ( - current_span.start_child if current_span else sentry_sdk.start_span - ) - span_op = op or OP.FUNCTION span_name = name or qualname_from_function(f) or "" - with start_span_func( + with current_span.start_child( op=span_op, name=span_name, ) as span: From c20429b7f14e091ba7837da3ced1f9d12466b1a4 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 15:23:03 +0200 Subject: [PATCH 46/51] Apply suggestions from code review Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- sentry_sdk/tracing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 9344ab90f8..567465f54f 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1341,13 +1341,13 @@ def _set_initial_sampling_decision(self, sampling_context): if TYPE_CHECKING: @overload - def trace(func=None, *, op=None, name=None, attributes=None): - # type: (Optional[Callable[P, R]], Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[[Callable[P, R]], Callable[P, R]] + def trace(*, op=None, name=None, attributes=None): + # type: (Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[[Callable[P, R]], Callable[P, R]] pass @overload - def trace(func, *, op=None, name=None, attributes=None): - # type: (Callable[P, R], Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[P, R] + def trace(func): + # type: (Callable[P, R]) -> Callable[P, R] pass From 2d93a6531c6347a0706ae48090739002ce508e8d Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 15:42:40 +0200 Subject: [PATCH 47/51] typing --- sentry_sdk/tracing.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 567465f54f..1509c76c78 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1341,13 +1341,15 @@ def _set_initial_sampling_decision(self, sampling_context): if TYPE_CHECKING: @overload - def trace(*, op=None, name=None, attributes=None): - # type: (Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[[Callable[P, R]], Callable[P, R]] + def trace(func): + # type: (Callable[P, R]) -> Callable[P, R] + # Handles: @trace pass @overload - def trace(func): - # type: (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 From b0cc71ac69d69fd5022327a495dd8f70e82ca080 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 15:43:27 +0200 Subject: [PATCH 48/51] typing --- sentry_sdk/tracing.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 1509c76c78..cf50bd3306 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1340,18 +1340,18 @@ def _set_initial_sampling_decision(self, sampling_context): if TYPE_CHECKING: - @overload - def trace(func): - # type: (Callable[P, R]) -> Callable[P, R] - # Handles: @trace - pass - @overload 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, *, 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]]] From 42afdfa0208ddbbc682629787d1cadee17b2727e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 5 Aug 2025 15:45:11 +0200 Subject: [PATCH 49/51] moved up --- sentry_sdk/tracing_utils.py | 120 ++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index a7fe1dc754..447a708d4d 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -770,66 +770,6 @@ def normalize_incoming_data(incoming_data): return data -def get_current_span(scope=None): - # type: (Optional[sentry_sdk.Scope]) -> Optional[Span] - """ - Returns the currently active span if there is one running, otherwise `None` - """ - scope = scope or sentry_sdk.get_current_scope() - current_span = scope.span - return current_span - - -def _generate_sample_rand( - trace_id, # type: Optional[str] - *, - interval=(0.0, 1.0), # type: tuple[float, float] -): - # type: (...) -> Decimal - """Generate a sample_rand value from a trace ID. - - The generated value will be pseudorandomly chosen from the provided - interval. Specifically, given (lower, upper) = interval, the generated - value will be in the range [lower, upper). The value has 6-digit precision, - so when printing with .6f, the value will never be rounded up. - - The pseudorandom number generator is seeded with the trace ID. - """ - lower, upper = interval - if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly - raise ValueError("Invalid interval: lower must be less than upper") - - rng = Random(trace_id) - sample_rand = upper - while sample_rand >= upper: - sample_rand = rng.uniform(lower, upper) - - # Round down to exactly six decimal-digit precision. - # Setting the context is needed to avoid an InvalidOperation exception - # in case the user has changed the default precision or set traps. - with localcontext(DefaultContext) as ctx: - ctx.prec = 6 - return Decimal(sample_rand).quantize( - Decimal("0.000001"), - rounding=ROUND_DOWN, - ) - - -def _sample_rand_range(parent_sampled, sample_rate): - # type: (Optional[bool], Optional[float]) -> tuple[float, float] - """ - Compute the lower (inclusive) and upper (exclusive) bounds of the range of values - that a generated sample_rand value must fall into, given the parent_sampled and - sample_rate values. - """ - if parent_sampled is None or sample_rate is None: - return 0.0, 1.0 - elif parent_sampled is True: - return 0.0, sample_rate - else: # parent_sampled is False - return sample_rate, 1.0 - - def create_span_decorator(op=None, name=None, attributes=None): # type: (Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any """ @@ -912,6 +852,66 @@ def sync_wrapper(*args, **kwargs): return span_decorator +def get_current_span(scope=None): + # type: (Optional[sentry_sdk.Scope]) -> Optional[Span] + """ + Returns the currently active span if there is one running, otherwise `None` + """ + scope = scope or sentry_sdk.get_current_scope() + current_span = scope.span + return current_span + + +def _generate_sample_rand( + trace_id, # type: Optional[str] + *, + interval=(0.0, 1.0), # type: tuple[float, float] +): + # type: (...) -> Decimal + """Generate a sample_rand value from a trace ID. + + The generated value will be pseudorandomly chosen from the provided + interval. Specifically, given (lower, upper) = interval, the generated + value will be in the range [lower, upper). The value has 6-digit precision, + so when printing with .6f, the value will never be rounded up. + + The pseudorandom number generator is seeded with the trace ID. + """ + lower, upper = interval + if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly + raise ValueError("Invalid interval: lower must be less than upper") + + rng = Random(trace_id) + sample_rand = upper + while sample_rand >= upper: + sample_rand = rng.uniform(lower, upper) + + # Round down to exactly six decimal-digit precision. + # Setting the context is needed to avoid an InvalidOperation exception + # in case the user has changed the default precision or set traps. + with localcontext(DefaultContext) as ctx: + ctx.prec = 6 + return Decimal(sample_rand).quantize( + Decimal("0.000001"), + rounding=ROUND_DOWN, + ) + + +def _sample_rand_range(parent_sampled, sample_rate): + # type: (Optional[bool], Optional[float]) -> tuple[float, float] + """ + Compute the lower (inclusive) and upper (exclusive) bounds of the range of values + that a generated sample_rand value must fall into, given the parent_sampled and + sample_rate values. + """ + if parent_sampled is None or sample_rate is None: + return 0.0, 1.0 + elif parent_sampled is True: + return 0.0, sample_rate + else: # parent_sampled is False + return sample_rate, 1.0 + + # Circular imports from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, From aa3c33411d7c41be20a84396cf3609cd050877aa Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 6 Aug 2025 11:38:05 +0200 Subject: [PATCH 50/51] fix circular import --- sentry_sdk/tracing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index cf50bd3306..e9d726cc66 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -7,7 +7,6 @@ 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 from sentry_sdk.utils import ( get_current_thread_meta, is_valid_sample_rate, @@ -1408,6 +1407,8 @@ def make_db_query(sql): # Function implementation pass """ + from sentry_sdk.tracing_utils import create_span_decorator + decorator = create_span_decorator( op=op, name=name, From dc63e439d0a867cf74ae491ff94f413e1c270441 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 6 Aug 2025 11:58:33 +0200 Subject: [PATCH 51/51] linting --- sentry_sdk/integrations/starlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 24707a18b1..6ab80712e5 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -17,7 +17,7 @@ from starlite.plugins.base import get_plugin_for_value # type: ignore from starlite.routes.http import HTTPRoute # type: ignore from starlite.utils import ConnectionDataExtractor, is_async_callable, Ref # type: ignore - from pydantic import BaseModel # type: ignore + from pydantic import BaseModel except ImportError: raise DidNotEnable("Starlite is not installed")