From ebbf070f6ebcd290f041521dd5c9f2d54f49f569 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 6 Aug 2026 16:13:29 +0200 Subject: [PATCH 1/3] ref(server-utils)!: Consolidate AI integration option types into shared `GenAiOptions` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every AI integration declared its own structurally-identical options interface (`OpenAiOptions`, `AnthropicAiOptions`, `GoogleGenAIOptions`, `LangChainOptions`, `LangGraphOptions`, `WorkersAiOptions`, `VercelAiOptions`, and the internal `VercelAiChannelOptions`) — all just `{ recordInputs?: boolean; recordOutputs?: boolean }`. Rename the existing internal `AIRecordingOptions` to `GenAiOptions`, give it crisp provider-agnostic JSDoc, export it from the package index, and use it everywhere the per-integration types were used. Deletes all the duplicated interfaces. The vercel-edge integration keeps its own (intentionally field-less) local options type, since that entrypoint cannot honor input/output recording. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/integrations/tracing/vercelai.ts | 4 ++-- .../deno/src/integrations/tracing/vercelai.ts | 4 ++-- .../server-utils/src/ai/anthropic-ai/index.ts | 11 ++++++----- .../server-utils/src/ai/anthropic-ai/types.ts | 14 ++------------ packages/server-utils/src/ai/core/utils.ts | 14 +++++++++++--- .../server-utils/src/ai/google-genai/index.ts | 9 +++++---- .../server-utils/src/ai/google-genai/types.ts | 11 ----------- .../src/ai/langchain/embeddings.ts | 8 ++++---- .../server-utils/src/ai/langchain/index.ts | 4 ++-- .../server-utils/src/ai/langchain/types.ts | 19 ++----------------- .../server-utils/src/ai/langgraph/index.ts | 11 ++++++----- .../server-utils/src/ai/langgraph/types.ts | 13 ++----------- .../server-utils/src/ai/langgraph/utils.ts | 5 +++-- packages/server-utils/src/ai/openai/index.ts | 9 +++++---- packages/server-utils/src/ai/openai/types.ts | 14 ++------------ .../server-utils/src/ai/workers-ai/index.ts | 6 +++--- .../server-utils/src/ai/workers-ai/types.ts | 4 ---- packages/server-utils/src/index.ts | 3 ++- .../integrations/tracing-channel/anthropic.ts | 11 ++++++----- .../tracing-channel/google-genai.ts | 11 ++++++----- .../integrations/tracing-channel/langchain.ts | 10 +++++----- .../integrations/tracing-channel/langgraph.ts | 9 +++++---- .../integrations/tracing-channel/openai.ts | 10 +++++----- .../integrations/tracing-channel/vercel-ai.ts | 7 +++---- packages/server-utils/src/vercel-ai/index.ts | 19 ++----------------- .../src/vercel-ai/vercel-ai-dc-subscriber.ts | 17 ++++++----------- .../vercel-ai-orchestrion-subscriber.ts | 16 ++++++++-------- 27 files changed, 105 insertions(+), 168 deletions(-) diff --git a/packages/cloudflare/src/integrations/tracing/vercelai.ts b/packages/cloudflare/src/integrations/tracing/vercelai.ts index 14658f1907a9..6c797f54ba79 100644 --- a/packages/cloudflare/src/integrations/tracing/vercelai.ts +++ b/packages/cloudflare/src/integrations/tracing/vercelai.ts @@ -10,9 +10,9 @@ import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, extendIntegration } from '@sentry/core'; -import { addVercelAiProcessors, vercelAiIntegration, type VercelAiOptions } from '@sentry/server-utils'; +import { addVercelAiProcessors, vercelAiIntegration, type GenAiOptions } from '@sentry/server-utils'; -const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { +const _vercelAIIntegration = ((options: GenAiOptions = {}) => { const inner = vercelAiIntegration(options); return extendIntegration(inner, { diff --git a/packages/deno/src/integrations/tracing/vercelai.ts b/packages/deno/src/integrations/tracing/vercelai.ts index 0a9dc56961d8..dd4bec0bad43 100644 --- a/packages/deno/src/integrations/tracing/vercelai.ts +++ b/packages/deno/src/integrations/tracing/vercelai.ts @@ -7,10 +7,10 @@ import { defineIntegration, extendIntegration } from '@sentry/core'; import { addVercelAiProcessors, vercelAiIntegration as serverUtilsVercelAiIntegration, - type VercelAiOptions, + type GenAiOptions, } from '@sentry/server-utils'; -const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { +const _vercelAIIntegration = ((options: GenAiOptions = {}) => { const inner = serverUtilsVercelAiIntegration(options); return extendIntegration(inner, { diff --git a/packages/server-utils/src/ai/anthropic-ai/index.ts b/packages/server-utils/src/ai/anthropic-ai/index.ts index 1d0f007f18ed..8314def5e129 100644 --- a/packages/server-utils/src/ai/anthropic-ai/index.ts +++ b/packages/server-utils/src/ai/anthropic-ai/index.ts @@ -25,6 +25,7 @@ import { } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../core/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; +import type { GenAiOptions } from '../core/utils'; import { getGenAiSpanOp, resolveAIRecordingOptions, @@ -33,7 +34,7 @@ import { } from '../core/utils'; import { ANTHROPIC_METHOD_REGISTRY } from './constants'; import { instrumentAsyncIterableStream, instrumentMessageStream } from './streaming'; -import type { AnthropicAiOptions, AnthropicAiResponse, AnthropicAiStreamingEvent, ContentBlock } from './types'; +import type { AnthropicAiResponse, AnthropicAiStreamingEvent, ContentBlock } from './types'; import { handleResponseError, messagesFromParams, setMessagesAttribute } from './utils'; // Set only while a streaming helper (e.g. `messages.stream()`) synchronously delegates to the @@ -200,7 +201,7 @@ function handleStreamingRequest( operationName: string, methodPath: string, params: Record | undefined, - options: AnthropicAiOptions, + options: GenAiOptions, isStreamRequested: boolean, isStreamingMethod: boolean, ): R | Promise { @@ -267,7 +268,7 @@ function instrumentMethod( methodPath: string, instrumentedMethod: InstrumentedMethodEntry, context: unknown, - options: AnthropicAiOptions, + options: GenAiOptions, ): (...args: T) => R | Promise { return new Proxy(originalMethod, { apply(target, thisArg, args: T): R | Promise { @@ -360,7 +361,7 @@ function instrumentMethod( * `instanceof` checks behave exactly as on an uninstrumented client, and non-instrumented * methods are left untouched. */ -function instrumentClientInPlace(client: T, options: AnthropicAiOptions): T { +function instrumentClientInPlace(client: T, options: GenAiOptions): T { for (const methodPath of Object.keys(ANTHROPIC_METHOD_REGISTRY) as Array) { const segments = methodPath.split('.'); const methodName = segments.pop() as string; @@ -402,6 +403,6 @@ function instrumentClientInPlace(client: T, options: Anthropic * @param options - Optional configuration for recording inputs and outputs * @returns The instrumented client with the same type as the input */ -export function instrumentAnthropicAiClient(anthropicAiClient: T, options?: AnthropicAiOptions): T { +export function instrumentAnthropicAiClient(anthropicAiClient: T, options?: GenAiOptions): T { return instrumentClientInPlace(anthropicAiClient, resolveAIRecordingOptions(options)); } diff --git a/packages/server-utils/src/ai/anthropic-ai/types.ts b/packages/server-utils/src/ai/anthropic-ai/types.ts index ba281ef82a0d..8078bc4b2978 100644 --- a/packages/server-utils/src/ai/anthropic-ai/types.ts +++ b/packages/server-utils/src/ai/anthropic-ai/types.ts @@ -1,16 +1,6 @@ +import type { GenAiOptions } from '../core/utils'; import type { ANTHROPIC_METHOD_REGISTRY } from './constants'; -export interface AnthropicAiOptions { - /** - * Enable or disable input recording. - */ - recordInputs?: boolean; - /** - * Enable or disable output recording. - */ - recordOutputs?: boolean; -} - export type Message = { role: 'user' | 'assistant'; content: string | unknown[]; @@ -81,7 +71,7 @@ export interface AnthropicAiClient { */ export interface AnthropicAiIntegration { name: string; - options: AnthropicAiOptions; + options: GenAiOptions; } /** diff --git a/packages/server-utils/src/ai/core/utils.ts b/packages/server-utils/src/ai/core/utils.ts index c770778a3369..0ab462495f91 100644 --- a/packages/server-utils/src/ai/core/utils.ts +++ b/packages/server-utils/src/ai/core/utils.ts @@ -17,8 +17,16 @@ import { } from '@sentry/conventions/attributes'; import { GENERAL_FUNCTION_SPAN_OP } from '@sentry/conventions/op'; -export interface AIRecordingOptions { +export interface GenAiOptions { + /** + * Record input messages/prompts on gen_ai spans. Defaults to the global + * `dataCollection.genAI.inputs` setting; an explicit value here takes precedence. + */ recordInputs?: boolean; + /** + * Record output text/responses on gen_ai spans. Defaults to the global + * `dataCollection.genAI.outputs` setting; an explicit value here takes precedence. + */ recordOutputs?: boolean; } @@ -60,13 +68,13 @@ export function getGenAiSpanOp(operationName: string): string { * Resolves AI recording options by falling back to the client's `dataCollection.genAI` settings. * Precedence: explicit option > dataCollection.genAI > true (genAI data collected by default) */ -export function resolveAIRecordingOptions(options?: T): T & Required { +export function resolveAIRecordingOptions(options?: T): T & Required { const genAI = getClient()?.getDataCollectionOptions().genAI; return { ...options, recordInputs: options?.recordInputs ?? genAI?.inputs ?? true, recordOutputs: options?.recordOutputs ?? genAI?.outputs ?? true, - } as T & Required; + } as T & Required; } /** diff --git a/packages/server-utils/src/ai/google-genai/index.ts b/packages/server-utils/src/ai/google-genai/index.ts index c7ec34b5b70d..4caf275ba38c 100644 --- a/packages/server-utils/src/ai/google-genai/index.ts +++ b/packages/server-utils/src/ai/google-genai/index.ts @@ -32,10 +32,11 @@ import { GEN_AI_USAGE_TOTAL_TOKENS, } from '@sentry/conventions/attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; +import type { GenAiOptions } from '../core/utils'; import { buildMethodPath, extractSystemInstructions, getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils'; import { GOOGLE_GENAI_METHOD_REGISTRY, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; import { instrumentStream } from './streaming'; -import type { Candidate, ContentPart, GoogleGenAIOptions, GoogleGenAIResponse } from './types'; +import type { Candidate, ContentPart, GoogleGenAIResponse } from './types'; import type { ContentListUnion, Message, PartListUnion } from './utils'; import { contentUnionToMessages } from './utils'; @@ -260,7 +261,7 @@ function instrumentMethod( methodPath: string, instrumentedMethod: InstrumentedMethodEntry, context: unknown, - options: GoogleGenAIOptions, + options: GenAiOptions, ): (...args: T) => R | Promise { const isEmbeddings = instrumentedMethod.operation === 'embeddings'; @@ -339,7 +340,7 @@ function instrumentMethod( * Create a deep proxy for Google GenAI client instrumentation * Recursively instruments methods and handles special cases like chats.create */ -function createDeepProxy(target: T, currentPath = '', options: GoogleGenAIOptions): T { +function createDeepProxy(target: T, currentPath = '', options: GenAiOptions): T { return new Proxy(target, { get: (t, prop, receiver) => { const value = Reflect.get(t, prop, receiver); @@ -406,6 +407,6 @@ function createDeepProxy(target: T, currentPath = '', options: * const response = await chat.sendMessage({ message: 'Hello' }); * ``` */ -export function instrumentGoogleGenAIClient(client: T, options?: GoogleGenAIOptions): T { +export function instrumentGoogleGenAIClient(client: T, options?: GenAiOptions): T { return createDeepProxy(client, '', resolveAIRecordingOptions(options)); } diff --git a/packages/server-utils/src/ai/google-genai/types.ts b/packages/server-utils/src/ai/google-genai/types.ts index d1729dd68f36..1de6c249acd6 100644 --- a/packages/server-utils/src/ai/google-genai/types.ts +++ b/packages/server-utils/src/ai/google-genai/types.ts @@ -1,16 +1,5 @@ import type { GOOGLE_GENAI_METHOD_REGISTRY } from './constants'; -export interface GoogleGenAIOptions { - /** - * Enable or disable input recording. - */ - recordInputs?: boolean; - /** - * Enable or disable output recording. - */ - recordOutputs?: boolean; -} - /** * Google GenAI Content Part * @see https://ai.google.dev/api/rest/v1/Content#Part diff --git a/packages/server-utils/src/ai/langchain/embeddings.ts b/packages/server-utils/src/ai/langchain/embeddings.ts index 23ba6fe07f69..41145b2789bf 100644 --- a/packages/server-utils/src/ai/langchain/embeddings.ts +++ b/packages/server-utils/src/ai/langchain/embeddings.ts @@ -17,9 +17,9 @@ import { GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE, } from '../core/gen-ai-attributes'; +import type { GenAiOptions } from '../core/utils'; import { resolveAIRecordingOptions } from '../core/utils'; import { LANGCHAIN_ORIGIN } from './constants'; -import type { LangChainOptions } from './types'; /** * Infers the AI provider system name from the embedding class instance. @@ -70,7 +70,7 @@ function extractEmbeddingAttributes(instance: unknown): Record export function _INTERNAL_getLangChainEmbeddingsSpanOptions( instance: unknown, input: unknown, - options: LangChainOptions = {}, + options: GenAiOptions = {}, ): { name: string; op: string; attributes: Record } { const { recordInputs } = resolveAIRecordingOptions(options); const attributes = extractEmbeddingAttributes(instance); @@ -94,7 +94,7 @@ export function _INTERNAL_getLangChainEmbeddingsSpanOptions( */ export function instrumentEmbeddingMethod( originalMethod: (...args: unknown[]) => Promise, - options: LangChainOptions = {}, + options: GenAiOptions = {}, ): (...args: unknown[]) => Promise { return new Proxy(originalMethod, { apply(target, thisArg, args: unknown[]): Promise { @@ -128,7 +128,7 @@ export function instrumentEmbeddingMethod( * await embeddings.embedDocuments(['doc1', 'doc2']); * ``` */ -export function instrumentLangChainEmbeddings(instance: T, options?: LangChainOptions): T { +export function instrumentLangChainEmbeddings(instance: T, options?: GenAiOptions): T { const embeddingsInstance = instance as Record; if (typeof embeddingsInstance.embedQuery === 'function') { diff --git a/packages/server-utils/src/ai/langchain/index.ts b/packages/server-utils/src/ai/langchain/index.ts index 1f6ba8754f79..15688863c2da 100644 --- a/packages/server-utils/src/ai/langchain/index.ts +++ b/packages/server-utils/src/ai/langchain/index.ts @@ -16,13 +16,13 @@ import { GEN_AI_TOOL_DEFINITIONS, GEN_AI_TOOL_NAME, } from '@sentry/conventions/attributes'; +import type { GenAiOptions } from '../core/utils'; import { resolveAIRecordingOptions } from '../core/utils'; import { LANGCHAIN_ORIGIN } from './constants'; import type { LangChainCallbackHandler, LangChainLLMResult, LangChainMessage, - LangChainOptions, LangChainSerialized, } from './types'; import { @@ -40,7 +40,7 @@ import { * * This is a stateful handler that tracks spans across multiple LangChain executions. */ -export function createLangChainCallbackHandler(options: LangChainOptions = {}): LangChainCallbackHandler { +export function createLangChainCallbackHandler(options: GenAiOptions = {}): LangChainCallbackHandler { const { recordInputs, recordOutputs } = resolveAIRecordingOptions(options); // Internal state - single instance tracks all spans diff --git a/packages/server-utils/src/ai/langchain/types.ts b/packages/server-utils/src/ai/langchain/types.ts index 30034e3a1c4d..1129aaa877ff 100644 --- a/packages/server-utils/src/ai/langchain/types.ts +++ b/packages/server-utils/src/ai/langchain/types.ts @@ -1,19 +1,4 @@ -/** - * Options for LangChain integration - */ -export interface LangChainOptions { - /** - * Whether to record input messages/prompts - * @default false (respects `dataCollection.genAI.inputs`) - */ - recordInputs?: boolean; - - /** - * Whether to record output text and responses - * @default false (respects `dataCollection.genAI.outputs`) - */ - recordOutputs?: boolean; -} +import type { GenAiOptions } from '../core/utils'; /** * LangChain Serialized type (compatible with @langchain/core) @@ -107,7 +92,7 @@ export interface LangChainLLMResult { */ export interface LangChainIntegration { name: string; - options: LangChainOptions; + options: GenAiOptions; } /** diff --git a/packages/server-utils/src/ai/langgraph/index.ts b/packages/server-utils/src/ai/langgraph/index.ts index 5a12a972dc9d..a3e87b4f63fe 100644 --- a/packages/server-utils/src/ai/langgraph/index.ts +++ b/packages/server-utils/src/ai/langgraph/index.ts @@ -18,12 +18,13 @@ import { GEN_AI_TOOL_DEFINITIONS, } from '@sentry/conventions/attributes'; import { GEN_AI_INVOKE_AGENT_OPERATION_ATTRIBUTE } from '../core/gen-ai-attributes'; +import type { GenAiOptions } from '../core/utils'; import { extractSystemInstructions, resolveAIRecordingOptions } from '../core/utils'; import { createLangChainCallbackHandler } from '../langchain'; import type { BaseChatModel, LangChainMessage } from '../langchain/types'; import { normalizeLangChainMessages } from '../langchain/utils'; import { LANGGRAPH_ORIGIN } from './constants'; -import type { CompiledGraph, LangGraphOptions } from './types'; +import type { CompiledGraph } from './types'; import { extractAgentNameFromParams, extractLLMFromParams, @@ -43,7 +44,7 @@ const SENTRY_PATCHED = '__sentry_patched__'; */ export function instrumentStateGraphCompile( originalCompile: (...args: unknown[]) => CompiledGraph, - options: LangGraphOptions, + options: GenAiOptions, ): (...args: unknown[]) => CompiledGraph { if (Object.prototype.hasOwnProperty.call(originalCompile, SENTRY_PATCHED)) { return originalCompile; @@ -91,7 +92,7 @@ export function instrumentCompiledGraphInvoke( originalInvoke: (...args: unknown[]) => Promise, graphInstance: CompiledGraph, compileOptions: Record, - options: LangGraphOptions, + options: GenAiOptions, llm?: BaseChatModel | null, sentryCallbackHandler?: unknown, ): (...args: unknown[]) => Promise { @@ -203,7 +204,7 @@ export function instrumentCompiledGraphInvoke( */ export function instrumentCreateReactAgent( originalCreateReactAgent: (...args: unknown[]) => CompiledGraph, - options?: LangGraphOptions, + options?: GenAiOptions, ): (...args: unknown[]) => CompiledGraph { if (Object.prototype.hasOwnProperty.call(originalCreateReactAgent, SENTRY_PATCHED)) { return originalCreateReactAgent; @@ -284,7 +285,7 @@ export function instrumentCreateReactAgent( // eslint-disable-next-line @typescript-eslint/no-explicit-any export function instrumentStateGraph any }>( stateGraph: T, - options?: LangGraphOptions, + options?: GenAiOptions, ): T { stateGraph.compile = instrumentStateGraphCompile(stateGraph.compile, resolveAIRecordingOptions(options)); diff --git a/packages/server-utils/src/ai/langgraph/types.ts b/packages/server-utils/src/ai/langgraph/types.ts index b16f9718c69e..f02b2bb4aa03 100644 --- a/packages/server-utils/src/ai/langgraph/types.ts +++ b/packages/server-utils/src/ai/langgraph/types.ts @@ -1,13 +1,4 @@ -export interface LangGraphOptions { - /** - * Enable or disable input recording. - */ - recordInputs?: boolean; - /** - * Enable or disable output recording. - */ - recordOutputs?: boolean; -} +import type { GenAiOptions } from '../core/utils'; /** * LangGraph Tool definition from lc_kwargs @@ -81,5 +72,5 @@ export interface CompiledGraph { */ export interface LangGraphIntegration { name: string; - options: LangGraphOptions; + options: GenAiOptions; } diff --git a/packages/server-utils/src/ai/langgraph/utils.ts b/packages/server-utils/src/ai/langgraph/utils.ts index 36c70cc3bd9b..fe980178b743 100644 --- a/packages/server-utils/src/ai/langgraph/utils.ts +++ b/packages/server-utils/src/ai/langgraph/utils.ts @@ -26,7 +26,8 @@ import { GEN_AI_EXECUTE_TOOL_OPERATION_ATTRIBUTE, GEN_AI_TOOL_CALL_ID_ATTRIBUTE import type { BaseChatModel, LangChainMessage } from '../langchain/types'; import { normalizeLangChainMessages } from '../langchain/utils'; import { LANGGRAPH_ORIGIN } from './constants'; -import type { CompiledGraph, LangGraphOptions, LangGraphTool } from './types'; +import type { GenAiOptions } from '../core/utils'; +import type { CompiledGraph, LangGraphTool } from './types'; /** * Extract LLM model object from createReactAgent params @@ -59,7 +60,7 @@ export function extractAgentNameFromParams(args: unknown[]): string | null { * * Wraps each tool's invoke() method in place. A marker prevents double-wrapping. */ -export function wrapToolsWithSpans(tools: unknown[], options: LangGraphOptions, agentName?: string): unknown[] { +export function wrapToolsWithSpans(tools: unknown[], options: GenAiOptions, agentName?: string): unknown[] { const SENTRY_WRAPPED = '__sentry_tool_wrapped__'; for (const tool of tools) { diff --git a/packages/server-utils/src/ai/openai/index.ts b/packages/server-utils/src/ai/openai/index.ts index ded2f28d755d..87a85cb2465c 100644 --- a/packages/server-utils/src/ai/openai/index.ts +++ b/packages/server-utils/src/ai/openai/index.ts @@ -20,6 +20,7 @@ import { GEN_AI_TOOL_DEFINITIONS, } from '@sentry/conventions/attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; +import type { GenAiOptions } from '../core/utils'; import { buildMethodPath, extractSystemInstructions, @@ -29,7 +30,7 @@ import { } from '../core/utils'; import { OPENAI_METHOD_REGISTRY } from './constants'; import { instrumentStream } from './streaming'; -import type { ChatCompletionChunk, OpenAiOptions, OpenAIStream, ResponseStreamingEvent } from './types'; +import type { ChatCompletionChunk, OpenAIStream, ResponseStreamingEvent } from './types'; import { addResponseAttributes, extractRequestParameters } from './utils'; /** @@ -135,7 +136,7 @@ function instrumentMethod( methodPath: string, instrumentedMethod: InstrumentedMethodEntry, context: unknown, - options: OpenAiOptions, + options: GenAiOptions, ): (...args: T) => Promise { return function instrumentedCall(...args: T): Promise { const operationName = instrumentedMethod.operation || 'unknown'; @@ -224,7 +225,7 @@ function instrumentMethod( /** * Create a deep proxy for OpenAI client instrumentation */ -function createDeepProxy(target: T, currentPath = '', options: OpenAiOptions): T { +function createDeepProxy(target: T, currentPath = '', options: GenAiOptions): T { return new Proxy(target, { get(obj: object, prop: string): unknown { const value = (obj as Record)[prop]; @@ -260,6 +261,6 @@ function createDeepProxy(target: T, currentPath = '', options: * Instrument an OpenAI client with Sentry tracing * Can be used across Node.js, Cloudflare Workers, and Vercel Edge */ -export function instrumentOpenAiClient(client: T, options?: OpenAiOptions): T { +export function instrumentOpenAiClient(client: T, options?: GenAiOptions): T { return createDeepProxy(client, '', resolveAIRecordingOptions(options)); } diff --git a/packages/server-utils/src/ai/openai/types.ts b/packages/server-utils/src/ai/openai/types.ts index dd6872bb691b..0700c8b978a3 100644 --- a/packages/server-utils/src/ai/openai/types.ts +++ b/packages/server-utils/src/ai/openai/types.ts @@ -1,3 +1,4 @@ +import type { GenAiOptions } from '../core/utils'; import type { OPENAI_METHOD_REGISTRY } from './constants'; /** @@ -13,17 +14,6 @@ export type AttributeValue = | Array | Array; -export interface OpenAiOptions { - /** - * Enable or disable input recording. - */ - recordInputs?: boolean; - /** - * Enable or disable output recording. - */ - recordOutputs?: boolean; -} - export interface OpenAiClient { responses?: { create: (...args: unknown[]) => Promise; @@ -357,7 +347,7 @@ export interface OpenAIStream extends AsyncIterable { */ export interface OpenAiIntegration { name: string; - options: OpenAiOptions; + options: GenAiOptions; } /** diff --git a/packages/server-utils/src/ai/workers-ai/index.ts b/packages/server-utils/src/ai/workers-ai/index.ts index cf5b0a467484..59deb7542f84 100644 --- a/packages/server-utils/src/ai/workers-ai/index.ts +++ b/packages/server-utils/src/ai/workers-ai/index.ts @@ -6,10 +6,10 @@ import { startSpanManual, } from '@sentry/core'; import type { Span } from '@sentry/core'; +import type { GenAiOptions } from '../core/utils'; import { resolveAIRecordingOptions } from '../core/utils'; import { WORKERS_AI_INTEGRATION_NAME } from './constants'; import { instrumentWorkersAiStream } from './streaming'; -import type { WorkersAiOptions } from './types'; import { addRequestAttributes, addResponseAttributes, extractRequestAttributes, getOperationName } from './utils'; // Adapted from /server-utils/src/vercel-ai/util.ts @@ -29,7 +29,7 @@ function isReadableStream(value: unknown): value is ReadableStream { function instrumentRun( originalRun: (...args: unknown[]) => Promise, context: unknown, - options: WorkersAiOptions & Required>, + options: Required, ): (...args: unknown[]) => Promise { return function instrumentedRun(...args: unknown[]): Promise { // When another integration (e.g. Vercel AI via `workers-ai-provider`) is driving this binding, @@ -124,7 +124,7 @@ function instrumentRun( * const result = await ai.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' }); * ``` */ -export function instrumentWorkersAiClient(client: T, options?: WorkersAiOptions): T { +export function instrumentWorkersAiClient(client: T, options?: GenAiOptions): T { const resolvedOptions = resolveAIRecordingOptions(options); const instrumented = new Proxy(client, { diff --git a/packages/server-utils/src/ai/workers-ai/types.ts b/packages/server-utils/src/ai/workers-ai/types.ts index 7d49d491d9db..f3de2dfc9b2f 100644 --- a/packages/server-utils/src/ai/workers-ai/types.ts +++ b/packages/server-utils/src/ai/workers-ai/types.ts @@ -1,7 +1,3 @@ -import type { AIRecordingOptions } from '../core/utils'; - -export interface WorkersAiOptions extends AIRecordingOptions {} - /** * Minimal shape of the Cloudflare Workers AI binding (`env.AI`). * We only rely on the `run` method, everything else is passed through untouched. diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 7667cc4b2b9f..f6831d08c537 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -25,7 +25,8 @@ export type { TracingChannelPayloadWithSpan, } from './tracing-channel'; export type { InstrumentationConfig } from './orchestrion'; -export { vercelAiIntegration, type VercelAiOptions } from './vercel-ai'; +export type { GenAiOptions } from './ai/core/utils'; +export { vercelAiIntegration } from './vercel-ai'; export { fastifyIntegration, // oxlint-disable-next-line typescript/no-deprecated diff --git a/packages/server-utils/src/integrations/tracing-channel/anthropic.ts b/packages/server-utils/src/integrations/tracing-channel/anthropic.ts index d3f13aa765da..7b26e7c523a7 100644 --- a/packages/server-utils/src/integrations/tracing-channel/anthropic.ts +++ b/packages/server-utils/src/integrations/tracing-channel/anthropic.ts @@ -7,10 +7,11 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; +import type { GenAiOptions } from '../../ai/core/utils'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../../ai/core/utils'; import { addPrivateRequestAttributes, addResponseAttributes, extractRequestAttributes } from '../../ai/anthropic-ai'; import { instrumentAsyncIterableStream, instrumentMessageStream } from '../../ai/anthropic-ai/streaming'; -import type { AnthropicAiOptions, AnthropicAiResponse } from '../../ai/anthropic-ai/types'; +import type { AnthropicAiResponse } from '../../ai/anthropic-ai/types'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import { anthropicAiModuleNames } from '../../orchestrion/config/anthropic-ai'; @@ -41,7 +42,7 @@ interface AnthropicChannelContext { result?: unknown; } -const _anthropicIntegration = ((options: AnthropicAiOptions = {}) => { +const _anthropicIntegration = ((options: GenAiOptions = {}) => { return { name: INTEGRATION_NAME, setup(client) { @@ -50,7 +51,7 @@ const _anthropicIntegration = ((options: AnthropicAiOptions = {}) => { }; }) satisfies IntegrationFn; -function instrumentAnthropic(options: AnthropicAiOptions): void { +function instrumentAnthropic(options: GenAiOptions): void { for (const { channel, operation, methodPath, stream } of INSTRUMENTED_CHANNELS) { bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(channel), @@ -77,7 +78,7 @@ function createGenAiSpan( data: AnthropicChannelContext, operation: string, methodPath: string, - options: AnthropicAiOptions, + options: GenAiOptions, ): Span | undefined { const args = data.arguments ?? []; @@ -139,7 +140,7 @@ function wrapStreamResult( span: Span, data: AnthropicChannelContext, stream: StreamMode, - options: AnthropicAiOptions, + options: GenAiOptions, ): boolean { const { recordOutputs } = resolveAIRecordingOptions(options); const result = data.result; diff --git a/packages/server-utils/src/integrations/tracing-channel/google-genai.ts b/packages/server-utils/src/integrations/tracing-channel/google-genai.ts index b958263d41a1..2b3355a8071d 100644 --- a/packages/server-utils/src/integrations/tracing-channel/google-genai.ts +++ b/packages/server-utils/src/integrations/tracing-channel/google-genai.ts @@ -9,10 +9,11 @@ import { spanToJSON, startInactiveSpan, } from '@sentry/core'; +import type { GenAiOptions } from '../../ai/core/utils'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../../ai/core/utils'; import { addPrivateRequestAttributes, addResponseAttributes, extractRequestAttributes } from '../../ai/google-genai'; import { instrumentStream } from '../../ai/google-genai/streaming'; -import type { GoogleGenAIOptions, GoogleGenAIResponse } from '../../ai/google-genai/types'; +import type { GoogleGenAIResponse } from '../../ai/google-genai/types'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import { googleGenAiModuleNames } from '../../orchestrion/config/google-genai'; @@ -39,7 +40,7 @@ interface GoogleGenAIChannelContext { result?: unknown; } -const _googleGenAIIntegration = ((options: GoogleGenAIOptions = {}) => { +const _googleGenAIIntegration = ((options: GenAiOptions = {}) => { return { name: INTEGRATION_NAME, setup(client) { @@ -48,7 +49,7 @@ const _googleGenAIIntegration = ((options: GoogleGenAIOptions = {}) => { }; }) satisfies IntegrationFn; -function instrumentGoogleGenai(options: GoogleGenAIOptions): void { +function instrumentGoogleGenai(options: GenAiOptions): void { for (const { channel, operation } of INSTRUMENTED_CHANNELS) { bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(channel), @@ -77,7 +78,7 @@ function instrumentGoogleGenai(options: GoogleGenAIOptions): void { function createGenAiSpan( data: GoogleGenAIChannelContext, operation: string, - options: GoogleGenAIOptions, + options: GenAiOptions, ): Span | undefined { // When another provider (e.g. LangChain) is driving the SDK, it records the spans itself and marks this // provider as skipped; skip here to avoid double spans. @@ -131,7 +132,7 @@ function isAsyncIterable(value: unknown): value is AsyncIterableStream { * For a stream we patch `result[Symbol.asyncIterator]` in place so `instrumentStream` ends the * span when iteration finishes. */ -function wrapStreamResult(span: Span, data: GoogleGenAIChannelContext, options: GoogleGenAIOptions): boolean { +function wrapStreamResult(span: Span, data: GoogleGenAIChannelContext, options: GenAiOptions): boolean { const result = data.result; if (!isAsyncIterable(result)) { return false; diff --git a/packages/server-utils/src/integrations/tracing-channel/langchain.ts b/packages/server-utils/src/integrations/tracing-channel/langchain.ts index 41c756266a2a..c4b7a6e3791f 100644 --- a/packages/server-utils/src/integrations/tracing-channel/langchain.ts +++ b/packages/server-utils/src/integrations/tracing-channel/langchain.ts @@ -6,7 +6,7 @@ import { GOOGLE_GENAI_INTEGRATION_NAME } from '../../ai/google-genai/constants'; import { createLangChainCallbackHandler } from '../../ai/langchain'; import { LANGCHAIN_INTEGRATION_NAME } from '../../ai/langchain/constants'; import { _INTERNAL_getLangChainEmbeddingsSpanOptions } from '../../ai/langchain/embeddings'; -import type { LangChainOptions } from '../../ai/langchain/types'; +import type { GenAiOptions } from '../../ai/core/utils'; import { _INTERNAL_mergeLangChainCallbackHandler } from '../../ai/langchain/utils'; import { OPENAI_INTEGRATION_NAME } from '../../ai/openai/constants'; import { CHANNELS } from '../../orchestrion/channels'; @@ -42,7 +42,7 @@ function markProvidersSkipped(): void { _INTERNAL_skipAiProviderWrapping(SKIPPED_PROVIDERS); } -const _langChainIntegration = ((options: LangChainOptions = {}) => { +const _langChainIntegration = ((options: GenAiOptions = {}) => { return { name: INTEGRATION_NAME, setup(client) { @@ -60,7 +60,7 @@ const _langChainIntegration = ((options: LangChainOptions = {}) => { }; }) satisfies IntegrationFn; -function instrumentChatModels(options: LangChainOptions): void { +function instrumentChatModels(options: GenAiOptions): void { // One stateful handler tracks spans across the whole run tree, just like the OTel path. const sentryHandler = createLangChainCallbackHandler(options); @@ -90,7 +90,7 @@ function instrumentChatModels(options: LangChainOptions): void { } // Embeddings don't use the callback system. Wrap the method in its own span -function instrumentEmbeddings(options: LangChainOptions): void { +function instrumentEmbeddings(options: GenAiOptions): void { for (const channelName of langchainEmbeddingsChannels) { bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(channelName), @@ -100,7 +100,7 @@ function instrumentEmbeddings(options: LangChainOptions): void { } } -function createEmbeddingsSpan(data: EmbeddingsChannelContext, options: LangChainOptions): Span { +function createEmbeddingsSpan(data: EmbeddingsChannelContext, options: GenAiOptions): Span { // `embedQuery`/`embedDocuments` call the provider SDK internally, so skip that SDK's own // instrumentation before its channel fires (the producer runs at the embeddings channel's `start`). markProvidersSkipped(); diff --git a/packages/server-utils/src/integrations/tracing-channel/langgraph.ts b/packages/server-utils/src/integrations/tracing-channel/langgraph.ts index c3837b8dd8ea..1efb73f271ce 100644 --- a/packages/server-utils/src/integrations/tracing-channel/langgraph.ts +++ b/packages/server-utils/src/integrations/tracing-channel/langgraph.ts @@ -1,11 +1,12 @@ import * as diagnosticsChannel from 'node:diagnostics_channel'; import type { IntegrationFn } from '@sentry/core'; import { debug, defineIntegration } from '@sentry/core'; +import type { GenAiOptions } from '../../ai/core/utils'; import { resolveAIRecordingOptions } from '../../ai/core/utils'; import { createLangChainCallbackHandler } from '../../ai/langchain'; import { instrumentCompiledGraphInvoke } from '../../ai/langgraph'; import { LANGGRAPH_INTEGRATION_NAME } from '../../ai/langgraph/constants'; -import type { CompiledGraph, LangGraphOptions } from '../../ai/langgraph/types'; +import type { CompiledGraph } from '../../ai/langgraph/types'; import { extractAgentNameFromParams, extractLLMFromParams, wrapToolsWithSpans } from '../../ai/langgraph/utils'; import { DEBUG_BUILD } from '../../debug-build'; import { CHANNELS } from '../../orchestrion/channels'; @@ -30,7 +31,7 @@ interface CreateReactAgentChannelContext { // nested graph so its `invoke` is wrapped once (by the createReactAgent handler), not twice. let insideCreateReactAgent = false; -const _langGraphIntegration = ((options: LangGraphOptions = {}) => { +const _langGraphIntegration = ((options: GenAiOptions = {}) => { return { name: INTEGRATION_NAME, setup(client) { @@ -43,7 +44,7 @@ const _langGraphIntegration = ((options: LangGraphOptions = {}) => { }; }) satisfies IntegrationFn; -function instrumentLanggraph(options: LangGraphOptions): void { +function instrumentLanggraph(options: GenAiOptions): void { const resolvedOptions = resolveAIRecordingOptions(options); const sentryHandler = createLangChainCallbackHandler(resolvedOptions); @@ -105,7 +106,7 @@ function getFirstArgObject(args: unknown[] | undefined): Record function wrapCompiledGraphInvoke( graph: unknown, compileOptions: Record, - options: LangGraphOptions, + options: GenAiOptions, llm: ReturnType, sentryHandler: unknown, ): void { diff --git a/packages/server-utils/src/integrations/tracing-channel/openai.ts b/packages/server-utils/src/integrations/tracing-channel/openai.ts index 0e481c985c80..cf63fce499f7 100644 --- a/packages/server-utils/src/integrations/tracing-channel/openai.ts +++ b/packages/server-utils/src/integrations/tracing-channel/openai.ts @@ -6,10 +6,10 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; +import type { GenAiOptions } from '../../ai/core/utils'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../../ai/core/utils'; import { addRequestAttributes, extractRequestAttributes } from '../../ai/openai'; import { instrumentStream } from '../../ai/openai/streaming'; -import type { OpenAiOptions } from '../../ai/openai/types'; import { addResponseAttributes } from '../../ai/openai/utils'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan } from '../../tracing-channel'; @@ -38,7 +38,7 @@ interface OpenAiChatChannelContext { result?: unknown; } -const _openaiIntegration = ((options: OpenAiOptions = {}) => { +const _openaiIntegration = ((options: GenAiOptions = {}) => { return { name: INTEGRATION_NAME, setup(client) { @@ -47,7 +47,7 @@ const _openaiIntegration = ((options: OpenAiOptions = {}) => { }; }) satisfies IntegrationFn; -function instrumentOpenai(options: OpenAiOptions): void { +function instrumentOpenai(options: GenAiOptions): void { for (const { channel, operation } of INSTRUMENTED_CHANNELS) { bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(channel), @@ -67,7 +67,7 @@ function instrumentOpenai(options: OpenAiOptions): void { * Build the span for an instrumented `create` call. * Returning `undefined` opts the payload out so no span is opened. */ -function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, options: OpenAiOptions): Span | undefined { +function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, options: GenAiOptions): Span | undefined { // When another provider (e.g. LangChain) is driving the SDK, it records the spans itself and marks this // provider as skipped; skip here to avoid double spans. if (_INTERNAL_shouldSkipAiProviderWrapping(INTEGRATION_NAME)) { @@ -111,7 +111,7 @@ function isAsyncIterable(value: unknown): value is AsyncIterableStream { * to hand span-ending ownership to `instrumentStream`; `false` for non-streaming/errored results, which end * via the normal `beforeSpanEnd` path. */ -function wrapStreamResult(span: Span, data: OpenAiChatChannelContext, options: OpenAiOptions): boolean { +function wrapStreamResult(span: Span, data: OpenAiChatChannelContext, options: GenAiOptions): boolean { const result = data.result; if (!isAsyncIterable(result)) { return false; diff --git a/packages/server-utils/src/integrations/tracing-channel/vercel-ai.ts b/packages/server-utils/src/integrations/tracing-channel/vercel-ai.ts index 91bd475d20af..6444ef24dcf0 100644 --- a/packages/server-utils/src/integrations/tracing-channel/vercel-ai.ts +++ b/packages/server-utils/src/integrations/tracing-channel/vercel-ai.ts @@ -1,13 +1,12 @@ import type { Client, IntegrationFn } from '@sentry/core'; import { defineIntegration, extendIntegration } from '@sentry/core'; +import type { GenAiOptions } from '../../ai/core/utils'; import { vercelAiIntegration as baseVercelAiIntegration } from '../../vercel-ai'; import * as dc from 'node:diagnostics_channel'; import { vercelAiModuleNames } from '../../orchestrion/config/vercel-ai'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; import { subscribeVercelAiOrchestrionChannels } from '../../vercel-ai/vercel-ai-orchestrion-subscriber'; -type VercelAiOptions = Parameters[0]; - // In channel-based (orchestrion) mode we emit our own `gen_ai.*` spans from the // diagnostics channels. The `ai` SDK would otherwise emit its own native // OpenTelemetry spans whenever the user enables `experimental_telemetry`, which @@ -16,7 +15,7 @@ type VercelAiOptions = Parameters[0]; // `experimental_telemetry.isEnabled` to `false`, so `ai` falls back to its // internal no-op tracer and never creates the native spans in the first place. // See `subscribeVercelAiOrchestrionChannels`. -const _vercelAiIntegration = ((options: VercelAiOptions = {}) => { +const _vercelAiIntegration = ((options: GenAiOptions = {}) => { const parentIntegration = baseVercelAiIntegration(options); // The native `ai:telemetry` half is the base integration's own `setupOnce`; the @@ -29,7 +28,7 @@ const _vercelAiIntegration = ((options: VercelAiOptions = {}) => { }); }) satisfies IntegrationFn; -function instrumentVercelAiOrchestrion(options: VercelAiOptions): void { +function instrumentVercelAiOrchestrion(options: GenAiOptions): void { subscribeVercelAiOrchestrionChannels(dc.tracingChannel, options); } diff --git a/packages/server-utils/src/vercel-ai/index.ts b/packages/server-utils/src/vercel-ai/index.ts index 416ed37e92ed..f880379c9df7 100644 --- a/packages/server-utils/src/vercel-ai/index.ts +++ b/packages/server-utils/src/vercel-ai/index.ts @@ -1,24 +1,9 @@ import { defineIntegration, waitForTracingChannelBinding, type IntegrationFn } from '@sentry/core'; +import type { GenAiOptions } from '../ai/core/utils'; import { subscribeVercelAiTracingChannel } from './vercel-ai-dc-subscriber'; import * as dc from 'node:diagnostics_channel'; -export interface VercelAiOptions { - /** - * Enable or disable input recording. Enabled if `dataCollection.genAI.inputs` is `true` - * or if you set `isEnabled` to `true` in your ai SDK method telemetry settings. - * Integration-level options take precedence over global `dataCollection` config. - */ - recordInputs?: boolean; - - /** - * Enable or disable output recording. Enabled if `dataCollection.genAI.outputs` is `true` - * or if you set `isEnabled` to `true` in your ai SDK method telemetry settings. - * Integration-level options take precedence over global `dataCollection` config. - */ - recordOutputs?: boolean; -} - -const _vercelAiIntegration = ((options: VercelAiOptions = {}) => { +const _vercelAiIntegration = ((options: GenAiOptions = {}) => { return { name: 'VercelAI' as const, setupOnce() { diff --git a/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts index 8e1f65c35934..4205424f7d14 100644 --- a/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts @@ -37,6 +37,7 @@ import { withScope, } from '@sentry/core'; import type { TracingChannel } from 'node:diagnostics_channel'; +import type { GenAiOptions } from '../ai/core/utils'; import { getProviderMetadataAttributes } from '../ai/vercel-ai'; import { WORKERS_AI_INTEGRATION_NAME } from '../ai/workers-ai/constants'; import { bindTracingChannelToSpan } from '../tracing-channel'; @@ -196,12 +197,6 @@ export interface VercelAiChannelMessage { */ export type VercelAiTracingChannelFactory = (name: string) => TracingChannel; -/** Integration-level recording options, pinned at subscribe time so we never look the integration up per event. */ -export interface VercelAiChannelOptions { - recordInputs?: boolean; - recordOutputs?: boolean; -} - /** * Subscribe Sentry span handlers to the `ai` SDK's native telemetry tracing channel (`ai:telemetry`, * available in `ai` >= 7) and emit fully-formed `gen_ai.*` spans directly — no OpenTelemetry span @@ -216,7 +211,7 @@ export interface VercelAiChannelOptions { */ export function subscribeVercelAiTracingChannel( tracingChannel: VercelAiTracingChannelFactory, - options: VercelAiChannelOptions = {}, + options: GenAiOptions = {}, ): void { bindTracingChannelToSpan( tracingChannel(AI_SDK_TELEMETRY_TRACING_CHANNEL), @@ -243,7 +238,7 @@ export function subscribeVercelAiTracingChannel( */ function deferStreamedModelCallEnd( data: VercelAiChannelMessage, - options: VercelAiChannelOptions, + options: GenAiOptions, end: (error?: unknown) => void, ): boolean { if (data.type !== 'languageModelCall' || !isObjectLike(data.result)) { @@ -349,7 +344,7 @@ function addTokensToSpan(span: Span, attribute: string, value: number | undefine */ export function createSpanFromMessage( data: VercelAiChannelMessage, - channelOptions: VercelAiChannelOptions, + channelOptions: GenAiOptions, ): Span | undefined { const { type, event } = data; @@ -489,7 +484,7 @@ function buildToolSpan(event: Record, recordInputs: boolean): S export function enrichSpanOnEnd( span: Span, data: VercelAiChannelMessage, - channelOptions: VercelAiChannelOptions, + channelOptions: GenAiOptions, ): void { const { type, result } = data; if (!isObjectLike(result)) { @@ -677,7 +672,7 @@ export function captureToolError(span: Span, data: VercelAiChannelMessage, error function getRecordingOptions( event: Record, - channelOptions: VercelAiChannelOptions, + channelOptions: GenAiOptions, ): { recordInputs: boolean; recordOutputs: boolean; diff --git a/packages/server-utils/src/vercel-ai/vercel-ai-orchestrion-subscriber.ts b/packages/server-utils/src/vercel-ai/vercel-ai-orchestrion-subscriber.ts index 72fc3a04b4bc..d4f1400f7e6d 100644 --- a/packages/server-utils/src/vercel-ai/vercel-ai-orchestrion-subscriber.ts +++ b/packages/server-utils/src/vercel-ai/vercel-ai-orchestrion-subscriber.ts @@ -8,6 +8,7 @@ import { SPAN_STATUS_ERROR, withActiveSpan, } from '@sentry/core'; +import type { GenAiOptions } from '../ai/core/utils'; import { DEBUG_BUILD } from '../debug-build'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan, type TracingChannelPayloadWithSpan } from '../tracing-channel'; @@ -19,7 +20,6 @@ import { enrichSpanOnEnd, streamedResultToChannelResult, type VercelAiChannelMessage, - type VercelAiChannelOptions, type VercelAiTracingChannelFactory, } from './vercel-ai-dc-subscriber'; import { asString, isReadableStream, tapModelCallStream } from './util'; @@ -142,7 +142,7 @@ let subscribed = false; */ export function subscribeVercelAiOrchestrionChannels( tracingChannel: VercelAiTracingChannelFactory, - options: VercelAiChannelOptions = {}, + options: GenAiOptions = {}, ): void { if (subscribed) { return; @@ -220,7 +220,7 @@ function bindOperation( tracingChannel: VercelAiTracingChannelFactory, channelName: string, build: MessageBuilder, - options: VercelAiChannelOptions, + options: GenAiOptions, ): void { const channel = tracingChannel(channelName); @@ -396,7 +396,7 @@ function suppressNativeTelemetry(callOptions: Record, telemetry function subscribeResolveLanguageModel( tracingChannel: VercelAiTracingChannelFactory, channelName: string, - options: VercelAiChannelOptions, + options: GenAiOptions, ): void { tracingChannel(channelName).subscribe({ end(rawCtx) { @@ -451,7 +451,7 @@ function resolveModelCallParent(): Span | undefined { * `languageModelCall` span. Shared by the v5/v6 `resolveLanguageModel` path and the v4 path (which * patches the passed model at the operation start, as v4 has no `resolveLanguageModel`). */ -function patchModelMethods(model: PatchableModel, options: VercelAiChannelOptions): void { +function patchModelMethods(model: PatchableModel, options: GenAiOptions): void { if (model[PATCHED]) { return; } @@ -463,7 +463,7 @@ function patchModelMethods(model: PatchableModel, options: VercelAiChannelOption function patchModelMethod( model: PatchableModel, method: 'doGenerate' | 'doStream', - options: VercelAiChannelOptions, + options: GenAiOptions, ): void { const original = model[method]; if (typeof original !== 'function') { @@ -568,7 +568,7 @@ function patchModelMethod( * (before the call runs) is picked up. Tools without an `execute` (client-side tools) are skipped, * matching `executeTools`. */ -function patchOperationTools(tools: Record, options: VercelAiChannelOptions): void { +function patchOperationTools(tools: Record, options: GenAiOptions): void { // This runs inside the channel `start` transform (no upstream try/catch), so a throw here would break // the user's `ai` call. Instrumentation must never do that — degrade to no tool span instead. try { @@ -586,7 +586,7 @@ function patchToolExecute( toolName: string, tool: PatchableTool, tools: Record, - options: VercelAiChannelOptions, + options: GenAiOptions, ): void { const original = tool.execute; if (typeof original !== 'function' || tool[TOOL_PATCHED]) { From b6b3f390a8c0c69e856665d041992bce1e633e46 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 6 Aug 2026 16:23:44 +0200 Subject: [PATCH 2/3] style: Reflow signatures shortened by the GenAiOptions rename Shortening the option type names collapsed these multi-line signatures/imports under the print width, so oxfmt reformats them onto a single line. Apply the formatting so the CI format check passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/server-utils/src/ai/langchain/index.ts | 7 +------ .../src/integrations/tracing-channel/google-genai.ts | 6 +----- .../src/vercel-ai/vercel-ai-dc-subscriber.ts | 11 ++--------- .../src/vercel-ai/vercel-ai-orchestrion-subscriber.ts | 6 +----- 4 files changed, 5 insertions(+), 25 deletions(-) diff --git a/packages/server-utils/src/ai/langchain/index.ts b/packages/server-utils/src/ai/langchain/index.ts index 15688863c2da..9dc2d4711b8a 100644 --- a/packages/server-utils/src/ai/langchain/index.ts +++ b/packages/server-utils/src/ai/langchain/index.ts @@ -19,12 +19,7 @@ import { import type { GenAiOptions } from '../core/utils'; import { resolveAIRecordingOptions } from '../core/utils'; import { LANGCHAIN_ORIGIN } from './constants'; -import type { - LangChainCallbackHandler, - LangChainLLMResult, - LangChainMessage, - LangChainSerialized, -} from './types'; +import type { LangChainCallbackHandler, LangChainLLMResult, LangChainMessage, LangChainSerialized } from './types'; import { extractChatModelRequestAttributes, extractLLMRequestAttributes, diff --git a/packages/server-utils/src/integrations/tracing-channel/google-genai.ts b/packages/server-utils/src/integrations/tracing-channel/google-genai.ts index 2b3355a8071d..4653cd731863 100644 --- a/packages/server-utils/src/integrations/tracing-channel/google-genai.ts +++ b/packages/server-utils/src/integrations/tracing-channel/google-genai.ts @@ -75,11 +75,7 @@ function instrumentGoogleGenai(options: GenAiOptions): void { * Build the span for an instrumented call. * Returning `undefined` opts the payload out so no span is opened. */ -function createGenAiSpan( - data: GoogleGenAIChannelContext, - operation: string, - options: GenAiOptions, -): Span | undefined { +function createGenAiSpan(data: GoogleGenAIChannelContext, operation: string, options: GenAiOptions): Span | undefined { // When another provider (e.g. LangChain) is driving the SDK, it records the spans itself and marks this // provider as skipped; skip here to avoid double spans. if (_INTERNAL_shouldSkipAiProviderWrapping(INTEGRATION_NAME)) { diff --git a/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts index 4205424f7d14..e07f2be32ca8 100644 --- a/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/vercel-ai/vercel-ai-dc-subscriber.ts @@ -342,10 +342,7 @@ function addTokensToSpan(span: Span, attribute: string, value: number | undefine * invoke_agent span, matching the OTel-based output), so we reuse the active span and mark the * payload to skip ending it. */ -export function createSpanFromMessage( - data: VercelAiChannelMessage, - channelOptions: GenAiOptions, -): Span | undefined { +export function createSpanFromMessage(data: VercelAiChannelMessage, channelOptions: GenAiOptions): Span | undefined { const { type, event } = data; if (type === 'step' || !event || typeof event !== 'object') { @@ -481,11 +478,7 @@ function buildToolSpan(event: Record, recordInputs: boolean): S * Everything here is guarded: when a field is missing or the shape differs across `ai` versions, * we simply don't set the attribute rather than emit a malformed span. */ -export function enrichSpanOnEnd( - span: Span, - data: VercelAiChannelMessage, - channelOptions: GenAiOptions, -): void { +export function enrichSpanOnEnd(span: Span, data: VercelAiChannelMessage, channelOptions: GenAiOptions): void { const { type, result } = data; if (!isObjectLike(result)) { return; diff --git a/packages/server-utils/src/vercel-ai/vercel-ai-orchestrion-subscriber.ts b/packages/server-utils/src/vercel-ai/vercel-ai-orchestrion-subscriber.ts index d4f1400f7e6d..747f0c7e4dec 100644 --- a/packages/server-utils/src/vercel-ai/vercel-ai-orchestrion-subscriber.ts +++ b/packages/server-utils/src/vercel-ai/vercel-ai-orchestrion-subscriber.ts @@ -460,11 +460,7 @@ function patchModelMethods(model: PatchableModel, options: GenAiOptions): void { patchModelMethod(model, 'doStream', options); } -function patchModelMethod( - model: PatchableModel, - method: 'doGenerate' | 'doStream', - options: GenAiOptions, -): void { +function patchModelMethod(model: PatchableModel, method: 'doGenerate' | 'doStream', options: GenAiOptions): void { const original = model[method]; if (typeof original !== 'function') { return; From 3cbd0dfc31b70c46548ae92c41c1d9231a09ff87 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 6 Aug 2026 16:50:30 +0200 Subject: [PATCH 3/3] docs: Add migration entry for VercelAiOptions -> GenAiOptions Co-Authored-By: Claude Opus 4.8 (1M context) --- MIGRATION.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index cfaa4fa824c0..6cc57c419370 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -696,6 +696,14 @@ Sentry.init({ - (Vercel AI) The internal JSON-stringify workaround for array span attributes was removed. - AI integrations are no longer available in the browser SDK. They remain available in the server-side SDKs. - The AI instrumentation code moved out of `@sentry/core` into `@sentry/server-utils`. If you imported any AI helper **directly from `@sentry/core`**, import it from `@sentry/server-utils` instead (or keep importing it from your platform SDK, e.g. `@sentry/node`, if it re-exported that helper before — platform SDK availability is unchanged from v10). Affected helpers: `instrumentOpenAiClient`, `instrumentAnthropicAiClient`, `instrumentGoogleGenAIClient`, `instrumentWorkersAiClient`, `createLangChainCallbackHandler`, `instrumentLangChainEmbeddings`, `instrumentStateGraph`, `instrumentStateGraphCompile`, `instrumentCreateReactAgent`, `addVercelAiProcessors`. +- The `VercelAiOptions` type exported from `@sentry/server-utils` was removed. All AI integrations now share a single `GenAiOptions` type (`{ recordInputs?, recordOutputs? }`). Import `GenAiOptions` instead: + + ```js + // Before (v10) + import type { VercelAiOptions } from '@sentry/server-utils'; + // After (v11) + import type { GenAiOptions } from '@sentry/server-utils'; + ``` - The following low-level AI exports are no longer part of the public API (they were provider-instrumentation internals exported from `@sentry/core`): - Attribute/stream/util helpers: `extractOpenAiRequestAttributes`, `addOpenAiRequestAttributes`, `addOpenAiResponseAttributes`, `extractOpenAiRequestParameters`, `instrumentOpenAiStream`, `extractAnthropicRequestAttributes`, `addAnthropicRequestAttributes`, `addAnthropicResponseAttributes`, `instrumentAsyncIterableStream`, `instrumentMessageStream`, `extractGoogleGenAIRequestAttributes`, `addGoogleGenAIRequestAttributes`, `addGoogleGenAIResponseAttributes`, `instrumentGoogleGenAIStream`, `getProviderMetadataAttributes`, `getTruncatedJsonString`, `shouldEnableTruncation`, `resolveAIRecordingOptions`, `wrapToolsWithSpans`, `extractLLMFromParams`, `extractAgentNameFromParams`, `instrumentCompiledGraphInvoke`. - Integration-name constants: `OPENAI_INTEGRATION_NAME`, `ANTHROPIC_AI_INTEGRATION_NAME`, `GOOGLE_GENAI_INTEGRATION_NAME`, `LANGCHAIN_INTEGRATION_NAME`, `LANGGRAPH_INTEGRATION_NAME`.