Skip to content
Merged
Changes from 6 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
264 changes: 178 additions & 86 deletions sentry_sdk/integrations/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@
Omit = None

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

Check failure on line 55 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`, `RawMessageDeltaEvent`, etc. imports fail (line 54-55), `message_types_have_raw_prefix` is set to `True`, but the `Raw*` type names remain undefined. However, when `message_types_have_raw_prefix` is `True`, the code path in `new_iterator()` (lines 436-447) and `new_iterator_async()` (lines 500-512) evaluates `isinstance(event, (RawMessageStartEvent, ...))`, causing a `NameError` at runtime.

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

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

View workflow job for this annotation

GitHub Actions / warden: code-review

Import logic will cause ImportError on newer anthropic SDK versions

Lines 57-64 unconditionally import non-Raw types (MessageStartEvent, etc.) after attempting to import Raw types. According to the referenced anthropic SDK commit, newer versions of the SDK renamed these types to have the "Raw" prefix. If the Raw* imports succeed (lines 46-53), the non-Raw imports (lines 57-64) will fail with ImportError since those types no longer exist in newer SDK versions. This will cause the Sentry SDK to fail initialization with 'DidNotEnable: Anthropic not installed'.

if TYPE_CHECKING:
from anthropic.types import MessageStreamEvent, TextBlockParam
Expand All @@ -49,6 +73,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 +416,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,
),
)

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

View workflow job for this annotation

GitHub Actions / warden: find-bugs

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

When the `RawMessageStartEvent`, `RawMessageDeltaEvent`, etc. imports fail (line 54-55), `message_types_have_raw_prefix` is set to `True`, but the `Raw*` type names remain undefined. However, when `message_types_have_raw_prefix` is `True`, the code path in `new_iterator()` (lines 436-447) and `new_iterator_async()` (lines 500-512) evaluates `isinstance(event, (RawMessageStartEvent, ...))`, causing a `NameError` at runtime.
) or not isinstance(
event,
(
MessageStartEvent,
MessageDeltaEvent,
MessageStopEvent,
ContentBlockStartEvent,
ContentBlockDeltaEvent,
ContentBlockStopEvent,
),
):

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

View workflow job for this annotation

GitHub Actions / warden: code-review

[VDT-8BN] Import logic will cause ImportError on newer anthropic SDK versions (additional location)

Lines 57-64 unconditionally import non-Raw types (MessageStartEvent, etc.) after attempting to import Raw types. According to the referenced anthropic SDK commit, newer versions of the SDK renamed these types to have the "Raw" prefix. If the Raw* imports succeed (lines 46-53), the non-Raw imports (lines 57-64) will fail with ImportError since those types no longer exist in newer SDK versions. This will cause the Sentry SDK to fail initialization with 'DidNotEnable: Anthropic not installed'.
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 523 in sentry_sdk/integrations/anthropic.py

View workflow job for this annotation

GitHub Actions / warden: find-bugs

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

When the `RawMessageStartEvent`, `RawMessageDeltaEvent`, etc. imports fail (line 54-55), `message_types_have_raw_prefix` is set to `True`, but the `Raw*` type names remain undefined. However, when `message_types_have_raw_prefix` is `True`, the code path in `new_iterator()` (lines 436-447) and `new_iterator_async()` (lines 500-512) evaluates `isinstance(event, (RawMessageStartEvent, ...))`, causing a `NameError` at runtime.

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

View workflow job for this annotation

GitHub Actions / warden: code-review

[VDT-8BN] Import logic will cause ImportError on newer anthropic SDK versions (additional location)

Lines 57-64 unconditionally import non-Raw types (MessageStartEvent, etc.) after attempting to import Raw types. According to the referenced anthropic SDK commit, newer versions of the SDK renamed these types to have the "Raw" prefix. If the Raw* imports succeed (lines 46-53), the non-Raw imports (lines 57-64) will fail with ImportError since those types no longer exist in newer SDK versions. This will cause the Sentry SDK to fail initialization with 'DidNotEnable: Anthropic not installed'.
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 +588,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 +622,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