diff --git a/CHANGELOG.md b/CHANGELOG.md index e0ed1a1a61..be7325f9eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,58 @@ Want to give it a spin? Be our guest and [let us know](https://github.com/getsen - [2.x to 3.x migration guide](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x) - [detailed changelog](https://github.com/getsentry/sentry-python/blob/major/3.0/MIGRATION_GUIDE.md) +## 2.74.0 + +### New Features ✨ + +#### Anthropic + +- Record reasoning parts in streaming output by @alexander-alderman-webb in [#8193](https://github.com/getsentry/sentry-python/pull/8193) +- Record reasoning parts in nonstreaming output by @alexander-alderman-webb in [#8192](https://github.com/getsentry/sentry-python/pull/8192) + +#### Other + +- (typesafe) Handle inline images by @alexander-alderman-webb in [#8164](https://github.com/getsentry/sentry-python/pull/8164) + +### Bug Fixes 🐛 + +#### Anthropic + +- Preserve cached `_iterator` property by @alexander-alderman-webb in [#8163](https://github.com/getsentry/sentry-python/pull/8163) +- Stop unpacking output arrays by @alexander-alderman-webb in [#8160](https://github.com/getsentry/sentry-python/pull/8160) + +#### Other + +- (aiohttp) Leave existing `baggage` header alone if it has existin… by @pabloDeputter in [#8116](https://github.com/getsentry/sentry-python/pull/8116) +- (boto3) Adapt to botocore no longer signing baggage by @sentrivana in [#8113](https://github.com/getsentry/sentry-python/pull/8113) +- (ci) Restrict pytest because of incompatibilities by @sentrivana in [#8140](https://github.com/getsentry/sentry-python/pull/8140) +- (falcon) Handle integer HTTP statuses by @jap and @sentrivana in [#8139](https://github.com/getsentry/sentry-python/pull/8139) +- (sampling) Respect incoming sample rate if not explicitly overridden by @sentrivana in [#8185](https://github.com/getsentry/sentry-python/pull/8185) +- (tracing) Drop trailing comma when serializing baggage without third-party items by @alphasafal and @pabloDeputter in [#8119](https://github.com/getsentry/sentry-python/pull/8119) +- (wsgi) Stop overwriting `server.address` by @alexander-alderman-webb in [#8171](https://github.com/getsentry/sentry-python/pull/8171) + +### Internal Changes 🔧 + +#### Deps + +- Lock file maintenance by @renovate in [#8055](https://github.com/getsentry/sentry-python/pull/8055) +- Bump grpcio to 1.84.1 by @geoffg-sentry with @sentry-junior in [#8136](https://github.com/getsentry/sentry-python/pull/8136) +- Bump uv to 0.13.5 by @geoffg-sentry with @sentry-junior in [#8135](https://github.com/getsentry/sentry-python/pull/8135) +- Bump brotli to 1.2.1 by @geoffg-sentry with @sentry-junior in [#8122](https://github.com/getsentry/sentry-python/pull/8122) + +#### Other + +- (mcp) Remove unused select_mcp_transactions fixture by @sentrivana with @sentry-junior in [#8150](https://github.com/getsentry/sentry-python/pull/8150) +- (release) Bump craft to 3.9.0 and update config by @BYK with @sentry-junior in [#8181](https://github.com/getsentry/sentry-python/pull/8181) +- (sessions) Remove unused TERMINAL_SESSION_STATES constant by @sentrivana with @sentry-junior in [#8151](https://github.com/getsentry/sentry-python/pull/8151) +- (starlette) Remove unused \_serialize_request_body_data helper by @sentry-junior in [#8146](https://github.com/getsentry/sentry-python/pull/8146) +- (toxgen) Remove unused `get_file_hash()` and `get_last_updated()` helpers by @sentry-junior in [#8173](https://github.com/getsentry/sentry-python/pull/8173) +- (tracing) Remove unused base64_stripped regex by @sentry-junior in [#8148](https://github.com/getsentry/sentry-python/pull/8148) +- (utils) Remove unused to_timestamp helper and epoch constant by @sentry-junior in [#8147](https://github.com/getsentry/sentry-python/pull/8147) +- 🤖 Update test matrix with new releases (11/02) by @sentrivana with @github-actions in [#8172](https://github.com/getsentry/sentry-python/pull/8172) +- Upgrade `sphinx` and remove `packaging` upper bound by @alexander-alderman-webb in [#8183](https://github.com/getsentry/sentry-python/pull/8183) +- Update test matrix with new releases (27/10) by @alexander-alderman-webb in [#8126](https://github.com/getsentry/sentry-python/pull/8126) + ## 2.73.0 ### New Features ✨ diff --git a/sentry_sdk/crons/decorator.py b/sentry_sdk/crons/decorator.py index b13d350e15..fd4677709d 100644 --- a/sentry_sdk/crons/decorator.py +++ b/sentry_sdk/crons/decorator.py @@ -126,6 +126,7 @@ async def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": with self: return await fn(*args, **kwargs) + inner._sentry_monitored = True # type: ignore[attr-defined] return inner def _sync_wrapper(self, fn: "Callable[P, R]") -> "Callable[P, R]": @@ -134,4 +135,5 @@ def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R": with self: return fn(*args, **kwargs) + inner._sentry_monitored = True # type: ignore[attr-defined] return inner diff --git a/sentry_sdk/crons/utils.py b/sentry_sdk/crons/utils.py index d5fa55e500..01a70c1c1b 100644 --- a/sentry_sdk/crons/utils.py +++ b/sentry_sdk/crons/utils.py @@ -1,6 +1,9 @@ +import re +from datetime import timedelta, timezone from typing import TYPE_CHECKING if TYPE_CHECKING: + from datetime import tzinfo from typing import Optional, Tuple from sentry_sdk._types import MonitorConfigSchedule, MonitorConfigScheduleUnit @@ -27,3 +30,26 @@ def _get_interval_schedule(seconds: float) -> "Optional[MonitorConfigSchedule]": "unit": unit, } return None + + +# IANA zone names start with an uppercase letter. This rejects names like +# "local" or "localtime" that tzlocal can give a zone. +_IANA_TIMEZONE_NAME = re.compile(r"[A-Z][A-Za-z0-9._+-]*(?:/[A-Za-z0-9._+-]+)*") + + +def _get_timezone_name(tz: "tzinfo") -> "Optional[str]": + """ + Return the IANA name of the timezone, or None if it has none. + """ + name = getattr(tz, "key", None) or getattr(tz, "zone", None) + if isinstance(name, str): + return name if _IANA_TIMEZONE_NAME.fullmatch(name) else None + + if isinstance(tz, timezone): + offset = tz.utcoffset(None) + if offset % timedelta(hours=1): + return None + hours = offset // timedelta(hours=1) + return "Etc/GMT%+d" % -hours if hours else "UTC" + + return None diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 8d119e2380..9f6aa97e93 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -56,6 +56,12 @@ except ImportError: raise DidNotEnable("Anthropic not installed or incompatible") +try: + from anthropic.types import ThinkingBlock, ThinkingDelta +except ImportError: + ThinkingBlock = None # type: ignore[assignment,misc] + ThinkingDelta = None # type: ignore[assignment,misc] + if TYPE_CHECKING: from typing import ( Any, @@ -77,7 +83,7 @@ ToolUnionParam, ) - from sentry_sdk._types import TextPart + from sentry_sdk._types import ReasoningPart, TextPart class _PatchedRawMessageStream(Stream[Any]): _span: Span @@ -85,7 +91,7 @@ class _PatchedRawMessageStream(Stream[Any]): _model: Optional[ModelParam] _usage: "_RecordedUsage" - _content_blocks: list[Any] + _content_blocks: "list[dict[str, Any]]" _response_id: Optional[str] _finish_reason: Optional[str] @@ -95,7 +101,7 @@ class _PatchedMessageStream(MessageStream): _model: Optional[ModelParam] _usage: "_RecordedUsage" - _content_blocks: list[Any] + _content_blocks: "list[dict[str, Any]]" _response_id: Optional[str] _finish_reason: Optional[str] @@ -105,7 +111,7 @@ class _PatchedRawAsyncMessageStream(AsyncStream[Any]): _model: Optional[ModelParam] _usage: "_RecordedUsage" - _content_blocks: list[Any] + _content_blocks: "list[dict[str, Any]]" _response_id: Optional[str] _finish_reason: Optional[str] @@ -115,7 +121,7 @@ class _PatchedAsyncMessageStream(AsyncMessageStream): _model: Optional[ModelParam] _usage: "_RecordedUsage" - _content_blocks: list[Any] + _content_blocks: "list[dict[str, Any]]" _response_id: Optional[str] _finish_reason: Optional[str] @@ -353,22 +359,32 @@ def _collect_ai_data( event: "MessageStreamEvent", model: "str | None", usage: "_RecordedUsage", - content_blocks: "list[str]", + content_blocks: "list[dict[str, Any]]", response_id: "str | None" = None, finish_reason: "str | None" = None, -) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]": +) -> "tuple[str | None, _RecordedUsage, list[dict[str, Any]], str | None, str | None]": """ Collect model information, token usage, and collect content blocks from the AI streaming response. """ with capture_internal_exceptions(): if hasattr(event, "type"): if event.type == "content_block_start": - pass + if ThinkingBlock is not None and isinstance( + event.content_block, ThinkingBlock + ): + content_blocks.append({"type": "thinking", "thinking": ""}) + elif event.content_block.type in ("text", "tool_use"): + content_blocks.append({"type": "text", "text": ""}) + else: + content_blocks.append({"type": event.content_block.type}) elif event.type == "content_block_delta": - if hasattr(event.delta, "text"): - content_blocks.append(event.delta.text) + block = content_blocks[event.index] + if ThinkingDelta is not None and isinstance(event.delta, ThinkingDelta): + block["thinking"] += event.delta.thinking + elif hasattr(event.delta, "text"): + block["text"] += event.delta.text elif hasattr(event.delta, "partial_json"): - content_blocks.append(event.delta.partial_json) + block["text"] += event.delta.partial_json elif event.type == "content_block_stop": pass @@ -664,7 +680,7 @@ def _set_output_data( output_tokens: "int | None", cache_read_input_tokens: "int | None", cache_write_input_tokens: "int | None", - content_blocks: "list[Any]", + content_blocks: "list[dict[str, Any]]", response_id: "str | None" = None, finish_reason: "str | None" = None, ) -> None: @@ -678,30 +694,30 @@ def _set_output_data( span.set_attribute(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason]) if sentry_sdk.get_client().options["data_collection"]["gen_ai"]["outputs"]: - output_messages: "dict[str, list[Any]]" = { - "response": [], - "tool": [], - } + parts: "list[Union[TextPart, ReasoningPart]]" = [] + tool_calls = [] for output in content_blocks: if output["type"] == "text": - output_messages["response"].append(output["text"]) + parts.append({"type": "text", "content": output["text"]}) + elif output["type"] == "thinking": + parts.append({"type": "reasoning", "content": output["thinking"]}) elif output["type"] == "tool_use": - output_messages["tool"].append(output) + tool_calls.append(output) - if len(output_messages["tool"]) > 0: + if len(tool_calls) > 0: set_data_normalized( span, SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, - output_messages["tool"], + tool_calls, unpack=False, ) - if len(output_messages["response"]) > 0: + if len(parts) > 0: set_data_normalized( span, - SPANDATA.GEN_AI_RESPONSE_TEXT, - output_messages["response"], + SPANDATA.GEN_AI_OUTPUT_MESSAGES, + [{"role": "assistant", "parts": parts}], unpack=False, ) @@ -977,7 +993,7 @@ def _set_streaming_output_data( integration: "AnthropicIntegration", model: "Optional[str]", usage: "_RecordedUsage", - content_blocks: "list[str]", + content_blocks: "list[dict[str, Any]]", response_id: "Optional[str]", finish_reason: "Optional[str]", ) -> None: @@ -1000,7 +1016,7 @@ def _set_streaming_output_data( 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"}], + content_blocks=content_blocks, response_id=response_id, finish_reason=finish_reason, ) diff --git a/sentry_sdk/integrations/arq.py b/sentry_sdk/integrations/arq.py index 6e806fa8fa..6901e9136c 100644 --- a/sentry_sdk/integrations/arq.py +++ b/sentry_sdk/integrations/arq.py @@ -1,10 +1,11 @@ import sys import time -from datetime import timedelta, timezone +from functools import wraps import sentry_sdk from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.crons import monitor +from sentry_sdk.crons.utils import _get_timezone_name from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version from sentry_sdk.integrations.logging import ignore_logger_for_events from sentry_sdk.traces import SegmentNameSource @@ -174,6 +175,7 @@ def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": def _wrap_coroutine(name: str, coroutine: "WorkerCoroutine") -> "WorkerCoroutine": + @wraps(coroutine) async def _sentry_coroutine( ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any" ) -> "Any": @@ -278,6 +280,9 @@ def _monitor_cron_job( return cron_job._sentry_monitored = True + if getattr(cron_job.coroutine, "_sentry_monitored", False): + return + # arq names cron jobs "cron:" by default. monitor_slug = cron_job.name if monitor_slug.startswith("cron:"): @@ -345,7 +350,7 @@ def _get_crontab(cron_job: "CronJob") -> "Optional[str]": return " ".join(fields) # type: ignore[arg-type] -def _get_timezone_name(worker: "Worker") -> "Optional[str]": +def _get_worker_timezone_name(worker: "Worker") -> "Optional[str]": tz = getattr(worker, "timezone", None) if tz is None: # Before arq 0.25, cron jobs ran in naive local time. @@ -353,27 +358,15 @@ def _get_timezone_name(worker: "Worker") -> "Optional[str]": return "UTC" return None - # zoneinfo and pytz - name = getattr(tz, "key", None) or getattr(tz, "zone", None) - if isinstance(name, str): - return name - # arq defaults to the system's fixed UTC offset at startup. - if isinstance(tz, timezone): - offset = tz.utcoffset(None) - if offset % timedelta(hours=1): - return None - hours = offset // timedelta(hours=1) - return "Etc/GMT%+d" % -hours if hours else "UTC" - - return None + return _get_timezone_name(tz) def _get_monitor_config( cron_job: "CronJob", worker: "Worker" ) -> "Optional[MonitorConfig]": crontab = _get_crontab(cron_job) - timezone_name = _get_timezone_name(worker) + timezone_name = _get_worker_timezone_name(worker) if crontab is None or timezone_name is None: return None return { diff --git a/sentry_sdk/profiler/continuous_profiler.py b/sentry_sdk/profiler/continuous_profiler.py index 2e82289e72..0d58f65c92 100644 --- a/sentry_sdk/profiler/continuous_profiler.py +++ b/sentry_sdk/profiler/continuous_profiler.py @@ -265,9 +265,6 @@ def ensure_running(self) -> None: def teardown(self) -> None: raise NotImplementedError - def pause(self) -> None: - raise NotImplementedError - def reset_buffer(self) -> None: self.buffer = ProfileBuffer( self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index dabd931aa6..de2c5ab98b 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1012,7 +1012,12 @@ def start_span( self, ) - if sample_rate is not None: + keep_incoming_sample_rate = ( + propagation_context.parent_sampled is not None + and not callable(self.get_client().options.get("traces_sampler")) + ) + + if sample_rate is not None and not keep_incoming_sample_rate: self._update_sample_rate(sample_rate) if sampled is False or sampled is None: diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index b1a3c9b9e7..2c71cfddaa 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -57,7 +57,7 @@ def __str__(self) -> str: # The source of a segment name, see -# https://getsentry.github.io/sentry-conventions/attributes/sentry/#sentry-segment-name-source +# https://conventions.sentry.dev/attributes/sentry/#sentry-segment-name-source class SegmentNameSource(str, Enum): COMPONENT = "component" CUSTOM = "custom" diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 5287d697dc..30bee4fe4d 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -28,6 +28,11 @@ # Python 3.10 and below BaseExceptionGroup = None # type: ignore +try: + from importlib import metadata as importlib_metadata +except ImportError: + importlib_metadata = None # type: ignore[assignment] + from typing import TYPE_CHECKING import sentry_sdk @@ -1626,6 +1631,14 @@ def package_version(package: str) -> "Optional[Tuple[int, ...]]": _replace_hyphens_dots_and_underscores_with_dashes(package) ) + if importlib_metadata is not None: + # Look up the single distribution instead of scanning (and parsing + # the metadata of) every installed package on each call. + try: + return parse_version(importlib_metadata.version(normalized_package)) + except importlib_metadata.PackageNotFoundError: + return None + installed_packages = { _replace_hyphens_dots_and_underscores_with_dashes(module): v for module, v in _get_installed_modules().items() diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index a3e271e61e..a6167afa90 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -23,6 +23,13 @@ async def __call__(self, *args, **kwargs): from anthropic.types.message_delta_event import MessageDeltaEvent from anthropic.types.message_start_event import MessageStartEvent +try: + from anthropic.types import ThinkingBlock, ThinkingDelta +except ImportError: + ThinkingBlock = None + ThinkingDelta = None + + try: from anthropic import APIStatusError from anthropic.types import ErrorResponse, OverloadedError @@ -75,7 +82,18 @@ async def __call__(self, *args, **kwargs): id="msg_01XFDUDYJgAACzvnptvVoYEL", model="model", role="assistant", - content=[TextBlock(type="text", text="Hi, I'm Claude.")], + content=( + [ + ThinkingBlock( + type="thinking", + thinking="Let me reason step by step...", + signature="sig123", + ), + TextBlock(type="text", text="Hi, I'm Claude."), + ] + if ThinkingBlock is not None + else [TextBlock(type="text", text="Hi, I'm Claude.")] + ), type="message", stop_reason="end_turn", usage=Usage(input_tokens=10, output_tokens=20), @@ -135,6 +153,151 @@ def data_collection_tool_use_message(): ) +@pytest.fixture +def streaming_anthropic_response(): + def inner(text_deltas, stop_reason): + thinking_events = [] + if ThinkingBlock is not None: + thinking_events = [ + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=ThinkingBlock( + type="thinking", + thinking="", + signature="sig123", + ), + ), + ContentBlockDeltaEvent( + delta=ThinkingDelta( + thinking="Let me reason step by step...", + type="thinking_delta", + ), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + ] + text_index = 1 if ThinkingBlock is not None else 0 + return [ + MessageStartEvent( + message=Message( + id="msg_01XFDUDYJgAACzvnptvVoYEL", + model="model", + role="assistant", + content=[TextBlock(type="text", text="")], + type="message", + stop_reason="end_turn", + usage=Usage(input_tokens=10, output_tokens=20), + ), + type="message_start", + ), + *thinking_events, + ContentBlockStartEvent( + type="content_block_start", + index=text_index, + content_block=TextBlock(type="text", text=""), + ), + *[ + ContentBlockDeltaEvent( + delta=TextDelta(text=text, type="text_delta"), + index=text_index, + type="content_block_delta", + ) + for text in text_deltas + ], + ContentBlockStopEvent(type="content_block_stop", index=text_index), + MessageDeltaEvent( + delta=Delta(stop_reason=stop_reason), + usage=MessageDeltaUsage(output_tokens=10), + type="message_delta", + ), + ] + + return inner + + +@pytest.fixture +def streaming_anthropic_text_response(): + return [ + MessageStartEvent( + message=Message( + id="msg_01XFDUDYJgAACzvnptvVoYEL", + model="model", + role="assistant", + content=[TextBlock(type="text", text="")], + type="message", + stop_reason="end_turn", + usage=Usage(input_tokens=10, output_tokens=20), + ), + 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(stop_reason="max_tokens"), + usage=MessageDeltaUsage(output_tokens=10), + type="message_delta", + ), + ] + + +@pytest.fixture +def streaming_anthropic_error_response(): + return [ + MessageStartEvent( + message=Message( + id="msg_01XFDUDYJgAACzvnptvVoYEL", + model="model", + role="assistant", + content=[TextBlock(type="text", text="")], + type="message", + stop_reason="end_turn", + usage=Usage(input_tokens=10, output_tokens=20), + ), + 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", + ), + ErrorResponse( + type="error", + error=OverloadedError(message="Overloaded", type="overloaded_error"), + ), + ] + + def test_nonstreaming_create_message( sentry_init, capture_items, @@ -196,9 +359,20 @@ def test_nonstreaming_create_message( "content": "Hello, Claude", }, ] - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ["Hi, I'm Claude."] - ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi, I'm Claude."}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi, I'm Claude."}] + ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": expected_parts} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -263,7 +437,7 @@ def test_nonstreaming_create_message_no_sensitive_data( assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -340,9 +514,20 @@ async def test_nonstreaming_create_message_async( "content": "Hello, Claude", }, ] - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ["Hi, I'm Claude."] - ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi, I'm Claude."}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi, I'm Claude."}] + ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": expected_parts} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -409,7 +594,7 @@ async def test_nonstreaming_create_message_async_no_sensitive_data( assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -426,43 +611,20 @@ def test_streaming_create_message( capture_items, get_model_response, server_side_event_chunks, + streaming_anthropic_response, ): client = Anthropic(api_key="z") 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=""), + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", ), - 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(stop_reason="max_tokens"), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", - ), - ] + stop_reason="max_tokens", + ) ) ) @@ -524,9 +686,20 @@ def test_streaming_create_message( "content": "Hello, Claude", }, ] - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ["Hi! I'm Claude!"] - ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": expected_parts} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -544,43 +717,20 @@ def test_streaming_create_message_no_sensitive_data( capture_items, get_model_response, server_side_event_chunks, + streaming_anthropic_response, ): client = Anthropic(api_key="z") 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(stop_reason="max_tokens"), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", ), - ] + stop_reason="max_tokens", + ) ) ) @@ -633,7 +783,7 @@ def test_streaming_create_message_no_sensitive_data( assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -651,44 +801,12 @@ def test_streaming_create_message_close( capture_items, get_model_response, server_side_event_chunks, + streaming_anthropic_text_response, ): client = Anthropic(api_key="z") 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(stop_reason="max_tokens"), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", - ), - ] - ) + server_side_event_chunks(streaming_anthropic_text_response) ) sentry_init( @@ -741,7 +859,9 @@ def test_streaming_create_message_close( span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps(["Hi!"]) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": [{"type": "text", "content": "Hi!"}]} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -762,39 +882,12 @@ def test_streaming_create_message_api_error( capture_items, get_model_response, server_side_event_chunks, + streaming_anthropic_error_response, ): client = Anthropic(api_key="z") 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", - ), - ErrorResponse( - type="error", - error=OverloadedError( - message="Overloaded", type="overloaded_error" - ), - ), - ] - ) + server_side_event_chunks(streaming_anthropic_error_response) ) sentry_init( @@ -845,7 +938,9 @@ def test_streaming_create_message_api_error( span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps(["Hi!"]) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": [{"type": "text", "content": "Hi!"}]} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -864,43 +959,20 @@ def test_stream_messages( capture_items, get_model_response, server_side_event_chunks, + streaming_anthropic_response, ): client = Anthropic(api_key="z") 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", + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", ), - 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(stop_reason="max_tokens"), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", - ), - ] + stop_reason="max_tokens", + ) ) ) @@ -962,9 +1034,20 @@ def test_stream_messages( "content": "Hello, Claude", }, ] - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ["Hi! I'm Claude!"] - ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": expected_parts} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -982,43 +1065,20 @@ def test_stream_messages_no_sensitive_data( capture_items, get_model_response, server_side_event_chunks, + streaming_anthropic_response, ): client = Anthropic(api_key="z") 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(stop_reason="max_tokens"), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", ), - ] + stop_reason="max_tokens", + ) ) ) @@ -1071,7 +1131,7 @@ def test_stream_messages_no_sensitive_data( assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -1089,44 +1149,12 @@ def test_stream_messages_close( capture_items, get_model_response, server_side_event_chunks, + streaming_anthropic_text_response, ): client = Anthropic(api_key="z") 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(stop_reason="max_tokens"), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", - ), - ] - ) + server_side_event_chunks(streaming_anthropic_text_response) ) sentry_init( @@ -1183,7 +1211,9 @@ def test_stream_messages_close( span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps(["Hi!"]) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": [{"type": "text", "content": "Hi!"}]} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -1204,39 +1234,12 @@ def test_stream_messages_api_error( capture_items, get_model_response, server_side_event_chunks, + streaming_anthropic_error_response, ): client = Anthropic(api_key="z") 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", - ), - ErrorResponse( - type="error", - error=OverloadedError( - message="Overloaded", type="overloaded_error" - ), - ), - ] - ) + server_side_event_chunks(streaming_anthropic_error_response) ) sentry_init( @@ -1287,7 +1290,9 @@ def test_stream_messages_api_error( span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps(["Hi!"]) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": [{"type": "text", "content": "Hi!"}]} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -1308,44 +1313,21 @@ async def test_streaming_create_message_async( get_model_response, async_iterator, server_side_event_chunks, + streaming_anthropic_response, ): client = AsyncAnthropic(api_key="z") response = get_model_response( async_iterator( 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(stop_reason="max_tokens"), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", - ), - ] + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", + ), + stop_reason="max_tokens", + ) ) ), ) @@ -1409,9 +1391,20 @@ async def test_streaming_create_message_async( "content": "Hello, Claude", }, ] - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ["Hi! I'm Claude!"] - ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": expected_parts} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -1431,44 +1424,21 @@ async def test_streaming_create_message_async_no_sensitive_data( get_model_response, async_iterator, server_side_event_chunks, + streaming_anthropic_response, ): client = AsyncAnthropic(api_key="z") response = get_model_response( async_iterator( 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(stop_reason="max_tokens"), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", - ), - ] + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", + ), + stop_reason="max_tokens", + ) ) ), ) @@ -1523,7 +1493,7 @@ async def test_streaming_create_message_async_no_sensitive_data( assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -1543,46 +1513,12 @@ async def test_streaming_create_message_async_close( get_model_response, async_iterator, server_side_event_chunks, + streaming_anthropic_text_response, ): client = AsyncAnthropic(api_key="z") response = get_model_response( - async_iterator( - 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(stop_reason="max_tokens"), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", - ), - ] - ) - ) + async_iterator(server_side_event_chunks(streaming_anthropic_text_response)) ) sentry_init( @@ -1634,7 +1570,9 @@ async def test_streaming_create_message_async_close( span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps(["Hi!"]) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": [{"type": "text", "content": "Hi!"}]} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -1657,41 +1595,12 @@ async def test_streaming_create_message_async_api_error( get_model_response, async_iterator, server_side_event_chunks, + streaming_anthropic_error_response, ): client = AsyncAnthropic(api_key="z") response = get_model_response( - async_iterator( - 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", - ), - ErrorResponse( - type="error", - error=OverloadedError( - message="Overloaded", type="overloaded_error" - ), - ), - ] - ) - ) + async_iterator(server_side_event_chunks(streaming_anthropic_error_response)) ) sentry_init( @@ -1742,7 +1651,9 @@ async def test_streaming_create_message_async_api_error( span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps(["Hi!"]) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": [{"type": "text", "content": "Hi!"}]} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -1763,44 +1674,21 @@ async def test_stream_message_async( get_model_response, async_iterator, server_side_event_chunks, + streaming_anthropic_response, ): client = AsyncAnthropic(api_key="z") response = get_model_response( async_iterator( 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", - ), - ] + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", + ), + stop_reason=None, + ) ) ), ) @@ -1864,9 +1752,20 @@ async def test_stream_message_async( "content": "Hello, Claude", }, ] - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ["Hi! I'm Claude!"] - ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": expected_parts} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -1885,44 +1784,21 @@ async def test_stream_message_async_no_sensitive_data( get_model_response, async_iterator, server_side_event_chunks, + streaming_anthropic_response, ): client = AsyncAnthropic(api_key="z") response = get_model_response( async_iterator( 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", - ), - ] + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", + ), + stop_reason=None, + ) ) ), ) @@ -1977,7 +1853,7 @@ async def test_stream_message_async_no_sensitive_data( assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -2000,41 +1876,12 @@ async def test_stream_messages_async_api_error( get_model_response, async_iterator, server_side_event_chunks, + streaming_anthropic_error_response, ): client = AsyncAnthropic(api_key="z") response = get_model_response( - async_iterator( - 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", - ), - ErrorResponse( - type="error", - error=OverloadedError( - message="Overloaded", type="overloaded_error" - ), - ), - ] - ) - ) + async_iterator(server_side_event_chunks(streaming_anthropic_error_response)) ) sentry_init( @@ -2086,7 +1933,9 @@ async def test_stream_messages_async_api_error( span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps(["Hi!"]) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": [{"type": "text", "content": "Hi!"}]} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -2097,58 +1946,24 @@ async def test_stream_messages_async_api_error( == "msg_01XFDUDYJgAACzvnptvVoYEL" ) - assert span["status"] == "error" - - -@pytest.mark.asyncio -async def test_stream_messages_async_close( - sentry_init, - capture_items, - get_model_response, - async_iterator, - server_side_event_chunks, -): - client = AsyncAnthropic(api_key="z") - - response = get_model_response( - async_iterator( - 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(stop_reason="max_tokens"), - usage=MessageDeltaUsage(output_tokens=10), - type="message_delta", - ), - ] - ) - ) - ) - + assert span["status"] == "error" + + +@pytest.mark.asyncio +async def test_stream_messages_async_close( + sentry_init, + capture_items, + get_model_response, + async_iterator, + server_side_event_chunks, + streaming_anthropic_text_response, +): + client = AsyncAnthropic(api_key="z") + + response = get_model_response( + async_iterator(server_side_event_chunks(streaming_anthropic_text_response)) + ) + sentry_init( integrations=[AnthropicIntegration()], disabled_integrations=[StdlibIntegration], @@ -2206,7 +2021,9 @@ async def test_stream_messages_async_close( span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps(["Hi!"]) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": [{"type": "text", "content": "Hi!"}]} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -2345,9 +2162,12 @@ def test_streaming_create_message_with_input_json_delta( span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "What is the weather like in San Francisco?"}]' ) - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ['{"location": "San Francisco, CA"}'] - ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + { + "role": "assistant", + "parts": [{"type": "text", "content": '{"location": "San Francisco, CA"}'}], + } + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41 @@ -2479,7 +2299,7 @@ def test_streaming_create_message_with_input_json_delta_no_sensitive_data( assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41 @@ -2614,9 +2434,12 @@ def test_stream_messages_with_input_json_delta( span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "What is the weather like in San Francisco?"}]' ) - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ['{"location": "San Francisco, CA"}'] - ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + { + "role": "assistant", + "parts": [{"type": "text", "content": '{"location": "San Francisco, CA"}'}], + } + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41 @@ -2748,7 +2571,7 @@ def test_stream_messages_with_input_json_delta_no_sensitive_data( assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41 @@ -2890,9 +2713,12 @@ async def test_streaming_create_message_with_input_json_delta_async( span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "What is the weather like in San Francisco?"}]' ) - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ['{"location": "San Francisco, CA"}'] - ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + { + "role": "assistant", + "parts": [{"type": "text", "content": '{"location": "San Francisco, CA"}'}], + } + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41 @@ -3031,7 +2857,7 @@ async def test_streaming_create_message_with_input_json_delta_async_no_sensitive assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41 @@ -3174,9 +3000,12 @@ async def test_stream_message_with_input_json_delta_async( span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "What is the weather like in San Francisco?"}]' ) - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ['{"location": "San Francisco, CA"}'] - ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + { + "role": "assistant", + "parts": [{"type": "text", "content": '{"location": "San Francisco, CA"}'}], + } + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41 @@ -3316,7 +3145,7 @@ async def test_stream_message_with_input_json_delta_async_no_sensitive_data( assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model" assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41 @@ -3510,7 +3339,14 @@ async def test_span_origin_async( reason="Versions <0.27.0 do not include InputJSONDelta.", ) def test_collect_ai_data_with_input_json_delta(): - event = ContentBlockDeltaEvent( + start_event = ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=ToolUseBlock( + type="tool_use", id="toolu_123", name="get_weather", input={} + ), + ) + delta_event = ContentBlockDeltaEvent( delta=InputJSONDelta(partial_json="test", type="input_json_delta"), index=0, type="content_block_delta", @@ -3521,15 +3357,16 @@ def test_collect_ai_data_with_input_json_delta(): usage.output_tokens = 20 usage.input_tokens = 10 - content_blocks = [] + content_blocks: list = [] + _collect_ai_data(start_event, model, usage, content_blocks) model, new_usage, new_content_blocks, response_id, finish_reason = _collect_ai_data( - event, model, usage, content_blocks + delta_event, model, usage, content_blocks ) assert model is None assert new_usage.input_tokens == usage.input_tokens assert new_usage.output_tokens == usage.output_tokens - assert new_content_blocks == ["test"] + assert new_content_blocks == [{"type": "text", "text": "test"}] assert response_id is None assert finish_reason is None @@ -3565,9 +3402,12 @@ def test_set_output_data_with_input_json_delta(sentry_init): content_blocks=[{"text": "".join(json_deltas), "type": "text"}], ) - assert span._attributes.get(SPANDATA.GEN_AI_RESPONSE_TEXT) == json.dumps( - ["{'test': 'data','more': 'json'}"] - ) + assert json.loads(span._attributes.get(SPANDATA.GEN_AI_OUTPUT_MESSAGES)) == [ + { + "role": "assistant", + "parts": [{"type": "text", "content": "{'test': 'data','more': 'json'}"}], + } + ] assert span._attributes.get(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS) == 10 assert span._attributes.get(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS) == 20 assert span._attributes.get(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS) == 30 @@ -3710,9 +3550,20 @@ def test_nonstreaming_create_message_with_system_prompt( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ["Hi, I'm Claude."] - ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi, I'm Claude."}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi, I'm Claude."}] + ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": expected_parts} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -3776,7 +3627,7 @@ def test_nonstreaming_create_message_with_system_prompt_no_sensitive_data( assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -3852,9 +3703,20 @@ async def test_nonstreaming_create_message_with_system_prompt_async( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ["Hi, I'm Claude."] - ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi, I'm Claude."}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi, I'm Claude."}] + ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": expected_parts} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -3919,7 +3781,7 @@ async def test_nonstreaming_create_message_with_system_prompt_async_no_sensitive assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20 @@ -3933,44 +3795,21 @@ def test_streaming_create_message_with_system_prompt( capture_items, get_model_response, server_side_event_chunks, + streaming_anthropic_response, ): """Test that system prompts are properly captured in streaming mode.""" client = Anthropic(api_key="z") 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", + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", ), - ] + stop_reason=None, + ) ) ) @@ -4035,9 +3874,20 @@ def test_streaming_create_message_with_system_prompt( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ["Hi! I'm Claude!"] - ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": expected_parts} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -4050,44 +3900,21 @@ def test_streaming_create_message_with_system_prompt_no_sensitive_data( capture_items, get_model_response, server_side_event_chunks, + streaming_anthropic_response, ): """Test that system prompts are properly captured in streaming mode.""" client = Anthropic(api_key="z") 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", + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", ), - ] + stop_reason=None, + ) ) ) @@ -4141,7 +3968,7 @@ def test_streaming_create_message_with_system_prompt_no_sensitive_data( assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -4154,44 +3981,21 @@ def test_stream_messages_with_system_prompt( capture_items, get_model_response, server_side_event_chunks, + streaming_anthropic_response, ): """Test that system prompts are properly captured in streaming mode.""" client = Anthropic(api_key="z") 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", + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", ), - ] + stop_reason=None, + ) ) ) @@ -4252,9 +4056,20 @@ def test_stream_messages_with_system_prompt( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ["Hi! I'm Claude!"] - ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": expected_parts} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -4267,44 +4082,21 @@ def test_stream_messages_with_system_prompt_no_sensitive_data( capture_items, get_model_response, server_side_event_chunks, + streaming_anthropic_response, ): """Test that system prompts are properly captured in streaming mode.""" client = Anthropic(api_key="z") 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", + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", ), - ] + stop_reason=None, + ) ) ) @@ -4355,7 +4147,7 @@ def test_stream_messages_with_system_prompt_no_sensitive_data( assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -4370,6 +4162,7 @@ async def test_stream_message_with_system_prompt_async( get_model_response, async_iterator, server_side_event_chunks, + streaming_anthropic_response, ): """Test that system prompts are properly captured in streaming mode (async).""" client = AsyncAnthropic(api_key="z") @@ -4377,38 +4170,14 @@ async def test_stream_message_with_system_prompt_async( response = get_model_response( async_iterator( 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", - ), - ] + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", + ), + stop_reason=None, + ) ) ) ) @@ -4472,9 +4241,20 @@ async def test_stream_message_with_system_prompt_async( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ["Hi! I'm Claude!"] - ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": expected_parts} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -4489,6 +4269,7 @@ async def test_stream_message_with_system_prompt_async_no_sensitive_data( get_model_response, async_iterator, server_side_event_chunks, + streaming_anthropic_response, ): """Test that system prompts are properly captured in streaming mode (async).""" client = AsyncAnthropic(api_key="z") @@ -4496,38 +4277,14 @@ async def test_stream_message_with_system_prompt_async_no_sensitive_data( response = get_model_response( async_iterator( 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", - ), - ] + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", + ), + stop_reason=None, + ) ) ) ) @@ -4580,7 +4337,7 @@ async def test_stream_message_with_system_prompt_async_no_sensitive_data( assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -4595,6 +4352,7 @@ async def test_streaming_create_message_with_system_prompt_async( get_model_response, async_iterator, server_side_event_chunks, + streaming_anthropic_response, ): """Test that system prompts are properly captured in streaming mode (async).""" client = AsyncAnthropic(api_key="z") @@ -4602,38 +4360,14 @@ async def test_streaming_create_message_with_system_prompt_async( response = get_model_response( async_iterator( 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", - ), - ] + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", + ), + stop_reason=None, + ) ) ) ) @@ -4700,9 +4434,20 @@ async def test_streaming_create_message_with_system_prompt_async( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" - assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == json.dumps( - ["Hi! I'm Claude!"] - ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) + assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ + {"role": "assistant", "parts": expected_parts} + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 @@ -4717,6 +4462,7 @@ async def test_streaming_create_message_with_system_prompt_async_no_sensitive_da get_model_response, async_iterator, server_side_event_chunks, + streaming_anthropic_response, ): """Test that system prompts are properly captured in streaming mode (async).""" client = AsyncAnthropic(api_key="z") @@ -4724,38 +4470,14 @@ async def test_streaming_create_message_with_system_prompt_async_no_sensitive_da response = get_model_response( async_iterator( 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", - ), - ] + streaming_anthropic_response( + text_deltas=( + "Hi", + "!", + " I'm Claude!", + ), + stop_reason=None, + ) ) ) ) @@ -4810,7 +4532,7 @@ async def test_streaming_create_message_with_system_prompt_async_no_sensitive_da assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] - assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"] + assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span["attributes"] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10 assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 diff --git a/tests/integrations/arq/test_arq_crons.py b/tests/integrations/arq/test_arq_crons.py index 3087e6e594..88686e02b1 100644 --- a/tests/integrations/arq/test_arq_crons.py +++ b/tests/integrations/arq/test_arq_crons.py @@ -7,6 +7,7 @@ from arq.connections import ArqRedis from fakeredis.aioredis import FakeRedis +import sentry_sdk from sentry_sdk.integrations.arq import ArqIntegration, _get_monitor_config try: @@ -183,6 +184,25 @@ async def test_cron_job_monitored_once(sentry_init, capture_envelopes): assert statuses[:4] == ["in_progress", "ok", "in_progress", "ok"] +@sentry_sdk.monitor(monitor_slug="decorated") +async def decorated_job(ctx): + pass + + +@pytest.mark.asyncio +async def test_monitor_decorated_cron_job_reports_once(sentry_init, capture_envelopes): + sentry_init(integrations=[ArqIntegration(monitor_cron_jobs=True)]) + envelopes = capture_envelopes() + + await _run_cron_jobs( + cron(decorated_job, run_at_startup=True), timezone=timezone.utc + ) + + check_ins = _check_ins(envelopes) + assert {c["monitor_slug"] for c in check_ins} == {"decorated"} + assert [c["status"] for c in check_ins[:2]] == ["in_progress", "ok"] + + class _Worker: def __init__(self, timezone): self.timezone = timezone diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 5fbbbb700f..4f6797ce3b 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -295,7 +295,7 @@ def getresponse(self, *args, **kwargs): expected_outgoing_baggage = ( "sentry-trace_id=771a43a4192642f0b136d5159a501700," "sentry-public_key=49d0f7386ad645858ae85020e393bef3," - "sentry-sample_rate=1.0," + "sentry-sample_rate=0.01337," "sentry-user_id=Am%C3%A9lie," "sentry-sample_rand=0.000005," "sentry-sampled=true" diff --git a/tests/test_dsc.py b/tests/test_dsc.py index 76b922fd1d..0fc661de92 100644 --- a/tests/test_dsc.py +++ b/tests/test_dsc.py @@ -113,7 +113,7 @@ def test_dsc_continuation_of_trace(sentry_init, capture_envelopes): dsn="https://mysecret@o1234.ingest.sentry.io/12312012", release="myapp@0.0.1", environment="canary", - traces_sample_rate=1.0, + traces_sample_rate=0.5, transport=TestTransportWithOptions, ) envelopes = capture_envelopes() @@ -158,7 +158,7 @@ def test_dsc_continuation_of_trace(sentry_init, capture_envelopes): assert "sample_rate" in envelope_trace_header assert type(envelope_trace_header["sample_rate"]) == str - assert envelope_trace_header["sample_rate"] == "1.0" + assert envelope_trace_header["sample_rate"] == "0.01337" assert "sampled" in envelope_trace_header assert type(envelope_trace_header["sampled"]) == str diff --git a/tests/test_utils.py b/tests/test_utils.py index 75e9925c86..84ecb683c7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1295,6 +1295,24 @@ def test_package_version_is_none(): assert package_version("non_existent_package") is None +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires importlib.metadata") +def test_package_version_installed_package(): + from importlib import metadata + + assert package_version("pytest") == parse_version(metadata.version("pytest")) + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires importlib.metadata") +def test_package_version_does_not_scan_all_installed_packages(monkeypatch): + def _fail(): + raise AssertionError("package_version must not scan all installed packages") + + monkeypatch.setattr("sentry_sdk.utils._get_installed_modules", _fail) + + assert package_version("pytest") is not None + assert package_version("non_existent_package") is None + + @pytest.mark.parametrize( "kwargs, expected", [ diff --git a/tests/tracing/test_integration_tests.py b/tests/tracing/test_integration_tests.py index 5be25a4344..626bf5498a 100644 --- a/tests/tracing/test_integration_tests.py +++ b/tests/tracing/test_integration_tests.py @@ -118,7 +118,7 @@ def test_continue_trace(sentry_init, capture_items, parent_sampled, sample_rate) assert not baggage.mutable if parent_sampled is not None: - expected_sample_rate = str(float(parent_sampled)) + expected_sample_rate = parent_sample_rate else: if sample_rate is None: expected_sample_rate = parent_sample_rate @@ -168,7 +168,7 @@ def test_continue_trace(sentry_init, capture_items, parent_sampled, sample_rate) ) if parent_sampled is not None: - expected_sample_rate = str(float(parent_sampled)) + expected_sample_rate = parent_sample_rate else: if sample_rate is None: expected_sample_rate = parent_sample_rate diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index e18177c3bc..6384fb4085 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -1049,12 +1049,7 @@ def test_outgoing_traceparent_and_baggage_incoming_trace( baggage_items = dict(tuple(item.split("=")) for item in baggage.split(",")) # As we've received incoming baggage, we shouldn't modify it ourselves - # and should propagate it as-is. However, in the case where our - # effective sample rate overrides the parent sample rate, we update the - # sample rate in the baggage as a consequence of updating the sample rate - # in the DSC. - if traces_sample_rate is not None: - incoming_baggage["sentry-sample_rate"] = str(float(parent_sampled)) + # and should propagate it as-is assert baggage_items == incoming_baggage @@ -1099,12 +1094,9 @@ def test_outgoing_traceparent_and_baggage_inconsistent_incoming_trace( span_id = span.span_id assert traceparent == f"{trace_id}-{span_id}-0" - # We shouldn't be updating incoming baggage, but in the case where our - # effective sample rate overrides the parent sample rate, we do this as - # a consequence of updating the sample rate in the DSC. + # We shouldn't be updating incoming baggage baggage = sentry_sdk.get_baggage() baggage_items = dict(tuple(item.split("=")) for item in baggage.split(",")) - incoming_baggage["sentry-sample_rate"] = "0.0" assert baggage_items == incoming_baggage diff --git a/warden.toml b/warden.toml index 8b1ff4ea9d..43cc01fa3d 100644 --- a/warden.toml +++ b/warden.toml @@ -32,13 +32,7 @@ reportOn = "medium" [[skills]] name = "code-review" - -[[skills.triggers]] -type = "pull_request" -actions = ["opened", "synchronize", "reopened"] - -[[skills]] -name = "find-bugs" +incremental = true [[skills.triggers]] type = "pull_request" @@ -46,6 +40,7 @@ actions = ["opened", "synchronize", "reopened"] [[skills]] name = "skill-scanner" +incremental = true [[skills.triggers]] type = "pull_request"