33
44import sentry_sdk
55from sentry_sdk .ai .monitoring import record_token_usage
6+ from sentry_sdk .ai .utils import set_data_normalized
67from sentry_sdk .consts import OP , SPANDATA
78from sentry_sdk .integrations import _check_minimum_version , DidNotEnable , Integration
89from sentry_sdk .scope import should_send_default_pii
910from sentry_sdk .utils import (
1011 capture_internal_exceptions ,
1112 event_from_exception ,
1213 package_version ,
14+ safe_serialize ,
1315)
1416
1517try :
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
144165def _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