Skip to content

Commit f6b8909

Browse files
merge master
2 parents a3cc18f + b905cd3 commit f6b8909

File tree

4 files changed

+48
-13
lines changed

4 files changed

+48
-13
lines changed

sentry_sdk/integrations/anthropic.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ def _collect_ai_data(
190190
usage: "_RecordedUsage",
191191
content_blocks: "list[str]",
192192
response_id: "str | None" = None,
193-
) -> "tuple[str | None, _RecordedUsage, list[str], str | None]":
193+
finish_reason: "str | None" = None,
194+
) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]":
194195
"""
195196
Collect model information, token usage, and collect content blocks from the AI streaming response.
196197
"""
@@ -228,6 +229,7 @@ def _collect_ai_data(
228229
usage,
229230
content_blocks,
230231
response_id,
232+
finish_reason,
231233
)
232234

233235
# Counterintuitive, but message_delta contains cumulative token counts :)
@@ -252,18 +254,17 @@ def _collect_ai_data(
252254
usage.cache_read_input_tokens = cache_read_input_tokens
253255
# TODO: Record event.usage.server_tool_use
254256

255-
return (
256-
model,
257-
usage,
258-
content_blocks,
259-
response_id,
260-
)
257+
if event.delta.stop_reason is not None:
258+
finish_reason = event.delta.stop_reason
259+
260+
return (model, usage, content_blocks, response_id, finish_reason)
261261

262262
return (
263263
model,
264264
usage,
265265
content_blocks,
266266
response_id,
267+
finish_reason,
267268
)
268269

269270

@@ -472,6 +473,7 @@ def _wrap_synchronous_message_iterator(
472473
stream._usage,
473474
stream._content_blocks,
474475
stream._response_id,
476+
stream._finish_reason,
475477
)
476478
del stream._span
477479

@@ -489,6 +491,7 @@ async def _wrap_asynchronous_message_iterator(
489491
usage = _RecordedUsage()
490492
content_blocks: "list[str]" = []
491493
response_id = None
494+
finish_reason = None
492495

493496
try:
494497
async for event in iterator:
@@ -513,12 +516,14 @@ async def _wrap_asynchronous_message_iterator(
513516
usage,
514517
content_blocks,
515518
response_id,
519+
finish_reason,
516520
) = _collect_ai_data(
517521
event,
518522
model,
519523
usage,
520524
content_blocks,
521525
response_id,
526+
finish_reason,
522527
)
523528
yield event
524529
finally:
@@ -542,6 +547,7 @@ async def _wrap_asynchronous_message_iterator(
542547
content_blocks=[{"text": "".join(content_blocks), "type": "text"}],
543548
finish_span=True,
544549
response_id=response_id,
550+
finish_reason=finish_reason,
545551
)
546552

547553

@@ -556,12 +562,15 @@ def _set_output_data(
556562
content_blocks: "list[Any]",
557563
finish_span: bool = False,
558564
response_id: "str | None" = None,
565+
finish_reason: "str | None" = None,
559566
) -> None:
560567
"""
561568
Set output data for the span based on the AI response."""
562569
span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, model)
563570
if response_id is not None:
564571
span.set_data(SPANDATA.GEN_AI_RESPONSE_ID, response_id)
572+
if finish_reason is not None:
573+
span.set_data(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason])
565574
if should_send_default_pii() and integration.include_prompts:
566575
output_messages: "dict[str, list[Any]]" = {
567576
"response": [],
@@ -665,6 +674,7 @@ def _sentry_patched_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A
665674
content_blocks=content_blocks,
666675
finish_span=True,
667676
response_id=getattr(result, "id", None),
677+
finish_reason=getattr(result, "stop_reason", None),
668678
)
669679
else:
670680
span.set_data("unknown_response", True)
@@ -720,6 +730,7 @@ def _initialize_data_accumulation_state(stream: "Union[Stream, MessageStream]")
720730
stream._usage = _RecordedUsage()
721731
stream._content_blocks = []
722732
stream._response_id = None
733+
stream._finish_reason = None
723734

724735

725736
def _accumulate_event_data(
@@ -729,18 +740,20 @@ def _accumulate_event_data(
729740
"""
730741
Update accumulated output from a single stream event.
731742
"""
732-
(model, usage, content_blocks, response_id) = _collect_ai_data(
743+
(model, usage, content_blocks, response_id, finish_reason) = _collect_ai_data(
733744
event,
734745
stream._model,
735746
stream._usage,
736747
stream._content_blocks,
737748
stream._response_id,
749+
stream._finish_reason,
738750
)
739751

740752
stream._model = model
741753
stream._usage = usage
742754
stream._content_blocks = content_blocks
743755
stream._response_id = response_id
756+
stream._finish_reason = finish_reason
744757

745758

746759
def _finish_streaming_span(
@@ -750,6 +763,7 @@ def _finish_streaming_span(
750763
usage: "_RecordedUsage",
751764
content_blocks: "list[str]",
752765
response_id: "Optional[str]",
766+
finish_reason: "Optional[str]",
753767
) -> None:
754768
"""
755769
Set output attributes on the AI Client Span and end the span.
@@ -773,6 +787,7 @@ def _finish_streaming_span(
773787
content_blocks=[{"text": "".join(content_blocks), "type": "text"}],
774788
finish_span=True,
775789
response_id=response_id,
790+
finish_reason=finish_reason,
776791
)
777792

778793

@@ -822,6 +837,7 @@ def __next__(self: "Stream") -> "RawMessageStreamEvent":
822837
self._usage,
823838
self._content_blocks,
824839
self._response_id,
840+
self._finish_reason,
825841
)
826842
del self._span
827843
reraise(*exc_info)
@@ -854,6 +870,7 @@ def close(self: "Stream") -> None:
854870
self._usage,
855871
self._content_blocks,
856872
self._response_id,
873+
self._finish_reason,
857874
)
858875
del self._span
859876

@@ -1023,6 +1040,7 @@ def __next__(self: "MessageStream") -> "MessageStreamEvent":
10231040
self._usage,
10241041
self._content_blocks,
10251042
self._response_id,
1043+
self._finish_reason,
10261044
)
10271045
del self._span
10281046
reraise(*exc_info)
@@ -1055,6 +1073,7 @@ def close(self: "MessageStream") -> None:
10551073
self._usage,
10561074
self._content_blocks,
10571075
self._response_id,
1076+
self._finish_reason,
10581077
)
10591078
del self._span
10601079

sentry_sdk/integrations/langchain.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,8 @@ def on_llm_end(
554554
finish_reason = generation.generation_info.get("finish_reason")
555555
if finish_reason is not None:
556556
span.set_data(
557-
SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reason
557+
SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS,
558+
[finish_reason],
558559
)
559560
except AttributeError:
560561
pass

tests/integrations/anthropic/test_anthropic.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ async def __call__(self, *args, **kwargs):
6969
role="assistant",
7070
content=[TextBlock(type="text", text="Hi, I'm Claude.")],
7171
type="message",
72+
stop_reason="end_turn",
7273
usage=Usage(input_tokens=10, output_tokens=20),
7374
)
7475

@@ -142,6 +143,7 @@ def test_nonstreaming_create_message(
142143
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
143144
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False
144145
assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
146+
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["end_turn"]
145147

146148

147149
@pytest.mark.asyncio
@@ -264,7 +266,7 @@ def test_streaming_create_message(
264266
),
265267
ContentBlockStopEvent(type="content_block_stop", index=0),
266268
MessageDeltaEvent(
267-
delta=Delta(),
269+
delta=Delta(stop_reason="max_tokens"),
268270
usage=MessageDeltaUsage(output_tokens=10),
269271
type="message_delta",
270272
),
@@ -329,6 +331,7 @@ def test_streaming_create_message(
329331
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
330332
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
331333
assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
334+
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["max_tokens"]
332335

333336

334337
def test_streaming_create_message_next_consumption(
@@ -580,7 +583,7 @@ def test_stream_messages(
580583
),
581584
ContentBlockStopEvent(type="content_block_stop", index=0),
582585
MessageDeltaEvent(
583-
delta=Delta(),
586+
delta=Delta(stop_reason="max_tokens"),
584587
usage=MessageDeltaUsage(output_tokens=10),
585588
type="message_delta",
586589
),
@@ -646,6 +649,7 @@ def test_stream_messages(
646649
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
647650
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
648651
assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
652+
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["max_tokens"]
649653

650654

651655
def test_stream_messages_next_consumption(
@@ -905,7 +909,7 @@ async def test_streaming_create_message_async(
905909
),
906910
ContentBlockStopEvent(type="content_block_stop", index=0),
907911
MessageDeltaEvent(
908-
delta=Delta(),
912+
delta=Delta(stop_reason="max_tokens"),
909913
usage=MessageDeltaUsage(output_tokens=10),
910914
type="message_delta",
911915
),
@@ -917,6 +921,7 @@ async def test_streaming_create_message_async(
917921
sentry_init(
918922
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
919923
traces_sample_rate=1.0,
924+
default_integrations=False,
920925
send_default_pii=send_default_pii,
921926
)
922927
events = capture_events()
@@ -972,6 +977,7 @@ async def test_streaming_create_message_async(
972977
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
973978
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
974979
assert span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
980+
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["max_tokens"]
975981

976982

977983
@pytest.mark.asyncio
@@ -1884,14 +1890,15 @@ def test_collect_ai_data_with_input_json_delta():
18841890

18851891
content_blocks = []
18861892

1887-
model, new_usage, new_content_blocks, response_id = _collect_ai_data(
1893+
model, new_usage, new_content_blocks, response_id, finish_reason = _collect_ai_data(
18881894
event, model, usage, content_blocks
18891895
)
18901896
assert model is None
18911897
assert new_usage.input_tokens == usage.input_tokens
18921898
assert new_usage.output_tokens == usage.output_tokens
18931899
assert new_content_blocks == ["test"]
18941900
assert response_id is None
1901+
assert finish_reason is None
18951902

18961903

18971904
@pytest.mark.skipif(
@@ -2179,6 +2186,7 @@ def test_nonstreaming_create_message_with_system_prompt(
21792186
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
21802187
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
21812188
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False
2189+
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["end_turn"]
21822190

21832191

21842192
@pytest.mark.asyncio
@@ -2264,6 +2272,7 @@ async def test_nonstreaming_create_message_with_system_prompt_async(
22642272
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
22652273
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
22662274
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False
2275+
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["end_turn"]
22672276

22682277

22692278
@pytest.mark.parametrize(

tests/integrations/langchain/test_langchain.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,12 @@ def test_langchain_agent(
297297
f"and include_prompts={include_prompts}"
298298
)
299299

300+
# Verify finish_reasons is always an array of strings
301+
assert chat_spans[0]["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == [
302+
"function_call"
303+
]
304+
assert chat_spans[1]["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["stop"]
305+
300306
# Verify that available tools are always recorded regardless of PII settings
301307
for chat_span in chat_spans:
302308
span_data = chat_span.get("data", {})

0 commit comments

Comments
 (0)