From 1af3203fb161b8a15318780e00223cea1c8cf0b9 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Tue, 5 Aug 2025 14:30:57 +0200 Subject: [PATCH 01/21] feat(anthropic) update span attributes from old AI attributes to new GEN_AI ones Additional simplifications and streamlining by using common utilities --- sentry_sdk/integrations/anthropic.py | 183 ++++++++++++++++----------- 1 file changed, 109 insertions(+), 74 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 1e1f9112a1..52662d80eb 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -3,6 +3,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.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii @@ -10,9 +11,11 @@ capture_internal_exceptions, event_from_exception, package_version, + safe_serialize, ) try: + from anthropic import NOT_GIVEN from anthropic.resources import AsyncMessages, Messages if TYPE_CHECKING: @@ -53,8 +56,11 @@ def _capture_exception(exc): sentry_sdk.capture_event(event, hint=hint) -def _calculate_token_usage(result, span): - # type: (Messages, Span) -> None +def _get_token_usage(result): + # type: (Messages) -> tuple[int, int] + """ + Get token usage from the Anthropic response. + """ input_tokens = 0 output_tokens = 0 if hasattr(result, "usage"): @@ -64,37 +70,13 @@ def _calculate_token_usage(result, span): if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int): output_tokens = usage.output_tokens - total_tokens = input_tokens + output_tokens + return input_tokens, output_tokens - record_token_usage( - span, - input_tokens=input_tokens, - output_tokens=output_tokens, - total_tokens=total_tokens, - ) - -def _get_responses(content): - # type: (list[Any]) -> list[dict[str, Any]] +def _collect_ai_data(event, model, input_tokens, output_tokens, content_blocks): + # type: (MessageStreamEvent, str | None, int, int, list[str]) -> tuple[str | None, int, int, list[str]] """ - Get JSON of a Anthropic responses. - """ - responses = [] - for item in content: - if hasattr(item, "text"): - responses.append( - { - "type": item.type, - "text": item.text, - } - ) - return responses - - -def _collect_ai_data(event, input_tokens, output_tokens, content_blocks): - # type: (MessageStreamEvent, int, int, list[str]) -> tuple[int, int, list[str]] - """ - Count token usage and collect content blocks from the AI streaming response. + Collect model information, token usage, and collect content blocks from the AI streaming response. """ with capture_internal_exceptions(): if hasattr(event, "type"): @@ -102,6 +84,7 @@ def _collect_ai_data(event, input_tokens, output_tokens, content_blocks): usage = event.message.usage input_tokens += usage.input_tokens output_tokens += usage.output_tokens + model = event.message.model or model elif event.type == "content_block_start": pass elif event.type == "content_block_delta": @@ -114,31 +97,69 @@ def _collect_ai_data(event, input_tokens, output_tokens, content_blocks): elif event.type == "message_delta": output_tokens += event.usage.output_tokens - return input_tokens, output_tokens, content_blocks + return model, input_tokens, output_tokens, content_blocks -def _add_ai_data_to_span( - span, integration, input_tokens, output_tokens, content_blocks -): - # type: (Span, AnthropicIntegration, int, int, list[str]) -> None +def _set_input_data(span, kwargs, integration): + # type: (Span, dict[str, Any], AnthropicIntegration) -> None """ - Add token usage and content blocks from the AI streaming response to the span. + Set input data for the span based on the provided keyword arguments for the anthropic message creation. """ - with capture_internal_exceptions(): - if should_send_default_pii() and integration.include_prompts: - complete_message = "".join(content_blocks) - span.set_data( - SPANDATA.AI_RESPONSES, - [{"type": "text", "text": complete_message}], - ) - total_tokens = input_tokens + output_tokens - record_token_usage( - span, - input_tokens=input_tokens, - output_tokens=output_tokens, - total_tokens=total_tokens, + messages = kwargs.get("messages") + if ( + messages is not None + and len(messages) > 0 + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages) + + kwargs_keys_to_attributes = { + "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, + "model": SPANDATA.GEN_AI_REQUEST_MODEL, + "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, + "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, + } + for key, attribute in kwargs_keys_to_attributes.items(): + value = kwargs.get(key) + if value is not NOT_GIVEN or value is not None: + set_data_normalized(span, attribute, value) + + # Input attributes: Tools + tools = kwargs.get("tools") + if tools is not NOT_GIVEN and tools is not None and len(tools) > 0: + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools) ) - span.set_data(SPANDATA.AI_STREAMING, True) + + +def _set_output_data( + span, + integration, + model, + input_tokens, + output_tokens, + content_blocks, + finish_span=True, +): + # type: (Span, AnthropicIntegration, str | None, int | None, int | None, list[Any], bool) -> None + """ + Set output data for the span based on the AI response.""" + span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, model) + if should_send_default_pii() and integration.include_prompts: + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, content_blocks) + + record_token_usage( + span, + input_tokens=input_tokens, + output_tokens=output_tokens, + ) + + # TODO: GEN_AI_RESPONSE_TOOL_CALLS ? + + if finish_span: + span.__exit__(None, None, None) def _sentry_patched_create_common(f, *args, **kwargs): @@ -162,24 +183,22 @@ def _sentry_patched_create_common(f, *args, **kwargs): ) span.__enter__() - result = yield f, args, kwargs + _set_input_data(span, kwargs, integration) - # add data to span and finish it - messages = list(kwargs["messages"]) - model = kwargs.get("model") + result = yield f, args, kwargs with capture_internal_exceptions(): - span.set_data(SPANDATA.AI_MODEL_ID, model) - span.set_data(SPANDATA.AI_STREAMING, False) - - if should_send_default_pii() and integration.include_prompts: - span.set_data(SPANDATA.AI_INPUT_MESSAGES, messages) - if hasattr(result, "content"): - if should_send_default_pii() and integration.include_prompts: - span.set_data(SPANDATA.AI_RESPONSES, _get_responses(result.content)) - _calculate_token_usage(result, span) - span.__exit__(None, None, None) + input_tokens, output_tokens = _get_token_usage(result) + _set_output_data( + span, + integration, + getattr(result, "model", None), + input_tokens, + output_tokens, + content_blocks=result.content, + finish_span=True, + ) # Streaming response elif hasattr(result, "_iterator"): @@ -187,37 +206,53 @@ def _sentry_patched_create_common(f, *args, **kwargs): def new_iterator(): # type: () -> Iterator[MessageStreamEvent] + model = None input_tokens = 0 output_tokens = 0 content_blocks = [] # type: list[str] for event in old_iterator: - input_tokens, output_tokens, content_blocks = _collect_ai_data( - event, input_tokens, output_tokens, content_blocks + model, input_tokens, output_tokens, content_blocks = ( + _collect_ai_data( + event, model, input_tokens, output_tokens, content_blocks + ) ) yield event - _add_ai_data_to_span( - span, integration, input_tokens, output_tokens, content_blocks + _set_output_data( + span, + integration, + model=model, + input_tokens=input_tokens, + output_tokens=output_tokens, + content_blocks=content_blocks, + finish_span=True, ) - span.__exit__(None, None, None) async def new_iterator_async(): # type: () -> AsyncIterator[MessageStreamEvent] + model = None input_tokens = 0 output_tokens = 0 content_blocks = [] # type: list[str] async for event in old_iterator: - input_tokens, output_tokens, content_blocks = _collect_ai_data( - event, input_tokens, output_tokens, content_blocks + model, input_tokens, output_tokens, content_blocks = ( + _collect_ai_data( + event, model, input_tokens, output_tokens, content_blocks + ) ) yield event - _add_ai_data_to_span( - span, integration, input_tokens, output_tokens, content_blocks + _set_output_data( + span, + integration, + model=model, + input_tokens=input_tokens, + output_tokens=output_tokens, + content_blocks=content_blocks, + finish_span=True, ) - span.__exit__(None, None, None) if str(type(result._iterator)) == "": result._iterator = new_iterator_async() From 115af469371640643654029f55471cef135bc4af Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Tue, 5 Aug 2025 14:37:19 +0200 Subject: [PATCH 02/21] test(anthropic) update tests to use GEN_AI attributes --- .../integrations/anthropic/test_anthropic.py | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index e6e1a40aa9..9667540436 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -114,21 +114,21 @@ def test_nonstreaming_create_message( assert span["op"] == OP.ANTHROPIC_MESSAGES_CREATE assert span["description"] == "Anthropic messages create" - assert span["data"][SPANDATA.AI_MODEL_ID] == "model" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages - assert span["data"][SPANDATA.AI_RESPONSES] == [ + assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == messages + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == [ {"type": "text", "text": "Hi, I'm Claude."} ] else: - assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] - assert SPANDATA.AI_RESPONSES not in span["data"] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] - assert span["data"]["gen_ai.usage.input_tokens"] == 10 - assert span["data"]["gen_ai.usage.output_tokens"] == 20 - assert span["data"]["gen_ai.usage.total_tokens"] == 30 - assert span["data"][SPANDATA.AI_STREAMING] is False + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 + assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 + assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30 + assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False @pytest.mark.asyncio @@ -182,21 +182,21 @@ async def test_nonstreaming_create_message_async( assert span["op"] == OP.ANTHROPIC_MESSAGES_CREATE assert span["description"] == "Anthropic messages create" - assert span["data"][SPANDATA.AI_MODEL_ID] == "model" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages - assert span["data"][SPANDATA.AI_RESPONSES] == [ + assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == messages + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == [ {"type": "text", "text": "Hi, I'm Claude."} ] else: - assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] - assert SPANDATA.AI_RESPONSES not in span["data"] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] - assert span["data"]["gen_ai.usage.input_tokens"] == 10 - assert span["data"]["gen_ai.usage.output_tokens"] == 20 - assert span["data"]["gen_ai.usage.total_tokens"] == 30 - assert span["data"][SPANDATA.AI_STREAMING] is False + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 + assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 + assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30 + assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False @pytest.mark.parametrize( @@ -281,22 +281,22 @@ def test_streaming_create_message( assert span["op"] == OP.ANTHROPIC_MESSAGES_CREATE assert span["description"] == "Anthropic messages create" - assert span["data"][SPANDATA.AI_MODEL_ID] == "model" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages - assert span["data"][SPANDATA.AI_RESPONSES] == [ + assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == messages + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == [ {"type": "text", "text": "Hi! I'm Claude!"} ] else: - assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] - assert SPANDATA.AI_RESPONSES not in span["data"] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] - assert span["data"]["gen_ai.usage.input_tokens"] == 10 - assert span["data"]["gen_ai.usage.output_tokens"] == 30 - assert span["data"]["gen_ai.usage.total_tokens"] == 40 - assert span["data"][SPANDATA.AI_STREAMING] is True + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 + assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 30 + assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 40 + assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True @pytest.mark.asyncio @@ -384,22 +384,22 @@ async def test_streaming_create_message_async( assert span["op"] == OP.ANTHROPIC_MESSAGES_CREATE assert span["description"] == "Anthropic messages create" - assert span["data"][SPANDATA.AI_MODEL_ID] == "model" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages - assert span["data"][SPANDATA.AI_RESPONSES] == [ + assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == messages + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == [ {"type": "text", "text": "Hi! I'm Claude!"} ] else: - assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] - assert SPANDATA.AI_RESPONSES not in span["data"] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] - assert span["data"]["gen_ai.usage.input_tokens"] == 10 - assert span["data"]["gen_ai.usage.output_tokens"] == 30 - assert span["data"]["gen_ai.usage.total_tokens"] == 40 - assert span["data"][SPANDATA.AI_STREAMING] is True + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 + assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 30 + assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 40 + assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True @pytest.mark.skipif( @@ -514,21 +514,21 @@ def test_streaming_create_message_with_input_json_delta( assert span["op"] == OP.ANTHROPIC_MESSAGES_CREATE assert span["description"] == "Anthropic messages create" - assert span["data"][SPANDATA.AI_MODEL_ID] == "model" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages - assert span["data"][SPANDATA.AI_RESPONSES] == [ + assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == messages + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == [ {"text": "{'location': 'San Francisco, CA'}", "type": "text"} ] else: - assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] - assert SPANDATA.AI_RESPONSES not in span["data"] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] - assert span["data"]["gen_ai.usage.input_tokens"] == 366 - assert span["data"]["gen_ai.usage.output_tokens"] == 51 - assert span["data"]["gen_ai.usage.total_tokens"] == 417 - assert span["data"][SPANDATA.AI_STREAMING] is True + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366 + assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 51 + assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 417 + assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True @pytest.mark.asyncio @@ -650,22 +650,22 @@ async def test_streaming_create_message_with_input_json_delta_async( assert span["op"] == OP.ANTHROPIC_MESSAGES_CREATE assert span["description"] == "Anthropic messages create" - assert span["data"][SPANDATA.AI_MODEL_ID] == "model" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages - assert span["data"][SPANDATA.AI_RESPONSES] == [ + assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == messages + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == [ {"text": "{'location': 'San Francisco, CA'}", "type": "text"} ] else: - assert SPANDATA.AI_INPUT_MESSAGES not in span["data"] - assert SPANDATA.AI_RESPONSES not in span["data"] + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] - assert span["data"]["gen_ai.usage.input_tokens"] == 366 - assert span["data"]["gen_ai.usage.output_tokens"] == 51 - assert span["data"]["gen_ai.usage.total_tokens"] == 417 - assert span["data"][SPANDATA.AI_STREAMING] is True + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366 + assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 51 + assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 417 + assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True def test_exception_message_create(sentry_init, capture_events): @@ -810,7 +810,7 @@ def test_add_ai_data_to_span_with_input_json_delta(sentry_init): assert span._data.get("ai.responses") == [ {"type": "text", "text": "{'test': 'data','more': 'json'}"} ] - assert span._data.get("ai.streaming") is True - assert span._data.get("gen_ai.usage.input_tokens") == 10 - assert span._data.get("gen_ai.usage.output_tokens") == 20 - assert span._data.get("gen_ai.usage.total_tokens") == 30 + assert span._data.get(SPANDATA.GEN_AI_RESPONSE_STREAMING) is True + assert span._data.get(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS) == 10 + assert span._data.get(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS) == 20 + assert span._data.get(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS) == 30 From 8abce4738cbe2a0693dd22cf781b781595fc565a Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Wed, 6 Aug 2025 19:39:03 +0200 Subject: [PATCH 03/21] feat(ai) adding `unpack` parameter to `set_data_normalized` to control whether to automatically unpack single elements in lists --- sentry_sdk/ai/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/ai/utils.py b/sentry_sdk/ai/utils.py index a3c62600c0..a61f19bf1d 100644 --- a/sentry_sdk/ai/utils.py +++ b/sentry_sdk/ai/utils.py @@ -7,8 +7,8 @@ from sentry_sdk.utils import logger -def _normalize_data(data): - # type: (Any) -> Any +def _normalize_data(data, unpack=True): + # type: (Any, bool) -> Any # convert pydantic data (e.g. OpenAI v1+) to json compatible format if hasattr(data, "model_dump"): @@ -18,7 +18,7 @@ def _normalize_data(data): logger.warning("Could not convert pydantic data to JSON: %s", e) return data if isinstance(data, list): - if len(data) == 1: + if unpack and len(data) == 1: return _normalize_data(data[0]) # remove empty dimensions return list(_normalize_data(x) for x in data) if isinstance(data, dict): @@ -27,9 +27,9 @@ def _normalize_data(data): return data -def set_data_normalized(span, key, value): - # type: (Span, str, Any) -> None - normalized = _normalize_data(value) +def set_data_normalized(span, key, value, unpack=True): + # type: (Span, str, Any, bool) -> None + normalized = _normalize_data(value, unpack=unpack) if isinstance(normalized, (int, float, bool, str)): span.set_data(key, normalized) else: From 8a0894f8c3289415f9a76953f07dfe8a806fed55 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Wed, 6 Aug 2025 19:39:40 +0200 Subject: [PATCH 04/21] fix(anthropic) fix some span attribute values --- sentry_sdk/integrations/anthropic.py | 29 ++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 52662d80eb..d8621b0ce5 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -1,4 +1,5 @@ from functools import wraps +import json from typing import TYPE_CHECKING import sentry_sdk @@ -112,18 +113,23 @@ def _set_input_data(span, kwargs, integration): and should_send_default_pii() and integration.include_prompts ): - set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages) + set_data_normalized( + span, SPANDATA.GEN_AI_REQUEST_MESSAGES, safe_serialize(messages) + ) + + set_data_normalized( + span, SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False) + ) kwargs_keys_to_attributes = { "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, "model": SPANDATA.GEN_AI_REQUEST_MODEL, - "stream": SPANDATA.GEN_AI_RESPONSE_STREAMING, "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, } for key, attribute in kwargs_keys_to_attributes.items(): value = kwargs.get(key) - if value is not NOT_GIVEN or value is not None: + if value is not NOT_GIVEN and value is not None: set_data_normalized(span, attribute, value) # Input attributes: Tools @@ -141,14 +147,19 @@ def _set_output_data( input_tokens, output_tokens, content_blocks, - finish_span=True, + finish_span=False, ): # type: (Span, AnthropicIntegration, str | None, int | None, int | None, list[Any], bool) -> None """ Set output data for the span based on the AI response.""" span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, model) if should_send_default_pii() and integration.include_prompts: - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, content_blocks) + set_data_normalized( + span, + SPANDATA.GEN_AI_RESPONSE_TEXT, + json.dumps(content_blocks), + unpack=False, + ) record_token_usage( span, @@ -196,7 +207,9 @@ def _sentry_patched_create_common(f, *args, **kwargs): getattr(result, "model", None), input_tokens, output_tokens, - content_blocks=result.content, + content_blocks=[ + content_block.to_dict() for content_block in result.content + ], finish_span=True, ) @@ -225,7 +238,7 @@ def new_iterator(): model=model, input_tokens=input_tokens, output_tokens=output_tokens, - content_blocks=content_blocks, + content_blocks=[{"text": "".join(content_blocks), "type": "text"}], finish_span=True, ) @@ -250,7 +263,7 @@ async def new_iterator_async(): model=model, input_tokens=input_tokens, output_tokens=output_tokens, - content_blocks=content_blocks, + content_blocks=[{"text": "".join(content_blocks), "type": "text"}], finish_span=True, ) From 29f6ba2580ce3381649488e7faf6bce20cbf3d15 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Wed, 6 Aug 2025 19:40:11 +0200 Subject: [PATCH 05/21] test(anthropic) fixup tests --- .../integrations/anthropic/test_anthropic.py | 94 ++++++++++++------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index 9667540436..b7c3cfd508 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -20,7 +20,7 @@ async def __call__(self, *args, **kwargs): from anthropic.types.message_delta_event import MessageDeltaEvent from anthropic.types.message_start_event import MessageStartEvent -from sentry_sdk.integrations.anthropic import _add_ai_data_to_span, _collect_ai_data +from sentry_sdk.integrations.anthropic import _set_output_data, _collect_ai_data from sentry_sdk.utils import package_version try: @@ -117,10 +117,14 @@ def test_nonstreaming_create_message( assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == messages - assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == [ - {"type": "text", "text": "Hi, I'm Claude."} - ] + assert ( + span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + == '[{"role": "user", "content": "Hello, Claude"}]' + ) + assert ( + span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + == '[{"text": "Hi, I\'m Claude.", "type": "text"}]' + ) else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] @@ -185,10 +189,14 @@ async def test_nonstreaming_create_message_async( assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == messages - assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == [ - {"type": "text", "text": "Hi, I'm Claude."} - ] + assert ( + span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + == '[{"role": "user", "content": "Hello, Claude"}]' + ) + assert ( + span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + == '[{"text": "Hi, I\'m Claude.", "type": "text"}]' + ) else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] @@ -284,10 +292,14 @@ def test_streaming_create_message( assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == messages - assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == [ - {"type": "text", "text": "Hi! I'm Claude!"} - ] + assert ( + span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + == '[{"role": "user", "content": "Hello, Claude"}]' + ) + assert ( + span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + == '[{"text": "Hi! I\'m Claude!", "type": "text"}]' + ) else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] @@ -387,10 +399,14 @@ async def test_streaming_create_message_async( assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == messages - assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == [ - {"type": "text", "text": "Hi! I'm Claude!"} - ] + assert ( + span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + == '[{"role": "user", "content": "Hello, Claude"}]' + ) + assert ( + span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + == '[{"text": "Hi! I\'m Claude!", "type": "text"}]' + ) else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] @@ -517,10 +533,14 @@ def test_streaming_create_message_with_input_json_delta( assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == messages - assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == [ - {"text": "{'location': 'San Francisco, CA'}", "type": "text"} - ] + assert ( + span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + == '[{"role": "user", "content": "What is the weather like in San Francisco?"}]' + ) + assert ( + span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + == '[{"text": "{\'location\': \'San Francisco, CA\'}", "type": "text"}]' + ) else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] @@ -653,10 +673,14 @@ async def test_streaming_create_message_with_input_json_delta_async( assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: - assert span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == messages - assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == [ - {"text": "{'location': 'San Francisco, CA'}", "type": "text"} - ] + assert ( + span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + == '[{"role": "user", "content": "What is the weather like in San Francisco?"}]' + ) + assert ( + span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] + == '[{"text": "{\'location\': \'San Francisco, CA\'}", "type": "text"}]' + ) else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] @@ -770,15 +794,16 @@ def test_collect_ai_data_with_input_json_delta(): index=0, type="content_block_delta", ) - + model = None input_tokens = 10 output_tokens = 20 content_blocks = [] - new_input_tokens, new_output_tokens, new_content_blocks = _collect_ai_data( - event, input_tokens, output_tokens, content_blocks + model, new_input_tokens, new_output_tokens, new_content_blocks = _collect_ai_data( + event, model, input_tokens, output_tokens, content_blocks ) + assert model is None assert new_input_tokens == input_tokens assert new_output_tokens == output_tokens assert new_content_blocks == ["test"] @@ -788,7 +813,7 @@ def test_collect_ai_data_with_input_json_delta(): ANTHROPIC_VERSION < (0, 27), reason="Versions <0.27.0 do not include InputJSONDelta.", ) -def test_add_ai_data_to_span_with_input_json_delta(sentry_init): +def test_set_output_data_with_input_json_delta(sentry_init): sentry_init( integrations=[AnthropicIntegration(include_prompts=True)], traces_sample_rate=1.0, @@ -799,18 +824,19 @@ def test_add_ai_data_to_span_with_input_json_delta(sentry_init): span = start_span() integration = AnthropicIntegration() - _add_ai_data_to_span( + _set_output_data( span, integration, + model="", input_tokens=10, output_tokens=20, content_blocks=["{'test': 'data',", "'more': 'json'}"], ) - assert span._data.get("ai.responses") == [ - {"type": "text", "text": "{'test': 'data','more': 'json'}"} - ] - assert span._data.get(SPANDATA.GEN_AI_RESPONSE_STREAMING) is True + assert ( + span._data.get(SPANDATA.GEN_AI_RESPONSE_TEXT) + == "[\"{'test': 'data',\", \"'more': 'json'}\"]" + ) assert span._data.get(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS) == 10 assert span._data.get(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS) == 20 assert span._data.get(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS) == 30 From 5087bc4e6ca27246cf3535d144e37571276414a5 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Wed, 6 Aug 2025 20:00:28 +0200 Subject: [PATCH 06/21] fix(anthropic) import error for older versions of anthropic --- sentry_sdk/integrations/anthropic.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index d8621b0ce5..9f4b9845d3 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -16,7 +16,11 @@ ) try: - from anthropic import NOT_GIVEN + try: + from anthropic import NOT_GIVEN + except ImportError: + NOT_GIVEN = None + from anthropic.resources import AsyncMessages, Messages if TYPE_CHECKING: From 0f1405cb172d1d6cdf44c281c6ba2f664025f44b Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Wed, 6 Aug 2025 20:29:34 +0200 Subject: [PATCH 07/21] fix(anthropic) compatibility fix for v0.16 --- sentry_sdk/integrations/anthropic.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 9f4b9845d3..26b0258c87 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -212,7 +212,12 @@ def _sentry_patched_create_common(f, *args, **kwargs): input_tokens, output_tokens, content_blocks=[ - content_block.to_dict() for content_block in result.content + ( + content_block.to_dict() + if hasattr(content_block, "to_dict") + else content_block.model_dump() + ) + for content_block in result.content ], finish_span=True, ) From cb7940d5f25a8320c09773f272db4d1c09f08497 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Thu, 7 Aug 2025 10:57:14 +0200 Subject: [PATCH 08/21] fix(ai): propagating `unpack` parameter into recursive calls --- sentry_sdk/ai/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/ai/utils.py b/sentry_sdk/ai/utils.py index a61f19bf1d..a58ba02d69 100644 --- a/sentry_sdk/ai/utils.py +++ b/sentry_sdk/ai/utils.py @@ -19,10 +19,10 @@ def _normalize_data(data, unpack=True): return data if isinstance(data, list): if unpack and len(data) == 1: - return _normalize_data(data[0]) # remove empty dimensions - return list(_normalize_data(x) for x in data) + return _normalize_data(data[0], unpack=unpack) # remove empty dimensions + return list(_normalize_data(x, unpack=unpack) for x in data) if isinstance(data, dict): - return {k: _normalize_data(v) for (k, v) in data.items()} + return {k: _normalize_data(v, unpack) for (k, v) in data.items()} return data From 04399618b22ff1d83b5e0a2060f8c4616e35adc8 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Thu, 7 Aug 2025 10:57:45 +0200 Subject: [PATCH 09/21] fix(anthropic) using generic operation name instead of anthropic specific one --- sentry_sdk/integrations/anthropic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 26b0258c87..25cc1a9c25 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -192,7 +192,7 @@ def _sentry_patched_create_common(f, *args, **kwargs): return f(*args, **kwargs) span = sentry_sdk.start_span( - op=OP.ANTHROPIC_MESSAGES_CREATE, + op=OP.GEN_AI_CHAT, description="Anthropic messages create", origin=AnthropicIntegration.origin, ) From 35ae58411f504c4b1e90ab0d1f6aec6b0149794c Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Thu, 7 Aug 2025 11:03:19 +0200 Subject: [PATCH 10/21] test(anthropic) fix tests to use correct span op field --- tests/integrations/anthropic/test_anthropic.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index b7c3cfd508..b0458d6a8d 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -112,7 +112,7 @@ def test_nonstreaming_create_message( assert len(event["spans"]) == 1 (span,) = event["spans"] - assert span["op"] == OP.ANTHROPIC_MESSAGES_CREATE + assert span["op"] == OP.GEN_AI_CHAT assert span["description"] == "Anthropic messages create" assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" @@ -184,7 +184,7 @@ async def test_nonstreaming_create_message_async( assert len(event["spans"]) == 1 (span,) = event["spans"] - assert span["op"] == OP.ANTHROPIC_MESSAGES_CREATE + assert span["op"] == OP.GEN_AI_CHAT assert span["description"] == "Anthropic messages create" assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" @@ -287,7 +287,7 @@ def test_streaming_create_message( assert len(event["spans"]) == 1 (span,) = event["spans"] - assert span["op"] == OP.ANTHROPIC_MESSAGES_CREATE + assert span["op"] == OP.GEN_AI_CHAT assert span["description"] == "Anthropic messages create" assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" @@ -394,7 +394,7 @@ async def test_streaming_create_message_async( assert len(event["spans"]) == 1 (span,) = event["spans"] - assert span["op"] == OP.ANTHROPIC_MESSAGES_CREATE + assert span["op"] == OP.GEN_AI_CHAT assert span["description"] == "Anthropic messages create" assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" @@ -528,7 +528,7 @@ def test_streaming_create_message_with_input_json_delta( assert len(event["spans"]) == 1 (span,) = event["spans"] - assert span["op"] == OP.ANTHROPIC_MESSAGES_CREATE + assert span["op"] == OP.GEN_AI_CHAT assert span["description"] == "Anthropic messages create" assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" @@ -668,7 +668,7 @@ async def test_streaming_create_message_with_input_json_delta_async( assert len(event["spans"]) == 1 (span,) = event["spans"] - assert span["op"] == OP.ANTHROPIC_MESSAGES_CREATE + assert span["op"] == OP.GEN_AI_CHAT assert span["description"] == "Anthropic messages create" assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" From 2a720b532c228eb75bd2deb0ba211024cccb9753 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Thu, 7 Aug 2025 14:08:40 +0200 Subject: [PATCH 11/21] fix(anthropic) add `top_k` parameter to span attribute mapping --- sentry_sdk/integrations/anthropic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 25cc1a9c25..102778168e 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -129,6 +129,7 @@ def _set_input_data(span, kwargs, integration): "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, "model": SPANDATA.GEN_AI_REQUEST_MODEL, "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE, + "top_k": SPANDATA.GEN_AI_REQUEST_TOP_K, "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P, } for key, attribute in kwargs_keys_to_attributes.items(): From 5956ca8fc92e74f12c58e51bd4dd9b0af5fda323 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 8 Aug 2025 10:24:42 +0200 Subject: [PATCH 12/21] set span name and some nitpicking --- sentry_sdk/integrations/anthropic.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 102778168e..cff8c711ce 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -192,9 +192,11 @@ def _sentry_patched_create_common(f, *args, **kwargs): except TypeError: return f(*args, **kwargs) + model = kwargs.get("model", "") + span = sentry_sdk.start_span( op=OP.GEN_AI_CHAT, - description="Anthropic messages create", + name=f"chat {model}", origin=AnthropicIntegration.origin, ) span.__enter__() @@ -207,11 +209,11 @@ def _sentry_patched_create_common(f, *args, **kwargs): if hasattr(result, "content"): input_tokens, output_tokens = _get_token_usage(result) _set_output_data( - span, - integration, - getattr(result, "model", None), - input_tokens, - output_tokens, + span=span, + integration=integration, + model=getattr(result, "model", None), + input_tokens=input_tokens, + output_tokens=output_tokens, content_blocks=[ ( content_block.to_dict() @@ -243,8 +245,8 @@ def new_iterator(): yield event _set_output_data( - span, - integration, + span=span, + integration=integration, model=model, input_tokens=input_tokens, output_tokens=output_tokens, @@ -268,8 +270,8 @@ async def new_iterator_async(): yield event _set_output_data( - span, - integration, + span=span, + integration=integration, model=model, input_tokens=input_tokens, output_tokens=output_tokens, From 86dabfa33ecf84ea42d056ab2f8b2607b67550d6 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 8 Aug 2025 10:26:25 +0200 Subject: [PATCH 13/21] nit --- sentry_sdk/ai/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/ai/utils.py b/sentry_sdk/ai/utils.py index a58ba02d69..cf52cba6e8 100644 --- a/sentry_sdk/ai/utils.py +++ b/sentry_sdk/ai/utils.py @@ -22,7 +22,7 @@ def _normalize_data(data, unpack=True): return _normalize_data(data[0], unpack=unpack) # remove empty dimensions return list(_normalize_data(x, unpack=unpack) for x in data) if isinstance(data, dict): - return {k: _normalize_data(v, unpack) for (k, v) in data.items()} + return {k: _normalize_data(v, unpack=unpack) for (k, v) in data.items()} return data From cb0d7c51efde975ecd37144af24b55d463aecd57 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 8 Aug 2025 10:30:11 +0200 Subject: [PATCH 14/21] updated tests --- tests/integrations/anthropic/test_anthropic.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index b0458d6a8d..e7d0ca984a 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -113,7 +113,7 @@ def test_nonstreaming_create_message( (span,) = event["spans"] assert span["op"] == OP.GEN_AI_CHAT - assert span["description"] == "Anthropic messages create" + assert span["description"] == "chat model" assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: @@ -185,7 +185,7 @@ async def test_nonstreaming_create_message_async( (span,) = event["spans"] assert span["op"] == OP.GEN_AI_CHAT - assert span["description"] == "Anthropic messages create" + assert span["description"] == "chat model" assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: @@ -288,7 +288,7 @@ def test_streaming_create_message( (span,) = event["spans"] assert span["op"] == OP.GEN_AI_CHAT - assert span["description"] == "Anthropic messages create" + assert span["description"] == "chat model" assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: @@ -395,7 +395,7 @@ async def test_streaming_create_message_async( (span,) = event["spans"] assert span["op"] == OP.GEN_AI_CHAT - assert span["description"] == "Anthropic messages create" + assert span["description"] == "chat model" assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: @@ -529,7 +529,7 @@ def test_streaming_create_message_with_input_json_delta( (span,) = event["spans"] assert span["op"] == OP.GEN_AI_CHAT - assert span["description"] == "Anthropic messages create" + assert span["description"] == "chat model" assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: @@ -669,7 +669,7 @@ async def test_streaming_create_message_with_input_json_delta_async( (span,) = event["spans"] assert span["op"] == OP.GEN_AI_CHAT - assert span["description"] == "Anthropic messages create" + assert span["description"] == "chat model" assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" if send_default_pii and include_prompts: From 2616601ad516f213b282da1233b582f088a41b2a Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 8 Aug 2025 10:42:45 +0200 Subject: [PATCH 15/21] fix --- sentry_sdk/integrations/anthropic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index cff8c711ce..b6ade2f652 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -196,7 +196,7 @@ def _sentry_patched_create_common(f, *args, **kwargs): span = sentry_sdk.start_span( op=OP.GEN_AI_CHAT, - name=f"chat {model}", + name=f"chat {model}".strip(), origin=AnthropicIntegration.origin, ) span.__enter__() From 9c52cd6e45c427e8263860030cff392eb9f4d021 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Fri, 8 Aug 2025 11:46:25 +0200 Subject: [PATCH 16/21] fix(anthropic) make result content blocks more resilient --- sentry_sdk/integrations/anthropic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index b6ade2f652..406457d863 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -215,12 +215,12 @@ def _sentry_patched_create_common(f, *args, **kwargs): input_tokens=input_tokens, output_tokens=output_tokens, content_blocks=[ - ( - content_block.to_dict() - if hasattr(content_block, "to_dict") - else content_block.model_dump() - ) + { + "type": "text", + "text": content_block.text, + } for content_block in result.content + if hasattr(content_block, "text") ], finish_span=True, ) From e5f98847ad326c3ec16a8e1c8bd4ea784be54815 Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Fri, 8 Aug 2025 11:50:55 +0200 Subject: [PATCH 17/21] test(anthropic) fix `test_set_output_data_with_input_json_delta` --- tests/integrations/anthropic/test_anthropic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index e7d0ca984a..eba07a1df6 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -823,19 +823,19 @@ def test_set_output_data_with_input_json_delta(sentry_init): with start_transaction(name="test"): span = start_span() integration = AnthropicIntegration() - + json_deltas = ["{'test': 'data',", "'more': 'json'}"] _set_output_data( span, integration, model="", input_tokens=10, output_tokens=20, - content_blocks=["{'test': 'data',", "'more': 'json'}"], + content_blocks=[{"text": "".join(json_deltas), "type": "text"}], ) assert ( span._data.get(SPANDATA.GEN_AI_RESPONSE_TEXT) - == "[\"{'test': 'data',\", \"'more': 'json'}\"]" + == "[{\"text\": \"{'test': 'data','more': 'json'}\", \"type\": \"text\"}]" ) assert span._data.get(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS) == 10 assert span._data.get(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS) == 20 From b0b2331aac09214c7ec2c997c8d2ffeee3481711 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 8 Aug 2025 12:35:55 +0200 Subject: [PATCH 18/21] fixed tests --- tests/integrations/anthropic/test_anthropic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index eba07a1df6..5852ecd32c 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -123,7 +123,7 @@ def test_nonstreaming_create_message( ) assert ( span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] - == '[{"text": "Hi, I\'m Claude.", "type": "text"}]' + == '[{"type": "text", "text": "Hi, I\'m Claude."}]' ) else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] @@ -195,7 +195,7 @@ async def test_nonstreaming_create_message_async( ) assert ( span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] - == '[{"text": "Hi, I\'m Claude.", "type": "text"}]' + == '[{"type": "text", "text": "Hi, I\'m Claude."}]' ) else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] From 2c5c2fdb120a9c04480f55a214ab996d45dd243e Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 8 Aug 2025 12:44:10 +0200 Subject: [PATCH 19/21] resilient --- sentry_sdk/integrations/anthropic.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 406457d863..05d45ef62f 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -208,20 +208,23 @@ def _sentry_patched_create_common(f, *args, **kwargs): with capture_internal_exceptions(): if hasattr(result, "content"): input_tokens, output_tokens = _get_token_usage(result) + + content_blocks = [] + for content_block in result.content: + if hasattr(content_block, "to_dict"): + content_blocks.append(content_block.to_dict()) + elif hasattr(content_block, "model_dump"): + content_blocks.append(content_block.model_dump()) + elif hasattr(content_block, "text"): + content_blocks.append({"type": "text", "text": content_block.text}) + _set_output_data( span=span, integration=integration, model=getattr(result, "model", None), input_tokens=input_tokens, output_tokens=output_tokens, - content_blocks=[ - { - "type": "text", - "text": content_block.text, - } - for content_block in result.content - if hasattr(content_block, "text") - ], + content_blocks=content_blocks, finish_span=True, ) From 46e166de006b256e8e6b9671760f2b63c42c4c4b Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 8 Aug 2025 12:47:13 +0200 Subject: [PATCH 20/21] fix again --- tests/integrations/anthropic/test_anthropic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index 5852ecd32c..eba07a1df6 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -123,7 +123,7 @@ def test_nonstreaming_create_message( ) assert ( span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] - == '[{"type": "text", "text": "Hi, I\'m Claude."}]' + == '[{"text": "Hi, I\'m Claude.", "type": "text"}]' ) else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] @@ -195,7 +195,7 @@ async def test_nonstreaming_create_message_async( ) assert ( span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] - == '[{"type": "text", "text": "Hi, I\'m Claude."}]' + == '[{"text": "Hi, I\'m Claude.", "type": "text"}]' ) else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] From 6236f9d49e89d3fc391c94d90201ee444fa30792 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Fri, 8 Aug 2025 12:52:43 +0200 Subject: [PATCH 21/21] mypy --- 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 6ab80712e5..24707a18b1 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 + from pydantic import BaseModel # type: ignore except ImportError: raise DidNotEnable("Starlite is not installed")