From 7e5363c04ee4af3e65d7d92b2788bbcf70897f67 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 13 Mar 2026 10:49:33 +0000 Subject: [PATCH 1/3] feat(anthropic): Set gen_ai.response.id span attribute Extract the response ID from Anthropic API responses and set it as the gen_ai.response.id span attribute. For non-streaming responses, read result.id directly. For streaming responses, capture event.message.id from the message_start event. Refs PY-2137 Co-Authored-By: Claude Opus 4.6 --- sentry_sdk/integrations/anthropic.py | 19 ++++++++++++++++++- .../integrations/anthropic/test_anthropic.py | 7 ++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 0aa812cab3..203d49b543 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -126,7 +126,8 @@ def _collect_ai_data( model: "str | None", usage: "_RecordedUsage", content_blocks: "list[str]", -) -> "tuple[str | None, _RecordedUsage, list[str]]": + response_id: "str | None" = None, +) -> "tuple[str | None, _RecordedUsage, list[str], str | None]": """ Collect model information, token usage, and collect content blocks from the AI streaming response. """ @@ -146,6 +147,7 @@ def _collect_ai_data( # https://github.com/anthropics/anthropic-sdk-python/blob/9c485f6966e10ae0ea9eabb3a921d2ea8145a25b/src/anthropic/lib/streaming/_messages.py#L433-L518 if event.type == "message_start": model = event.message.model or model + response_id = getattr(event.message, "id", None) or response_id incoming_usage = event.message.usage usage.output_tokens = incoming_usage.output_tokens @@ -162,6 +164,7 @@ def _collect_ai_data( model, usage, content_blocks, + response_id, ) # Counterintuitive, but message_delta contains cumulative token counts :) @@ -190,12 +193,14 @@ def _collect_ai_data( model, usage, content_blocks, + response_id, ) return ( model, usage, content_blocks, + response_id, ) @@ -348,10 +353,13 @@ def _set_output_data( cache_write_input_tokens: "int | None", content_blocks: "list[Any]", finish_span: bool = False, + response_id: "str | None" = None, ) -> None: """ Set output data for the span based on the AI response.""" span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, model) + if response_id is not None: + span.set_data(SPANDATA.GEN_AI_RESPONSE_ID, response_id) if should_send_default_pii() and integration.include_prompts: output_messages: "dict[str, list[Any]]" = { "response": [], @@ -443,6 +451,7 @@ def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A cache_write_input_tokens=cache_write_input_tokens, content_blocks=content_blocks, finish_span=True, + response_id=getattr(result, "id", None), ) # Streaming response @@ -453,17 +462,20 @@ def new_iterator() -> "Iterator[MessageStreamEvent]": model = None usage = _RecordedUsage() content_blocks: "list[str]" = [] + response_id = None for event in old_iterator: ( model, usage, content_blocks, + response_id, ) = _collect_ai_data( event, model, usage, content_blocks, + response_id, ) yield event @@ -485,23 +497,27 @@ def new_iterator() -> "Iterator[MessageStreamEvent]": cache_write_input_tokens=usage.cache_write_input_tokens, content_blocks=[{"text": "".join(content_blocks), "type": "text"}], finish_span=True, + response_id=response_id, ) async def new_iterator_async() -> "AsyncIterator[MessageStreamEvent]": model = None usage = _RecordedUsage() content_blocks: "list[str]" = [] + response_id = None async for event in old_iterator: ( model, usage, content_blocks, + response_id, ) = _collect_ai_data( event, model, usage, content_blocks, + response_id, ) yield event @@ -523,6 +539,7 @@ async def new_iterator_async() -> "AsyncIterator[MessageStreamEvent]": cache_write_input_tokens=usage.cache_write_input_tokens, content_blocks=[{"text": "".join(content_blocks), "type": "text"}], finish_span=True, + response_id=response_id, ) if str(type(result._iterator)) == "": diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index ea48f5d4db..cc1c2c5f0d 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -134,6 +134,7 @@ def test_nonstreaming_create_message( 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 + assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "id" @pytest.mark.asyncio @@ -204,6 +205,7 @@ async def test_nonstreaming_create_message_async( 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 + assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "id" @pytest.mark.parametrize( @@ -306,6 +308,7 @@ def test_streaming_create_message( assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20 assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True + assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "id" @pytest.mark.asyncio @@ -411,6 +414,7 @@ async def test_streaming_create_message_async( assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20 assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True + assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "id" @pytest.mark.skipif( @@ -852,13 +856,14 @@ def test_collect_ai_data_with_input_json_delta(): content_blocks = [] - model, new_usage, new_content_blocks = _collect_ai_data( + model, new_usage, new_content_blocks, response_id = _collect_ai_data( event, model, usage, content_blocks ) assert model is None assert new_usage.input_tokens == usage.input_tokens assert new_usage.output_tokens == usage.output_tokens assert new_content_blocks == ["test"] + assert response_id is None @pytest.mark.skipif( From 4368a6b9242ea552a680991686cb9821a6280b6d Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 13 Mar 2026 10:59:32 +0000 Subject: [PATCH 2/3] test(anthropic): Use realistic message ID in EXAMPLE_MESSAGE Replace the generic "id" value with a realistic Anthropic message ID format to make test assertions more robust and prevent false positives from coincidental matches. Co-Authored-By: Claude Opus 4.6 --- tests/integrations/anthropic/test_anthropic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index cc1c2c5f0d..3b5ea287bb 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -58,7 +58,7 @@ async def __call__(self, *args, **kwargs): ANTHROPIC_VERSION = package_version("anthropic") EXAMPLE_MESSAGE = Message( - id="id", + id="msg_01XFDUDYJgAACzvnptvVoYEL", model="model", role="assistant", content=[TextBlock(type="text", text="Hi, I'm Claude.")], @@ -134,7 +134,7 @@ def test_nonstreaming_create_message( 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 - assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "id" + assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL" @pytest.mark.asyncio @@ -205,7 +205,7 @@ async def test_nonstreaming_create_message_async( 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 - assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "id" + assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL" @pytest.mark.parametrize( @@ -308,7 +308,7 @@ def test_streaming_create_message( assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20 assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True - assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "id" + assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL" @pytest.mark.asyncio @@ -414,7 +414,7 @@ async def test_streaming_create_message_async( assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20 assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True - assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "id" + assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL" @pytest.mark.skipif( From 9ab864e8b32a5d1e02148b42c1169e1824ffecc4 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 13 Mar 2026 11:50:09 +0000 Subject: [PATCH 3/3] cleanup --- 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 203d49b543..3c150e41a9 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -147,7 +147,7 @@ def _collect_ai_data( # https://github.com/anthropics/anthropic-sdk-python/blob/9c485f6966e10ae0ea9eabb3a921d2ea8145a25b/src/anthropic/lib/streaming/_messages.py#L433-L518 if event.type == "message_start": model = event.message.model or model - response_id = getattr(event.message, "id", None) or response_id + response_id = event.message.id incoming_usage = event.message.usage usage.output_tokens = incoming_usage.output_tokens