Skip to content

Commit 1af3203

Browse files
committed
feat(anthropic) update span attributes from old AI attributes to new GEN_AI ones
Additional simplifications and streamlining by using common utilities
1 parent 19914cd commit 1af3203

File tree

1 file changed

+109
-74
lines changed

1 file changed

+109
-74
lines changed

sentry_sdk/integrations/anthropic.py

Lines changed: 109 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33

44
import sentry_sdk
55
from sentry_sdk.ai.monitoring import record_token_usage
6+
from sentry_sdk.ai.utils import set_data_normalized
67
from sentry_sdk.consts import OP, SPANDATA
78
from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
89
from sentry_sdk.scope import should_send_default_pii
910
from sentry_sdk.utils import (
1011
capture_internal_exceptions,
1112
event_from_exception,
1213
package_version,
14+
safe_serialize,
1315
)
1416

1517
try:
18+
from anthropic import NOT_GIVEN
1619
from anthropic.resources import AsyncMessages, Messages
1720

1821
if TYPE_CHECKING:
@@ -53,8 +56,11 @@ def _capture_exception(exc):
5356
sentry_sdk.capture_event(event, hint=hint)
5457

5558

56-
def _calculate_token_usage(result, span):
57-
# type: (Messages, Span) -> None
59+
def _get_token_usage(result):
60+
# type: (Messages) -> tuple[int, int]
61+
"""
62+
Get token usage from the Anthropic response.
63+
"""
5864
input_tokens = 0
5965
output_tokens = 0
6066
if hasattr(result, "usage"):
@@ -64,44 +70,21 @@ def _calculate_token_usage(result, span):
6470
if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int):
6571
output_tokens = usage.output_tokens
6672

67-
total_tokens = input_tokens + output_tokens
73+
return input_tokens, output_tokens
6874

69-
record_token_usage(
70-
span,
71-
input_tokens=input_tokens,
72-
output_tokens=output_tokens,
73-
total_tokens=total_tokens,
74-
)
7575

76-
77-
def _get_responses(content):
78-
# type: (list[Any]) -> list[dict[str, Any]]
76+
def _collect_ai_data(event, model, input_tokens, output_tokens, content_blocks):
77+
# type: (MessageStreamEvent, str | None, int, int, list[str]) -> tuple[str | None, int, int, list[str]]
7978
"""
80-
Get JSON of a Anthropic responses.
81-
"""
82-
responses = []
83-
for item in content:
84-
if hasattr(item, "text"):
85-
responses.append(
86-
{
87-
"type": item.type,
88-
"text": item.text,
89-
}
90-
)
91-
return responses
92-
93-
94-
def _collect_ai_data(event, input_tokens, output_tokens, content_blocks):
95-
# type: (MessageStreamEvent, int, int, list[str]) -> tuple[int, int, list[str]]
96-
"""
97-
Count token usage and collect content blocks from the AI streaming response.
79+
Collect model information, token usage, and collect content blocks from the AI streaming response.
9880
"""
9981
with capture_internal_exceptions():
10082
if hasattr(event, "type"):
10183
if event.type == "message_start":
10284
usage = event.message.usage
10385
input_tokens += usage.input_tokens
10486
output_tokens += usage.output_tokens
87+
model = event.message.model or model
10588
elif event.type == "content_block_start":
10689
pass
10790
elif event.type == "content_block_delta":
@@ -114,31 +97,69 @@ def _collect_ai_data(event, input_tokens, output_tokens, content_blocks):
11497
elif event.type == "message_delta":
11598
output_tokens += event.usage.output_tokens
11699

117-
return input_tokens, output_tokens, content_blocks
100+
return model, input_tokens, output_tokens, content_blocks
118101

119102

120-
def _add_ai_data_to_span(
121-
span, integration, input_tokens, output_tokens, content_blocks
122-
):
123-
# type: (Span, AnthropicIntegration, int, int, list[str]) -> None
103+
def _set_input_data(span, kwargs, integration):
104+
# type: (Span, dict[str, Any], AnthropicIntegration) -> None
124105
"""
125-
Add token usage and content blocks from the AI streaming response to the span.
106+
Set input data for the span based on the provided keyword arguments for the anthropic message creation.
126107
"""
127-
with capture_internal_exceptions():
128-
if should_send_default_pii() and integration.include_prompts:
129-
complete_message = "".join(content_blocks)
130-
span.set_data(
131-
SPANDATA.AI_RESPONSES,
132-
[{"type": "text", "text": complete_message}],
133-
)
134-
total_tokens = input_tokens + output_tokens
135-
record_token_usage(
136-
span,
137-
input_tokens=input_tokens,
138-
output_tokens=output_tokens,
139-
total_tokens=total_tokens,
108+
messages = kwargs.get("messages")
109+
if (
110+
messages is not None
111+
and len(messages) > 0
112+
and should_send_default_pii()
113+
and integration.include_prompts
114+
):
115+
set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages)
116+
117+
kwargs_keys_to_attributes = {
118+
"max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS,
119+
"model": SPANDATA.GEN_AI_REQUEST_MODEL,
120+
"stream": SPANDATA.GEN_AI_RESPONSE_STREAMING,
121+
"temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE,
122+
"top_p": SPANDATA.GEN_AI_REQUEST_TOP_P,
123+
}
124+
for key, attribute in kwargs_keys_to_attributes.items():
125+
value = kwargs.get(key)
126+
if value is not NOT_GIVEN or value is not None:
127+
set_data_normalized(span, attribute, value)
128+
129+
# Input attributes: Tools
130+
tools = kwargs.get("tools")
131+
if tools is not NOT_GIVEN and tools is not None and len(tools) > 0:
132+
set_data_normalized(
133+
span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)
140134
)
141-
span.set_data(SPANDATA.AI_STREAMING, True)
135+
136+
137+
def _set_output_data(
138+
span,
139+
integration,
140+
model,
141+
input_tokens,
142+
output_tokens,
143+
content_blocks,
144+
finish_span=True,
145+
):
146+
# type: (Span, AnthropicIntegration, str | None, int | None, int | None, list[Any], bool) -> None
147+
"""
148+
Set output data for the span based on the AI response."""
149+
span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, model)
150+
if should_send_default_pii() and integration.include_prompts:
151+
set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, content_blocks)
152+
153+
record_token_usage(
154+
span,
155+
input_tokens=input_tokens,
156+
output_tokens=output_tokens,
157+
)
158+
159+
# TODO: GEN_AI_RESPONSE_TOOL_CALLS ?
160+
161+
if finish_span:
162+
span.__exit__(None, None, None)
142163

143164

144165
def _sentry_patched_create_common(f, *args, **kwargs):
@@ -162,62 +183,76 @@ def _sentry_patched_create_common(f, *args, **kwargs):
162183
)
163184
span.__enter__()
164185

165-
result = yield f, args, kwargs
186+
_set_input_data(span, kwargs, integration)
166187

167-
# add data to span and finish it
168-
messages = list(kwargs["messages"])
169-
model = kwargs.get("model")
188+
result = yield f, args, kwargs
170189

171190
with capture_internal_exceptions():
172-
span.set_data(SPANDATA.AI_MODEL_ID, model)
173-
span.set_data(SPANDATA.AI_STREAMING, False)
174-
175-
if should_send_default_pii() and integration.include_prompts:
176-
span.set_data(SPANDATA.AI_INPUT_MESSAGES, messages)
177-
178191
if hasattr(result, "content"):
179-
if should_send_default_pii() and integration.include_prompts:
180-
span.set_data(SPANDATA.AI_RESPONSES, _get_responses(result.content))
181-
_calculate_token_usage(result, span)
182-
span.__exit__(None, None, None)
192+
input_tokens, output_tokens = _get_token_usage(result)
193+
_set_output_data(
194+
span,
195+
integration,
196+
getattr(result, "model", None),
197+
input_tokens,
198+
output_tokens,
199+
content_blocks=result.content,
200+
finish_span=True,
201+
)
183202

184203
# Streaming response
185204
elif hasattr(result, "_iterator"):
186205
old_iterator = result._iterator
187206

188207
def new_iterator():
189208
# type: () -> Iterator[MessageStreamEvent]
209+
model = None
190210
input_tokens = 0
191211
output_tokens = 0
192212
content_blocks = [] # type: list[str]
193213

194214
for event in old_iterator:
195-
input_tokens, output_tokens, content_blocks = _collect_ai_data(
196-
event, input_tokens, output_tokens, content_blocks
215+
model, input_tokens, output_tokens, content_blocks = (
216+
_collect_ai_data(
217+
event, model, input_tokens, output_tokens, content_blocks
218+
)
197219
)
198220
yield event
199221

200-
_add_ai_data_to_span(
201-
span, integration, input_tokens, output_tokens, content_blocks
222+
_set_output_data(
223+
span,
224+
integration,
225+
model=model,
226+
input_tokens=input_tokens,
227+
output_tokens=output_tokens,
228+
content_blocks=content_blocks,
229+
finish_span=True,
202230
)
203-
span.__exit__(None, None, None)
204231

205232
async def new_iterator_async():
206233
# type: () -> AsyncIterator[MessageStreamEvent]
234+
model = None
207235
input_tokens = 0
208236
output_tokens = 0
209237
content_blocks = [] # type: list[str]
210238

211239
async for event in old_iterator:
212-
input_tokens, output_tokens, content_blocks = _collect_ai_data(
213-
event, input_tokens, output_tokens, content_blocks
240+
model, input_tokens, output_tokens, content_blocks = (
241+
_collect_ai_data(
242+
event, model, input_tokens, output_tokens, content_blocks
243+
)
214244
)
215245
yield event
216246

217-
_add_ai_data_to_span(
218-
span, integration, input_tokens, output_tokens, content_blocks
247+
_set_output_data(
248+
span,
249+
integration,
250+
model=model,
251+
input_tokens=input_tokens,
252+
output_tokens=output_tokens,
253+
content_blocks=content_blocks,
254+
finish_span=True,
219255
)
220-
span.__exit__(None, None, None)
221256

222257
if str(type(result._iterator)) == "<class 'async_generator'>":
223258
result._iterator = new_iterator_async()

0 commit comments

Comments
 (0)