@@ -69,20 +69,13 @@ function isToolError(obj: unknown): obj is ToolError {
6969 ) ;
7070}
7171
72- function isToolResult ( obj : unknown ) : obj is { type : 'tool-result' ; toolCallId : string } {
73- if ( typeof obj !== 'object' || obj === null ) {
74- return false ;
75- }
76-
77- const candidate = obj as Record < string , unknown > ;
78- return candidate . type === 'tool-result' && typeof candidate . toolCallId === 'string' ;
79- }
80-
8172/**
82- * Check for tool errors in the result and capture them
83- * Tool errors are not rejected in Vercel V5, it is added as metadata to the result content
73+ * Process tool call results: capture tool errors and clean up span context mappings.
74+ *
75+ * Error checking runs first (needs span context for linking), then cleanup removes all entries.
76+ * Tool errors are not rejected in Vercel AI V5 — they appear as metadata in the result content.
8477 */
85- export function checkResultForToolErrors ( result : unknown ) : void {
78+ export function processToolCallResults ( result : unknown ) : void {
8679 if ( typeof result !== 'object' || result === null || ! ( 'content' in result ) ) {
8780 return ;
8881 }
@@ -92,57 +85,65 @@ export function checkResultForToolErrors(result: unknown): void {
9285 return ;
9386 }
9487
95- for ( const item of resultObj . content ) {
96- // Clean up successful tool call entries to prevent memory leaks
97- if ( isToolResult ( item ) ) {
98- _INTERNAL_cleanupToolCallSpanContext ( item . toolCallId ) ;
88+ captureToolErrors ( resultObj . content ) ;
89+ cleanupToolCallSpanContexts ( resultObj . content ) ;
90+ }
91+
92+ function captureToolErrors ( content : Array < object > ) : void {
93+ for ( const item of content ) {
94+ if ( ! isToolError ( item ) ) {
9995 continue ;
10096 }
10197
102- if ( isToolError ( item ) ) {
103- // Try to get the span context associated with this tool call ID
104- const spanContext = _INTERNAL_getSpanContextForToolCallId ( item . toolCallId ) ;
105-
106- if ( spanContext ) {
107- // We have the span context, so link the error using span and trace IDs
108- withScope ( scope => {
109- // Set the span and trace context for proper linking
110- scope . setContext ( 'trace' , {
111- trace_id : spanContext . traceId ,
112- span_id : spanContext . spanId ,
113- } ) ;
98+ // Try to get the span context associated with this tool call ID
99+ const spanContext = _INTERNAL_getSpanContextForToolCallId ( item . toolCallId ) ;
114100
115- scope . setTag ( 'vercel.ai.tool.name' , item . toolName ) ;
116- scope . setTag ( 'vercel.ai.tool.callId' , item . toolCallId ) ;
101+ if ( spanContext ) {
102+ // We have the span context, so link the error using span and trace IDs
103+ withScope ( scope => {
104+ scope . setContext ( 'trace' , {
105+ trace_id : spanContext . traceId ,
106+ span_id : spanContext . spanId ,
107+ } ) ;
117108
118- scope . setLevel ( 'error' ) ;
109+ scope . setTag ( 'vercel.ai.tool.name' , item . toolName ) ;
110+ scope . setTag ( 'vercel.ai.tool.callId' , item . toolCallId ) ;
111+ scope . setLevel ( 'error' ) ;
119112
120- captureException ( item . error , {
121- mechanism : {
122- type : 'auto.vercelai.otel' ,
123- handled : false ,
124- } ,
125- } ) ;
113+ captureException ( item . error , {
114+ mechanism : {
115+ type : 'auto.vercelai.otel' ,
116+ handled : false ,
117+ } ,
126118 } ) ;
127-
128- // Clean up the span mapping since we've processed this tool error
129- // We won't get multiple { type: 'tool-error' } parts for the same toolCallId.
130- _INTERNAL_cleanupToolCallSpanContext ( item . toolCallId ) ;
131- } else {
132- // Fallback: capture without span linking
133- withScope ( scope => {
134- scope . setTag ( 'vercel.ai.tool.name' , item . toolName ) ;
135- scope . setTag ( 'vercel.ai.tool.callId' , item . toolCallId ) ;
136- scope . setLevel ( 'error' ) ;
137-
138- captureException ( item . error , {
139- mechanism : {
140- type : 'auto.vercelai.otel' ,
141- handled : false ,
142- } ,
143- } ) ;
119+ } ) ;
120+ } else {
121+ // Fallback: capture without span linking
122+ withScope ( scope => {
123+ scope . setTag ( 'vercel.ai.tool.name' , item . toolName ) ;
124+ scope . setTag ( 'vercel.ai.tool.callId' , item . toolCallId ) ;
125+ scope . setLevel ( 'error' ) ;
126+
127+ captureException ( item . error , {
128+ mechanism : {
129+ type : 'auto.vercelai.otel' ,
130+ handled : false ,
131+ } ,
144132 } ) ;
145- }
133+ } ) ;
134+ }
135+ }
136+ }
137+
138+ export function cleanupToolCallSpanContexts ( content : Array < object > ) : void {
139+ for ( const item of content ) {
140+ if (
141+ typeof item === 'object' &&
142+ item !== null &&
143+ 'toolCallId' in item &&
144+ typeof ( item as Record < string , unknown > ) . toolCallId === 'string'
145+ ) {
146+ _INTERNAL_cleanupToolCallSpanContext ( ( item as Record < string , unknown > ) . toolCallId as string ) ;
146147 }
147148 }
148149}
@@ -264,7 +265,7 @@ export class SentryVercelAiInstrumentation extends InstrumentationBase {
264265 } ,
265266 ( ) => { } ,
266267 result => {
267- checkResultForToolErrors ( result ) ;
268+ processToolCallResults ( result ) ;
268269 } ,
269270 ) ;
270271 } ,
0 commit comments