Skip to content
Merged
Changes from 7 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4cf7e5a
ref(anthropic): Factor out streamed result handling
alexander-alderman-webb Feb 27, 2026
61a4cd0
ref(anthropic): Skip accumulation logic for unexpected types in strea…
alexander-alderman-webb Feb 27, 2026
6cdab16
.
alexander-alderman-webb Feb 27, 2026
db5dbb9
.
alexander-alderman-webb Feb 27, 2026
091506c
Merge branch 'webb/anthropic/separate-output-handling' into webb/anth…
alexander-alderman-webb Feb 27, 2026
d47b64d
.
alexander-alderman-webb Feb 27, 2026
384351f
remove duplicate import
alexander-alderman-webb Feb 27, 2026
55d5c27
.
alexander-alderman-webb Feb 27, 2026
fd84837
simplify
alexander-alderman-webb Feb 27, 2026
53859e4
.
alexander-alderman-webb Mar 10, 2026
e2d6d78
remove unused import
alexander-alderman-webb Mar 10, 2026
a01f7c1
add docstring
alexander-alderman-webb Mar 10, 2026
7837439
add return
alexander-alderman-webb Mar 10, 2026
0e06f49
remove return statement
alexander-alderman-webb Mar 10, 2026
c396a32
merge
alexander-alderman-webb Mar 10, 2026
d4022c5
Merge branch 'master' into webb/anthropic/only-raw-message-stream-events
alexander-alderman-webb Mar 10, 2026
4e3d1db
Merge branch 'master' into webb/anthropic/separate-output-handling
alexander-alderman-webb Mar 10, 2026
531c6a8
Merge branch 'webb/anthropic/separate-output-handling' into webb/anth…
alexander-alderman-webb Mar 10, 2026
917b8d4
.
alexander-alderman-webb Mar 11, 2026
aa6e58a
.
alexander-alderman-webb Mar 11, 2026
bd00e4e
.
alexander-alderman-webb Mar 11, 2026
a6da66e
merge
alexander-alderman-webb Mar 11, 2026
c988c26
fix type
alexander-alderman-webb Mar 11, 2026
3009ba6
Merge branch 'webb/anthropic/separate-output-handling' into webb/anth…
alexander-alderman-webb Mar 11, 2026
ab5e3bb
Merge branch 'master' into webb/anthropic/separate-output-handling
alexander-alderman-webb Mar 12, 2026
571db4e
Merge branch 'webb/anthropic/separate-output-handling' into webb/anth…
alexander-alderman-webb Mar 12, 2026
2d66d56
merge master
alexander-alderman-webb Mar 13, 2026
ad4b3c2
add docstring explaining type alises
alexander-alderman-webb Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 177 additions & 86 deletions sentry_sdk/integrations/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,29 @@

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 (
RawMessageStartEvent,
RawMessageDeltaEvent,
RawMessageStopEvent,
RawContentBlockStartEvent,
RawContentBlockDeltaEvent,
RawContentBlockStopEvent,
)
except ImportError:
message_types_have_raw_prefix = True

Check failure on line 54 in sentry_sdk/integrations/anthropic.py

View workflow job for this annotation

GitHub Actions / warden: code-review

Inverted boolean flag causes incorrect event type filtering

The `message_types_have_raw_prefix` flag is set incorrectly: it starts as `False` and is set to `True` only when the `RawMessage*Event` imports fail. This is backwards - if the `Raw*` types can be imported, the SDK has the raw prefix (flag should be `True`). If imports fail, it means there's no raw prefix (flag should be `False`). This causes the streaming event type filtering logic (lines 434-459, 499-524) to behave incorrectly, potentially skipping data collection for valid events or attempting to use `RawMessage*Event` types that weren't successfully imported.

Check failure on line 54 in sentry_sdk/integrations/anthropic.py

View workflow job for this annotation

GitHub Actions / warden: find-bugs

NameError when Raw* types fail to import but are still referenced in isinstance checks

When the `RawMessageStartEvent` and related types fail to import (older anthropic SDK versions), `message_types_have_raw_prefix` is set to `True`. However, the streaming iterator logic then checks `message_types_have_raw_prefix and not isinstance(event, (RawMessageStartEvent, ...))`, which will cause a `NameError` because `RawMessageStartEvent` is undefined when the import failed. The boolean flag semantics are inverted - it's `True` when Raw types DON'T exist, yet the code tries to use those types in that branch.

from anthropic.types import (
MessageStartEvent,
MessageDeltaEvent,
MessageStopEvent,
ContentBlockStartEvent,
ContentBlockDeltaEvent,
ContentBlockStopEvent,
)

if TYPE_CHECKING:
from anthropic.types import MessageStreamEvent, TextBlockParam
except ImportError:
Expand All @@ -49,6 +72,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
Expand Down Expand Up @@ -389,6 +415,152 @@
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:
if (
message_types_have_raw_prefix
and not isinstance(
event,
(
RawMessageStartEvent,
RawMessageDeltaEvent,
RawMessageStopEvent,
RawContentBlockStartEvent,
RawContentBlockDeltaEvent,
RawContentBlockStopEvent,
),
)
) or not isinstance(

Check failure on line 447 in sentry_sdk/integrations/anthropic.py

View workflow job for this annotation

GitHub Actions / warden: find-bugs

[BPG-XWY] NameError when Raw* types fail to import but are still referenced in isinstance checks (additional location)

When the `RawMessageStartEvent` and related types fail to import (older anthropic SDK versions), `message_types_have_raw_prefix` is set to `True`. However, the streaming iterator logic then checks `message_types_have_raw_prefix and not isinstance(event, (RawMessageStartEvent, ...))`, which will cause a `NameError` because `RawMessageStartEvent` is undefined when the import failed. The boolean flag semantics are inverted - it's `True` when Raw types DON'T exist, yet the code tries to use those types in that branch.
event,
(
MessageStartEvent,
MessageDeltaEvent,
MessageStopEvent,
ContentBlockStartEvent,
ContentBlockDeltaEvent,
ContentBlockStopEvent,
),
):

Check failure on line 457 in sentry_sdk/integrations/anthropic.py

View workflow job for this annotation

GitHub Actions / warden: code-review

[B3A-766] Inverted boolean flag causes incorrect event type filtering (additional location)

The `message_types_have_raw_prefix` flag is set incorrectly: it starts as `False` and is set to `True` only when the `RawMessage*Event` imports fail. This is backwards - if the `Raw*` types can be imported, the SDK has the raw prefix (flag should be `True`). If imports fail, it means there's no raw prefix (flag should be `False`). This causes the streaming event type filtering logic (lines 434-459, 499-524) to behave incorrectly, potentially skipping data collection for valid events or attempting to use `RawMessage*Event` types that weren't successfully imported.
yield event
continue

(
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:
if (
message_types_have_raw_prefix
and not isinstance(
event,
(
RawMessageStartEvent,
RawMessageDeltaEvent,
RawMessageStopEvent,
RawContentBlockStartEvent,
RawContentBlockDeltaEvent,
RawContentBlockStopEvent,
),
)
) or not isinstance(
event,
(
MessageStartEvent,
MessageDeltaEvent,
MessageStopEvent,
ContentBlockStartEvent,
ContentBlockDeltaEvent,
ContentBlockStopEvent,
),
):

Check failure on line 522 in sentry_sdk/integrations/anthropic.py

View workflow job for this annotation

GitHub Actions / warden: code-review

[B3A-766] Inverted boolean flag causes incorrect event type filtering (additional location)

The `message_types_have_raw_prefix` flag is set incorrectly: it starts as `False` and is set to `True` only when the `RawMessage*Event` imports fail. This is backwards - if the `Raw*` types can be imported, the SDK has the raw prefix (flag should be `True`). If imports fail, it means there's no raw prefix (flag should be `False`). This causes the streaming event type filtering logic (lines 434-459, 499-524) to behave incorrectly, potentially skipping data collection for valid events or attempting to use `RawMessage*Event` types that weren't successfully imported.

Check failure on line 522 in sentry_sdk/integrations/anthropic.py

View workflow job for this annotation

GitHub Actions / warden: find-bugs

[BPG-XWY] NameError when Raw* types fail to import but are still referenced in isinstance checks (additional location)

When the `RawMessageStartEvent` and related types fail to import (older anthropic SDK versions), `message_types_have_raw_prefix` is set to `True`. However, the streaming iterator logic then checks `message_types_have_raw_prefix and not isinstance(event, (RawMessageStartEvent, ...))`, which will cause a `NameError` because `RawMessageStartEvent` is undefined when the import failed. The boolean flag semantics are inverted - it's `True` when Raw types DON'T exist, yet the code tries to use those types in that branch.
yield event
continue

(
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)) == "<class 'async_generator'>":
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:
Expand All @@ -415,6 +587,11 @@

result = yield f, args, kwargs

is_streaming_response = kwargs.get("stream", False)
if is_streaming_response:
_patch_streaming_response_iterator(result, span, integration)
return result

with capture_internal_exceptions():
if hasattr(result, "content"):
(
Expand Down Expand Up @@ -444,92 +621,6 @@
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)) == "<class 'async_generator'>":
result._iterator = new_iterator_async()
else:
result._iterator = new_iterator()

else:
span.set_data("unknown_response", True)
span.__exit__(None, None, None)
Expand Down
Loading