Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
remove not needed stuff and update tests
  • Loading branch information
nicohrubec committed Feb 26, 2026
commit 744d63d9407a5f61298b9419ed4dc1461a47b1fe
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export type { MetricOptions } from './metrics/public-api';
export { createConsolaReporter } from './integrations/consola';
export { addVercelAiProcessors } from './tracing/vercel-ai';
export { _INTERNAL_getSpanContextForToolCallId, _INTERNAL_cleanupToolCallSpanContext } from './tracing/vercel-ai/utils';
export { toolCallSpanContextMap as _INTERNAL_toolCallSpanContextMap } from './tracing/vercel-ai/constants';
export { instrumentOpenAiClient } from './tracing/openai';
export { OPENAI_INTEGRATION_NAME } from './tracing/openai/constants';
export { instrumentAnthropicAiClient } from './tracing/anthropic-ai';
Expand Down
108 changes: 0 additions & 108 deletions packages/core/test/lib/tracing/vercel-ai-tool-call-span-map.test.ts

This file was deleted.

119 changes: 57 additions & 62 deletions packages/node/src/integrations/tracing/vercelai/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { InstrumentationConfig, InstrumentationModuleDefinition } from '@opentelemetry/instrumentation';
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
import type { Span } from '@sentry/core';
import {
_INTERNAL_cleanupToolCallSpanContext,
_INTERNAL_getSpanContextForToolCallId,
Expand Down Expand Up @@ -42,114 +43,108 @@ interface RecordingOptions {
recordOutputs?: boolean;
}

interface ToolErrorPart {
type: 'tool-error';
interface ToolError {
type: 'tool-error' | 'tool-result' | 'tool-call';
toolCallId: string;
toolName: string;
input?: {
[key: string]: unknown;
};
error: Error;
dynamic?: boolean;
}

interface ToolResultPart {
type: 'tool-result';
toolCallId: string;
toolName: string;
}

function isToolErrorPart(obj: unknown): obj is ToolErrorPart {
function isToolError(obj: unknown): obj is ToolError {
if (typeof obj !== 'object' || obj === null) {
return false;
}

const candidate = obj as Record<string, unknown>;
return (
'type' in candidate &&
'error' in candidate &&
'toolName' in candidate &&
'toolCallId' in candidate &&
candidate.type === 'tool-error' &&
typeof candidate.toolName === 'string' &&
typeof candidate.toolCallId === 'string' &&
candidate.error instanceof Error
);
}

function isToolResultPart(obj: unknown): obj is ToolResultPart {
function isToolResult(obj: unknown): obj is { type: 'tool-result'; toolCallId: string } {
if (typeof obj !== 'object' || obj === null) {
return false;
}

const candidate = obj as Record<string, unknown>;
return (
candidate.type === 'tool-result' &&
typeof candidate.toolName === 'string' &&
typeof candidate.toolCallId === 'string'
);
return candidate.type === 'tool-result' && typeof candidate.toolCallId === 'string';
}

/**
* Check for tool errors in the result and capture them
* Tool errors are not rejected in Vercel V5, it is added as metadata to the result content
*/
function checkResultForToolErrors(result: unknown): void {
export function _INTERNAL_checkResultForToolErrors(result: unknown): void {
if (typeof result !== 'object' || result === null || !('content' in result)) {
return;
}

const resultObj = result as { content: unknown };
const resultObj = result as { content: Array<object> };
if (!Array.isArray(resultObj.content)) {
return;
}

for (const item of resultObj.content) {
// Successful tool calls should not keep toolCallId -> span context mappings alive.
if (isToolResultPart(item)) {
// Clean up successful tool call entries to prevent memory leaks
if (isToolResult(item)) {
_INTERNAL_cleanupToolCallSpanContext(item.toolCallId);
continue;
}

if (!isToolErrorPart(item)) {
continue;
}

// Try to get the span context associated with this tool call ID
const spanContext = _INTERNAL_getSpanContextForToolCallId(item.toolCallId);
if (isToolError(item)) {
// Try to get the span context associated with this tool call ID
const spanContext = _INTERNAL_getSpanContextForToolCallId(item.toolCallId);

if (spanContext) {
// We have a span context, so link the error using span and trace IDs from the span
withScope(scope => {
// Set the span and trace context for proper linking
scope.setContext('trace', {
trace_id: spanContext.traceId,
span_id: spanContext.spanId,
});
if (spanContext) {
// We have the span context, so link the error using span and trace IDs
withScope(scope => {
// Set the span and trace context for proper linking
scope.setContext('trace', {
trace_id: spanContext.traceId,
span_id: spanContext.spanId,
});

scope.setTag('vercel.ai.tool.name', item.toolName);
scope.setTag('vercel.ai.tool.callId', item.toolCallId);
scope.setTag('vercel.ai.tool.name', item.toolName);
scope.setTag('vercel.ai.tool.callId', item.toolCallId);

scope.setLevel('error');
scope.setLevel('error');

captureException(item.error, {
mechanism: {
type: 'auto.vercelai.otel',
handled: false,
},
captureException(item.error, {
mechanism: {
type: 'auto.vercelai.otel',
handled: false,
},
});
});
});
} else {
// Fallback: capture without span linking
withScope(scope => {
scope.setTag('vercel.ai.tool.name', item.toolName);
scope.setTag('vercel.ai.tool.callId', item.toolCallId);
scope.setLevel('error');

captureException(item.error, {
mechanism: {
type: 'auto.vercelai.otel',
handled: false,
},

// Clean up the span mapping since we've processed this tool error
// We won't get multiple { type: 'tool-error' } parts for the same toolCallId.
_INTERNAL_cleanupToolCallSpanContext(item.toolCallId);
} else {
// Fallback: capture without span linking
withScope(scope => {
scope.setTag('vercel.ai.tool.name', item.toolName);
scope.setTag('vercel.ai.tool.callId', item.toolCallId);
scope.setLevel('error');

captureException(item.error, {
mechanism: {
type: 'auto.vercelai.otel',
handled: false,
},
});
});
});
}
}

// Clean up the span mapping since we've processed this tool error
// We won't get multiple { type: 'tool-error' } parts for the same toolCallId.
_INTERNAL_cleanupToolCallSpanContext(item.toolCallId);
}
}

Expand Down Expand Up @@ -270,7 +265,7 @@ export class SentryVercelAiInstrumentation extends InstrumentationBase {
},
() => {},
result => {
checkResultForToolErrors(result);
_INTERNAL_checkResultForToolErrors(result);
},
);
},
Comment thread
sentry[bot] marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, expect, test } from 'vitest';
import { determineRecordingSettings } from '../../../../src/integrations/tracing/vercelai/instrumentation';
import { beforeEach, describe, expect, test } from 'vitest';
import { _INTERNAL_getSpanContextForToolCallId, _INTERNAL_toolCallSpanContextMap } from '@sentry/core';
import {
determineRecordingSettings,
_INTERNAL_checkResultForToolErrors,
} from '../../../../src/integrations/tracing/vercelai/instrumentation';

describe('determineRecordingSettings', () => {
test('should use integration recording options when provided (recordInputs: true, recordOutputs: false)', () => {
Expand Down Expand Up @@ -212,3 +216,66 @@ describe('determineRecordingSettings', () => {
});
});
});

describe('checkResultForToolErrors', () => {
beforeEach(() => {
_INTERNAL_toolCallSpanContextMap.clear();
});

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

_INTERNAL_checkResultForToolErrors({
content: [{ type: 'tool-result', toolCallId: 'tool-1', toolName: 'bash' }],
});

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

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

_INTERNAL_checkResultForToolErrors({
content: [{ type: 'tool-error', toolCallId: 'tool-1', toolName: 'bash', error: new Error('fail') }],
});

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

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

_INTERNAL_checkResultForToolErrors({
content: [
{ type: 'tool-result', toolCallId: 'tool-1', toolName: 'bash' },
{ type: 'tool-error', toolCallId: 'tool-2', toolName: 'bash', error: new Error('fail') },
],
});

expect(_INTERNAL_getSpanContextForToolCallId('tool-1')).toBeUndefined();
expect(_INTERNAL_getSpanContextForToolCallId('tool-2')).toBeUndefined();
});

test('does not throw for tool-error with unknown toolCallId', () => {
_INTERNAL_checkResultForToolErrors({
content: [{ type: 'tool-error', toolCallId: 'unknown', toolName: 'bash', error: new Error('fail') }],
});

// Should not throw, just captures without span linking
});

test('ignores results without content array', () => {
_INTERNAL_toolCallSpanContextMap.set('tool-1', { traceId: 't1', spanId: 's1' });

_INTERNAL_checkResultForToolErrors({});
_INTERNAL_checkResultForToolErrors(null);
_INTERNAL_checkResultForToolErrors({ content: 'not-an-array' });

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