UNPKG

@sentry/node

Version:

Sentry Node SDK using OpenTelemetry for performance instrumentation

244 lines (212 loc) 8.45 kB
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; import { SDK_VERSION, getClient, handleCallbackErrors, addNonEnumerableProperty, getActiveSpan, _INTERNAL_getSpanForToolCallId, withScope, captureException, _INTERNAL_cleanupToolCallSpan } from '@sentry/core'; import { INTEGRATION_NAME } from './constants.js'; // List of patched methods // From: https://sdk.vercel.ai/docs/ai-sdk-core/telemetry#collected-data const INSTRUMENTED_METHODS = [ 'generateText', 'streamText', 'generateObject', 'streamObject', 'embed', 'embedMany', ] ; function isToolError(obj) { if (typeof obj !== 'object' || obj === null) { return false; } const candidate = obj ; return ( 'type' in candidate && 'error' in candidate && 'toolName' in candidate && 'toolCallId' in candidate && candidate.type === 'tool-error' && candidate.error instanceof Error ); } /** * 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) { if (typeof result !== 'object' || result === null || !('content' in result)) { return; } const resultObj = result ; if (!Array.isArray(resultObj.content)) { return; } for (const item of resultObj.content) { if (isToolError(item)) { // Try to get the span associated with this tool call ID const associatedSpan = _INTERNAL_getSpanForToolCallId(item.toolCallId) ; if (associatedSpan) { // We have the span, so link the error using span and trace IDs from the span const spanContext = associatedSpan.spanContext(); 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.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_cleanupToolCallSpan(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, }, }); }); } } } } /** * Determines whether to record inputs and outputs for Vercel AI telemetry based on the configuration hierarchy. * * The order of precedence is: * 1. The vercel ai integration options * 2. The experimental_telemetry options in the vercel ai method calls * 3. When telemetry is explicitly enabled (isEnabled: true), default to recording * 4. Otherwise, use the sendDefaultPii option from client options */ function determineRecordingSettings( integrationRecordingOptions, methodTelemetryOptions, telemetryExplicitlyEnabled, defaultRecordingEnabled, ) { const recordInputs = integrationRecordingOptions?.recordInputs !== undefined ? integrationRecordingOptions.recordInputs : methodTelemetryOptions.recordInputs !== undefined ? methodTelemetryOptions.recordInputs : telemetryExplicitlyEnabled === true ? true // When telemetry is explicitly enabled, default to recording inputs : defaultRecordingEnabled; const recordOutputs = integrationRecordingOptions?.recordOutputs !== undefined ? integrationRecordingOptions.recordOutputs : methodTelemetryOptions.recordOutputs !== undefined ? methodTelemetryOptions.recordOutputs : telemetryExplicitlyEnabled === true ? true // When telemetry is explicitly enabled, default to recording inputs : defaultRecordingEnabled; return { recordInputs, recordOutputs }; } /** * This detects is added by the Sentry Vercel AI Integration to detect if the integration should * be enabled. * * It also patches the `ai` module to enable Vercel AI telemetry automatically for all methods. */ class SentryVercelAiInstrumentation extends InstrumentationBase { __init() {this._isPatched = false;} __init2() {this._callbacks = [];} constructor(config = {}) { super('@sentry/instrumentation-vercel-ai', SDK_VERSION, config);SentryVercelAiInstrumentation.prototype.__init.call(this);SentryVercelAiInstrumentation.prototype.__init2.call(this); } /** * Initializes the instrumentation by defining the modules to be patched. */ init() { const module = new InstrumentationNodeModuleDefinition('ai', ['>=3.0.0 <6'], this._patch.bind(this)); return module; } /** * Call the provided callback when the module is patched. * If it has already been patched, the callback will be called immediately. */ callWhenPatched(callback) { if (this._isPatched) { callback(); } else { this._callbacks.push(callback); } } /** * Patches module exports to enable Vercel AI telemetry. */ _patch(moduleExports) { this._isPatched = true; this._callbacks.forEach(callback => callback()); this._callbacks = []; const generatePatch = (originalMethod) => { return new Proxy(originalMethod, { apply: (target, thisArg, args) => { const existingExperimentalTelemetry = args[0].experimental_telemetry || {}; const isEnabled = existingExperimentalTelemetry.isEnabled; const client = getClient(); const integration = client?.getIntegrationByName(INTEGRATION_NAME); const integrationOptions = integration?.options; const shouldRecordInputsAndOutputs = integration ? Boolean(client?.getOptions().sendDefaultPii) : false; const { recordInputs, recordOutputs } = determineRecordingSettings( integrationOptions, existingExperimentalTelemetry, isEnabled, shouldRecordInputsAndOutputs, ); args[0].experimental_telemetry = { ...existingExperimentalTelemetry, isEnabled: isEnabled !== undefined ? isEnabled : true, recordInputs, recordOutputs, }; return handleCallbackErrors( () => Reflect.apply(target, thisArg, args), error => { // This error bubbles up to unhandledrejection handler (if not handled before), // where we do not know the active span anymore // So to circumvent this, we set the active span on the error object // which is picked up by the unhandledrejection handler if (error && typeof error === 'object') { addNonEnumerableProperty(error, '_sentry_active_span', getActiveSpan()); } }, () => {}, result => { checkResultForToolErrors(result); }, ); }, }); }; // Is this an ESM module? // https://tc39.es/ecma262/#sec-module-namespace-objects if (Object.prototype.toString.call(moduleExports) === '[object Module]') { // In ESM we take the usual route and just replace the exports we want to instrument for (const method of INSTRUMENTED_METHODS) { moduleExports[method] = generatePatch(moduleExports[method]); } return moduleExports; } else { // In CJS we can't replace the exports in the original module because they // don't have setters, so we create a new object with the same properties const patchedModuleExports = INSTRUMENTED_METHODS.reduce((acc, curr) => { acc[curr] = generatePatch(moduleExports[curr]); return acc; }, {} ); return { ...moduleExports, ...patchedModuleExports }; } } } export { SentryVercelAiInstrumentation, determineRecordingSettings }; //# sourceMappingURL=instrumentation.js.map