Skip to content

Commit f698420

Browse files
committed
refactor
1 parent 3ef0c4a commit f698420

2 files changed

Lines changed: 70 additions & 87 deletions

File tree

packages/node/src/integrations/tracing/vercelai/instrumentation.ts

Lines changed: 57 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -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
},

packages/node/test/integrations/tracing/vercelai/instrumentation.test.ts

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { _INTERNAL_getSpanContextForToolCallId, _INTERNAL_toolCallSpanContextMap } from '@sentry/core';
22
import { beforeEach, describe, expect, test } from 'vitest';
33
import {
4-
checkResultForToolErrors,
4+
cleanupToolCallSpanContexts,
55
determineRecordingSettings,
66
} from '../../../../src/integrations/tracing/vercelai/instrumentation';
77

@@ -217,65 +217,47 @@ describe('determineRecordingSettings', () => {
217217
});
218218
});
219219

220-
describe('checkResultForToolErrors', () => {
220+
describe('cleanupToolCallSpanContexts', () => {
221221
beforeEach(() => {
222222
_INTERNAL_toolCallSpanContextMap.clear();
223223
});
224224

225-
test('cleans up span context map on successful tool-result', () => {
225+
test('cleans up span context for tool-result items', () => {
226226
_INTERNAL_toolCallSpanContextMap.set('tool-1', { traceId: 't1', spanId: 's1' });
227227
_INTERNAL_toolCallSpanContextMap.set('tool-2', { traceId: 't2', spanId: 's2' });
228228

229-
checkResultForToolErrors({
230-
content: [{ type: 'tool-result', toolCallId: 'tool-1', toolName: 'bash' }],
231-
});
229+
cleanupToolCallSpanContexts([{ type: 'tool-result', toolCallId: 'tool-1', toolName: 'bash' }]);
232230

233231
expect(_INTERNAL_getSpanContextForToolCallId('tool-1')).toBeUndefined();
234-
// tool-2 should be unaffected
235232
expect(_INTERNAL_getSpanContextForToolCallId('tool-2')).toEqual({ traceId: 't2', spanId: 's2' });
236233
});
237234

238-
test('cleans up span context map on tool-error', () => {
235+
test('cleans up span context for tool-error items', () => {
239236
_INTERNAL_toolCallSpanContextMap.set('tool-1', { traceId: 't1', spanId: 's1' });
240237

241-
checkResultForToolErrors({
242-
content: [{ type: 'tool-error', toolCallId: 'tool-1', toolName: 'bash', error: new Error('fail') }],
243-
});
238+
cleanupToolCallSpanContexts([{ type: 'tool-error', toolCallId: 'tool-1', toolName: 'bash', error: new Error('fail') }]);
244239

245240
expect(_INTERNAL_getSpanContextForToolCallId('tool-1')).toBeUndefined();
246241
});
247242

248-
test('handles mixed tool-result and tool-error in same content array', () => {
243+
test('cleans up mixed tool-result and tool-error in same content array', () => {
249244
_INTERNAL_toolCallSpanContextMap.set('tool-1', { traceId: 't1', spanId: 's1' });
250245
_INTERNAL_toolCallSpanContextMap.set('tool-2', { traceId: 't2', spanId: 's2' });
251246

252-
checkResultForToolErrors({
253-
content: [
254-
{ type: 'tool-result', toolCallId: 'tool-1', toolName: 'bash' },
255-
{ type: 'tool-error', toolCallId: 'tool-2', toolName: 'bash', error: new Error('fail') },
256-
],
257-
});
247+
cleanupToolCallSpanContexts([
248+
{ type: 'tool-result', toolCallId: 'tool-1', toolName: 'bash' },
249+
{ type: 'tool-error', toolCallId: 'tool-2', toolName: 'bash', error: new Error('fail') },
250+
]);
258251

259252
expect(_INTERNAL_getSpanContextForToolCallId('tool-1')).toBeUndefined();
260253
expect(_INTERNAL_getSpanContextForToolCallId('tool-2')).toBeUndefined();
261254
});
262255

263-
test('does not throw for tool-error with unknown toolCallId', () => {
264-
checkResultForToolErrors({
265-
content: [{ type: 'tool-error', toolCallId: 'unknown', toolName: 'bash', error: new Error('fail') }],
266-
});
267-
268-
// Should not throw, just captures without span linking
269-
});
270-
271-
test('ignores results without content array', () => {
256+
test('ignores items without toolCallId', () => {
272257
_INTERNAL_toolCallSpanContextMap.set('tool-1', { traceId: 't1', spanId: 's1' });
273258

274-
checkResultForToolErrors({});
275-
checkResultForToolErrors(null);
276-
checkResultForToolErrors({ content: 'not-an-array' });
259+
cleanupToolCallSpanContexts([{ type: 'text', text: 'hello' } as unknown as object]);
277260

278-
// Map should be untouched
279261
expect(_INTERNAL_getSpanContextForToolCallId('tool-1')).toEqual({ traceId: 't1', spanId: 's1' });
280262
});
281263
});

0 commit comments

Comments
 (0)