From bace8b1eb757443edf23103b3c52689fbed19e00 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 18 Sep 2025 16:43:20 +0200 Subject: [PATCH 01/20] Make agents integrations set the span status in case of error --- sentry_sdk/consts.py | 4 +++- sentry_sdk/integrations/anthropic.py | 6 +++++- sentry_sdk/integrations/cohere.py | 6 +++++- sentry_sdk/integrations/huggingface_hub.py | 8 +++++--- sentry_sdk/integrations/langchain.py | 4 ++-- sentry_sdk/integrations/openai.py | 4 +++- .../integrations/openai_agents/spans/execute_tool.py | 2 +- sentry_sdk/integrations/openai_agents/utils.py | 6 +++++- 8 files changed, 29 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 91a1740526..606cf804b6 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -765,11 +765,12 @@ class SPANSTATUS: CANCELLED = "cancelled" DATA_LOSS = "data_loss" DEADLINE_EXCEEDED = "deadline_exceeded" + ERROR = "error" # OTel status code: https://opentelemetry.io/docs/concepts/signals/traces/#span-status FAILED_PRECONDITION = "failed_precondition" INTERNAL_ERROR = "internal_error" INVALID_ARGUMENT = "invalid_argument" NOT_FOUND = "not_found" - OK = "ok" + OK = "ok" # HTTP 200 and OTel status code: https://opentelemetry.io/docs/concepts/signals/traces/#span-status OUT_OF_RANGE = "out_of_range" PERMISSION_DENIED = "permission_denied" RESOURCE_EXHAUSTED = "resource_exhausted" @@ -777,6 +778,7 @@ class SPANSTATUS: UNAVAILABLE = "unavailable" UNIMPLEMENTED = "unimplemented" UNKNOWN_ERROR = "unknown_error" + UNSET = "unset" # OTel status code: https://opentelemetry.io/docs/concepts/signals/traces/#span-status class OP: diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 4f4c0b1a2a..3aed4e12b0 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -4,7 +4,7 @@ import sentry_sdk from sentry_sdk.ai.monitoring import record_token_usage from sentry_sdk.ai.utils import set_data_normalized, get_start_span_function -from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import ( @@ -52,6 +52,10 @@ def setup_once(): def _capture_exception(exc): # type: (Any) -> None + span = sentry_sdk.get_current_span() + if span is not None: + span.set_status(SPANSTATUS.ERROR) + event, hint = event_from_exception( exc, client_options=sentry_sdk.get_client().options, diff --git a/sentry_sdk/integrations/cohere.py b/sentry_sdk/integrations/cohere.py index 57ffdb908a..b035b071e4 100644 --- a/sentry_sdk/integrations/cohere.py +++ b/sentry_sdk/integrations/cohere.py @@ -2,7 +2,7 @@ from sentry_sdk import consts from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.consts import SPANDATA +from sentry_sdk.consts import SPANDATA, SPANSTATUS from sentry_sdk.ai.utils import set_data_normalized from typing import TYPE_CHECKING @@ -84,6 +84,10 @@ def setup_once(): def _capture_exception(exc): # type: (Any) -> None + span = sentry_sdk.get_current_span() + if span is not None: + span.set_status(SPANSTATUS.ERROR) + event, hint = event_from_exception( exc, client_options=sentry_sdk.get_client().options, diff --git a/sentry_sdk/integrations/huggingface_hub.py b/sentry_sdk/integrations/huggingface_hub.py index cb76ccf507..931677309a 100644 --- a/sentry_sdk/integrations/huggingface_hub.py +++ b/sentry_sdk/integrations/huggingface_hub.py @@ -4,7 +4,7 @@ import sentry_sdk from sentry_sdk.ai.monitoring import record_token_usage from sentry_sdk.ai.utils import set_data_normalized -from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import ( @@ -52,6 +52,10 @@ def setup_once(): def _capture_exception(exc): # type: (Any) -> None + span = sentry_sdk.get_current_span() + if span is not None: + span.set_status(SPANSTATUS.ERROR) + event, hint = event_from_exception( exc, client_options=sentry_sdk.get_client().options, @@ -127,8 +131,6 @@ def new_huggingface_task(*args, **kwargs): try: res = f(*args, **kwargs) except Exception as e: - # Error Handling - span.set_status("error") _capture_exception(e) span.__exit__(None, None, None) raise e from None diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index 1401be06e1..e14c948f29 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -5,7 +5,7 @@ import sentry_sdk from sentry_sdk.ai.monitoring import set_ai_pipeline_name from sentry_sdk.ai.utils import set_data_normalized, get_start_span_function -from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii from sentry_sdk.tracing import Span @@ -116,7 +116,7 @@ def _handle_error(self, run_id, error): span_data = self.span_map[run_id] span = span_data.span - span.set_status("unknown") + span.set_status(SPANSTATUS.ERROR) sentry_sdk.capture_exception(error, span.scope) diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 467116c8f4..7c7d09e8f5 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -4,7 +4,7 @@ from sentry_sdk import consts from sentry_sdk.ai.monitoring import record_token_usage from sentry_sdk.ai.utils import set_data_normalized -from sentry_sdk.consts import SPANDATA +from sentry_sdk.consts import SPANDATA, SPANSTATUS from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import ( @@ -83,6 +83,8 @@ def _capture_exception(exc, manual_span_cleanup=True): # Close an eventually open span # We need to do this by hand because we are not using the start_span context manager current_span = sentry_sdk.get_current_span() + if current_span is not None: + current_span.set_status(SPANSTATUS.ERROR) if manual_span_cleanup and current_span is not None: current_span.__exit__(None, None, None) diff --git a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py index 5f9e4cb340..ad70762cd0 100644 --- a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py +++ b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py @@ -42,7 +42,7 @@ def update_execute_tool_span(span, agent, tool, result): if isinstance(result, str) and result.startswith( "An error occurred while running the tool" ): - span.set_status(SPANSTATUS.INTERNAL_ERROR) + span.set_status(SPANSTATUS.ERROR) if should_send_default_pii(): span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, result) diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index a0487e0e3a..9690f86344 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -1,6 +1,6 @@ import sentry_sdk from sentry_sdk.ai.utils import set_data_normalized -from sentry_sdk.consts import SPANDATA +from sentry_sdk.consts import SPANDATA, SPANSTATUS from sentry_sdk.integrations import DidNotEnable from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import event_from_exception, safe_serialize @@ -20,6 +20,10 @@ def _capture_exception(exc): # type: (Any) -> None + span = sentry_sdk.get_current_span() + if span is not None: + span.set_status(SPANSTATUS.ERROR) + event, hint = event_from_exception( exc, client_options=sentry_sdk.get_client().options, From 0ae5b1efa679337653762be17c66df9a2c05223c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 09:19:51 +0200 Subject: [PATCH 02/20] Anthropic span and trx status --- sentry_sdk/integrations/anthropic.py | 5 ++ .../integrations/anthropic/test_anthropic.py | 49 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 3aed4e12b0..0d692175c0 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -55,6 +55,8 @@ def _capture_exception(exc): span = sentry_sdk.get_current_span() if span is not None: span.set_status(SPANSTATUS.ERROR) + if span.containing_transaction is not None: + span.containing_transaction.set_status(SPANSTATUS.ERROR) event, hint = event_from_exception( exc, @@ -63,6 +65,9 @@ def _capture_exception(exc): ) sentry_sdk.capture_event(event, hint=hint) + if span is not None: + span.__exit__(None, None, None) + def _get_token_usage(result): # type: (Messages) -> tuple[int, int] diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index 3893626026..cc48b7596c 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -698,8 +698,52 @@ def test_exception_message_create(sentry_init, capture_events): max_tokens=1024, ) - (event,) = events + (event, transaction) = events assert event["level"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + +def test_span_status_error(sentry_init, capture_events): + sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) + events = capture_events() + + with start_transaction(name="anthropic"): + client = Anthropic(api_key="z") + client.messages._post = mock.Mock( + side_effect=AnthropicError("API rate limit reached") + ) + with pytest.raises(AnthropicError): + client.messages.create( + model="some-model", + messages=[{"role": "system", "content": "I'm throwing an exception"}], + max_tokens=1024, + ) + + (_, transaction) = events + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + +@pytest.mark.asyncio +async def test_span_status_error_async(sentry_init, capture_events): + sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) + events = capture_events() + + with start_transaction(name="anthropic"): + client = AsyncAnthropic(api_key="z") + client.messages._post = AsyncMock( + side_effect=AnthropicError("API rate limit reached") + ) + with pytest.raises(AnthropicError): + await client.messages.create( + model="some-model", + messages=[{"role": "system", "content": "I'm throwing an exception"}], + max_tokens=1024, + ) + + (_, transaction) = events + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" @pytest.mark.asyncio @@ -718,8 +762,9 @@ async def test_exception_message_create_async(sentry_init, capture_events): max_tokens=1024, ) - (event,) = events + (event, transaction) = events assert event["level"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" def test_span_origin(sentry_init, capture_events): From a8d568faa1963b1bf50f13c9cdaad337ce34c667 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 09:39:23 +0200 Subject: [PATCH 03/20] correctly finish errored spans --- sentry_sdk/integrations/anthropic.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 0d692175c0..ae9919cb92 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -65,9 +65,6 @@ def _capture_exception(exc): ) sentry_sdk.capture_event(event, hint=hint) - if span is not None: - span.__exit__(None, None, None) - def _get_token_usage(result): # type: (Messages) -> tuple[int, int] @@ -366,7 +363,12 @@ def _sentry_patched_create_sync(*args, **kwargs): integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) kwargs["integration"] = integration - return _execute_sync(f, *args, **kwargs) + try: + return _execute_sync(f, *args, **kwargs) + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_create_sync @@ -399,6 +401,11 @@ async def _sentry_patched_create_async(*args, **kwargs): integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) kwargs["integration"] = integration - return await _execute_async(f, *args, **kwargs) + try: + return await _execute_async(f, *args, **kwargs) + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_create_async From aba787ae6ffa29811ca67a9fd5cca9229032e711 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 09:58:39 +0200 Subject: [PATCH 04/20] HuggingFace span error status --- sentry_sdk/integrations/huggingface_hub.py | 555 +++++++++--------- .../huggingface_hub/test_huggingface_hub.py | 19 + 2 files changed, 305 insertions(+), 269 deletions(-) diff --git a/sentry_sdk/integrations/huggingface_hub.py b/sentry_sdk/integrations/huggingface_hub.py index 931677309a..3daeb1b4ef 100644 --- a/sentry_sdk/integrations/huggingface_hub.py +++ b/sentry_sdk/integrations/huggingface_hub.py @@ -55,6 +55,8 @@ def _capture_exception(exc): span = sentry_sdk.get_current_span() if span is not None: span.set_status(SPANSTATUS.ERROR) + if span.containing_transaction is not None: + span.containing_transaction.set_status(SPANSTATUS.ERROR) event, hint = event_from_exception( exc, @@ -69,311 +71,326 @@ def _wrap_huggingface_task(f, op): @wraps(f) def new_huggingface_task(*args, **kwargs): # type: (*Any, **Any) -> Any - integration = sentry_sdk.get_client().get_integration(HuggingfaceHubIntegration) - if integration is None: - return f(*args, **kwargs) - - prompt = None - if "prompt" in kwargs: - prompt = kwargs["prompt"] - elif "messages" in kwargs: - prompt = kwargs["messages"] - elif len(args) >= 2: - if isinstance(args[1], str) or isinstance(args[1], list): - prompt = args[1] - - if prompt is None: - # invalid call, dont instrument, let it return error - return f(*args, **kwargs) - - client = args[0] - model = client.model or kwargs.get("model") or "" - operation_name = op.split(".")[-1] - - span = sentry_sdk.start_span( - op=op, - name=f"{operation_name} {model}", - origin=HuggingfaceHubIntegration.origin, - ) - span.__enter__() - - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, operation_name) - - if model: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model) - - # Input attributes - if should_send_default_pii() and integration.include_prompts: - set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False + try: + integration = sentry_sdk.get_client().get_integration( + HuggingfaceHubIntegration + ) + if integration is None: + return f(*args, **kwargs) + + prompt = None + if "prompt" in kwargs: + prompt = kwargs["prompt"] + elif "messages" in kwargs: + prompt = kwargs["messages"] + elif len(args) >= 2: + if isinstance(args[1], str) or isinstance(args[1], list): + prompt = args[1] + + if prompt is None: + # invalid call, dont instrument, let it return error + return f(*args, **kwargs) + + client = args[0] + model = client.model or kwargs.get("model") or "" + operation_name = op.split(".")[-1] + + span = sentry_sdk.start_span( + op=op, + name=f"{operation_name} {model}", + origin=HuggingfaceHubIntegration.origin, ) + span.__enter__() - attribute_mapping = { - "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, - "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, - } - - for attribute, span_attribute in attribute_mapping.items(): - value = kwargs.get(attribute, None) - if value is not None: - if isinstance(value, (int, float, bool, str)): - span.set_data(span_attribute, value) - else: - set_data_normalized(span, span_attribute, value, unpack=False) + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, operation_name) - # LLM Execution - try: - res = f(*args, **kwargs) - except Exception as e: - _capture_exception(e) - span.__exit__(None, None, None) - raise e from None - - # Output attributes - finish_reason = None - response_model = None - response_text_buffer: list[str] = [] - tokens_used = 0 - tool_calls = None - usage = None - - with capture_internal_exceptions(): - if isinstance(res, str) and res is not None: - response_text_buffer.append(res) - - if hasattr(res, "generated_text") and res.generated_text is not None: - response_text_buffer.append(res.generated_text) - - if hasattr(res, "model") and res.model is not None: - response_model = res.model - - if hasattr(res, "details") and hasattr(res.details, "finish_reason"): - finish_reason = res.details.finish_reason - - if ( - hasattr(res, "details") - and hasattr(res.details, "generated_tokens") - and res.details.generated_tokens is not None - ): - tokens_used = res.details.generated_tokens - - if hasattr(res, "usage") and res.usage is not None: - usage = res.usage - - if hasattr(res, "choices") and res.choices is not None: - for choice in res.choices: - if hasattr(choice, "finish_reason"): - finish_reason = choice.finish_reason - if hasattr(choice, "message") and hasattr( - choice.message, "tool_calls" - ): - tool_calls = choice.message.tool_calls - if ( - hasattr(choice, "message") - and hasattr(choice.message, "content") - and choice.message.content is not None - ): - response_text_buffer.append(choice.message.content) - - if response_model is not None: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - - if finish_reason is not None: + if model: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model) + + # Input attributes + if should_send_default_pii() and integration.include_prompts: set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False ) - if should_send_default_pii() and integration.include_prompts: - if tool_calls is not None and len(tool_calls) > 0: + attribute_mapping = { + "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, + "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, + } + + for attribute, span_attribute in attribute_mapping.items(): + value = kwargs.get(attribute, None) + if value is not None: + if isinstance(value, (int, float, bool, str)): + span.set_data(span_attribute, value) + else: + set_data_normalized(span, span_attribute, value, unpack=False) + + # LLM Execution + try: + res = f(*args, **kwargs) + except Exception as e: + _capture_exception(e) + raise e from None + + # Output attributes + finish_reason = None + response_model = None + response_text_buffer: list[str] = [] + tokens_used = 0 + tool_calls = None + usage = None + + with capture_internal_exceptions(): + if isinstance(res, str) and res is not None: + response_text_buffer.append(res) + + if hasattr(res, "generated_text") and res.generated_text is not None: + response_text_buffer.append(res.generated_text) + + if hasattr(res, "model") and res.model is not None: + response_model = res.model + + if hasattr(res, "details") and hasattr(res.details, "finish_reason"): + finish_reason = res.details.finish_reason + + if ( + hasattr(res, "details") + and hasattr(res.details, "generated_tokens") + and res.details.generated_tokens is not None + ): + tokens_used = res.details.generated_tokens + + if hasattr(res, "usage") and res.usage is not None: + usage = res.usage + + if hasattr(res, "choices") and res.choices is not None: + for choice in res.choices: + if hasattr(choice, "finish_reason"): + finish_reason = choice.finish_reason + if hasattr(choice, "message") and hasattr( + choice.message, "tool_calls" + ): + tool_calls = choice.message.tool_calls + if ( + hasattr(choice, "message") + and hasattr(choice.message, "content") + and choice.message.content is not None + ): + response_text_buffer.append(choice.message.content) + + if response_model is not None: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + + if finish_reason is not None: set_data_normalized( span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, ) - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: set_data_normalized( span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, ) - if usage is not None: - record_token_usage( - span, - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - ) - elif tokens_used > 0: - record_token_usage( - span, - total_tokens=tokens_used, - ) - - # If the response is not a generator (meaning a streaming response) - # we are done and can return the response - if not inspect.isgenerator(res): - span.__exit__(None, None, None) - return res - - if kwargs.get("details", False): - # text-generation stream output - def new_details_iterator(): - # type: () -> Iterable[Any] - finish_reason = None - response_text_buffer: list[str] = [] - tokens_used = 0 - - with capture_internal_exceptions(): - for chunk in res: - if ( - hasattr(chunk, "token") - and hasattr(chunk.token, "text") - and chunk.token.text is not None - ): - response_text_buffer.append(chunk.token.text) - - if hasattr(chunk, "details") and hasattr( - chunk.details, "finish_reason" - ): - finish_reason = chunk.details.finish_reason - - if ( - hasattr(chunk, "details") - and hasattr(chunk.details, "generated_tokens") - and chunk.details.generated_tokens is not None - ): - tokens_used = chunk.details.generated_tokens - - yield chunk - - if finish_reason is not None: + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: set_data_normalized( span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, ) - if should_send_default_pii() and integration.include_prompts: - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if tokens_used > 0: - record_token_usage( - span, - total_tokens=tokens_used, - ) + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + elif tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) + # If the response is not a generator (meaning a streaming response) + # we are done and can return the response + if not inspect.isgenerator(res): span.__exit__(None, None, None) + return res + + if kwargs.get("details", False): + # text-generation stream output + def new_details_iterator(): + # type: () -> Iterable[Any] + finish_reason = None + response_text_buffer: list[str] = [] + tokens_used = 0 + + with capture_internal_exceptions(): + for chunk in res: + if ( + hasattr(chunk, "token") + and hasattr(chunk.token, "text") + and chunk.token.text is not None + ): + response_text_buffer.append(chunk.token.text) + + if hasattr(chunk, "details") and hasattr( + chunk.details, "finish_reason" + ): + finish_reason = chunk.details.finish_reason + + if ( + hasattr(chunk, "details") + and hasattr(chunk.details, "generated_tokens") + and chunk.details.generated_tokens is not None + ): + tokens_used = chunk.details.generated_tokens + + yield chunk + + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) - return new_details_iterator() - - else: - # chat-completion stream output - def new_iterator(): - # type: () -> Iterable[str] - finish_reason = None - response_model = None - response_text_buffer: list[str] = [] - tool_calls = None - usage = None - - with capture_internal_exceptions(): - for chunk in res: - if hasattr(chunk, "model") and chunk.model is not None: - response_model = chunk.model - - if hasattr(chunk, "usage") and chunk.usage is not None: - usage = chunk.usage - - if isinstance(chunk, str): - if chunk is not None: - response_text_buffer.append(chunk) - - if hasattr(chunk, "choices") and chunk.choices is not None: - for choice in chunk.choices: - if ( - hasattr(choice, "delta") - and hasattr(choice.delta, "content") - and choice.delta.content is not None - ): - response_text_buffer.append( - choice.delta.content + if ( + should_send_default_pii() + and integration.include_prompts + ): + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, ) - if ( - hasattr(choice, "finish_reason") - and choice.finish_reason is not None - ): - finish_reason = choice.finish_reason - - if ( - hasattr(choice, "delta") - and hasattr(choice.delta, "tool_calls") - and choice.delta.tool_calls is not None - ): - tool_calls = choice.delta.tool_calls + if tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) - yield chunk + span.__exit__(None, None, None) - if response_model is not None: - span.set_data( - SPANDATA.GEN_AI_RESPONSE_MODEL, response_model - ) + return new_details_iterator() - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) + else: + # chat-completion stream output + def new_iterator(): + # type: () -> Iterable[str] + finish_reason = None + response_model = None + response_text_buffer: list[str] = [] + tool_calls = None + usage = None + + with capture_internal_exceptions(): + for chunk in res: + if hasattr(chunk, "model") and chunk.model is not None: + response_model = chunk.model + + if hasattr(chunk, "usage") and chunk.usage is not None: + usage = chunk.usage + + if isinstance(chunk, str): + if chunk is not None: + response_text_buffer.append(chunk) + + if ( + hasattr(chunk, "choices") + and chunk.choices is not None + ): + for choice in chunk.choices: + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "content") + and choice.delta.content is not None + ): + response_text_buffer.append( + choice.delta.content + ) + + if ( + hasattr(choice, "finish_reason") + and choice.finish_reason is not None + ): + finish_reason = choice.finish_reason + + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "tool_calls") + and choice.delta.tool_calls is not None + ): + tool_calls = choice.delta.tool_calls + + yield chunk + + if response_model is not None: + span.set_data( + SPANDATA.GEN_AI_RESPONSE_MODEL, response_model + ) - if should_send_default_pii() and integration.include_prompts: - if tool_calls is not None and len(tool_calls) > 0: + if finish_reason is not None: set_data_normalized( span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, ) - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: + if ( + should_send_default_pii() + and integration.include_prompts + ): + if tool_calls is not None and len(tool_calls) > 0: set_data_normalized( span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, ) - if usage is not None: - record_token_usage( - span, - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - ) + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) - span.__exit__(None, None, None) + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + + span.__exit__(None, None, None) - return new_iterator() + return new_iterator() + finally: + current_span = sentry_sdk.get_current_span() + if current_span is not None and current_span.status == SPANSTATUS.ERROR: + current_span.__exit__(None, None, None) return new_huggingface_task diff --git a/tests/integrations/huggingface_hub/test_huggingface_hub.py b/tests/integrations/huggingface_hub/test_huggingface_hub.py index 86f9c10109..ea5f7a14eb 100644 --- a/tests/integrations/huggingface_hub/test_huggingface_hub.py +++ b/tests/integrations/huggingface_hub/test_huggingface_hub.py @@ -654,6 +654,25 @@ def test_chat_completion_api_error( assert span["data"] == expected_data +def test_span_status_error(sentry_init, capture_events, mock_hf_api_with_errors): + # type: (Any, Any, Any) -> None + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + client = InferenceClient(model="test-model") + + with sentry_sdk.start_transaction(name="test"): + with pytest.raises(HfHubHTTPError): + client.chat_completion( + messages=[{"role": "user", "content": "Hello!"}], + ) + + error, transaction = events + assert error["level"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + + @pytest.mark.parametrize("send_default_pii", [True, False]) @pytest.mark.parametrize("include_prompts", [True, False]) def test_chat_completion_with_tools( From 7b95818a60fe5c884d2e35a038308244b6a16413 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 10:27:13 +0200 Subject: [PATCH 05/20] fixed assertion --- sentry_sdk/integrations/openai.py | 91 ++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 26 deletions(-) diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 7c7d09e8f5..1e03619606 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -85,6 +85,9 @@ def _capture_exception(exc, manual_span_cleanup=True): current_span = sentry_sdk.get_current_span() if current_span is not None: current_span.set_status(SPANSTATUS.ERROR) + if current_span.containing_transaction is not None: + current_span.containing_transaction.set_status(SPANSTATUS.ERROR) + if manual_span_cleanup and current_span is not None: current_span.__exit__(None, None, None) @@ -458,12 +461,18 @@ def _execute_sync(f, *args, **kwargs): @wraps(f) def _sentry_patched_create_sync(*args, **kwargs): # type: (Any, Any) -> Any - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return f(*args, **kwargs) + try: + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return f(*args, **kwargs) - return _execute_sync(f, *args, **kwargs) + return _execute_sync(f, *args, **kwargs) + + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_create_sync @@ -493,12 +502,18 @@ async def _execute_async(f, *args, **kwargs): @wraps(f) async def _sentry_patched_create_async(*args, **kwargs): # type: (Any, Any) -> Any - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return await f(*args, **kwargs) + try: + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return await f(*args, **kwargs) - return await _execute_async(f, *args, **kwargs) + return await _execute_async(f, *args, **kwargs) + finally: + + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_create_async @@ -551,11 +566,17 @@ def _execute_sync(f, *args, **kwargs): @wraps(f) def _sentry_patched_create_sync(*args, **kwargs): # type: (Any, Any) -> Any - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) + try: + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) - return _execute_sync(f, *args, **kwargs) + return _execute_sync(f, *args, **kwargs) + + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_create_sync @@ -585,11 +606,17 @@ async def _execute_async(f, *args, **kwargs): @wraps(f) async def _sentry_patched_create_async(*args, **kwargs): # type: (Any, Any) -> Any - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) + try: + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + return await _execute_async(f, *args, **kwargs) - return await _execute_async(f, *args, **kwargs) + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_create_async @@ -644,11 +671,17 @@ def _execute_sync(f, *args, **kwargs): @wraps(f) def _sentry_patched_create_sync(*args, **kwargs): # type: (Any, Any) -> Any - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) + try: + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) - return _execute_sync(f, *args, **kwargs) + return _execute_sync(f, *args, **kwargs) + + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_create_sync @@ -678,10 +711,16 @@ async def _execute_async(f, *args, **kwargs): @wraps(f) async def _sentry_patched_responses_async(*args, **kwargs): # type: (Any, Any) -> Any - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) + try: + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) - return await _execute_async(f, *args, **kwargs) + return await _execute_async(f, *args, **kwargs) + + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_responses_async From cf568fb8a5386c0d657c4c9b6ece76a846f9c1b2 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 10:27:13 +0200 Subject: [PATCH 06/20] Close errored spans in OpenAI --- sentry_sdk/integrations/openai.py | 91 ++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 26 deletions(-) diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 7c7d09e8f5..1e03619606 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -85,6 +85,9 @@ def _capture_exception(exc, manual_span_cleanup=True): current_span = sentry_sdk.get_current_span() if current_span is not None: current_span.set_status(SPANSTATUS.ERROR) + if current_span.containing_transaction is not None: + current_span.containing_transaction.set_status(SPANSTATUS.ERROR) + if manual_span_cleanup and current_span is not None: current_span.__exit__(None, None, None) @@ -458,12 +461,18 @@ def _execute_sync(f, *args, **kwargs): @wraps(f) def _sentry_patched_create_sync(*args, **kwargs): # type: (Any, Any) -> Any - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return f(*args, **kwargs) + try: + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return f(*args, **kwargs) - return _execute_sync(f, *args, **kwargs) + return _execute_sync(f, *args, **kwargs) + + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_create_sync @@ -493,12 +502,18 @@ async def _execute_async(f, *args, **kwargs): @wraps(f) async def _sentry_patched_create_async(*args, **kwargs): # type: (Any, Any) -> Any - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return await f(*args, **kwargs) + try: + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return await f(*args, **kwargs) - return await _execute_async(f, *args, **kwargs) + return await _execute_async(f, *args, **kwargs) + finally: + + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_create_async @@ -551,11 +566,17 @@ def _execute_sync(f, *args, **kwargs): @wraps(f) def _sentry_patched_create_sync(*args, **kwargs): # type: (Any, Any) -> Any - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) + try: + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) - return _execute_sync(f, *args, **kwargs) + return _execute_sync(f, *args, **kwargs) + + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_create_sync @@ -585,11 +606,17 @@ async def _execute_async(f, *args, **kwargs): @wraps(f) async def _sentry_patched_create_async(*args, **kwargs): # type: (Any, Any) -> Any - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) + try: + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) + + return await _execute_async(f, *args, **kwargs) - return await _execute_async(f, *args, **kwargs) + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_create_async @@ -644,11 +671,17 @@ def _execute_sync(f, *args, **kwargs): @wraps(f) def _sentry_patched_create_sync(*args, **kwargs): # type: (Any, Any) -> Any - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) + try: + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) - return _execute_sync(f, *args, **kwargs) + return _execute_sync(f, *args, **kwargs) + + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_create_sync @@ -678,10 +711,16 @@ async def _execute_async(f, *args, **kwargs): @wraps(f) async def _sentry_patched_responses_async(*args, **kwargs): # type: (Any, Any) -> Any - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) + try: + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) - return await _execute_async(f, *args, **kwargs) + return await _execute_async(f, *args, **kwargs) + + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + span.__exit__(None, None, None) return _sentry_patched_responses_async From a9298b7f0d52e652fb9b515657ea0c931cc1a74b Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 10:29:33 +0200 Subject: [PATCH 07/20] fix assertion --- tests/integrations/huggingface_hub/test_huggingface_hub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/huggingface_hub/test_huggingface_hub.py b/tests/integrations/huggingface_hub/test_huggingface_hub.py index ea5f7a14eb..01ade52400 100644 --- a/tests/integrations/huggingface_hub/test_huggingface_hub.py +++ b/tests/integrations/huggingface_hub/test_huggingface_hub.py @@ -670,7 +670,7 @@ def test_span_status_error(sentry_init, capture_events, mock_hf_api_with_errors) error, transaction = events assert error["level"] == "error" assert transaction["spans"][0]["tags"]["status"] == "error" - assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" @pytest.mark.parametrize("send_default_pii", [True, False]) From df15184b768385c62c285660ecd55ce0bd382874 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 10:33:21 +0200 Subject: [PATCH 08/20] thats not the way --- sentry_sdk/integrations/anthropic.py | 14 +---- sentry_sdk/integrations/openai.py | 88 ++++++++-------------------- 2 files changed, 28 insertions(+), 74 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index ae9919cb92..b1dac5cb8e 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -363,12 +363,7 @@ def _sentry_patched_create_sync(*args, **kwargs): integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) kwargs["integration"] = integration - try: - return _execute_sync(f, *args, **kwargs) - finally: - span = sentry_sdk.get_current_span() - if span is not None and span.status == SPANSTATUS.ERROR: - span.__exit__(None, None, None) + return _execute_sync(f, *args, **kwargs) return _sentry_patched_create_sync @@ -401,11 +396,6 @@ async def _sentry_patched_create_async(*args, **kwargs): integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) kwargs["integration"] = integration - try: - return await _execute_async(f, *args, **kwargs) - finally: - span = sentry_sdk.get_current_span() - if span is not None and span.status == SPANSTATUS.ERROR: - span.__exit__(None, None, None) + return await _execute_async(f, *args, **kwargs) return _sentry_patched_create_async diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 1e03619606..fa2fb78cd6 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -461,18 +461,12 @@ def _execute_sync(f, *args, **kwargs): @wraps(f) def _sentry_patched_create_sync(*args, **kwargs): # type: (Any, Any) -> Any - try: - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return f(*args, **kwargs) - - return _execute_sync(f, *args, **kwargs) + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return f(*args, **kwargs) - finally: - span = sentry_sdk.get_current_span() - if span is not None and span.status == SPANSTATUS.ERROR: - span.__exit__(None, None, None) + return _execute_sync(f, *args, **kwargs) return _sentry_patched_create_sync @@ -502,18 +496,12 @@ async def _execute_async(f, *args, **kwargs): @wraps(f) async def _sentry_patched_create_async(*args, **kwargs): # type: (Any, Any) -> Any - try: - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None or "messages" not in kwargs: - # no "messages" means invalid call (in all versions of openai), let it return error - return await f(*args, **kwargs) + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None or "messages" not in kwargs: + # no "messages" means invalid call (in all versions of openai), let it return error + return await f(*args, **kwargs) - return await _execute_async(f, *args, **kwargs) - finally: - - span = sentry_sdk.get_current_span() - if span is not None and span.status == SPANSTATUS.ERROR: - span.__exit__(None, None, None) + return await _execute_async(f, *args, **kwargs) return _sentry_patched_create_async @@ -566,17 +554,11 @@ def _execute_sync(f, *args, **kwargs): @wraps(f) def _sentry_patched_create_sync(*args, **kwargs): # type: (Any, Any) -> Any - try: - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) - return _execute_sync(f, *args, **kwargs) - - finally: - span = sentry_sdk.get_current_span() - if span is not None and span.status == SPANSTATUS.ERROR: - span.__exit__(None, None, None) + return _execute_sync(f, *args, **kwargs) return _sentry_patched_create_sync @@ -606,17 +588,11 @@ async def _execute_async(f, *args, **kwargs): @wraps(f) async def _sentry_patched_create_async(*args, **kwargs): # type: (Any, Any) -> Any - try: - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) - - return await _execute_async(f, *args, **kwargs) + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) - finally: - span = sentry_sdk.get_current_span() - if span is not None and span.status == SPANSTATUS.ERROR: - span.__exit__(None, None, None) + return await _execute_async(f, *args, **kwargs) return _sentry_patched_create_async @@ -671,17 +647,11 @@ def _execute_sync(f, *args, **kwargs): @wraps(f) def _sentry_patched_create_sync(*args, **kwargs): # type: (Any, Any) -> Any - try: - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return f(*args, **kwargs) + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return f(*args, **kwargs) - return _execute_sync(f, *args, **kwargs) - - finally: - span = sentry_sdk.get_current_span() - if span is not None and span.status == SPANSTATUS.ERROR: - span.__exit__(None, None, None) + return _execute_sync(f, *args, **kwargs) return _sentry_patched_create_sync @@ -711,16 +681,10 @@ async def _execute_async(f, *args, **kwargs): @wraps(f) async def _sentry_patched_responses_async(*args, **kwargs): # type: (Any, Any) -> Any - try: - integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) - if integration is None: - return await f(*args, **kwargs) + integration = sentry_sdk.get_client().get_integration(OpenAIIntegration) + if integration is None: + return await f(*args, **kwargs) - return await _execute_async(f, *args, **kwargs) - - finally: - span = sentry_sdk.get_current_span() - if span is not None and span.status == SPANSTATUS.ERROR: - span.__exit__(None, None, None) + return await _execute_async(f, *args, **kwargs) return _sentry_patched_responses_async From bf2d0af7d26a1d40b085fecf0e65d1bdae3386d6 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 10:34:56 +0200 Subject: [PATCH 09/20] cleanup --- sentry_sdk/integrations/huggingface_hub.py | 552 +++++++++--------- .../integrations/openai_agents/utils.py | 2 + 2 files changed, 270 insertions(+), 284 deletions(-) diff --git a/sentry_sdk/integrations/huggingface_hub.py b/sentry_sdk/integrations/huggingface_hub.py index 3daeb1b4ef..f998486462 100644 --- a/sentry_sdk/integrations/huggingface_hub.py +++ b/sentry_sdk/integrations/huggingface_hub.py @@ -71,326 +71,310 @@ def _wrap_huggingface_task(f, op): @wraps(f) def new_huggingface_task(*args, **kwargs): # type: (*Any, **Any) -> Any - try: - integration = sentry_sdk.get_client().get_integration( - HuggingfaceHubIntegration - ) - if integration is None: - return f(*args, **kwargs) - - prompt = None - if "prompt" in kwargs: - prompt = kwargs["prompt"] - elif "messages" in kwargs: - prompt = kwargs["messages"] - elif len(args) >= 2: - if isinstance(args[1], str) or isinstance(args[1], list): - prompt = args[1] - - if prompt is None: - # invalid call, dont instrument, let it return error - return f(*args, **kwargs) - - client = args[0] - model = client.model or kwargs.get("model") or "" - operation_name = op.split(".")[-1] - - span = sentry_sdk.start_span( - op=op, - name=f"{operation_name} {model}", - origin=HuggingfaceHubIntegration.origin, - ) - span.__enter__() + integration = sentry_sdk.get_client().get_integration(HuggingfaceHubIntegration) + if integration is None: + return f(*args, **kwargs) + + prompt = None + if "prompt" in kwargs: + prompt = kwargs["prompt"] + elif "messages" in kwargs: + prompt = kwargs["messages"] + elif len(args) >= 2: + if isinstance(args[1], str) or isinstance(args[1], list): + prompt = args[1] + + if prompt is None: + # invalid call, dont instrument, let it return error + return f(*args, **kwargs) + + client = args[0] + model = client.model or kwargs.get("model") or "" + operation_name = op.split(".")[-1] + + span = sentry_sdk.start_span( + op=op, + name=f"{operation_name} {model}", + origin=HuggingfaceHubIntegration.origin, + ) + span.__enter__() - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, operation_name) + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, operation_name) - if model: - span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model) + if model: + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model) - # Input attributes - if should_send_default_pii() and integration.include_prompts: + # Input attributes + if should_send_default_pii() and integration.include_prompts: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False + ) + + attribute_mapping = { + "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, + "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, + "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, + } + + for attribute, span_attribute in attribute_mapping.items(): + value = kwargs.get(attribute, None) + if value is not None: + if isinstance(value, (int, float, bool, str)): + span.set_data(span_attribute, value) + else: + set_data_normalized(span, span_attribute, value, unpack=False) + + # LLM Execution + try: + res = f(*args, **kwargs) + except Exception as e: + _capture_exception(e) + raise e from None + + # Output attributes + finish_reason = None + response_model = None + response_text_buffer: list[str] = [] + tokens_used = 0 + tool_calls = None + usage = None + + with capture_internal_exceptions(): + if isinstance(res, str) and res is not None: + response_text_buffer.append(res) + + if hasattr(res, "generated_text") and res.generated_text is not None: + response_text_buffer.append(res.generated_text) + + if hasattr(res, "model") and res.model is not None: + response_model = res.model + + if hasattr(res, "details") and hasattr(res.details, "finish_reason"): + finish_reason = res.details.finish_reason + + if ( + hasattr(res, "details") + and hasattr(res.details, "generated_tokens") + and res.details.generated_tokens is not None + ): + tokens_used = res.details.generated_tokens + + if hasattr(res, "usage") and res.usage is not None: + usage = res.usage + + if hasattr(res, "choices") and res.choices is not None: + for choice in res.choices: + if hasattr(choice, "finish_reason"): + finish_reason = choice.finish_reason + if hasattr(choice, "message") and hasattr( + choice.message, "tool_calls" + ): + tool_calls = choice.message.tool_calls + if ( + hasattr(choice, "message") + and hasattr(choice.message, "content") + and choice.message.content is not None + ): + response_text_buffer.append(choice.message.content) + + if response_model is not None: + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) + + if finish_reason is not None: set_data_normalized( - span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, ) - attribute_mapping = { - "tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, - "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, - "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, - "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, - "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, - "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, - "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, - "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, - } - - for attribute, span_attribute in attribute_mapping.items(): - value = kwargs.get(attribute, None) - if value is not None: - if isinstance(value, (int, float, bool, str)): - span.set_data(span_attribute, value) - else: - set_data_normalized(span, span_attribute, value, unpack=False) - - # LLM Execution - try: - res = f(*args, **kwargs) - except Exception as e: - _capture_exception(e) - raise e from None - - # Output attributes - finish_reason = None - response_model = None - response_text_buffer: list[str] = [] - tokens_used = 0 - tool_calls = None - usage = None - - with capture_internal_exceptions(): - if isinstance(res, str) and res is not None: - response_text_buffer.append(res) - - if hasattr(res, "generated_text") and res.generated_text is not None: - response_text_buffer.append(res.generated_text) - - if hasattr(res, "model") and res.model is not None: - response_model = res.model - - if hasattr(res, "details") and hasattr(res.details, "finish_reason"): - finish_reason = res.details.finish_reason - - if ( - hasattr(res, "details") - and hasattr(res.details, "generated_tokens") - and res.details.generated_tokens is not None - ): - tokens_used = res.details.generated_tokens - - if hasattr(res, "usage") and res.usage is not None: - usage = res.usage - - if hasattr(res, "choices") and res.choices is not None: - for choice in res.choices: - if hasattr(choice, "finish_reason"): - finish_reason = choice.finish_reason - if hasattr(choice, "message") and hasattr( - choice.message, "tool_calls" - ): - tool_calls = choice.message.tool_calls - if ( - hasattr(choice, "message") - and hasattr(choice.message, "content") - and choice.message.content is not None - ): - response_text_buffer.append(choice.message.content) - - if response_model is not None: - span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model) - - if finish_reason is not None: + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: set_data_normalized( span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, ) - if should_send_default_pii() and integration.include_prompts: - if tool_calls is not None and len(tool_calls) > 0: + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: set_data_normalized( span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, ) - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) + elif tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) + + # If the response is not a generator (meaning a streaming response) + # we are done and can return the response + if not inspect.isgenerator(res): + span.__exit__(None, None, None) + return res + + if kwargs.get("details", False): + # text-generation stream output + def new_details_iterator(): + # type: () -> Iterable[Any] + finish_reason = None + response_text_buffer: list[str] = [] + tokens_used = 0 + + with capture_internal_exceptions(): + for chunk in res: + if ( + hasattr(chunk, "token") + and hasattr(chunk.token, "text") + and chunk.token.text is not None + ): + response_text_buffer.append(chunk.token.text) + + if hasattr(chunk, "details") and hasattr( + chunk.details, "finish_reason" + ): + finish_reason = chunk.details.finish_reason + + if ( + hasattr(chunk, "details") + and hasattr(chunk.details, "generated_tokens") + and chunk.details.generated_tokens is not None + ): + tokens_used = chunk.details.generated_tokens + + yield chunk + + if finish_reason is not None: set_data_normalized( span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, ) - if usage is not None: - record_token_usage( - span, - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - ) - elif tokens_used > 0: - record_token_usage( - span, - total_tokens=tokens_used, - ) + if should_send_default_pii() and integration.include_prompts: + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, + ) + + if tokens_used > 0: + record_token_usage( + span, + total_tokens=tokens_used, + ) - # If the response is not a generator (meaning a streaming response) - # we are done and can return the response - if not inspect.isgenerator(res): span.__exit__(None, None, None) - return res - - if kwargs.get("details", False): - # text-generation stream output - def new_details_iterator(): - # type: () -> Iterable[Any] - finish_reason = None - response_text_buffer: list[str] = [] - tokens_used = 0 - - with capture_internal_exceptions(): - for chunk in res: - if ( - hasattr(chunk, "token") - and hasattr(chunk.token, "text") - and chunk.token.text is not None - ): - response_text_buffer.append(chunk.token.text) - - if hasattr(chunk, "details") and hasattr( - chunk.details, "finish_reason" - ): - finish_reason = chunk.details.finish_reason - - if ( - hasattr(chunk, "details") - and hasattr(chunk.details, "generated_tokens") - and chunk.details.generated_tokens is not None - ): - tokens_used = chunk.details.generated_tokens - - yield chunk - - if finish_reason is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, - ) - if ( - should_send_default_pii() - and integration.include_prompts - ): - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, + return new_details_iterator() + + else: + # chat-completion stream output + def new_iterator(): + # type: () -> Iterable[str] + finish_reason = None + response_model = None + response_text_buffer: list[str] = [] + tool_calls = None + usage = None + + with capture_internal_exceptions(): + for chunk in res: + if hasattr(chunk, "model") and chunk.model is not None: + response_model = chunk.model + + if hasattr(chunk, "usage") and chunk.usage is not None: + usage = chunk.usage + + if isinstance(chunk, str): + if chunk is not None: + response_text_buffer.append(chunk) + + if hasattr(chunk, "choices") and chunk.choices is not None: + for choice in chunk.choices: + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "content") + and choice.delta.content is not None + ): + response_text_buffer.append( + choice.delta.content ) - if tokens_used > 0: - record_token_usage( - span, - total_tokens=tokens_used, - ) + if ( + hasattr(choice, "finish_reason") + and choice.finish_reason is not None + ): + finish_reason = choice.finish_reason - span.__exit__(None, None, None) + if ( + hasattr(choice, "delta") + and hasattr(choice.delta, "tool_calls") + and choice.delta.tool_calls is not None + ): + tool_calls = choice.delta.tool_calls - return new_details_iterator() + yield chunk - else: - # chat-completion stream output - def new_iterator(): - # type: () -> Iterable[str] - finish_reason = None - response_model = None - response_text_buffer: list[str] = [] - tool_calls = None - usage = None - - with capture_internal_exceptions(): - for chunk in res: - if hasattr(chunk, "model") and chunk.model is not None: - response_model = chunk.model - - if hasattr(chunk, "usage") and chunk.usage is not None: - usage = chunk.usage - - if isinstance(chunk, str): - if chunk is not None: - response_text_buffer.append(chunk) - - if ( - hasattr(chunk, "choices") - and chunk.choices is not None - ): - for choice in chunk.choices: - if ( - hasattr(choice, "delta") - and hasattr(choice.delta, "content") - and choice.delta.content is not None - ): - response_text_buffer.append( - choice.delta.content - ) - - if ( - hasattr(choice, "finish_reason") - and choice.finish_reason is not None - ): - finish_reason = choice.finish_reason - - if ( - hasattr(choice, "delta") - and hasattr(choice.delta, "tool_calls") - and choice.delta.tool_calls is not None - ): - tool_calls = choice.delta.tool_calls - - yield chunk - - if response_model is not None: - span.set_data( - SPANDATA.GEN_AI_RESPONSE_MODEL, response_model - ) + if response_model is not None: + span.set_data( + SPANDATA.GEN_AI_RESPONSE_MODEL, response_model + ) - if finish_reason is not None: + if finish_reason is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, + finish_reason, + ) + + if should_send_default_pii() and integration.include_prompts: + if tool_calls is not None and len(tool_calls) > 0: set_data_normalized( span, - SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, - finish_reason, + SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, + tool_calls, + unpack=False, ) - if ( - should_send_default_pii() - and integration.include_prompts - ): - if tool_calls is not None and len(tool_calls) > 0: + if len(response_text_buffer) > 0: + text_response = "".join(response_text_buffer) + if text_response: set_data_normalized( span, - SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - tool_calls, - unpack=False, + SPANDATA.GEN_AI_RESPONSE_TEXT, + text_response, ) - if len(response_text_buffer) > 0: - text_response = "".join(response_text_buffer) - if text_response: - set_data_normalized( - span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - text_response, - ) - - if usage is not None: - record_token_usage( - span, - input_tokens=usage.prompt_tokens, - output_tokens=usage.completion_tokens, - total_tokens=usage.total_tokens, - ) + if usage is not None: + record_token_usage( + span, + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + total_tokens=usage.total_tokens, + ) - span.__exit__(None, None, None) + span.__exit__(None, None, None) - return new_iterator() - finally: - current_span = sentry_sdk.get_current_span() - if current_span is not None and current_span.status == SPANSTATUS.ERROR: - current_span.__exit__(None, None, None) + return new_iterator() return new_huggingface_task diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index 9690f86344..863b85cd7d 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -23,6 +23,8 @@ def _capture_exception(exc): span = sentry_sdk.get_current_span() if span is not None: span.set_status(SPANSTATUS.ERROR) + if span.containing_transaction is not None: + span.containing_transaction.set_status(SPANSTATUS.ERROR) event, hint = event_from_exception( exc, From 64eccb3f97dc15570d7133e8697413a190616c94 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 10:36:31 +0200 Subject: [PATCH 10/20] transaction errored in cohere --- sentry_sdk/integrations/cohere.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry_sdk/integrations/cohere.py b/sentry_sdk/integrations/cohere.py index b035b071e4..cff70b0d34 100644 --- a/sentry_sdk/integrations/cohere.py +++ b/sentry_sdk/integrations/cohere.py @@ -87,6 +87,8 @@ def _capture_exception(exc): span = sentry_sdk.get_current_span() if span is not None: span.set_status(SPANSTATUS.ERROR) + if span.containing_transaction is not None: + span.containing_transaction.set_status(SPANSTATUS.ERROR) event, hint = event_from_exception( exc, From 61807ca65da80d28119db512f5b376ccbc51490e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 10:37:51 +0200 Subject: [PATCH 11/20] cleanup --- sentry_sdk/integrations/huggingface_hub.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/integrations/huggingface_hub.py b/sentry_sdk/integrations/huggingface_hub.py index f998486462..174c24b99a 100644 --- a/sentry_sdk/integrations/huggingface_hub.py +++ b/sentry_sdk/integrations/huggingface_hub.py @@ -134,6 +134,7 @@ def new_huggingface_task(*args, **kwargs): res = f(*args, **kwargs) except Exception as e: _capture_exception(e) + span.__exit__(None, None, None) raise e from None # Output attributes From 3106f4bd45c3678b359a65510349c54732b74e27 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 10:58:14 +0200 Subject: [PATCH 12/20] Refactor --- sentry_sdk/integrations/anthropic.py | 9 +++------ sentry_sdk/integrations/cohere.py | 10 ++++------ sentry_sdk/integrations/huggingface_hub.py | 9 +++------ sentry_sdk/integrations/openai.py | 8 +++----- sentry_sdk/integrations/openai_agents/utils.py | 9 +++------ sentry_sdk/tracing_utils.py | 11 ++++++++++- 6 files changed, 26 insertions(+), 30 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index b1dac5cb8e..544b22740f 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -4,9 +4,10 @@ import sentry_sdk from sentry_sdk.ai.monitoring import record_token_usage from sentry_sdk.ai.utils import set_data_normalized, get_start_span_function -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import set_span_errored from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, @@ -52,11 +53,7 @@ def setup_once(): def _capture_exception(exc): # type: (Any) -> None - span = sentry_sdk.get_current_span() - if span is not None: - span.set_status(SPANSTATUS.ERROR) - if span.containing_transaction is not None: - span.containing_transaction.set_status(SPANSTATUS.ERROR) + set_span_errored() event, hint = event_from_exception( exc, diff --git a/sentry_sdk/integrations/cohere.py b/sentry_sdk/integrations/cohere.py index cff70b0d34..3445900c80 100644 --- a/sentry_sdk/integrations/cohere.py +++ b/sentry_sdk/integrations/cohere.py @@ -2,11 +2,13 @@ from sentry_sdk import consts from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.consts import SPANDATA, SPANSTATUS +from sentry_sdk.consts import SPANDATA from sentry_sdk.ai.utils import set_data_normalized from typing import TYPE_CHECKING +from sentry_sdk.tracing_utils import set_span_errored + if TYPE_CHECKING: from typing import Any, Callable, Iterator from sentry_sdk.tracing import Span @@ -84,11 +86,7 @@ def setup_once(): def _capture_exception(exc): # type: (Any) -> None - span = sentry_sdk.get_current_span() - if span is not None: - span.set_status(SPANSTATUS.ERROR) - if span.containing_transaction is not None: - span.containing_transaction.set_status(SPANSTATUS.ERROR) + set_span_errored() event, hint = event_from_exception( exc, diff --git a/sentry_sdk/integrations/huggingface_hub.py b/sentry_sdk/integrations/huggingface_hub.py index 174c24b99a..2e2b382abd 100644 --- a/sentry_sdk/integrations/huggingface_hub.py +++ b/sentry_sdk/integrations/huggingface_hub.py @@ -4,9 +4,10 @@ import sentry_sdk from sentry_sdk.ai.monitoring import record_token_usage from sentry_sdk.ai.utils import set_data_normalized -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import set_span_errored from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, @@ -52,11 +53,7 @@ def setup_once(): def _capture_exception(exc): # type: (Any) -> None - span = sentry_sdk.get_current_span() - if span is not None: - span.set_status(SPANSTATUS.ERROR) - if span.containing_transaction is not None: - span.containing_transaction.set_status(SPANSTATUS.ERROR) + set_span_errored() event, hint = event_from_exception( exc, diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index fa2fb78cd6..4d72ec366c 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -4,9 +4,10 @@ from sentry_sdk import consts from sentry_sdk.ai.monitoring import record_token_usage from sentry_sdk.ai.utils import set_data_normalized -from sentry_sdk.consts import SPANDATA, SPANSTATUS +from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import set_span_errored from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, @@ -83,10 +84,7 @@ def _capture_exception(exc, manual_span_cleanup=True): # Close an eventually open span # We need to do this by hand because we are not using the start_span context manager current_span = sentry_sdk.get_current_span() - if current_span is not None: - current_span.set_status(SPANSTATUS.ERROR) - if current_span.containing_transaction is not None: - current_span.containing_transaction.set_status(SPANSTATUS.ERROR) + set_span_errored(current_span) if manual_span_cleanup and current_span is not None: current_span.__exit__(None, None, None) diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index 863b85cd7d..73d2858e7f 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -1,8 +1,9 @@ import sentry_sdk from sentry_sdk.ai.utils import set_data_normalized -from sentry_sdk.consts import SPANDATA, SPANSTATUS +from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.tracing_utils import set_span_errored from sentry_sdk.utils import event_from_exception, safe_serialize from typing import TYPE_CHECKING @@ -20,11 +21,7 @@ def _capture_exception(exc): # type: (Any) -> None - span = sentry_sdk.get_current_span() - if span is not None: - span.set_status(SPANSTATUS.ERROR) - if span.containing_transaction is not None: - span.containing_transaction.set_status(SPANSTATUS.ERROR) + set_span_errored() event, hint = event_from_exception( exc, diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index c1cfde293b..f79864be7f 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -11,7 +11,7 @@ import uuid import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANTEMPLATE +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS, SPANTEMPLATE from sentry_sdk.utils import ( capture_internal_exceptions, filename_for_module, @@ -882,6 +882,15 @@ def sync_wrapper(*args, **kwargs): return span_decorator +def set_span_errored(span=None): + # type: (Optional[Span]) -> None + span = span or get_current_span() + if span is not None: + span.set_status(SPANSTATUS.ERROR) + if span.containing_transaction is not None: + span.containing_transaction.set_status(SPANSTATUS.ERROR) + + def get_current_span(scope=None): # type: (Optional[sentry_sdk.Scope]) -> Optional[Span] """ From c0bda2b74b42e097ddcda3f10b67c11e6110237e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 11:02:26 +0200 Subject: [PATCH 13/20] cleanup --- sentry_sdk/tracing_utils.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index f79864be7f..2f3e334e3f 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -882,15 +882,6 @@ def sync_wrapper(*args, **kwargs): return span_decorator -def set_span_errored(span=None): - # type: (Optional[Span]) -> None - span = span or get_current_span() - if span is not None: - span.set_status(SPANSTATUS.ERROR) - if span.containing_transaction is not None: - span.containing_transaction.set_status(SPANSTATUS.ERROR) - - def get_current_span(scope=None): # type: (Optional[sentry_sdk.Scope]) -> Optional[Span] """ @@ -901,6 +892,19 @@ def get_current_span(scope=None): return current_span +def set_span_errored(span=None): + # type: (Optional[Span]) -> None + """ + Set the status of the current or given span to ERROR. + Also sets the status of the transaction (root span) to ERROR. + """ + span = span or get_current_span() + if span is not None: + span.set_status(SPANSTATUS.ERROR) + if span.containing_transaction is not None: + span.containing_transaction.set_status(SPANSTATUS.ERROR) + + def _generate_sample_rand( trace_id, # type: Optional[str] *, From e5c824a34b56c5b985749019faa87aeae856b48d Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 11:11:06 +0200 Subject: [PATCH 14/20] cohere test --- tests/integrations/anthropic/test_anthropic.py | 6 ++++-- tests/integrations/cohere/test_cohere.py | 18 ++++++++++++++++++ .../huggingface_hub/test_huggingface_hub.py | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index cc48b7596c..04ff12eb8b 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -719,7 +719,8 @@ def test_span_status_error(sentry_init, capture_events): max_tokens=1024, ) - (_, transaction) = events + (error, transaction) = events + assert error["level"] == "error" assert transaction["spans"][0]["tags"]["status"] == "error" assert transaction["contexts"]["trace"]["status"] == "error" @@ -741,7 +742,8 @@ async def test_span_status_error_async(sentry_init, capture_events): max_tokens=1024, ) - (_, transaction) = events + (error, transaction) = events + assert error["level"] == "error" assert transaction["spans"][0]["tags"]["status"] == "error" assert transaction["contexts"]["trace"]["status"] == "error" diff --git a/tests/integrations/cohere/test_cohere.py b/tests/integrations/cohere/test_cohere.py index ee876172d1..a97d2befae 100644 --- a/tests/integrations/cohere/test_cohere.py +++ b/tests/integrations/cohere/test_cohere.py @@ -167,6 +167,24 @@ def test_bad_chat(sentry_init, capture_events): assert event["level"] == "error" +def test_span_status_error(sentry_init, capture_events): + sentry_init(integrations=[CohereIntegration()], traces_sample_rate=1.0) + events = capture_events() + + with start_transaction(name="test"): + client = Client(api_key="z") + HTTPXClient.request = mock.Mock( + side_effect=httpx.HTTPError("API rate limit reached") + ) + with pytest.raises(httpx.HTTPError): + client.chat(model="some-model", message="hello") + + (error, transaction) = events + assert error["level"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + @pytest.mark.parametrize( "send_default_pii, include_prompts", [(True, True), (True, False), (False, True), (False, False)], diff --git a/tests/integrations/huggingface_hub/test_huggingface_hub.py b/tests/integrations/huggingface_hub/test_huggingface_hub.py index 01ade52400..5aa3928a67 100644 --- a/tests/integrations/huggingface_hub/test_huggingface_hub.py +++ b/tests/integrations/huggingface_hub/test_huggingface_hub.py @@ -667,7 +667,7 @@ def test_span_status_error(sentry_init, capture_events, mock_hf_api_with_errors) messages=[{"role": "user", "content": "Hello!"}], ) - error, transaction = events + (error, transaction) = events assert error["level"] == "error" assert transaction["spans"][0]["tags"]["status"] == "error" assert transaction["contexts"]["trace"]["status"] == "error" From ccd2593a8fb3821e3e5417a9fce68528a80ff008 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 11:14:03 +0200 Subject: [PATCH 15/20] langchain test --- .../integrations/langchain/test_langchain.py | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index b6b432c523..0bf1d9e14b 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -249,7 +249,7 @@ def test_langchain_error(sentry_init, capture_events): ] ) global stream_result_mock - stream_result_mock = Mock(side_effect=Exception("API rate limit error")) + stream_result_mock = Mock(side_effect=ValueError("API rate limit error")) llm = MockOpenAI( model_name="gpt-3.5-turbo", temperature=0, @@ -259,13 +259,53 @@ def test_langchain_error(sentry_init, capture_events): agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True) - with start_transaction(), pytest.raises(Exception): + with start_transaction(), pytest.raises(ValueError): list(agent_executor.stream({"input": "How many letters in the word eudca"})) error = events[0] assert error["level"] == "error" +def test_span_status_error(sentry_init, capture_events): + sentry_init( + integrations=[LangchainIntegration(include_prompts=True)], + traces_sample_rate=1.0, + ) + events = capture_events() + + with start_transaction(name="test"): + prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + "You are very powerful assistant, but don't know current events", + ), + ("user", "{input}"), + MessagesPlaceholder(variable_name="agent_scratchpad"), + ] + ) + global stream_result_mock + stream_result_mock = Mock(side_effect=ValueError("API rate limit error")) + llm = MockOpenAI( + model_name="gpt-3.5-turbo", + temperature=0, + openai_api_key="badkey", + ) + agent = create_openai_tools_agent(llm, [get_word_length], prompt) + + agent_executor = AgentExecutor( + agent=agent, tools=[get_word_length], verbose=True + ) + + with pytest.raises(ValueError): + list(agent_executor.stream({"input": "How many letters in the word eudca"})) + + (error, transaction) = events + assert error["level"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + def test_span_origin(sentry_init, capture_events): sentry_init( integrations=[LangchainIntegration()], From 7c064535988ec5079614b827d6c7c2aed2d3786b Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 11:15:49 +0200 Subject: [PATCH 16/20] openai tests --- tests/integrations/openai/test_openai.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 18968fb36a..e7fbf8a7d8 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -416,6 +416,26 @@ def test_bad_chat_completion(sentry_init, capture_events): assert event["level"] == "error" +def test_span_status_error(sentry_init, capture_events): + sentry_init(integrations=[OpenAIIntegration()], traces_sample_rate=1.0) + events = capture_events() + + with start_transaction(name="test"): + client = OpenAI(api_key="z") + client.chat.completions._post = mock.Mock( + side_effect=OpenAIError("API rate limit reached") + ) + with pytest.raises(OpenAIError): + client.chat.completions.create( + model="some-model", messages=[{"role": "system", "content": "hello"}] + ) + + (error, transaction) = events + assert error["level"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + @pytest.mark.asyncio async def test_bad_chat_completion_async(sentry_init, capture_events): sentry_init(integrations=[OpenAIIntegration()], traces_sample_rate=1.0) From b42c6c0bb9b50016d92a0a7fab1ebc11e1ba2acf Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 11:17:45 +0200 Subject: [PATCH 17/20] openai agents test --- .../openai_agents/test_openai_agents.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index 047b919213..bd7f15faff 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -657,6 +657,32 @@ async def test_error_handling(sentry_init, capture_events, test_agent): assert ai_client_span["tags"]["status"] == "internal_error" +@pytest.mark.asyncio +async def test_span_status_error(sentry_init, capture_events, test_agent): + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + with patch( + "agents.models.openai_responses.OpenAIResponsesModel.get_response" + ) as mock_get_response: + mock_get_response.side_effect = ValueError("Model Error") + + sentry_init( + integrations=[OpenAIAgentsIntegration()], + traces_sample_rate=1.0, + ) + + events = capture_events() + + with pytest.raises(ValueError, match="Model Error"): + await agents.Runner.run( + test_agent, "Test input", run_config=test_run_config + ) + + (error, transaction) = events + assert error["level"] == "error" + assert transaction["spans"][0]["tags"]["status"] == "error" + assert transaction["contexts"]["trace"]["status"] == "error" + + @pytest.mark.asyncio async def test_multiple_agents_asyncio( sentry_init, capture_events, test_agent, mock_model_response From c65f6c57c32e06a90d656b0a9cdfee37999ddc7f Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 11:41:56 +0200 Subject: [PATCH 18/20] complete error spans in anthropic --- sentry_sdk/integrations/anthropic.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 544b22740f..d9898fa1d1 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -4,7 +4,7 @@ import sentry_sdk from sentry_sdk.ai.monitoring import record_token_usage from sentry_sdk.ai.utils import set_data_normalized, get_start_span_function -from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii from sentry_sdk.tracing_utils import set_span_errored @@ -360,7 +360,13 @@ def _sentry_patched_create_sync(*args, **kwargs): integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) kwargs["integration"] = integration - return _execute_sync(f, *args, **kwargs) + try: + return _execute_sync(f, *args, **kwargs) + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + with capture_internal_exceptions(): + span.__exit__(None, None, None) return _sentry_patched_create_sync @@ -393,6 +399,12 @@ async def _sentry_patched_create_async(*args, **kwargs): integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) kwargs["integration"] = integration - return await _execute_async(f, *args, **kwargs) + try: + return await _execute_async(f, *args, **kwargs) + finally: + span = sentry_sdk.get_current_span() + if span is not None and span.status == SPANSTATUS.ERROR: + with capture_internal_exceptions(): + span.__exit__(None, None, None) return _sentry_patched_create_async From d6a3824a9c301b34d39b9048039271ef7111c5f1 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 11:54:13 +0200 Subject: [PATCH 19/20] Do not override an existing error status --- 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 fc43a33dc7..4edda21075 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -416,7 +416,8 @@ def __enter__(self): def __exit__(self, ty, value, tb): # type: (Optional[Any], Optional[Any], Optional[Any]) -> None if value is not None and should_be_treated_as_error(ty, value): - self.set_status(SPANSTATUS.INTERNAL_ERROR) + if self.status != SPANSTATUS.ERROR: + self.set_status(SPANSTATUS.INTERNAL_ERROR) with capture_internal_exceptions(): scope, old_span = self._context_manager_state From fbdf167490dc9a5e97386e8e079b5bcaaf61af5b Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 19 Sep 2025 12:07:31 +0200 Subject: [PATCH 20/20] langchain work --- sentry_sdk/integrations/langchain.py | 8 ++++---- tests/integrations/langchain/test_langchain.py | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index e14c948f29..17955d0af1 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -5,11 +5,10 @@ import sentry_sdk from sentry_sdk.ai.monitoring import set_ai_pipeline_name from sentry_sdk.ai.utils import set_data_normalized, get_start_span_function -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import _get_value +from sentry_sdk.tracing_utils import _get_value, set_span_errored from sentry_sdk.utils import logger, capture_internal_exceptions from typing import TYPE_CHECKING @@ -26,6 +25,7 @@ Union, ) from uuid import UUID + from sentry_sdk.tracing import Span try: @@ -116,7 +116,7 @@ def _handle_error(self, run_id, error): span_data = self.span_map[run_id] span = span_data.span - span.set_status(SPANSTATUS.ERROR) + set_span_errored(span) sentry_sdk.capture_exception(error, span.scope) diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 0bf1d9e14b..1e450936b5 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -267,6 +267,9 @@ def test_langchain_error(sentry_init, capture_events): def test_span_status_error(sentry_init, capture_events): + global llm_type + llm_type = "acme-llm" + sentry_init( integrations=[LangchainIntegration(include_prompts=True)], traces_sample_rate=1.0,