From 4cf7e5ae1a9e6bb709ee0af266125daff6a1b73c Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 27 Feb 2026 10:51:45 +0100 Subject: [PATCH 01/43] ref(anthropic): Factor out streamed result handling --- sentry_sdk/integrations/anthropic.py | 184 ++++++++++++++------------- 1 file changed, 98 insertions(+), 86 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index eca9e8bd3e..ada57aebd7 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -49,6 +49,9 @@ from sentry_sdk.tracing import Span from sentry_sdk._types import TextPart + from anthropic import AsyncStream + from anthropic.types import RawMessageStreamEvent + class _RecordedUsage: output_tokens: int = 0 @@ -389,6 +392,96 @@ def _set_output_data( span.__exit__(None, None, None) +def _set_streaming_output_data( + result: "AsyncStream[RawMessageStreamEvent]", + span: "sentry_sdk.tracing.Span", +): + integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) + + old_iterator = result._iterator + + def new_iterator() -> "Iterator[MessageStreamEvent]": + model = None + usage = _RecordedUsage() + content_blocks: "list[str]" = [] + + for event in old_iterator: + ( + model, + usage, + content_blocks, + ) = _collect_ai_data( + event, + model, + usage, + content_blocks, + ) + yield event + + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens for correct cost calculations. + total_input = ( + usage.input_tokens + + (usage.cache_read_input_tokens or 0) + + (usage.cache_write_input_tokens or 0) + ) + + _set_output_data( + span=span, + integration=integration, + model=model, + input_tokens=total_input, + output_tokens=usage.output_tokens, + cache_read_input_tokens=usage.cache_read_input_tokens, + cache_write_input_tokens=usage.cache_write_input_tokens, + content_blocks=[{"text": "".join(content_blocks), "type": "text"}], + finish_span=True, + ) + + async def new_iterator_async() -> "AsyncIterator[MessageStreamEvent]": + model = None + usage = _RecordedUsage() + content_blocks: "list[str]" = [] + + async for event in old_iterator: + ( + model, + usage, + content_blocks, + ) = _collect_ai_data( + event, + model, + usage, + content_blocks, + ) + yield event + + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens for correct cost calculations. + total_input = ( + usage.input_tokens + + (usage.cache_read_input_tokens or 0) + + (usage.cache_write_input_tokens or 0) + ) + + _set_output_data( + span=span, + integration=integration, + model=model, + input_tokens=total_input, + output_tokens=usage.output_tokens, + cache_read_input_tokens=usage.cache_read_input_tokens, + cache_write_input_tokens=usage.cache_write_input_tokens, + content_blocks=[{"text": "".join(content_blocks), "type": "text"}], + finish_span=True, + ) + + if str(type(result._iterator)) == "": + result._iterator = new_iterator_async() + else: + result._iterator = new_iterator() + + def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": integration = kwargs.pop("integration") if integration is None: @@ -415,6 +508,11 @@ def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A result = yield f, args, kwargs + is_streaming_response = kwargs.get("stream", False) + if is_streaming_response: + _set_streaming_output_data(result, span) + return result + with capture_internal_exceptions(): if hasattr(result, "content"): ( @@ -444,92 +542,6 @@ def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A content_blocks=content_blocks, finish_span=True, ) - - # Streaming response - elif hasattr(result, "_iterator"): - old_iterator = result._iterator - - def new_iterator() -> "Iterator[MessageStreamEvent]": - model = None - usage = _RecordedUsage() - content_blocks: "list[str]" = [] - - for event in old_iterator: - ( - model, - usage, - content_blocks, - ) = _collect_ai_data( - event, - model, - usage, - content_blocks, - ) - yield event - - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens for correct cost calculations. - total_input = ( - usage.input_tokens - + (usage.cache_read_input_tokens or 0) - + (usage.cache_write_input_tokens or 0) - ) - - _set_output_data( - span=span, - integration=integration, - model=model, - input_tokens=total_input, - output_tokens=usage.output_tokens, - cache_read_input_tokens=usage.cache_read_input_tokens, - cache_write_input_tokens=usage.cache_write_input_tokens, - content_blocks=[{"text": "".join(content_blocks), "type": "text"}], - finish_span=True, - ) - - async def new_iterator_async() -> "AsyncIterator[MessageStreamEvent]": - model = None - usage = _RecordedUsage() - content_blocks: "list[str]" = [] - - async for event in old_iterator: - ( - model, - usage, - content_blocks, - ) = _collect_ai_data( - event, - model, - usage, - content_blocks, - ) - yield event - - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens for correct cost calculations. - total_input = ( - usage.input_tokens - + (usage.cache_read_input_tokens or 0) - + (usage.cache_write_input_tokens or 0) - ) - - _set_output_data( - span=span, - integration=integration, - model=model, - input_tokens=total_input, - output_tokens=usage.output_tokens, - cache_read_input_tokens=usage.cache_read_input_tokens, - cache_write_input_tokens=usage.cache_write_input_tokens, - content_blocks=[{"text": "".join(content_blocks), "type": "text"}], - finish_span=True, - ) - - if str(type(result._iterator)) == "": - result._iterator = new_iterator_async() - else: - result._iterator = new_iterator() - else: span.set_data("unknown_response", True) span.__exit__(None, None, None) From 61a4cd0c1e3a24f5a1a421f369d7d7d242edc7ef Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 27 Feb 2026 10:59:15 +0100 Subject: [PATCH 02/43] ref(anthropic): Skip accumulation logic for unexpected types in streamed response --- sentry_sdk/integrations/anthropic.py | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index ada57aebd7..1db8de446d 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -38,6 +38,16 @@ Omit = None from anthropic.resources import AsyncMessages, Messages + from anthropic.types import RawMessageStreamEvent + + from anthropic.types import ( + RawMessageStartEvent, + RawMessageDeltaEvent, + RawMessageStopEvent, + RawContentBlockStartEvent, + RawContentBlockDeltaEvent, + RawContentBlockStopEvent, + ) if TYPE_CHECKING: from anthropic.types import MessageStreamEvent, TextBlockParam @@ -406,6 +416,20 @@ def new_iterator() -> "Iterator[MessageStreamEvent]": content_blocks: "list[str]" = [] for event in old_iterator: + if not isinstance( + event, + ( + RawMessageStartEvent, + RawMessageDeltaEvent, + RawMessageStopEvent, + RawContentBlockStartEvent, + RawContentBlockDeltaEvent, + RawContentBlockStopEvent, + ), + ): + yield event + continue + ( model, usage, @@ -444,6 +468,20 @@ async def new_iterator_async() -> "AsyncIterator[MessageStreamEvent]": content_blocks: "list[str]" = [] async for event in old_iterator: + if not isinstance( + event, + ( + RawMessageStartEvent, + RawMessageDeltaEvent, + RawMessageStopEvent, + RawContentBlockStartEvent, + RawContentBlockDeltaEvent, + RawContentBlockStopEvent, + ), + ): + yield event + continue + ( model, usage, From 6cdab1673211cae9482d3ba93b50ca76dabb8865 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 27 Feb 2026 13:29:09 +0100 Subject: [PATCH 03/43] . --- sentry_sdk/integrations/anthropic.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index ada57aebd7..7379d54311 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -392,10 +392,13 @@ def _set_output_data( span.__exit__(None, None, None) -def _set_streaming_output_data( +def _patch_streaming_response_iterator( result: "AsyncStream[RawMessageStreamEvent]", span: "sentry_sdk.tracing.Span", ): + """ + Responsible for closing the `gen_ai.chat` span and setting attributes acquired during response consumption. + """ integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) old_iterator = result._iterator @@ -510,7 +513,7 @@ def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A is_streaming_response = kwargs.get("stream", False) if is_streaming_response: - _set_streaming_output_data(result, span) + _patch_streaming_response_iterator(result, span) return result with capture_internal_exceptions(): From db5dbb921f70944c46d65dbfe870fad4a8cae565 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 27 Feb 2026 13:31:22 +0100 Subject: [PATCH 04/43] . --- sentry_sdk/integrations/anthropic.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 7379d54311..16e14c3500 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -395,12 +395,11 @@ def _set_output_data( def _patch_streaming_response_iterator( result: "AsyncStream[RawMessageStreamEvent]", span: "sentry_sdk.tracing.Span", -): + integration: "AnthropicIntegration", +) -> None: """ Responsible for closing the `gen_ai.chat` span and setting attributes acquired during response consumption. """ - integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) - old_iterator = result._iterator def new_iterator() -> "Iterator[MessageStreamEvent]": @@ -513,7 +512,7 @@ def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A is_streaming_response = kwargs.get("stream", False) if is_streaming_response: - _patch_streaming_response_iterator(result, span) + _patch_streaming_response_iterator(result, span, integration) return result with capture_internal_exceptions(): From d47b64dc29e09f59ca63de2d2423e2359502c64b Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 27 Feb 2026 14:03:49 +0100 Subject: [PATCH 05/43] . --- sentry_sdk/integrations/anthropic.py | 80 +++++++++++++++++++++------- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 8fb2e835d1..f8de17536e 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -40,13 +40,27 @@ from anthropic.resources import AsyncMessages, Messages from anthropic.types import RawMessageStreamEvent + message_types_have_raw_prefix = False + try: + # http://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a + from anthropic.types import ( + RawMessageStartEvent, + RawMessageDeltaEvent, + RawMessageStopEvent, + RawContentBlockStartEvent, + RawContentBlockDeltaEvent, + RawContentBlockStopEvent, + ) + except ImportError: + message_types_have_raw_prefix = True + from anthropic.types import ( - RawMessageStartEvent, - RawMessageDeltaEvent, - RawMessageStopEvent, - RawContentBlockStartEvent, - RawContentBlockDeltaEvent, - RawContentBlockStopEvent, + MessageStartEvent, + MessageDeltaEvent, + MessageStopEvent, + ContentBlockStartEvent, + ContentBlockDeltaEvent, + ContentBlockStopEvent, ) if TYPE_CHECKING: @@ -418,15 +432,28 @@ def new_iterator() -> "Iterator[MessageStreamEvent]": content_blocks: "list[str]" = [] for event in old_iterator: - if not isinstance( + if ( + message_types_have_raw_prefix + and not isinstance( + event, + ( + RawMessageStartEvent, + RawMessageDeltaEvent, + RawMessageStopEvent, + RawContentBlockStartEvent, + RawContentBlockDeltaEvent, + RawContentBlockStopEvent, + ), + ) + ) or not isinstance( event, ( - RawMessageStartEvent, - RawMessageDeltaEvent, - RawMessageStopEvent, - RawContentBlockStartEvent, - RawContentBlockDeltaEvent, - RawContentBlockStopEvent, + MessageStartEvent, + MessageDeltaEvent, + MessageStopEvent, + ContentBlockStartEvent, + ContentBlockDeltaEvent, + ContentBlockStopEvent, ), ): yield event @@ -470,15 +497,28 @@ async def new_iterator_async() -> "AsyncIterator[MessageStreamEvent]": content_blocks: "list[str]" = [] async for event in old_iterator: - if not isinstance( + if ( + message_types_have_raw_prefix + and not isinstance( + event, + ( + RawMessageStartEvent, + RawMessageDeltaEvent, + RawMessageStopEvent, + RawContentBlockStartEvent, + RawContentBlockDeltaEvent, + RawContentBlockStopEvent, + ), + ) + ) or not isinstance( event, ( - RawMessageStartEvent, - RawMessageDeltaEvent, - RawMessageStopEvent, - RawContentBlockStartEvent, - RawContentBlockDeltaEvent, - RawContentBlockStopEvent, + MessageStartEvent, + MessageDeltaEvent, + MessageStopEvent, + ContentBlockStartEvent, + ContentBlockDeltaEvent, + ContentBlockStopEvent, ), ): yield event From 384351f3ca05dc2981210d0f9d9f83dfb0c63303 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 27 Feb 2026 14:06:43 +0100 Subject: [PATCH 06/43] remove duplicate import --- sentry_sdk/integrations/anthropic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index f8de17536e..1e08105826 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -38,7 +38,6 @@ Omit = None from anthropic.resources import AsyncMessages, Messages - from anthropic.types import RawMessageStreamEvent message_types_have_raw_prefix = False try: From 55d5c27aa54a9f3285e86489ca49e88a63dc8890 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 27 Feb 2026 14:17:04 +0100 Subject: [PATCH 07/43] . --- sentry_sdk/integrations/anthropic.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 1e08105826..4f6bc48551 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -39,7 +39,6 @@ from anthropic.resources import AsyncMessages, Messages - message_types_have_raw_prefix = False try: # http://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a from anthropic.types import ( @@ -51,7 +50,12 @@ RawContentBlockStopEvent, ) except ImportError: - message_types_have_raw_prefix = True + RawMessageStartEvent = None + RawMessageDeltaEvent = None + RawMessageStopEvent = None + RawContentBlockStartEvent = None + RawContentBlockDeltaEvent = None + RawContentBlockStopEvent = None from anthropic.types import ( MessageStartEvent, @@ -432,7 +436,7 @@ def new_iterator() -> "Iterator[MessageStreamEvent]": for event in old_iterator: if ( - message_types_have_raw_prefix + RawMessageStartEvent is not None and not isinstance( event, ( @@ -497,7 +501,7 @@ async def new_iterator_async() -> "AsyncIterator[MessageStreamEvent]": async for event in old_iterator: if ( - message_types_have_raw_prefix + RawMessageStartEvent is not None and not isinstance( event, ( From fd84837d4e335e85924aedb7108364aa66df3446 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 27 Feb 2026 14:25:36 +0100 Subject: [PATCH 08/43] simplify --- sentry_sdk/integrations/anthropic.py | 48 ++-------------------------- 1 file changed, 2 insertions(+), 46 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 4f6bc48551..aa725b4a7b 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -39,24 +39,6 @@ from anthropic.resources import AsyncMessages, Messages - try: - # http://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a - from anthropic.types import ( - RawMessageStartEvent, - RawMessageDeltaEvent, - RawMessageStopEvent, - RawContentBlockStartEvent, - RawContentBlockDeltaEvent, - RawContentBlockStopEvent, - ) - except ImportError: - RawMessageStartEvent = None - RawMessageDeltaEvent = None - RawMessageStopEvent = None - RawContentBlockStartEvent = None - RawContentBlockDeltaEvent = None - RawContentBlockStopEvent = None - from anthropic.types import ( MessageStartEvent, MessageDeltaEvent, @@ -435,20 +417,7 @@ def new_iterator() -> "Iterator[MessageStreamEvent]": content_blocks: "list[str]" = [] for event in old_iterator: - if ( - RawMessageStartEvent is not None - and not isinstance( - event, - ( - RawMessageStartEvent, - RawMessageDeltaEvent, - RawMessageStopEvent, - RawContentBlockStartEvent, - RawContentBlockDeltaEvent, - RawContentBlockStopEvent, - ), - ) - ) or not isinstance( + if not isinstance( event, ( MessageStartEvent, @@ -500,20 +469,7 @@ async def new_iterator_async() -> "AsyncIterator[MessageStreamEvent]": content_blocks: "list[str]" = [] async for event in old_iterator: - if ( - RawMessageStartEvent is not None - and not isinstance( - event, - ( - RawMessageStartEvent, - RawMessageDeltaEvent, - RawMessageStopEvent, - RawContentBlockStartEvent, - RawContentBlockDeltaEvent, - RawContentBlockStopEvent, - ), - ) - ) or not isinstance( + if not isinstance( event, ( MessageStartEvent, From 9e306d45437d1daa05f1c64faf3f6b5958a50baf Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 27 Feb 2026 17:59:35 +0100 Subject: [PATCH 09/43] feat(anthropic): Emit gen_ai.chat spans for messages.stream() --- sentry_sdk/integrations/anthropic.py | 211 +++++++- .../integrations/anthropic/test_anthropic.py | 460 +++++++++++++++++- 2 files changed, 644 insertions(+), 27 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index aa725b4a7b..b9fcfe786f 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -38,6 +38,7 @@ Omit = None from anthropic.resources import AsyncMessages, Messages + from anthropic.lib.streaming._messages import MessageStreamManager from anthropic.types import ( MessageStartEvent, @@ -59,7 +60,13 @@ from sentry_sdk._types import TextPart from anthropic import AsyncStream - from anthropic.types import RawMessageStreamEvent + from anthropic.types import ( + RawMessageStreamEvent, + MessageParam, + ModelParam, + TextBlockParam, + ToolUnionParam, + ) class _RecordedUsage: @@ -84,6 +91,11 @@ def setup_once() -> None: Messages.create = _wrap_message_create(Messages.create) AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create) + Messages.stream = _wrap_message_stream(Messages.stream) + MessageStreamManager.__enter__ = _wrap_message_stream_manager_enter( + MessageStreamManager.__enter__ + ) + def _capture_exception(exc: "Any") -> None: set_span_errored() @@ -253,27 +265,32 @@ def _transform_system_instructions( ] -def _set_input_data( - span: "Span", kwargs: "dict[str, Any]", integration: "AnthropicIntegration" +def _common_set_input_data( + span: "Span", + integration: "AnthropicIntegration", + max_tokens: "int", + messages: "Iterable[MessageParam]", + model: "ModelParam", + system: "Optional[Union[str, Iterable[TextBlockParam]]]", + temperature: "Optional[float]", + top_k: "Optional[int]", + top_p: "Optional[float]", + tools: "Optional[Iterable[ToolUnionParam]]", ) -> None: """ Set input data for the span based on the provided keyword arguments for the anthropic message creation. """ span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") - system_instructions: "Union[str, Iterable[TextBlockParam]]" = kwargs.get("system") # type: ignore - messages = kwargs.get("messages") if ( messages is not None and len(messages) > 0 and should_send_default_pii() and integration.include_prompts ): - if isinstance(system_instructions, str) or isinstance( - system_instructions, Iterable - ): + if isinstance(system, str) or isinstance(system, Iterable): span.set_data( SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS, - json.dumps(_transform_system_instructions(system_instructions)), + json.dumps(_transform_system_instructions(system)), ) normalized_messages = [] @@ -329,27 +346,69 @@ def _set_input_data( span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False ) - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)) + if max_tokens is not None and _is_given(max_tokens): + span.set_data(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens) + if model is not None and _is_given(model): + span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model) + if temperature is not None and _is_given(temperature): + span.set_data(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature) + if top_k is not None and _is_given(top_k): + span.set_data(SPANDATA.GEN_AI_REQUEST_TOP_K, top_k) + if top_p is not None and _is_given(top_p): + span.set_data(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - kwargs_keys_to_attributes = { - "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(): - value = kwargs.get(key) - - if value is not None and _is_given(value): - span.set_data(attribute, value) - - # Input attributes: Tools - tools = kwargs.get("tools") if tools is not None and _is_given(tools) and len(tools) > 0: span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)) +def _set_create_input_data( + span: "Span", kwargs: "dict[str, Any]", integration: "AnthropicIntegration" +) -> None: + """ + Set input data for the span based on the provided keyword arguments for the anthropic message creation. + """ + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)) + + _common_set_input_data( + span=span, + integration=integration, + max_tokens=kwargs.get("max_tokens"), + messages=kwargs.get("messages"), + model=kwargs.get("model"), + system=kwargs.get("system"), + temperature=kwargs.get("temperature"), + top_k=kwargs.get("top_k"), + top_p=kwargs.get("top_p"), + tools=kwargs.get("tools"), + ) + + +def _set_stream_input_data( + span: "Span", + integration: "AnthropicIntegration", + max_tokens: "int", + messages: "Iterable[MessageParam]", + model: "ModelParam", + system: "Optional[Union[str, Iterable[TextBlockParam]]]", + temperature: "Optional[float]", + top_k: "Optional[int]", + top_p: "Optional[float]", + tools: "Optional[Iterable[ToolUnionParam]]", +) -> None: + _common_set_input_data( + span=span, + integration=integration, + max_tokens=max_tokens, + messages=messages, + model=model, + system=system, + temperature=temperature, + top_k=top_k, + top_p=top_p, + tools=tools, + ) + + def _set_output_data( span: "Span", integration: "AnthropicIntegration", @@ -543,7 +602,7 @@ def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A ) span.__enter__() - _set_input_data(span, kwargs, integration) + _set_create_input_data(span, kwargs, integration) result = yield f, args, kwargs @@ -664,6 +723,106 @@ async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": return _sentry_patched_create_async +def _sentry_patched_stream_common( + result, + max_tokens: "int", + messages: "Iterable[MessageParam]", + model: "ModelParam", + system: "Union[str, Iterable[TextBlockParam]]", + temperature: "float", + top_k: "int", + top_p: "float", + tools: "Iterable[ToolUnionParam]", +): + integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) + + if integration is None: + return result + + if messages is None: + return result + + try: + iter(messages) + except TypeError: + return result + + if model is None: + model = "" + + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + _set_stream_input_data( + span, + integration, + max_tokens=max_tokens, + messages=messages, + model=model, + system=system, + temperature=temperature, + top_k=top_k, + top_p=top_p, + tools=tools, + ) + _patch_streaming_response_iterator(result, span, integration) + + return result + + +def _wrap_message_stream(f: "Any") -> "Any": + """ + Attaches user-provided arguments to the returned context manager. + The attributes are set on `gen_ai.chat` spans in the patch for the context manager. + """ + + @wraps(f) + def _sentry_patched_stream(*args, **kwargs): + stream = f(*args, **kwargs) + + stream._max_tokens = kwargs.get("max_tokens") + stream._messages = kwargs.get("messages") + stream._model = kwargs.get("model") + stream._system = kwargs.get("system") + stream._temperature = kwargs.get("temperature") + stream._top_k = kwargs.get("top_k") + stream._top_p = kwargs.get("top_p") + stream._tools = kwargs.get("tools") + + return stream + + return _sentry_patched_stream + + +def _wrap_message_stream_manager_enter(f: "Any") -> "Any": + """ + Creates and manages `gen_ai.chat` spans. + """ + + @wraps(f) + def _sentry_patched_enter(self): + stream = f(self) + _sentry_patched_stream_common( + stream, + max_tokens=self._max_tokens, + messages=self._messages, + model=self._model, + system=self._system, + temperature=self._temperature, + top_k=self._top_k, + top_p=self._top_p, + tools=self._tools, + ) + return stream + + return _sentry_patched_enter + + def _is_given(obj: "Any") -> bool: """ Check for givenness safely across different anthropic versions. diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index 4361ba9629..2d5191c1c9 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -3,7 +3,7 @@ import json try: - from unittest.mock import AsyncMock + from unittest.mock import Mock, AsyncMock except ImportError: class AsyncMock(mock.MagicMock): @@ -313,6 +313,108 @@ def test_streaming_create_message( assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True +@pytest.mark.parametrize( + "send_default_pii, include_prompts", + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], +) +def test_stream_messages( + sentry_init, capture_events, send_default_pii, include_prompts +): + client = Anthropic(api_key="z") + returned_stream = Stream(cast_to=None, response=Mock(), client=client) + returned_stream._iterator = [ + MessageStartEvent( + message=EXAMPLE_MESSAGE, + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=TextBlock(type="text", text=""), + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="Hi", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text=" I'm Claude!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(), + usage=MessageDeltaUsage(output_tokens=10), + type="message_delta", + ), + ] + + sentry_init( + integrations=[AnthropicIntegration(include_prompts=include_prompts)], + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + ) + events = capture_events() + client.messages._post = mock.Mock(return_value=returned_stream) + + messages = [ + { + "role": "user", + "content": "Hello, Claude", + } + ] + + with start_transaction(name="anthropic"): + with client.messages.stream( + max_tokens=1024, + messages=messages, + model="model", + ) as stream: + for event in stream: + pass + + assert len(events) == 1 + (event,) = events + + assert event["type"] == "transaction" + assert event["transaction"] == "anthropic" + + assert len(event["spans"]) == 1 + (span,) = event["spans"] + + assert span["op"] == OP.GEN_AI_CHAT + assert span["description"] == "chat model" + assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" + + if send_default_pii and include_prompts: + assert ( + span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + == '[{"role": "user", "content": "Hello, Claude"}]' + ) + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!" + + else: + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] + + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 + 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 + + @pytest.mark.asyncio @pytest.mark.parametrize( "send_default_pii, include_prompts", @@ -552,6 +654,142 @@ def test_streaming_create_message_with_input_json_delta( assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True +@pytest.mark.skipif( + ANTHROPIC_VERSION < (0, 27), + reason="Versions <0.27.0 do not include InputJSONDelta, which was introduced in >=0.27.0 along with a new message delta type for tool calling.", +) +@pytest.mark.parametrize( + "send_default_pii, include_prompts", + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], +) +def test_stream_messages_with_input_json_delta( + sentry_init, capture_events, send_default_pii, include_prompts +): + client = Anthropic(api_key="z") + returned_stream = Stream(cast_to=None, response=Mock(), client=client) + returned_stream._iterator = [ + MessageStartEvent( + message=Message( + id="msg_0", + content=[], + model="claude-3-5-sonnet-20240620", + role="assistant", + stop_reason=None, + stop_sequence=None, + type="message", + usage=Usage(input_tokens=366, output_tokens=10), + ), + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=ToolUseBlock( + id="toolu_0", input={}, name="get_weather", type="tool_use" + ), + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json="", type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta( + partial_json='{"location": "', type="input_json_delta" + ), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json="S", type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json="an ", type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json="Francisco, C", type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json='A"}', type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(stop_reason="tool_use", stop_sequence=None), + usage=MessageDeltaUsage(output_tokens=41), + type="message_delta", + ), + ] + + sentry_init( + integrations=[AnthropicIntegration(include_prompts=include_prompts)], + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + ) + events = capture_events() + client.messages._post = mock.Mock(return_value=returned_stream) + + messages = [ + { + "role": "user", + "content": "What is the weather like in San Francisco?", + } + ] + + with start_transaction(name="anthropic"): + with client.messages.stream( + max_tokens=1024, + messages=messages, + model="model", + ) as stream: + for event in stream: + pass + + assert len(events) == 1 + (event,) = events + + assert event["type"] == "transaction" + assert event["transaction"] == "anthropic" + + assert len(event["spans"]) == 1 + (span,) = event["spans"] + + assert span["op"] == OP.GEN_AI_CHAT + assert span["description"] == "chat model" + assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" + + if send_default_pii and include_prompts: + 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] + == '{"location": "San Francisco, CA"}' + ) + else: + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] + + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366 + assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41 + assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 407 + assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True + + @pytest.mark.asyncio @pytest.mark.skipif( ANTHROPIC_VERSION < (0, 27), @@ -1350,6 +1588,120 @@ def test_streaming_create_message_with_system_prompt( assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True +@pytest.mark.parametrize( + "send_default_pii, include_prompts", + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], +) +def test_stream_messages_with_system_prompt( + sentry_init, capture_events, send_default_pii, include_prompts +): + """Test that system prompts are properly captured in streaming mode.""" + client = Anthropic(api_key="z") + returned_stream = Stream(cast_to=None, response=Mock(), client=client) + returned_stream._iterator = [ + MessageStartEvent( + message=EXAMPLE_MESSAGE, + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=TextBlock(type="text", text=""), + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="Hi", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text=" I'm Claude!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(), + usage=MessageDeltaUsage(output_tokens=10), + type="message_delta", + ), + ] + + sentry_init( + integrations=[AnthropicIntegration(include_prompts=include_prompts)], + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + ) + events = capture_events() + client.messages._post = mock.Mock(return_value=returned_stream) + + messages = [ + { + "role": "user", + "content": "Hello, Claude", + } + ] + + with start_transaction(name="anthropic"): + with client.messages.stream( + max_tokens=1024, + messages=messages, + model="model", + system="You are a helpful assistant.", + ) as stream: + for event in stream: + pass + + assert len(events) == 1 + (event,) = events + + assert event["type"] == "transaction" + assert event["transaction"] == "anthropic" + + assert len(event["spans"]) == 1 + (span,) = event["spans"] + + assert span["op"] == OP.GEN_AI_CHAT + assert span["description"] == "chat model" + assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" + + if send_default_pii and include_prompts: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"] + system_instructions = json.loads( + span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS] + ) + assert system_instructions == [ + {"type": "text", "content": "You are a helpful assistant."} + ] + + assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"] + stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + assert len(stored_messages) == 1 + assert stored_messages[0]["role"] == "user" + assert stored_messages[0]["content"] == "Hello, Claude" + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!" + + else: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS 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"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 + 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 + + @pytest.mark.asyncio @pytest.mark.parametrize( "send_default_pii, include_prompts", @@ -2412,6 +2764,62 @@ def test_input_tokens_include_cache_read_streaming(sentry_init, capture_events): (span,) = events[0]["spans"] + # input_tokens should be total: 19 + 2846 = test_stream_messages_input_tokens_include_cache_read_streaming + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 2865 + assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 2879 # 2865 + 14 + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 2846 + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 0 + + +def test_stream_messages_input_tokens_include_cache_read_streaming( + sentry_init, capture_events +): + """ + Test that gen_ai.usage.input_tokens includes cache_read tokens (streaming). + + Same cache-hit scenario as non-streaming, using realistic streaming events. + """ + client = Anthropic(api_key="z") + returned_stream = Stream(cast_to=None, response=Mock(), client=client) + returned_stream._iterator = [ + MessageStartEvent( + type="message_start", + message=Message( + id="id", + model="claude-sonnet-4-20250514", + role="assistant", + content=[], + type="message", + usage=Usage( + input_tokens=19, + output_tokens=0, + cache_read_input_tokens=2846, + cache_creation_input_tokens=0, + ), + ), + ), + MessageDeltaEvent( + type="message_delta", + delta=Delta(stop_reason="end_turn"), + usage=MessageDeltaUsage(output_tokens=14), + ), + ] + + sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) + events = capture_events() + client.messages._post = mock.Mock(return_value=returned_stream) + + with start_transaction(name="anthropic"): + with client.messages.stream( + max_tokens=1024, + messages=[{"role": "user", "content": "What is 5+5?"}], + model="claude-sonnet-4-20250514", + ) as stream: + for event in stream: + pass + + (span,) = events[0]["spans"] + # input_tokens should be total: 19 + 2846 = 2865 assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 2865 assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 2879 # 2865 + 14 @@ -2505,3 +2913,53 @@ def test_cache_tokens_streaming(sentry_init, capture_events): assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 210 assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80 assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20 + + +def test_stream_messages_cache_tokens(sentry_init, capture_events): + """Test cache tokens are tracked for streaming responses.""" + client = Anthropic(api_key="z") + returned_stream = Stream(cast_to=None, response=Mock(), client=client) + returned_stream._iterator = [ + MessageStartEvent( + type="message_start", + message=Message( + id="id", + model="claude-3-5-sonnet-20241022", + role="assistant", + content=[], + type="message", + usage=Usage( + input_tokens=100, + output_tokens=0, + cache_read_input_tokens=80, + cache_creation_input_tokens=20, + ), + ), + ), + MessageDeltaEvent( + type="message_delta", + delta=Delta(stop_reason="end_turn"), + usage=MessageDeltaUsage(output_tokens=10), + ), + ] + + sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) + events = capture_events() + client.messages._post = mock.Mock(return_value=returned_stream) + + with start_transaction(name="anthropic"): + with client.messages.stream( + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], + model="claude-3-5-sonnet-20241022", + ) as stream: + for event in stream: + pass + + (span,) = events[0]["spans"] + # input_tokens normalized: 100 + 80 (cache_read) + 20 (cache_write) = 200 + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 200 + assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 + assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 210 + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80 + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20 From 032e6a40bda8dafab925df297569949a9d7be2d2 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 27 Feb 2026 18:11:57 +0100 Subject: [PATCH 10/43] . --- sentry_sdk/integrations/anthropic.py | 48 +++++++++++++--------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index b9fcfe786f..2393b8360c 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -372,8 +372,8 @@ def _set_create_input_data( _common_set_input_data( span=span, integration=integration, - max_tokens=kwargs.get("max_tokens"), - messages=kwargs.get("messages"), + max_tokens=kwargs.get("max_tokens"), # type: ignore + messages=kwargs.get("messages"), # type: ignore model=kwargs.get("model"), system=kwargs.get("system"), temperature=kwargs.get("temperature"), @@ -724,7 +724,7 @@ async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": def _sentry_patched_stream_common( - result, + stream_manager: "MessageStreamManager", max_tokens: "int", messages: "Iterable[MessageParam]", model: "ModelParam", @@ -733,19 +733,19 @@ def _sentry_patched_stream_common( top_k: "int", top_p: "float", tools: "Iterable[ToolUnionParam]", -): +) -> None: integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) if integration is None: - return result + return stream_manager if messages is None: - return result + return stream_manager try: iter(messages) except TypeError: - return result + return stream_manager if model is None: model = "" @@ -770,9 +770,7 @@ def _sentry_patched_stream_common( top_p=top_p, tools=tools, ) - _patch_streaming_response_iterator(result, span, integration) - - return result + _patch_streaming_response_iterator(stream_manager, span, integration) def _wrap_message_stream(f: "Any") -> "Any": @@ -782,19 +780,19 @@ def _wrap_message_stream(f: "Any") -> "Any": """ @wraps(f) - def _sentry_patched_stream(*args, **kwargs): - stream = f(*args, **kwargs) + def _sentry_patched_stream(*args, **kwargs) -> "MessageStreamManager": + stream_manager = f(*args, **kwargs) - stream._max_tokens = kwargs.get("max_tokens") - stream._messages = kwargs.get("messages") - stream._model = kwargs.get("model") - stream._system = kwargs.get("system") - stream._temperature = kwargs.get("temperature") - stream._top_k = kwargs.get("top_k") - stream._top_p = kwargs.get("top_p") - stream._tools = kwargs.get("tools") + stream_manager._max_tokens = kwargs.get("max_tokens") + stream_manager._messages = kwargs.get("messages") + stream_manager._model = kwargs.get("model") + stream_manager._system = kwargs.get("system") + stream_manager._temperature = kwargs.get("temperature") + stream_manager._top_k = kwargs.get("top_k") + stream_manager._top_p = kwargs.get("top_p") + stream_manager._tools = kwargs.get("tools") - return stream + return stream_manager return _sentry_patched_stream @@ -805,10 +803,10 @@ def _wrap_message_stream_manager_enter(f: "Any") -> "Any": """ @wraps(f) - def _sentry_patched_enter(self): - stream = f(self) + def _sentry_patched_enter(self) -> "MessageStreamManager": + stream_manager = f(self) _sentry_patched_stream_common( - stream, + stream_manager=stream_manager, max_tokens=self._max_tokens, messages=self._messages, model=self._model, @@ -818,7 +816,7 @@ def _sentry_patched_enter(self): top_p=self._top_p, tools=self._tools, ) - return stream + return stream_manager return _sentry_patched_enter From bf009ca848482be62f75f97d119a8d8ff5dd46b9 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 27 Feb 2026 18:16:05 +0100 Subject: [PATCH 11/43] . --- sentry_sdk/integrations/anthropic.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 2393b8360c..f8d1c0d4f4 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -283,7 +283,7 @@ def _common_set_input_data( span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") if ( messages is not None - and len(messages) > 0 + and len(messages) > 0 # type: ignore and should_send_default_pii() and integration.include_prompts ): @@ -357,7 +357,7 @@ def _common_set_input_data( if top_p is not None and _is_given(top_p): span.set_data(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) - if tools is not None and _is_given(tools) and len(tools) > 0: + if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)) @@ -805,6 +805,9 @@ def _wrap_message_stream_manager_enter(f: "Any") -> "Any": @wraps(f) def _sentry_patched_enter(self) -> "MessageStreamManager": stream_manager = f(self) + if not hasattr(self, "_max_tokens"): + return stream_manager + _sentry_patched_stream_common( stream_manager=stream_manager, max_tokens=self._max_tokens, From 3b641bee40b820bf853f4c45e25ac49d1e2eb65a Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 27 Feb 2026 18:19:46 +0100 Subject: [PATCH 12/43] . --- sentry_sdk/integrations/anthropic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index f8d1c0d4f4..25ce670d39 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -780,7 +780,7 @@ def _wrap_message_stream(f: "Any") -> "Any": """ @wraps(f) - def _sentry_patched_stream(*args, **kwargs) -> "MessageStreamManager": + def _sentry_patched_stream(*args: "Any", **kwargs: "Any") -> "MessageStreamManager": stream_manager = f(*args, **kwargs) stream_manager._max_tokens = kwargs.get("max_tokens") @@ -803,7 +803,7 @@ def _wrap_message_stream_manager_enter(f: "Any") -> "Any": """ @wraps(f) - def _sentry_patched_enter(self) -> "MessageStreamManager": + def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStreamManager": stream_manager = f(self) if not hasattr(self, "_max_tokens"): return stream_manager From 697d621a912d0122886a0abaf0e2ec85698b25c8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 2 Mar 2026 09:20:30 +0100 Subject: [PATCH 13/43] feat(anthropic): Emit gen_ai.chat spans for asynchronous messages.stream() --- sentry_sdk/integrations/anthropic.py | 68 +++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 25ce670d39..1b6870a515 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -38,7 +38,10 @@ Omit = None from anthropic.resources import AsyncMessages, Messages - from anthropic.lib.streaming._messages import MessageStreamManager + from anthropic.lib.streaming._messages import ( + MessageStreamManager, + AsyncMessageStreamManager, + ) from anthropic.types import ( MessageStartEvent, @@ -66,6 +69,7 @@ ModelParam, TextBlockParam, ToolUnionParam, + AsyncMessageStream, ) @@ -96,6 +100,13 @@ def setup_once() -> None: MessageStreamManager.__enter__ ) + AsyncMessages.stream = _wrap_async_message_stream(AsyncMessages.stream) + AsyncMessageStreamManager.__aenter__ = ( + _wrap_async_message_stream_manager_aenter( + AsyncMessageStreamManager.__aenter__ + ) + ) + def _capture_exception(exc: "Any") -> None: set_span_errored() @@ -824,6 +835,61 @@ def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStreamManager return _sentry_patched_enter +def _wrap_async_message_stream(f: "Any") -> "Any": + """ + Attaches user-provided arguments to the returned context manager. + The attributes are set on `gen_ai.chat` spans in the patch for the context manager. + """ + + @wraps(f) + def _sentry_patched_stream( + *args: "Any", **kwargs: "Any" + ) -> "AsyncMessageStreamManager": + stream_manager = f(*args, **kwargs) + + stream_manager._max_tokens = kwargs.get("max_tokens") + stream_manager._messages = kwargs.get("messages") + stream_manager._model = kwargs.get("model") + stream_manager._system = kwargs.get("system") + stream_manager._temperature = kwargs.get("temperature") + stream_manager._top_k = kwargs.get("top_k") + stream_manager._top_p = kwargs.get("top_p") + stream_manager._tools = kwargs.get("tools") + + return stream_manager + + return _sentry_patched_stream + + +def _wrap_async_message_stream_manager_aenter(f: "Any") -> "Any": + """ + Creates and manages `gen_ai.chat` spans. + """ + + @wraps(f) + async def _sentry_patched_aenter( + self: "MessageStreamManager", + ) -> "AsyncMessageStream": + stream = await f(self) + if not hasattr(self, "_max_tokens"): + return stream + + _sentry_patched_stream_common( + stream=stream, + max_tokens=self._max_tokens, + messages=self._messages, + model=self._model, + system=self._system, + temperature=self._temperature, + top_k=self._top_k, + top_p=self._top_p, + tools=self._tools, + ) + return stream + + return _sentry_patched_aenter + + def _is_given(obj: "Any") -> bool: """ Check for givenness safely across different anthropic versions. From 321d754e349a18324de8f6d46a925c35bdb08bde Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 2 Mar 2026 09:21:58 +0100 Subject: [PATCH 14/43] add tests --- .../integrations/anthropic/test_anthropic.py | 401 +++++++++++++++++- 1 file changed, 400 insertions(+), 1 deletion(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index 2d5191c1c9..b715ebc979 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -1,6 +1,7 @@ import pytest from unittest import mock import json +import httpx try: from unittest.mock import Mock, AsyncMock @@ -12,7 +13,7 @@ async def __call__(self, *args, **kwargs): from anthropic import Anthropic, AnthropicError, AsyncAnthropic, AsyncStream, Stream -from anthropic.types import MessageDeltaUsage, TextDelta, Usage +from anthropic.types import MessageDeltaUsage, TextDelta, Usage, RawMessageStreamEvent from anthropic.types.content_block_delta_event import ContentBlockDeltaEvent from anthropic.types.content_block_start_event import ContentBlockStartEvent from anthropic.types.content_block_stop_event import ContentBlockStopEvent @@ -67,6 +68,13 @@ async def __call__(self, *args, **kwargs): ) +def sse_chunks(events): + for event in events: + payload = event.model_dump() + chunk = f"event: {payload['type']}\ndata: {json.dumps(payload)}\n\n" + yield chunk.encode("utf-8") + + async def async_iterator(values): for value in values: yield value @@ -520,6 +528,119 @@ async def test_streaming_create_message_async( assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True +@pytest.mark.asyncio +@pytest.mark.parametrize( + "send_default_pii, include_prompts", + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], +) +async def test_stream_message_async( + sentry_init, capture_events, send_default_pii, include_prompts +): + client = AsyncAnthropic(api_key="z") + + response = httpx.Response( + 200, + content=b"".join( + sse_chunks( + [ + MessageStartEvent( + message=EXAMPLE_MESSAGE, + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=TextBlock(type="text", text=""), + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="Hi", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text=" I'm Claude!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(), + usage=MessageDeltaUsage(output_tokens=10), + type="message_delta", + ), + ] + ) + ), + ) + returned_stream = AsyncStream( + cast_to=RawMessageStreamEvent, response=response, client=client + ) + + sentry_init( + integrations=[AnthropicIntegration(include_prompts=include_prompts)], + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + ) + events = capture_events() + client.messages._post = AsyncMock(return_value=returned_stream) + + messages = [ + { + "role": "user", + "content": "Hello, Claude", + } + ] + + with start_transaction(name="anthropic"): + async with client.messages.stream( + max_tokens=1024, + messages=messages, + model="model", + ) as stream: + async for event in stream: + pass + + assert len(events) == 1 + (event,) = events + + assert event["type"] == "transaction" + assert event["transaction"] == "anthropic" + + assert len(event["spans"]) == 1 + (span,) = event["spans"] + + assert span["op"] == OP.GEN_AI_CHAT + assert span["description"] == "chat model" + assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" + + if send_default_pii and include_prompts: + assert ( + span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] + == '[{"role": "user", "content": "Hello, Claude"}]' + ) + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!" + + else: + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] + + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 + 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 + + @pytest.mark.skipif( ANTHROPIC_VERSION < (0, 27), reason="Versions <0.27.0 do not include InputJSONDelta, which was introduced in >=0.27.0 along with a new message delta type for tool calling.", @@ -932,6 +1053,159 @@ async def test_streaming_create_message_with_input_json_delta_async( assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True +@pytest.mark.asyncio +@pytest.mark.skipif( + ANTHROPIC_VERSION < (0, 27), + reason="Versions <0.27.0 do not include InputJSONDelta, which was introduced in >=0.27.0 along with a new message delta type for tool calling.", +) +@pytest.mark.parametrize( + "send_default_pii, include_prompts", + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], +) +async def test_stream_message_with_input_json_delta_async( + sentry_init, capture_events, send_default_pii, include_prompts +): + client = AsyncAnthropic(api_key="z") + response = httpx.Response( + 200, + content=b"".join( + sse_chunks( + [ + MessageStartEvent( + message=Message( + id="msg_0", + content=[], + model="claude-3-5-sonnet-20240620", + role="assistant", + stop_reason=None, + stop_sequence=None, + type="message", + usage=Usage(input_tokens=366, output_tokens=10), + ), + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=ToolUseBlock( + id="toolu_0", input={}, name="get_weather", type="tool_use" + ), + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json="", type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta( + partial_json='{"location": "', type="input_json_delta" + ), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json="S", type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta( + partial_json="an ", type="input_json_delta" + ), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta( + partial_json="Francisco, C", type="input_json_delta" + ), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta( + partial_json='A"}', type="input_json_delta" + ), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(stop_reason="tool_use", stop_sequence=None), + usage=MessageDeltaUsage(output_tokens=41), + type="message_delta", + ), + ] + ) + ), + ) + returned_stream = AsyncStream( + cast_to=RawMessageStreamEvent, response=response, client=client + ) + + sentry_init( + integrations=[AnthropicIntegration(include_prompts=include_prompts)], + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + ) + events = capture_events() + client.messages._post = AsyncMock(return_value=returned_stream) + + messages = [ + { + "role": "user", + "content": "What is the weather like in San Francisco?", + } + ] + + with start_transaction(name="anthropic"): + async with client.messages.stream( + max_tokens=1024, + messages=messages, + model="model", + ) as stream: + async for event in stream: + pass + + assert len(events) == 1 + (event,) = events + + assert event["type"] == "transaction" + assert event["transaction"] == "anthropic" + + assert len(event["spans"]) == 1 + (span,) = event["spans"] + + assert span["op"] == OP.GEN_AI_CHAT + assert span["description"] == "chat model" + assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" + + if send_default_pii and include_prompts: + 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] + == '{"location": "San Francisco, CA"}' + ) + + else: + assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] + assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"] + + assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366 + assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41 + assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 407 + assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True + + def test_exception_message_create(sentry_init, capture_events): sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) events = capture_events() @@ -1702,6 +1976,131 @@ def test_stream_messages_with_system_prompt( assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True +@pytest.mark.asyncio +@pytest.mark.parametrize( + "send_default_pii, include_prompts", + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], +) +async def test_stream_message_with_system_prompt_async( + sentry_init, capture_events, send_default_pii, include_prompts +): + """Test that system prompts are properly captured in streaming mode (async).""" + client = AsyncAnthropic(api_key="z") + + response = httpx.Response( + 200, + content=b"".join( + sse_chunks( + [ + MessageStartEvent( + message=EXAMPLE_MESSAGE, + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=TextBlock(type="text", text=""), + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="Hi", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text=" I'm Claude!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(), + usage=MessageDeltaUsage(output_tokens=10), + type="message_delta", + ), + ] + ) + ), + ) + returned_stream = AsyncStream( + cast_to=RawMessageStreamEvent, response=response, client=client + ) + + sentry_init( + integrations=[AnthropicIntegration(include_prompts=include_prompts)], + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + ) + events = capture_events() + client.messages._post = AsyncMock(return_value=returned_stream) + + messages = [ + { + "role": "user", + "content": "Hello, Claude", + } + ] + + with start_transaction(name="anthropic"): + async with client.messages.stream( + max_tokens=1024, + messages=messages, + model="model", + system="You are a helpful assistant.", + ) as stream: + async for event in stream: + pass + + assert len(events) == 1 + (event,) = events + + assert event["type"] == "transaction" + assert event["transaction"] == "anthropic" + + assert len(event["spans"]) == 1 + (span,) = event["spans"] + + assert span["op"] == OP.GEN_AI_CHAT + assert span["description"] == "chat model" + assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat" + assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" + + if send_default_pii and include_prompts: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"] + system_instructions = json.loads( + span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS] + ) + assert system_instructions == [ + {"type": "text", "content": "You are a helpful assistant."} + ] + + assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"] + stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) + assert len(stored_messages) == 1 + assert stored_messages[0]["role"] == "user" + assert stored_messages[0]["content"] == "Hello, Claude" + assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!" + + else: + assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS 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"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 + 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 + + @pytest.mark.asyncio @pytest.mark.parametrize( "send_default_pii, include_prompts", From e06704755f1b979fa85010517d8617e085de4fee Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 2 Mar 2026 09:29:29 +0100 Subject: [PATCH 15/43] . --- sentry_sdk/integrations/anthropic.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 25ce670d39..2ffe861d68 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -66,6 +66,7 @@ ModelParam, TextBlockParam, ToolUnionParam, + MessageStream, ) @@ -724,7 +725,7 @@ async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": def _sentry_patched_stream_common( - stream_manager: "MessageStreamManager", + stream: "MessageStream", max_tokens: "int", messages: "Iterable[MessageParam]", model: "ModelParam", @@ -737,15 +738,15 @@ def _sentry_patched_stream_common( integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) if integration is None: - return stream_manager + return stream if messages is None: - return stream_manager + return stream try: iter(messages) except TypeError: - return stream_manager + return stream if model is None: model = "" @@ -770,7 +771,7 @@ def _sentry_patched_stream_common( top_p=top_p, tools=tools, ) - _patch_streaming_response_iterator(stream_manager, span, integration) + _patch_streaming_response_iterator(stream, span, integration) def _wrap_message_stream(f: "Any") -> "Any": @@ -803,13 +804,13 @@ def _wrap_message_stream_manager_enter(f: "Any") -> "Any": """ @wraps(f) - def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStreamManager": - stream_manager = f(self) + def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStream": + stream = f(self) if not hasattr(self, "_max_tokens"): - return stream_manager + return stream _sentry_patched_stream_common( - stream_manager=stream_manager, + stream=stream, max_tokens=self._max_tokens, messages=self._messages, model=self._model, @@ -819,7 +820,7 @@ def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStreamManager top_p=self._top_p, tools=self._tools, ) - return stream_manager + return stream return _sentry_patched_enter From d9bc06de04ff83be48a10671c9d108bf178f9f79 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 2 Mar 2026 09:32:02 +0100 Subject: [PATCH 16/43] . --- .../integrations/anthropic/test_anthropic.py | 407 ++++++++++-------- 1 file changed, 235 insertions(+), 172 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index 2d5191c1c9..33d971365d 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -1,9 +1,10 @@ import pytest from unittest import mock import json +import httpx try: - from unittest.mock import Mock, AsyncMock + from unittest.mock import AsyncMock except ImportError: class AsyncMock(mock.MagicMock): @@ -12,7 +13,7 @@ async def __call__(self, *args, **kwargs): from anthropic import Anthropic, AnthropicError, AsyncAnthropic, AsyncStream, Stream -from anthropic.types import MessageDeltaUsage, TextDelta, Usage +from anthropic.types import MessageDeltaUsage, TextDelta, Usage, RawMessageStreamEvent from anthropic.types.content_block_delta_event import ContentBlockDeltaEvent from anthropic.types.content_block_start_event import ContentBlockStartEvent from anthropic.types.content_block_stop_event import ContentBlockStopEvent @@ -67,6 +68,13 @@ async def __call__(self, *args, **kwargs): ) +def sse_chunks(events): + for event in events: + payload = event.model_dump() + chunk = f"event: {payload['type']}\ndata: {json.dumps(payload)}\n\n" + yield chunk.encode("utf-8") + + async def async_iterator(values): for value in values: yield value @@ -326,39 +334,49 @@ def test_stream_messages( sentry_init, capture_events, send_default_pii, include_prompts ): client = Anthropic(api_key="z") - returned_stream = Stream(cast_to=None, response=Mock(), client=client) - returned_stream._iterator = [ - MessageStartEvent( - message=EXAMPLE_MESSAGE, - type="message_start", - ), - ContentBlockStartEvent( - type="content_block_start", - index=0, - content_block=TextBlock(type="text", text=""), - ), - ContentBlockDeltaEvent( - delta=TextDelta(text="Hi", type="text_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=TextDelta(text="!", type="text_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=TextDelta(text=" I'm Claude!", type="text_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockStopEvent(type="content_block_stop", index=0), - MessageDeltaEvent( - delta=Delta(), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", + + response = httpx.Response( + 200, + content=b"".join( + sse_chunks( + [ + MessageStartEvent( + message=EXAMPLE_MESSAGE, + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=TextBlock(type="text", text=""), + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="Hi", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text=" I'm Claude!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(), + usage=MessageDeltaUsage(output_tokens=10), + type="message_delta", + ), + ] + ) ), - ] + ) + returned_stream = Stream( + cast_to=RawMessageStreamEvent, response=response, client=client + ) sentry_init( integrations=[AnthropicIntegration(include_prompts=include_prompts)], @@ -671,67 +689,83 @@ def test_stream_messages_with_input_json_delta( sentry_init, capture_events, send_default_pii, include_prompts ): client = Anthropic(api_key="z") - returned_stream = Stream(cast_to=None, response=Mock(), client=client) - returned_stream._iterator = [ - MessageStartEvent( - message=Message( - id="msg_0", - content=[], - model="claude-3-5-sonnet-20240620", - role="assistant", - stop_reason=None, - stop_sequence=None, - type="message", - usage=Usage(input_tokens=366, output_tokens=10), - ), - type="message_start", - ), - ContentBlockStartEvent( - type="content_block_start", - index=0, - content_block=ToolUseBlock( - id="toolu_0", input={}, name="get_weather", type="tool_use" - ), - ), - ContentBlockDeltaEvent( - delta=InputJSONDelta(partial_json="", type="input_json_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=InputJSONDelta( - partial_json='{"location": "', type="input_json_delta" - ), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=InputJSONDelta(partial_json="S", type="input_json_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=InputJSONDelta(partial_json="an ", type="input_json_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=InputJSONDelta(partial_json="Francisco, C", type="input_json_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=InputJSONDelta(partial_json='A"}', type="input_json_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockStopEvent(type="content_block_stop", index=0), - MessageDeltaEvent( - delta=Delta(stop_reason="tool_use", stop_sequence=None), - usage=MessageDeltaUsage(output_tokens=41), - type="message_delta", + + response = httpx.Response( + 200, + content=b"".join( + sse_chunks( + [ + MessageStartEvent( + message=Message( + id="msg_0", + content=[], + model="claude-3-5-sonnet-20240620", + role="assistant", + stop_reason=None, + stop_sequence=None, + type="message", + usage=Usage(input_tokens=366, output_tokens=10), + ), + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=ToolUseBlock( + id="toolu_0", input={}, name="get_weather", type="tool_use" + ), + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json="", type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta( + partial_json='{"location": "', type="input_json_delta" + ), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json="S", type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta( + partial_json="an ", type="input_json_delta" + ), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta( + partial_json="Francisco, C", type="input_json_delta" + ), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta( + partial_json='A"}', type="input_json_delta" + ), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(stop_reason="tool_use", stop_sequence=None), + usage=MessageDeltaUsage(output_tokens=41), + type="message_delta", + ), + ] + ) ), - ] + ) + returned_stream = Stream( + cast_to=RawMessageStreamEvent, response=response, client=client + ) sentry_init( integrations=[AnthropicIntegration(include_prompts=include_prompts)], @@ -1602,39 +1636,49 @@ def test_stream_messages_with_system_prompt( ): """Test that system prompts are properly captured in streaming mode.""" client = Anthropic(api_key="z") - returned_stream = Stream(cast_to=None, response=Mock(), client=client) - returned_stream._iterator = [ - MessageStartEvent( - message=EXAMPLE_MESSAGE, - type="message_start", - ), - ContentBlockStartEvent( - type="content_block_start", - index=0, - content_block=TextBlock(type="text", text=""), - ), - ContentBlockDeltaEvent( - delta=TextDelta(text="Hi", type="text_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=TextDelta(text="!", type="text_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=TextDelta(text=" I'm Claude!", type="text_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockStopEvent(type="content_block_stop", index=0), - MessageDeltaEvent( - delta=Delta(), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", + + response = httpx.Response( + 200, + content=b"".join( + sse_chunks( + [ + MessageStartEvent( + message=EXAMPLE_MESSAGE, + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=TextBlock(type="text", text=""), + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="Hi", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text=" I'm Claude!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(), + usage=MessageDeltaUsage(output_tokens=10), + type="message_delta", + ), + ] + ) ), - ] + ) + returned_stream = Stream( + cast_to=RawMessageStreamEvent, response=response, client=client + ) sentry_init( integrations=[AnthropicIntegration(include_prompts=include_prompts)], @@ -2780,30 +2824,39 @@ def test_stream_messages_input_tokens_include_cache_read_streaming( Same cache-hit scenario as non-streaming, using realistic streaming events. """ client = Anthropic(api_key="z") - returned_stream = Stream(cast_to=None, response=Mock(), client=client) - returned_stream._iterator = [ - MessageStartEvent( - type="message_start", - message=Message( - id="id", - model="claude-sonnet-4-20250514", - role="assistant", - content=[], - type="message", - usage=Usage( - input_tokens=19, - output_tokens=0, - cache_read_input_tokens=2846, - cache_creation_input_tokens=0, - ), - ), - ), - MessageDeltaEvent( - type="message_delta", - delta=Delta(stop_reason="end_turn"), - usage=MessageDeltaUsage(output_tokens=14), + response = httpx.Response( + 200, + content=b"".join( + sse_chunks( + [ + MessageStartEvent( + type="message_start", + message=Message( + id="id", + model="claude-sonnet-4-20250514", + role="assistant", + content=[], + type="message", + usage=Usage( + input_tokens=19, + output_tokens=0, + cache_read_input_tokens=2846, + cache_creation_input_tokens=0, + ), + ), + ), + MessageDeltaEvent( + type="message_delta", + delta=Delta(stop_reason="end_turn"), + usage=MessageDeltaUsage(output_tokens=14), + ), + ] + ) ), - ] + ) + returned_stream = Stream( + cast_to=RawMessageStreamEvent, response=response, client=client + ) sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) events = capture_events() @@ -2918,30 +2971,40 @@ def test_cache_tokens_streaming(sentry_init, capture_events): def test_stream_messages_cache_tokens(sentry_init, capture_events): """Test cache tokens are tracked for streaming responses.""" client = Anthropic(api_key="z") - returned_stream = Stream(cast_to=None, response=Mock(), client=client) - returned_stream._iterator = [ - MessageStartEvent( - type="message_start", - message=Message( - id="id", - model="claude-3-5-sonnet-20241022", - role="assistant", - content=[], - type="message", - usage=Usage( - input_tokens=100, - output_tokens=0, - cache_read_input_tokens=80, - cache_creation_input_tokens=20, - ), - ), - ), - MessageDeltaEvent( - type="message_delta", - delta=Delta(stop_reason="end_turn"), - usage=MessageDeltaUsage(output_tokens=10), + + response = httpx.Response( + 200, + content=b"".join( + sse_chunks( + [ + MessageStartEvent( + type="message_start", + message=Message( + id="id", + model="claude-3-5-sonnet-20241022", + role="assistant", + content=[], + type="message", + usage=Usage( + input_tokens=100, + output_tokens=0, + cache_read_input_tokens=80, + cache_creation_input_tokens=20, + ), + ), + ), + MessageDeltaEvent( + type="message_delta", + delta=Delta(stop_reason="end_turn"), + usage=MessageDeltaUsage(output_tokens=10), + ), + ] + ) ), - ] + ) + returned_stream = Stream( + cast_to=RawMessageStreamEvent, response=response, client=client + ) sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) events = capture_events() From 4ddf276ee36734d3b6d87aff0070783e819e7252 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 2 Mar 2026 09:38:16 +0100 Subject: [PATCH 17/43] . --- 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 33d971365d..1f577e416c 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -13,7 +13,7 @@ async def __call__(self, *args, **kwargs): from anthropic import Anthropic, AnthropicError, AsyncAnthropic, AsyncStream, Stream -from anthropic.types import MessageDeltaUsage, TextDelta, Usage, RawMessageStreamEvent +from anthropic.types import MessageDeltaUsage, TextDelta, Usage, MessageStreamEvent from anthropic.types.content_block_delta_event import ContentBlockDeltaEvent from anthropic.types.content_block_start_event import ContentBlockStartEvent from anthropic.types.content_block_stop_event import ContentBlockStopEvent @@ -375,7 +375,7 @@ def test_stream_messages( ), ) returned_stream = Stream( - cast_to=RawMessageStreamEvent, response=response, client=client + cast_to=MessageStreamEvent, response=response, client=client ) sentry_init( @@ -764,7 +764,7 @@ def test_stream_messages_with_input_json_delta( ), ) returned_stream = Stream( - cast_to=RawMessageStreamEvent, response=response, client=client + cast_to=MessageStreamEvent, response=response, client=client ) sentry_init( @@ -1677,7 +1677,7 @@ def test_stream_messages_with_system_prompt( ), ) returned_stream = Stream( - cast_to=RawMessageStreamEvent, response=response, client=client + cast_to=MessageStreamEvent, response=response, client=client ) sentry_init( @@ -2855,7 +2855,7 @@ def test_stream_messages_input_tokens_include_cache_read_streaming( ), ) returned_stream = Stream( - cast_to=RawMessageStreamEvent, response=response, client=client + cast_to=MessageStreamEvent, response=response, client=client ) sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) @@ -3003,7 +3003,7 @@ def test_stream_messages_cache_tokens(sentry_init, capture_events): ), ) returned_stream = Stream( - cast_to=RawMessageStreamEvent, response=response, client=client + cast_to=MessageStreamEvent, response=response, client=client ) sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) From 2c8629326c2291b821cc6f8a8ddfba0e714214c8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 2 Mar 2026 09:39:32 +0100 Subject: [PATCH 18/43] . --- 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 b1964483f9..60ea98c7fc 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -593,7 +593,7 @@ async def test_stream_message_async( ), ) returned_stream = AsyncStream( - cast_to=RawMessageStreamEvent, response=response, client=client + cast_to=MessageStreamEvent, response=response, client=client ) sentry_init( @@ -1171,7 +1171,7 @@ async def test_stream_message_with_input_json_delta_async( ), ) returned_stream = AsyncStream( - cast_to=RawMessageStreamEvent, response=response, client=client + cast_to=MessageStreamEvent, response=response, client=client ) sentry_init( @@ -2068,7 +2068,7 @@ async def test_stream_message_with_system_prompt_async( ), ) returned_stream = AsyncStream( - cast_to=RawMessageStreamEvent, response=response, client=client + cast_to=MessageStreamEvent, response=response, client=client ) sentry_init( From 53859e4762f4bec7e6d411738398740d59a2137e Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 10 Mar 2026 14:38:18 +0100 Subject: [PATCH 19/43] . --- sentry_sdk/integrations/anthropic.py | 214 +++++++++++++++------------ 1 file changed, 117 insertions(+), 97 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 16e14c3500..a21f79e4a6 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -1,5 +1,6 @@ import sys import json +import inspect from collections.abc import Iterable from functools import wraps from typing import TYPE_CHECKING @@ -37,6 +38,7 @@ except ImportError: Omit = None + from anthropic import Stream, AsyncStream from anthropic.resources import AsyncMessages, Messages if TYPE_CHECKING: @@ -45,11 +47,10 @@ raise DidNotEnable("Anthropic not installed") if TYPE_CHECKING: - from typing import Any, AsyncIterator, Iterator, List, Optional, Union + from typing import Any, AsyncIterator, Iterator, List, Optional, Union, Callable from sentry_sdk.tracing import Span from sentry_sdk._types import TextPart - from anthropic import AsyncStream from anthropic.types import RawMessageStreamEvent @@ -75,6 +76,117 @@ def setup_once() -> None: Messages.create = _wrap_message_create(Messages.create) AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create) + Stream.__iter__ = _wrap_stream_iter(Stream.__iter__) + AsyncStream.__aiter__ = _wrap_async_stream_aiter(AsyncStream.__aiter__) + + +def _wrap_stream_iter( + f: "Callable[..., Iterator[RawMessageStreamEvent]]", +) -> "Callable[..., Iterator[RawMessageStreamEvent]]": + @wraps(f) + def _patched_iter(self: "Stream") -> "Iterator[RawMessageStreamEvent]": + if not hasattr(self, "_sentry_span"): + for event in f(self): + yield event + + model = None + usage = _RecordedUsage() + content_blocks: "list[str]" = [] + + for event in f(self): + ( + model, + usage, + content_blocks, + ) = _collect_ai_data( + event, + model, + usage, + content_blocks, + ) + yield event + + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens for correct cost calculations. + total_input = ( + usage.input_tokens + + (usage.cache_read_input_tokens or 0) + + (usage.cache_write_input_tokens or 0) + ) + + span = self._sentry_span + integration = self._integration + + _set_output_data( + span=span, + integration=integration, + model=model, + input_tokens=total_input, + output_tokens=usage.output_tokens, + cache_read_input_tokens=usage.cache_read_input_tokens, + cache_write_input_tokens=usage.cache_write_input_tokens, + content_blocks=[{"text": "".join(content_blocks), "type": "text"}], + finish_span=True, + ) + + return f(self) + + return _patched_iter + + +def _wrap_async_stream_aiter( + f: "Callable[..., AsyncIterator[RawMessageStreamEvent]]", +) -> "Callable[..., AsyncIterator[RawMessageStreamEvent]]": + @wraps(f) + async def _patched_aiter( + self: "AsyncStream", + ) -> "AsyncIterator[RawMessageStreamEvent]": + if not hasattr(self, "_sentry_span"): + async for event in f(self): + yield event + + model = None + usage = _RecordedUsage() + content_blocks: "list[str]" = [] + + async for event in f(self): + ( + model, + usage, + content_blocks, + ) = _collect_ai_data( + event, + model, + usage, + content_blocks, + ) + yield event + + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens for correct cost calculations. + total_input = ( + usage.input_tokens + + (usage.cache_read_input_tokens or 0) + + (usage.cache_write_input_tokens or 0) + ) + + span = self._sentry_span + integration = self._integration + + _set_output_data( + span=span, + integration=integration, + model=model, + input_tokens=total_input, + output_tokens=usage.output_tokens, + cache_read_input_tokens=usage.cache_read_input_tokens, + cache_write_input_tokens=usage.cache_write_input_tokens, + content_blocks=[{"text": "".join(content_blocks), "type": "text"}], + finish_span=True, + ) + + return _patched_aiter + def _capture_exception(exc: "Any") -> None: set_span_errored() @@ -392,98 +504,6 @@ def _set_output_data( span.__exit__(None, None, None) -def _patch_streaming_response_iterator( - result: "AsyncStream[RawMessageStreamEvent]", - span: "sentry_sdk.tracing.Span", - integration: "AnthropicIntegration", -) -> None: - """ - Responsible for closing the `gen_ai.chat` span and setting attributes acquired during response consumption. - """ - old_iterator = result._iterator - - def new_iterator() -> "Iterator[MessageStreamEvent]": - model = None - usage = _RecordedUsage() - content_blocks: "list[str]" = [] - - for event in old_iterator: - ( - model, - usage, - content_blocks, - ) = _collect_ai_data( - event, - model, - usage, - content_blocks, - ) - yield event - - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens for correct cost calculations. - total_input = ( - usage.input_tokens - + (usage.cache_read_input_tokens or 0) - + (usage.cache_write_input_tokens or 0) - ) - - _set_output_data( - span=span, - integration=integration, - model=model, - input_tokens=total_input, - output_tokens=usage.output_tokens, - cache_read_input_tokens=usage.cache_read_input_tokens, - cache_write_input_tokens=usage.cache_write_input_tokens, - content_blocks=[{"text": "".join(content_blocks), "type": "text"}], - finish_span=True, - ) - - async def new_iterator_async() -> "AsyncIterator[MessageStreamEvent]": - model = None - usage = _RecordedUsage() - content_blocks: "list[str]" = [] - - async for event in old_iterator: - ( - model, - usage, - content_blocks, - ) = _collect_ai_data( - event, - model, - usage, - content_blocks, - ) - yield event - - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens for correct cost calculations. - total_input = ( - usage.input_tokens - + (usage.cache_read_input_tokens or 0) - + (usage.cache_write_input_tokens or 0) - ) - - _set_output_data( - span=span, - integration=integration, - model=model, - input_tokens=total_input, - output_tokens=usage.output_tokens, - cache_read_input_tokens=usage.cache_read_input_tokens, - cache_write_input_tokens=usage.cache_write_input_tokens, - content_blocks=[{"text": "".join(content_blocks), "type": "text"}], - finish_span=True, - ) - - if str(type(result._iterator)) == "": - result._iterator = new_iterator_async() - else: - result._iterator = new_iterator() - - def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "Any": integration = kwargs.pop("integration") if integration is None: @@ -510,9 +530,9 @@ def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A result = yield f, args, kwargs - is_streaming_response = kwargs.get("stream", False) - if is_streaming_response: - _patch_streaming_response_iterator(result, span, integration) + if isinstance(result, Stream) or isinstance(result, AsyncStream): + result._sentry_span = span + result._integration = integration return result with capture_internal_exceptions(): From e2d6d78204d619e9ae6f96215a072e421abb5d14 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 10 Mar 2026 14:39:10 +0100 Subject: [PATCH 20/43] remove unused import --- sentry_sdk/integrations/anthropic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index a21f79e4a6..845fd6e781 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -1,6 +1,5 @@ import sys import json -import inspect from collections.abc import Iterable from functools import wraps from typing import TYPE_CHECKING From a01f7c1698aeed44085ef2f6b73c2c9546b8649d Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 10 Mar 2026 14:41:17 +0100 Subject: [PATCH 21/43] add docstring --- sentry_sdk/integrations/anthropic.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 845fd6e781..72f4b9b406 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -82,6 +82,11 @@ def setup_once() -> None: def _wrap_stream_iter( f: "Callable[..., Iterator[RawMessageStreamEvent]]", ) -> "Callable[..., Iterator[RawMessageStreamEvent]]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Responsible for closing the AI Client Span. + """ + @wraps(f) def _patched_iter(self: "Stream") -> "Iterator[RawMessageStreamEvent]": if not hasattr(self, "_sentry_span"): @@ -136,6 +141,11 @@ def _patched_iter(self: "Stream") -> "Iterator[RawMessageStreamEvent]": def _wrap_async_stream_aiter( f: "Callable[..., AsyncIterator[RawMessageStreamEvent]]", ) -> "Callable[..., AsyncIterator[RawMessageStreamEvent]]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Responsible for closing the AI Client Span. + """ + @wraps(f) async def _patched_aiter( self: "AsyncStream", From 7837439b46c89287d1d82380e5696211dc7d68d5 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 10 Mar 2026 14:42:05 +0100 Subject: [PATCH 22/43] add return --- sentry_sdk/integrations/anthropic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 72f4b9b406..9fdc8b4bb4 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -92,6 +92,7 @@ def _patched_iter(self: "Stream") -> "Iterator[RawMessageStreamEvent]": if not hasattr(self, "_sentry_span"): for event in f(self): yield event + return model = None usage = _RecordedUsage() @@ -153,6 +154,7 @@ async def _patched_aiter( if not hasattr(self, "_sentry_span"): async for event in f(self): yield event + return model = None usage = _RecordedUsage() From 0e06f49dfc34fdbd0ce5e37b6f92af680730b277 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 10 Mar 2026 14:42:29 +0100 Subject: [PATCH 23/43] remove return statement --- sentry_sdk/integrations/anthropic.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 9fdc8b4bb4..8acf892d6c 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -134,8 +134,6 @@ def _patched_iter(self: "Stream") -> "Iterator[RawMessageStreamEvent]": finish_span=True, ) - return f(self) - return _patched_iter From 917b8d48752c26b5deab09f83b6e61ca0fe82007 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Mar 2026 11:52:05 +0100 Subject: [PATCH 24/43] . --- sentry_sdk/integrations/anthropic.py | 108 ++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 8acf892d6c..734d932f57 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -462,6 +462,101 @@ def _set_input_data( span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)) +def _wrap_synchronous_message_iterator( + iterator: "Iterator[RawMessageStreamEvent]", + span: "Span", + integration: "AnthropicIntegration", +) -> "Iterator[RawMessageStreamEvent]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Responsible for closing the AI Client Span. + """ + + model = None + usage = _RecordedUsage() + content_blocks: "list[str]" = [] + + for event in iterator: + ( + model, + usage, + content_blocks, + ) = _collect_ai_data( + event, + model, + usage, + content_blocks, + ) + yield event + + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens for correct cost calculations. + total_input = ( + usage.input_tokens + + (usage.cache_read_input_tokens or 0) + + (usage.cache_write_input_tokens or 0) + ) + + _set_output_data( + span=span, + integration=integration, + model=model, + input_tokens=total_input, + output_tokens=usage.output_tokens, + cache_read_input_tokens=usage.cache_read_input_tokens, + cache_write_input_tokens=usage.cache_write_input_tokens, + content_blocks=[{"text": "".join(content_blocks), "type": "text"}], + finish_span=True, + ) + + +async def _wrap_asynchronous_message_iterator( + iterator: "Iterator[RawMessageStreamEvent]", + span: "Span", + integration: "AnthropicIntegration", +) -> "Iterator[RawMessageStreamEvent]": + """ + Sets information received while iterating the response stream on the AI Client Span. + Responsible for closing the AI Client Span. + """ + model = None + usage = _RecordedUsage() + content_blocks: "list[str]" = [] + + async for event in iterator: + ( + model, + usage, + content_blocks, + ) = _collect_ai_data( + event, + model, + usage, + content_blocks, + ) + yield event + + # Anthropic's input_tokens excludes cached/cache_write tokens. + # Normalize to total input tokens for correct cost calculations. + total_input = ( + usage.input_tokens + + (usage.cache_read_input_tokens or 0) + + (usage.cache_write_input_tokens or 0) + ) + + _set_output_data( + span=span, + integration=integration, + model=model, + input_tokens=total_input, + output_tokens=usage.output_tokens, + cache_read_input_tokens=usage.cache_read_input_tokens, + cache_write_input_tokens=usage.cache_write_input_tokens, + content_blocks=[{"text": "".join(content_blocks), "type": "text"}], + finish_span=True, + ) + + def _set_output_data( span: "Span", integration: "AnthropicIntegration", @@ -539,9 +634,16 @@ def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A result = yield f, args, kwargs - if isinstance(result, Stream) or isinstance(result, AsyncStream): - result._sentry_span = span - result._integration = integration + if isinstance(result, Stream): + result._iterator = _wrap_synchronous_message_iterator( + result._iterator, span, integration + ) + return result + + if isinstance(result, AsyncStream): + result._iterator = _wrap_asynchronous_message_iterator( + result._iterator, span, integration + ) return result with capture_internal_exceptions(): From aa6e58ae700af254e9f42740333d08436367ebe1 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Mar 2026 11:53:09 +0100 Subject: [PATCH 25/43] . --- sentry_sdk/integrations/anthropic.py | 121 --------------------------- 1 file changed, 121 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 734d932f57..b8f9ac1a24 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -75,127 +75,6 @@ def setup_once() -> None: Messages.create = _wrap_message_create(Messages.create) AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create) - Stream.__iter__ = _wrap_stream_iter(Stream.__iter__) - AsyncStream.__aiter__ = _wrap_async_stream_aiter(AsyncStream.__aiter__) - - -def _wrap_stream_iter( - f: "Callable[..., Iterator[RawMessageStreamEvent]]", -) -> "Callable[..., Iterator[RawMessageStreamEvent]]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Responsible for closing the AI Client Span. - """ - - @wraps(f) - def _patched_iter(self: "Stream") -> "Iterator[RawMessageStreamEvent]": - if not hasattr(self, "_sentry_span"): - for event in f(self): - yield event - return - - model = None - usage = _RecordedUsage() - content_blocks: "list[str]" = [] - - for event in f(self): - ( - model, - usage, - content_blocks, - ) = _collect_ai_data( - event, - model, - usage, - content_blocks, - ) - yield event - - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens for correct cost calculations. - total_input = ( - usage.input_tokens - + (usage.cache_read_input_tokens or 0) - + (usage.cache_write_input_tokens or 0) - ) - - span = self._sentry_span - integration = self._integration - - _set_output_data( - span=span, - integration=integration, - model=model, - input_tokens=total_input, - output_tokens=usage.output_tokens, - cache_read_input_tokens=usage.cache_read_input_tokens, - cache_write_input_tokens=usage.cache_write_input_tokens, - content_blocks=[{"text": "".join(content_blocks), "type": "text"}], - finish_span=True, - ) - - return _patched_iter - - -def _wrap_async_stream_aiter( - f: "Callable[..., AsyncIterator[RawMessageStreamEvent]]", -) -> "Callable[..., AsyncIterator[RawMessageStreamEvent]]": - """ - Sets information received while iterating the response stream on the AI Client Span. - Responsible for closing the AI Client Span. - """ - - @wraps(f) - async def _patched_aiter( - self: "AsyncStream", - ) -> "AsyncIterator[RawMessageStreamEvent]": - if not hasattr(self, "_sentry_span"): - async for event in f(self): - yield event - return - - model = None - usage = _RecordedUsage() - content_blocks: "list[str]" = [] - - async for event in f(self): - ( - model, - usage, - content_blocks, - ) = _collect_ai_data( - event, - model, - usage, - content_blocks, - ) - yield event - - # Anthropic's input_tokens excludes cached/cache_write tokens. - # Normalize to total input tokens for correct cost calculations. - total_input = ( - usage.input_tokens - + (usage.cache_read_input_tokens or 0) - + (usage.cache_write_input_tokens or 0) - ) - - span = self._sentry_span - integration = self._integration - - _set_output_data( - span=span, - integration=integration, - model=model, - input_tokens=total_input, - output_tokens=usage.output_tokens, - cache_read_input_tokens=usage.cache_read_input_tokens, - cache_write_input_tokens=usage.cache_write_input_tokens, - content_blocks=[{"text": "".join(content_blocks), "type": "text"}], - finish_span=True, - ) - - return _patched_aiter - def _capture_exception(exc: "Any") -> None: set_span_errored() From bd00e4eeb2ac5ca25569f87f0b52f508589e3330 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Mar 2026 11:53:40 +0100 Subject: [PATCH 26/43] . --- 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 b8f9ac1a24..7d90e6ae8c 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -46,7 +46,7 @@ raise DidNotEnable("Anthropic not installed") if TYPE_CHECKING: - from typing import Any, AsyncIterator, Iterator, List, Optional, Union, Callable + from typing import Any, AsyncIterator, Iterator, List, Optional, Union from sentry_sdk.tracing import Span from sentry_sdk._types import TextPart From c988c263421cd4c6008251c2921237d835d8baa3 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Mar 2026 11:58:22 +0100 Subject: [PATCH 27/43] fix type --- sentry_sdk/integrations/anthropic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 7d90e6ae8c..a67f3c3909 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -390,10 +390,10 @@ def _wrap_synchronous_message_iterator( async def _wrap_asynchronous_message_iterator( - iterator: "Iterator[RawMessageStreamEvent]", + iterator: "AsyncIterator[RawMessageStreamEvent]", span: "Span", integration: "AnthropicIntegration", -) -> "Iterator[RawMessageStreamEvent]": +) -> "AsyncIterator[RawMessageStreamEvent]": """ Sets information received while iterating the response stream on the AI Client Span. Responsible for closing the AI Client Span. From 244ea773f57154c148ca443d19f68e2982b93f40 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Mar 2026 13:48:31 +0100 Subject: [PATCH 28/43] . --- sentry_sdk/integrations/anthropic.py | 39 +++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index be83d6e02c..4018f7fdb0 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -869,17 +869,50 @@ async def _sentry_patched_aenter( if not hasattr(self, "_max_tokens"): return stream - _sentry_patched_stream_common( - stream=stream, + integration = sentry_sdk.get_client().get_integration(AnthropicIntegration) + + if integration is None: + return stream + + if self._messages is None: + return stream + + try: + iter(self._messages) + except TypeError: + return stream + + model = self._model + if model is None: + model = "" + + span = get_start_span_function()( + op=OP.GEN_AI_CHAT, + name=f"chat {model}".strip(), + origin=AnthropicIntegration.origin, + ) + span.__enter__() + + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + _set_stream_input_data( + span, + integration, max_tokens=self._max_tokens, messages=self._messages, - model=self._model, + model=model, system=self._system, temperature=self._temperature, top_k=self._top_k, top_p=self._top_p, tools=self._tools, ) + + stream._iterator = _wrap_asynchronous_message_iterator( + iterator=stream._iterator, + span=span, + integration=integration, + ) + return stream return _sentry_patched_aenter From 724409d869809a4ef19cbf233c89c33479593986 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Mar 2026 17:49:21 +0100 Subject: [PATCH 29/43] tests --- tests/conftest.py | 62 ++- .../integrations/anthropic/test_anthropic.py | 419 +++++++++--------- 2 files changed, 253 insertions(+), 228 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0853013dfd..7f76fc2aee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,13 +57,6 @@ from collections.abc import Iterator try: - from anyio import create_memory_object_stream, create_task_group, EndOfStream - from mcp.types import ( - JSONRPCMessage, - JSONRPCNotification, - JSONRPCRequest, - ) - from mcp.shared.message import SessionMessage from httpx import ( ASGITransport, Request as HttpxRequest, @@ -71,6 +64,22 @@ AsyncByteStream, AsyncClient, ) +except ImportError: + ASGITransport = None + HttpxRequest = None + HttpxResponse = None + AsyncByteStream = None + AsyncClient = None + + +try: + from anyio import create_memory_object_stream, create_task_group, EndOfStream + from mcp.types import ( + JSONRPCMessage, + JSONRPCNotification, + JSONRPCRequest, + ) + from mcp.shared.message import SessionMessage except ImportError: create_memory_object_stream = None create_task_group = None @@ -81,12 +90,6 @@ JSONRPCRequest = None SessionMessage = None - ASGITransport = None - HttpxRequest = None - HttpxResponse = None - AsyncByteStream = None - AsyncClient = None - SENTRY_EVENT_SCHEMA = "./checkouts/data-schemas/relay/event.schema.json" @@ -1013,6 +1016,39 @@ async def inner(values): return inner +@pytest.fixture +def server_side_event_chunks(): + def inner(events): + for event in events: + payload = event.model_dump() + chunk = f"event: {payload['type']}\ndata: {json.dumps(payload)}\n\n" + yield chunk.encode("utf-8") + + return inner + + +@pytest.fixture +def get_model_response(): + def inner(response_content, serialize_pydantic=False): + model_request = HttpxRequest( + "POST", + "/responses", + ) + + if serialize_pydantic: + response_content = json.dumps(response_content.model_dump()).encode("utf-8") + + response = HttpxResponse( + 200, + request=model_request, + content=response_content, + ) + + return response + + return inner + + class MockServerRequestHandler(BaseHTTPRequestHandler): def do_GET(self): # noqa: N802 # Process an HTTP GET request and return a response. diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index 6fe6786993..bfa8e9a21e 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -68,18 +68,6 @@ async def __call__(self, *args, **kwargs): ) -def sse_chunks(events): - for event in events: - payload = event.model_dump() - chunk = f"event: {payload['type']}\ndata: {json.dumps(payload)}\n\n" - yield chunk.encode("utf-8") - - -async def async_iterator(values): - for value in values: - yield value - - @pytest.mark.parametrize( "send_default_pii, include_prompts", [ @@ -331,48 +319,50 @@ def test_streaming_create_message( ], ) def test_stream_messages( - sentry_init, capture_events, send_default_pii, include_prompts + sentry_init, + capture_events, + send_default_pii, + include_prompts, + get_model_response, + server_side_event_chunks, ): client = Anthropic(api_key="z") - response = httpx.Response( - 200, - content=b"".join( - sse_chunks( - [ - MessageStartEvent( - message=EXAMPLE_MESSAGE, - type="message_start", - ), - ContentBlockStartEvent( - type="content_block_start", - index=0, - content_block=TextBlock(type="text", text=""), - ), - ContentBlockDeltaEvent( - delta=TextDelta(text="Hi", type="text_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=TextDelta(text="!", type="text_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=TextDelta(text=" I'm Claude!", type="text_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockStopEvent(type="content_block_stop", index=0), - MessageDeltaEvent( - delta=Delta(), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", - ), - ] - ) - ), + response = get_model_response( + server_side_event_chunks( + [ + MessageStartEvent( + message=EXAMPLE_MESSAGE, + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=TextBlock(type="text", text=""), + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="Hi", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text=" I'm Claude!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(), + usage=MessageDeltaUsage(output_tokens=10), + type="message_delta", + ), + ] + ) ) returned_stream = Stream( cast_to=MessageStreamEvent, response=response, client=client @@ -686,82 +676,80 @@ def test_streaming_create_message_with_input_json_delta( ], ) def test_stream_messages_with_input_json_delta( - sentry_init, capture_events, send_default_pii, include_prompts + sentry_init, + capture_events, + send_default_pii, + include_prompts, + get_model_response, + server_side_event_chunks, ): client = Anthropic(api_key="z") - response = httpx.Response( - 200, - content=b"".join( - sse_chunks( - [ - MessageStartEvent( - message=Message( - id="msg_0", - content=[], - model="claude-3-5-sonnet-20240620", - role="assistant", - stop_reason=None, - stop_sequence=None, - type="message", - usage=Usage(input_tokens=366, output_tokens=10), - ), - type="message_start", - ), - ContentBlockStartEvent( - type="content_block_start", - index=0, - content_block=ToolUseBlock( - id="toolu_0", input={}, name="get_weather", type="tool_use" - ), - ), - ContentBlockDeltaEvent( - delta=InputJSONDelta(partial_json="", type="input_json_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=InputJSONDelta( - partial_json='{"location": "', type="input_json_delta" - ), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=InputJSONDelta(partial_json="S", type="input_json_delta"), - index=0, - type="content_block_delta", + response = get_model_response( + server_side_event_chunks( + [ + MessageStartEvent( + message=Message( + id="msg_0", + content=[], + model="claude-3-5-sonnet-20240620", + role="assistant", + stop_reason=None, + stop_sequence=None, + type="message", + usage=Usage(input_tokens=366, output_tokens=10), ), - ContentBlockDeltaEvent( - delta=InputJSONDelta( - partial_json="an ", type="input_json_delta" - ), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=InputJSONDelta( - partial_json="Francisco, C", type="input_json_delta" - ), - index=0, - type="content_block_delta", + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=ToolUseBlock( + id="toolu_0", input={}, name="get_weather", type="tool_use" ), - ContentBlockDeltaEvent( - delta=InputJSONDelta( - partial_json='A"}', type="input_json_delta" - ), - index=0, - type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json="", type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta( + partial_json='{"location": "', type="input_json_delta" ), - ContentBlockStopEvent(type="content_block_stop", index=0), - MessageDeltaEvent( - delta=Delta(stop_reason="tool_use", stop_sequence=None), - usage=MessageDeltaUsage(output_tokens=41), - type="message_delta", + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json="S", type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json="an ", type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta( + partial_json="Francisco, C", type="input_json_delta" ), - ] - ) - ), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=InputJSONDelta(partial_json='A"}', type="input_json_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(stop_reason="tool_use", stop_sequence=None), + usage=MessageDeltaUsage(output_tokens=41), + type="message_delta", + ), + ] + ) ) returned_stream = Stream( cast_to=MessageStreamEvent, response=response, client=client @@ -1632,49 +1620,51 @@ def test_streaming_create_message_with_system_prompt( ], ) def test_stream_messages_with_system_prompt( - sentry_init, capture_events, send_default_pii, include_prompts + sentry_init, + capture_events, + send_default_pii, + include_prompts, + get_model_response, + server_side_event_chunks, ): """Test that system prompts are properly captured in streaming mode.""" client = Anthropic(api_key="z") - response = httpx.Response( - 200, - content=b"".join( - sse_chunks( - [ - MessageStartEvent( - message=EXAMPLE_MESSAGE, - type="message_start", - ), - ContentBlockStartEvent( - type="content_block_start", - index=0, - content_block=TextBlock(type="text", text=""), - ), - ContentBlockDeltaEvent( - delta=TextDelta(text="Hi", type="text_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=TextDelta(text="!", type="text_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockDeltaEvent( - delta=TextDelta(text=" I'm Claude!", type="text_delta"), - index=0, - type="content_block_delta", - ), - ContentBlockStopEvent(type="content_block_stop", index=0), - MessageDeltaEvent( - delta=Delta(), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", - ), - ] - ) - ), + response = get_model_response( + server_side_event_chunks( + [ + MessageStartEvent( + message=EXAMPLE_MESSAGE, + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=TextBlock(type="text", text=""), + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="Hi", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text=" I'm Claude!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(), + usage=MessageDeltaUsage(output_tokens=10), + type="message_delta", + ), + ] + ) ) returned_stream = Stream( cast_to=MessageStreamEvent, response=response, client=client @@ -2816,7 +2806,10 @@ def test_input_tokens_include_cache_read_streaming(sentry_init, capture_events): def test_stream_messages_input_tokens_include_cache_read_streaming( - sentry_init, capture_events + sentry_init, + capture_events, + get_model_response, + server_side_event_chunks, ): """ Test that gen_ai.usage.input_tokens includes cache_read tokens (streaming). @@ -2824,35 +2817,32 @@ def test_stream_messages_input_tokens_include_cache_read_streaming( Same cache-hit scenario as non-streaming, using realistic streaming events. """ client = Anthropic(api_key="z") - response = httpx.Response( - 200, - content=b"".join( - sse_chunks( - [ - MessageStartEvent( - type="message_start", - message=Message( - id="id", - model="claude-sonnet-4-20250514", - role="assistant", - content=[], - type="message", - usage=Usage( - input_tokens=19, - output_tokens=0, - cache_read_input_tokens=2846, - cache_creation_input_tokens=0, - ), + response = get_model_response( + server_side_event_chunks( + [ + MessageStartEvent( + type="message_start", + message=Message( + id="id", + model="claude-sonnet-4-20250514", + role="assistant", + content=[], + type="message", + usage=Usage( + input_tokens=19, + output_tokens=0, + cache_read_input_tokens=2846, + cache_creation_input_tokens=0, ), ), - MessageDeltaEvent( - type="message_delta", - delta=Delta(stop_reason="end_turn"), - usage=MessageDeltaUsage(output_tokens=14), - ), - ] - ) - ), + ), + MessageDeltaEvent( + type="message_delta", + delta=Delta(stop_reason="end_turn"), + usage=MessageDeltaUsage(output_tokens=14), + ), + ] + ) ) returned_stream = Stream( cast_to=MessageStreamEvent, response=response, client=client @@ -2968,39 +2958,38 @@ def test_cache_tokens_streaming(sentry_init, capture_events): assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20 -def test_stream_messages_cache_tokens(sentry_init, capture_events): +def test_stream_messages_cache_tokens( + sentry_init, capture_events, get_model_response, server_side_event_chunks +): """Test cache tokens are tracked for streaming responses.""" client = Anthropic(api_key="z") - response = httpx.Response( - 200, - content=b"".join( - sse_chunks( - [ - MessageStartEvent( - type="message_start", - message=Message( - id="id", - model="claude-3-5-sonnet-20241022", - role="assistant", - content=[], - type="message", - usage=Usage( - input_tokens=100, - output_tokens=0, - cache_read_input_tokens=80, - cache_creation_input_tokens=20, - ), + response = get_model_response( + server_side_event_chunks( + [ + MessageStartEvent( + type="message_start", + message=Message( + id="id", + model="claude-3-5-sonnet-20241022", + role="assistant", + content=[], + type="message", + usage=Usage( + input_tokens=100, + output_tokens=0, + cache_read_input_tokens=80, + cache_creation_input_tokens=20, ), ), - MessageDeltaEvent( - type="message_delta", - delta=Delta(stop_reason="end_turn"), - usage=MessageDeltaUsage(output_tokens=10), - ), - ] - ) - ), + ), + MessageDeltaEvent( + type="message_delta", + delta=Delta(stop_reason="end_turn"), + usage=MessageDeltaUsage(output_tokens=10), + ), + ] + ) ) returned_stream = Stream( cast_to=MessageStreamEvent, response=response, client=client From 2693579394362a37b47ecbd1a9b8c2730e9e1bc8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Mar 2026 17:55:35 +0100 Subject: [PATCH 30/43] rename function --- sentry_sdk/integrations/anthropic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 6458d821c3..b0b2e5279b 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -267,7 +267,7 @@ def _transform_system_instructions( ] -def _common_set_input_data( +def _set_common_input_data( span: "Span", integration: "AnthropicIntegration", max_tokens: "int", @@ -371,7 +371,7 @@ def _set_create_input_data( """ span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)) - _common_set_input_data( + _set_common_input_data( span=span, integration=integration, max_tokens=kwargs.get("max_tokens"), # type: ignore @@ -397,7 +397,7 @@ def _set_stream_input_data( top_p: "Optional[float]", tools: "Optional[Iterable[ToolUnionParam]]", ) -> None: - _common_set_input_data( + _set_common_input_data( span=span, integration=integration, max_tokens=max_tokens, From 438a2ba052d5b70d338d25f997048eb749c81117 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Mar 2026 18:06:46 +0100 Subject: [PATCH 31/43] tests --- .../integrations/anthropic/test_anthropic.py | 130 +++++++++--------- 1 file changed, 67 insertions(+), 63 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index bfa8e9a21e..23d4cacda2 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -364,9 +364,6 @@ def test_stream_messages( ] ) ) - returned_stream = Stream( - cast_to=MessageStreamEvent, response=response, client=client - ) sentry_init( integrations=[AnthropicIntegration(include_prompts=include_prompts)], @@ -374,7 +371,6 @@ def test_stream_messages( send_default_pii=send_default_pii, ) events = capture_events() - client.messages._post = mock.Mock(return_value=returned_stream) messages = [ { @@ -383,14 +379,19 @@ def test_stream_messages( } ] - with start_transaction(name="anthropic"): - with client.messages.stream( - max_tokens=1024, - messages=messages, - model="model", - ) as stream: - for event in stream: - pass + with mock.patch.object( + client._client, + "send", + return_value=response, + ) as _: + with start_transaction(name="anthropic"): + with client.messages.stream( + max_tokens=1024, + messages=messages, + model="model", + ) as stream: + for event in stream: + pass assert len(events) == 1 (event,) = events @@ -398,8 +399,7 @@ def test_stream_messages( assert event["type"] == "transaction" assert event["transaction"] == "anthropic" - assert len(event["spans"]) == 1 - (span,) = event["spans"] + span = next(span for span in event["spans"] if span["op"] == OP.GEN_AI_CHAT) assert span["op"] == OP.GEN_AI_CHAT assert span["description"] == "chat model" @@ -751,9 +751,6 @@ def test_stream_messages_with_input_json_delta( ] ) ) - returned_stream = Stream( - cast_to=MessageStreamEvent, response=response, client=client - ) sentry_init( integrations=[AnthropicIntegration(include_prompts=include_prompts)], @@ -761,7 +758,6 @@ def test_stream_messages_with_input_json_delta( send_default_pii=send_default_pii, ) events = capture_events() - client.messages._post = mock.Mock(return_value=returned_stream) messages = [ { @@ -770,14 +766,19 @@ def test_stream_messages_with_input_json_delta( } ] - with start_transaction(name="anthropic"): - with client.messages.stream( - max_tokens=1024, - messages=messages, - model="model", - ) as stream: - for event in stream: - pass + with mock.patch.object( + client._client, + "send", + return_value=response, + ) as _: + with start_transaction(name="anthropic"): + with client.messages.stream( + max_tokens=1024, + messages=messages, + model="model", + ) as stream: + for event in stream: + pass assert len(events) == 1 (event,) = events @@ -1666,9 +1667,6 @@ def test_stream_messages_with_system_prompt( ] ) ) - returned_stream = Stream( - cast_to=MessageStreamEvent, response=response, client=client - ) sentry_init( integrations=[AnthropicIntegration(include_prompts=include_prompts)], @@ -1676,7 +1674,6 @@ def test_stream_messages_with_system_prompt( send_default_pii=send_default_pii, ) events = capture_events() - client.messages._post = mock.Mock(return_value=returned_stream) messages = [ { @@ -1685,15 +1682,20 @@ def test_stream_messages_with_system_prompt( } ] - with start_transaction(name="anthropic"): - with client.messages.stream( - max_tokens=1024, - messages=messages, - model="model", - system="You are a helpful assistant.", - ) as stream: - for event in stream: - pass + with mock.patch.object( + client._client, + "send", + return_value=response, + ) as _: + with start_transaction(name="anthropic"): + with client.messages.stream( + max_tokens=1024, + messages=messages, + model="model", + system="You are a helpful assistant.", + ) as stream: + for event in stream: + pass assert len(events) == 1 (event,) = events @@ -2844,22 +2846,23 @@ def test_stream_messages_input_tokens_include_cache_read_streaming( ] ) ) - returned_stream = Stream( - cast_to=MessageStreamEvent, response=response, client=client - ) sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) events = capture_events() - client.messages._post = mock.Mock(return_value=returned_stream) - with start_transaction(name="anthropic"): - with client.messages.stream( - max_tokens=1024, - messages=[{"role": "user", "content": "What is 5+5?"}], - model="claude-sonnet-4-20250514", - ) as stream: - for event in stream: - pass + with mock.patch.object( + client._client, + "send", + return_value=response, + ) as _: + with start_transaction(name="anthropic"): + with client.messages.stream( + max_tokens=1024, + messages=[{"role": "user", "content": "What is 5+5?"}], + model="claude-sonnet-4-20250514", + ) as stream: + for event in stream: + pass (span,) = events[0]["spans"] @@ -2991,22 +2994,23 @@ def test_stream_messages_cache_tokens( ] ) ) - returned_stream = Stream( - cast_to=MessageStreamEvent, response=response, client=client - ) sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0) events = capture_events() - client.messages._post = mock.Mock(return_value=returned_stream) - with start_transaction(name="anthropic"): - with client.messages.stream( - max_tokens=1024, - messages=[{"role": "user", "content": "Hello"}], - model="claude-3-5-sonnet-20241022", - ) as stream: - for event in stream: - pass + with mock.patch.object( + client._client, + "send", + return_value=response, + ) as _: + with start_transaction(name="anthropic"): + with client.messages.stream( + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], + model="claude-3-5-sonnet-20241022", + ) as stream: + for event in stream: + pass (span,) = events[0]["spans"] # input_tokens normalized: 100 + 80 (cache_read) + 20 (cache_write) = 200 From 21b97ba54f040f4c883a938bfa0d5d91a0dda118 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 11 Mar 2026 18:28:02 +0100 Subject: [PATCH 32/43] tests --- .../integrations/anthropic/test_anthropic.py | 126 ++++++++++-------- 1 file changed, 72 insertions(+), 54 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index b025279521..5dad85dc6d 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -539,14 +539,19 @@ async def test_streaming_create_message_async( ], ) async def test_stream_message_async( - sentry_init, capture_events, send_default_pii, include_prompts + sentry_init, + capture_events, + send_default_pii, + include_prompts, + get_model_response, + async_iterator, + server_side_event_chunks, ): client = AsyncAnthropic(api_key="z") - response = httpx.Response( - 200, - content=b"".join( - sse_chunks( + response = get_model_response( + async_iterator( + server_side_event_chunks( [ MessageStartEvent( message=EXAMPLE_MESSAGE, @@ -582,9 +587,6 @@ async def test_stream_message_async( ) ), ) - returned_stream = AsyncStream( - cast_to=MessageStreamEvent, response=response, client=client - ) sentry_init( integrations=[AnthropicIntegration(include_prompts=include_prompts)], @@ -592,7 +594,6 @@ async def test_stream_message_async( send_default_pii=send_default_pii, ) events = capture_events() - client.messages._post = AsyncMock(return_value=returned_stream) messages = [ { @@ -601,14 +602,19 @@ async def test_stream_message_async( } ] - with start_transaction(name="anthropic"): - async with client.messages.stream( - max_tokens=1024, - messages=messages, - model="model", - ) as stream: - async for event in stream: - pass + with mock.patch.object( + client._client, + "send", + return_value=response, + ) as _: + with start_transaction(name="anthropic"): + async with client.messages.stream( + max_tokens=1024, + messages=messages, + model="model", + ) as stream: + async for event in stream: + pass assert len(events) == 1 (event,) = events @@ -1083,13 +1089,18 @@ async def test_streaming_create_message_with_input_json_delta_async( ], ) async def test_stream_message_with_input_json_delta_async( - sentry_init, capture_events, send_default_pii, include_prompts + sentry_init, + capture_events, + send_default_pii, + include_prompts, + get_model_response, + async_iterator, + server_side_event_chunks, ): client = AsyncAnthropic(api_key="z") - response = httpx.Response( - 200, - content=b"".join( - sse_chunks( + response = get_model_response( + async_iterator( + server_side_event_chunks( [ MessageStartEvent( message=Message( @@ -1157,10 +1168,7 @@ async def test_stream_message_with_input_json_delta_async( ), ] ) - ), - ) - returned_stream = AsyncStream( - cast_to=MessageStreamEvent, response=response, client=client + ) ) sentry_init( @@ -1169,7 +1177,6 @@ async def test_stream_message_with_input_json_delta_async( send_default_pii=send_default_pii, ) events = capture_events() - client.messages._post = AsyncMock(return_value=returned_stream) messages = [ { @@ -1178,14 +1185,19 @@ async def test_stream_message_with_input_json_delta_async( } ] - with start_transaction(name="anthropic"): - async with client.messages.stream( - max_tokens=1024, - messages=messages, - model="model", - ) as stream: - async for event in stream: - pass + with mock.patch.object( + client._client, + "send", + return_value=response, + ) as _: + with start_transaction(name="anthropic"): + async with client.messages.stream( + max_tokens=1024, + messages=messages, + model="model", + ) as stream: + async for event in stream: + pass assert len(events) == 1 (event,) = events @@ -2015,15 +2027,20 @@ def test_stream_messages_with_system_prompt( ], ) async def test_stream_message_with_system_prompt_async( - sentry_init, capture_events, send_default_pii, include_prompts + sentry_init, + capture_events, + send_default_pii, + include_prompts, + get_model_response, + async_iterator, + server_side_event_chunks, ): """Test that system prompts are properly captured in streaming mode (async).""" client = AsyncAnthropic(api_key="z") - response = httpx.Response( - 200, - content=b"".join( - sse_chunks( + response = get_model_response( + async_iterator( + server_side_event_chunks( [ MessageStartEvent( message=EXAMPLE_MESSAGE, @@ -2057,10 +2074,7 @@ async def test_stream_message_with_system_prompt_async( ), ] ) - ), - ) - returned_stream = AsyncStream( - cast_to=MessageStreamEvent, response=response, client=client + ) ) sentry_init( @@ -2069,7 +2083,6 @@ async def test_stream_message_with_system_prompt_async( send_default_pii=send_default_pii, ) events = capture_events() - client.messages._post = AsyncMock(return_value=returned_stream) messages = [ { @@ -2078,15 +2091,20 @@ async def test_stream_message_with_system_prompt_async( } ] - with start_transaction(name="anthropic"): - async with client.messages.stream( - max_tokens=1024, - messages=messages, - model="model", - system="You are a helpful assistant.", - ) as stream: - async for event in stream: - pass + with mock.patch.object( + client._client, + "send", + return_value=response, + ) as _: + with start_transaction(name="anthropic"): + async with client.messages.stream( + max_tokens=1024, + messages=messages, + model="model", + system="You are a helpful assistant.", + ) as stream: + async for event in stream: + pass assert len(events) == 1 (event,) = events From fd213f4defff39287c0b6916b4ed29efa29d09fa Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Mar 2026 10:13:52 +0100 Subject: [PATCH 33/43] fix types --- sentry_sdk/integrations/anthropic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index b0b2e5279b..f7f480c61a 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -412,10 +412,10 @@ def _set_stream_input_data( def _wrap_synchronous_message_iterator( - iterator: "Iterator[RawMessageStreamEvent]", + iterator: "Iterator[RawMessageStreamEvent, MessageStreamEvent]", span: "Span", integration: "AnthropicIntegration", -) -> "Iterator[RawMessageStreamEvent]": +) -> "Iterator[RawMessageStreamEvent, MessageStreamEvent]": """ Sets information received while iterating the response stream on the AI Client Span. Responsible for closing the AI Client Span. @@ -738,7 +738,7 @@ async def _sentry_patched_create_async(*args: "Any", **kwargs: "Any") -> "Any": def _wrap_message_stream(f: "Any") -> "Any": """ Attaches user-provided arguments to the returned context manager. - The attributes are set on `gen_ai.chat` spans in the patch for the context manager. + The attributes are set on AI Client Spans in the patch for the context manager. """ @wraps(f) @@ -761,7 +761,7 @@ def _sentry_patched_stream(*args: "Any", **kwargs: "Any") -> "MessageStreamManag def _wrap_message_stream_manager_enter(f: "Any") -> "Any": """ - Creates and manages `gen_ai.chat` spans. + Creates and manages AI Client Spans. """ @wraps(f) From c0632a32acb1c33b89b247bb9edfff946412d926 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Mar 2026 10:16:05 +0100 Subject: [PATCH 34/43] fix types --- sentry_sdk/integrations/anthropic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index ff33f3249c..a9f69c0964 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -485,10 +485,10 @@ def _wrap_synchronous_message_iterator( async def _wrap_asynchronous_message_iterator( - iterator: "AsyncIterator[RawMessageStreamEvent]", + iterator: "AsyncIterator[RawMessageStreamEvent, MessageStreamEvent]", span: "Span", integration: "AnthropicIntegration", -) -> "AsyncIterator[RawMessageStreamEvent]": +) -> "AsyncIterator[RawMessageStreamEvent, MessageStreamEvent]": """ Sets information received while iterating the response stream on the AI Client Span. Responsible for closing the AI Client Span. @@ -833,7 +833,7 @@ def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStream": def _wrap_async_message_stream(f: "Any") -> "Any": """ Attaches user-provided arguments to the returned context manager. - The attributes are set on `gen_ai.chat` spans in the patch for the context manager. + The attributes are set on AI Client Spans in the patch for the context manager. """ @wraps(f) @@ -858,7 +858,7 @@ def _sentry_patched_stream( def _wrap_async_message_stream_manager_aenter(f: "Any") -> "Any": """ - Creates and manages `gen_ai.chat` spans. + Creates and manages AI Client Spans. """ @wraps(f) From 035dd5d6fecc35d4d61e34a8d8379dc0d464cd0f Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Mar 2026 10:16:58 +0100 Subject: [PATCH 35/43] . --- sentry_sdk/integrations/anthropic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index f7f480c61a..54aa40e9a4 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -412,10 +412,10 @@ def _set_stream_input_data( def _wrap_synchronous_message_iterator( - iterator: "Iterator[RawMessageStreamEvent, MessageStreamEvent]", + iterator: "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", span: "Span", integration: "AnthropicIntegration", -) -> "Iterator[RawMessageStreamEvent, MessageStreamEvent]": +) -> "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": """ Sets information received while iterating the response stream on the AI Client Span. Responsible for closing the AI Client Span. From 8f54d58cc254782e207f6bb4d0302b5407a77f7c Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Mar 2026 10:17:20 +0100 Subject: [PATCH 36/43] . --- sentry_sdk/integrations/anthropic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index a9f69c0964..4d61bf0fc8 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -423,10 +423,10 @@ def _set_stream_input_data( def _wrap_synchronous_message_iterator( - iterator: "Iterator[RawMessageStreamEvent, MessageStreamEvent]", + iterator: "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", span: "Span", integration: "AnthropicIntegration", -) -> "Iterator[RawMessageStreamEvent, MessageStreamEvent]": +) -> "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": """ Sets information received while iterating the response stream on the AI Client Span. Responsible for closing the AI Client Span. From c0f3c0bc3cbb819a86dd3fb203e276606e6a3997 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Mar 2026 10:21:36 +0100 Subject: [PATCH 37/43] typing --- sentry_sdk/integrations/anthropic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 4d61bf0fc8..7ad0d00ba6 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -485,10 +485,10 @@ def _wrap_synchronous_message_iterator( async def _wrap_asynchronous_message_iterator( - iterator: "AsyncIterator[RawMessageStreamEvent, MessageStreamEvent]", + iterator: "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", span: "Span", integration: "AnthropicIntegration", -) -> "AsyncIterator[RawMessageStreamEvent, MessageStreamEvent]": +) -> "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": """ Sets information received while iterating the response stream on the AI Client Span. Responsible for closing the AI Client Span. From 8e4b6e893cdad36780a2e156a2069bb163793159 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Mar 2026 10:37:40 +0100 Subject: [PATCH 38/43] set streaming true --- sentry_sdk/integrations/anthropic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 54aa40e9a4..b65c5359a8 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -397,6 +397,8 @@ def _set_stream_input_data( top_p: "Optional[float]", tools: "Optional[Iterable[ToolUnionParam]]", ) -> None: + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + _set_common_input_data( span=span, integration=integration, From 1e4fb121677e57a32a5d670a9bd5baef2940ce63 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Mar 2026 10:56:16 +0100 Subject: [PATCH 39/43] simplify setting input data --- sentry_sdk/integrations/anthropic.py | 34 +++------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index b65c5359a8..ebd3ff0745 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -385,34 +385,6 @@ def _set_create_input_data( ) -def _set_stream_input_data( - span: "Span", - integration: "AnthropicIntegration", - max_tokens: "int", - messages: "Iterable[MessageParam]", - model: "ModelParam", - system: "Optional[Union[str, Iterable[TextBlockParam]]]", - temperature: "Optional[float]", - top_k: "Optional[int]", - top_p: "Optional[float]", - tools: "Optional[Iterable[ToolUnionParam]]", -) -> None: - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - - _set_common_input_data( - span=span, - integration=integration, - max_tokens=max_tokens, - messages=messages, - model=model, - system=system, - temperature=temperature, - top_k=top_k, - top_p=top_p, - tools=tools, - ) - - def _wrap_synchronous_message_iterator( iterator: "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", span: "Span", @@ -797,9 +769,9 @@ def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStream": span.__enter__() span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - _set_stream_input_data( - span, - integration, + _set_common_input_data( + span=span, + integration=integration, max_tokens=self._max_tokens, messages=self._messages, model=model, From 34f863bdf499a0ab34f95cd1a58b9a6034278c84 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Mar 2026 10:58:22 +0100 Subject: [PATCH 40/43] simplify setting input data --- sentry_sdk/integrations/anthropic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index c03eed8589..db58f226ee 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -868,9 +868,9 @@ async def _sentry_patched_aenter( span.__enter__() span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - _set_stream_input_data( - span, - integration, + _set_common_input_data( + span=span, + integration=integration, max_tokens=self._max_tokens, messages=self._messages, model=model, From f187ad5376513f207738147e4d212f1c46af4318 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Mar 2026 10:59:26 +0100 Subject: [PATCH 41/43] fix self annotation --- 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 db58f226ee..5233807085 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -837,7 +837,7 @@ def _wrap_async_message_stream_manager_aenter(f: "Any") -> "Any": @wraps(f) async def _sentry_patched_aenter( - self: "MessageStreamManager", + self: "AsyncMessageStreamManager", ) -> "AsyncMessageStream": stream = await f(self) if not hasattr(self, "_max_tokens"): From 5817f961351fb248a3d771892cd7a6def99aabd5 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 12 Mar 2026 11:05:58 +0100 Subject: [PATCH 42/43] unused imports --- tests/integrations/anthropic/test_anthropic.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index 23d4cacda2..0a7900936e 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -1,7 +1,6 @@ import pytest from unittest import mock import json -import httpx try: from unittest.mock import AsyncMock @@ -13,7 +12,7 @@ async def __call__(self, *args, **kwargs): from anthropic import Anthropic, AnthropicError, AsyncAnthropic, AsyncStream, Stream -from anthropic.types import MessageDeltaUsage, TextDelta, Usage, MessageStreamEvent +from anthropic.types import MessageDeltaUsage, TextDelta, Usage from anthropic.types.content_block_delta_event import ContentBlockDeltaEvent from anthropic.types.content_block_start_event import ContentBlockStartEvent from anthropic.types.content_block_stop_event import ContentBlockStopEvent From 68dba0e11a0693d04e32a8dec487f07360fd0e05 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 13 Mar 2026 15:54:59 +0100 Subject: [PATCH 43/43] update type --- 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 096db6115e..bc208ac4f5 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -466,7 +466,7 @@ async def _wrap_asynchronous_message_iterator( iterator: "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]", span: "Span", integration: "AnthropicIntegration", -) -> "AsyncIterator[RawMessageStreamEvent]": +) -> "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]": """ Sets information received while iterating the response stream on the AI Client Span. Responsible for closing the AI Client Span.