From fa69f7b449a4289ab1a16be6cc1688ce194ed46f Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 6 Nov 2026 15:03:40 +0100 Subject: [PATCH 1/2] fix(server-utils): Record Vercel AI system instructions from message objects Closes: #26079 Co-Authored-By: Claude Opus 5.5 --- .../v6_v7/scenario-system-message.mjs | 42 +++++++++++++++++++ .../suites/tracing/vercelai/v6_v7/test.ts | 30 +++++++++++++ .../vercel-ai/vercel-ai-dc-subscriber.ts | 15 +++++-- .../vercel-ai-orchestrion-subscriber.ts | 2 +- 4 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-system-message.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-system-message.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-system-message.mjs new file mode 100644 index 000000000000..beb57a683162 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-system-message.mjs @@ -0,0 +1,42 @@ +import * as Sentry from '@sentry/node'; +import { generateText } from 'ai'; +import { MockLanguageModelV3 } from 'ai/test'; + +const model = () => + new MockLanguageModelV3({ + doGenerate: async () => ({ + finishReason: { unified: 'stop', raw: 'stop' }, + usage: { + inputTokens: { total: 10, noCache: 10, cached: 0 }, + outputTokens: { total: 20, noCache: 20, cached: 0 }, + totalTokens: { total: 30, noCache: 30, cached: 0 }, + }, + content: [{ type: 'text', text: 'ok' }], + warnings: [], + }), + }); + +const brief = { role: 'system', content: 'Be brief.' }; +const kind = { role: 'system', content: 'Be kind.' }; + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + // v6 reads `system`, v7 reads `instructions`. + await generateText({ + experimental_telemetry: { isEnabled: true, recordInputs: true }, + system: brief, + instructions: brief, + model: model(), + prompt: 'hi', + }); + await generateText({ + experimental_telemetry: { isEnabled: true, recordInputs: true }, + system: [brief, kind], + instructions: [brief, kind], + model: model(), + prompt: 'hi', + }); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts index 0ac372b9c59d..9600c8fbbbae 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts @@ -1043,6 +1043,36 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe }, ); + createEsmTests( + __dirname, + 'scenario-system-message.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('records system instructions from system message objects', async () => { + await createRunner() + .expect({ + span: container => { + const systemInstructions = container.items + .filter(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.invoke_agent') + .map(span => span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]?.value); + + expect(systemInstructions).toEqual([ + '[{"type":"text","content":"Be brief."}]', + '[{"type":"text","content":"Be brief."},{"type":"text","content":"Be kind."}]', + ]); + }, + }) + .start() + .completed(); + }); + }, + { + additionalDependencies: { + ai: vercelAiVersion, + }, + }, + ); + createEsmTests( __dirname, 'scenario-openai-conversation.mjs', diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts index c1daaf73fc84..64e9175226de 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts @@ -945,15 +945,24 @@ function resolveRecording( return globalDefault === true; } +/** The `ai` SDK accepts the system prompt as `string | SystemModelMessage | SystemModelMessage[]`. */ +function getSystemInstructionParts(instructions: unknown): { type: 'text'; content: string }[] { + const messages = Array.isArray(instructions) ? instructions : [instructions]; + return messages.flatMap(message => { + const content = asString(isObjectLike(message) ? message.content : message); + return content ? [{ type: 'text' as const, content }] : []; + }); +} + function buildInputMessageAttributes(event: Record): Record { const attributes: Record = {}; // `ai` >= 7 forbids system messages in `messages`/`prompt` and exposes the system prompt as a // separate `instructions` field. The OTel path lifts the system message out of the prompt into // `gen_ai.system_instructions` as `[{ type: 'text', content }]`; mirror that shape here. - const instructions = asString(event.instructions); - if (instructions) { - attributes[GEN_AI_SYSTEM_INSTRUCTIONS] = stringify([{ type: 'text', content: instructions }]); + const instructions = getSystemInstructionParts(event.instructions); + if (instructions.length) { + attributes[GEN_AI_SYSTEM_INSTRUCTIONS] = stringify(instructions); } // The AI SDK start events extend `StandardizedPrompt`; messages live on `messages`, otherwise the diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts index 2a59de79b4f8..2947af526c01 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts @@ -666,7 +666,7 @@ function buildTextMessage(type: 'generateText' | 'streamText' | 'generateObject' // The `ai` SDK takes the system prompt as a top-level `system` option (all of v4/v5/v6); the // shared core lifts `event.instructions` into the system-instructions attribute, matching v7's // native channel (which carries it as a distinct field rather than inside the messages array). - instructions: asString(options.system), + instructions: options.system, // Normalize to the message-array shape the shared core (and v7's channel) expects: a bare string // `prompt` becomes a single user message, matching the SDK's own normalization. messages: normalizePromptMessages(options), From 7ef90b4e8d858847cfdd870361257b437fb56fe1 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 6 Nov 2026 15:26:35 +0100 Subject: [PATCH 2/2] Use op constant in Vercel AI system message test --- .../suites/tracing/vercelai/v6_v7/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts index 9600c8fbbbae..ff22a55cc9c3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts @@ -1053,7 +1053,7 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe .expect({ span: container => { const systemInstructions = container.items - .filter(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.invoke_agent') + .filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_INVOKE_AGENT) .map(span => span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]?.value); expect(systemInstructions).toEqual([