diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-generate-object.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-generate-object.mjs new file mode 100644 index 000000000000..64d0d3ba0ec7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-generate-object.mjs @@ -0,0 +1,30 @@ +import * as Sentry from '@sentry/node'; +import { generateObject } from 'ai'; +import { MockLanguageModelV1 } from 'ai/test'; +import { z } from 'zod'; + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + // Test generateObject with schema + await generateObject({ + model: new MockLanguageModelV1({ + defaultObjectGenerationMode: 'json', + doGenerate: async () => ({ + rawCall: { rawPrompt: null, rawSettings: {} }, + finishReason: 'stop', + usage: { promptTokens: 15, completionTokens: 25 }, + text: '{ "name": "John Doe", "age": 30 }', + }), + }), + schema: z.object({ + name: z.string().describe('The name of the person'), + age: z.number().describe('The age of the person'), + }), + schemaName: 'Person', + schemaDescription: 'A person with name and age', + prompt: 'Generate a person object', + }); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test-generate-object.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test-generate-object.ts new file mode 100644 index 000000000000..1e87b63535ac --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test-generate-object.ts @@ -0,0 +1,67 @@ +import { afterAll, describe, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; + +describe('Vercel AI integration - generateObject', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + const EXPECTED_TRANSACTION = { + transaction: 'main', + spans: expect.arrayContaining([ + // generateObject span + expect.objectContaining({ + data: expect.objectContaining({ + 'vercel.ai.model.id': 'mock-model-id', + 'vercel.ai.model.provider': 'mock-provider', + 'vercel.ai.operationId': 'ai.generateObject', + 'vercel.ai.pipeline.name': 'generateObject', + 'vercel.ai.streaming': false, + 'vercel.ai.settings.mode': 'json', + 'vercel.ai.settings.output': 'object', + 'gen_ai.request.schema': expect.any(String), + 'gen_ai.response.model': 'mock-model-id', + 'gen_ai.usage.input_tokens': 15, + 'gen_ai.usage.output_tokens': 25, + 'gen_ai.usage.total_tokens': 40, + 'operation.name': 'ai.generateObject', + 'sentry.op': 'gen_ai.invoke_agent', + 'sentry.origin': 'auto.vercelai.otel', + }), + description: 'generateObject', + op: 'gen_ai.invoke_agent', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + // generateObject.doGenerate span + expect.objectContaining({ + data: expect.objectContaining({ + 'sentry.origin': 'auto.vercelai.otel', + 'sentry.op': 'gen_ai.generate_object', + 'operation.name': 'ai.generateObject.doGenerate', + 'vercel.ai.operationId': 'ai.generateObject.doGenerate', + 'vercel.ai.model.provider': 'mock-provider', + 'vercel.ai.model.id': 'mock-model-id', + 'vercel.ai.pipeline.name': 'generateObject.doGenerate', + 'vercel.ai.streaming': false, + 'gen_ai.system': 'mock-provider', + 'gen_ai.request.model': 'mock-model-id', + 'gen_ai.response.model': 'mock-model-id', + 'gen_ai.usage.input_tokens': 15, + 'gen_ai.usage.output_tokens': 25, + 'gen_ai.usage.total_tokens': 40, + }), + description: 'generate_object mock-model-id', + op: 'gen_ai.generate_object', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + ]), + }; + + createEsmAndCjsTests(__dirname, 'scenario-generate-object.mjs', 'instrument.mjs', (createRunner, test) => { + test('captures generateObject spans with schema attributes', async () => { + await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); + }); + }); +}); diff --git a/packages/core/src/utils/vercel-ai/index.ts b/packages/core/src/utils/vercel-ai/index.ts index 912dcaee3bc4..9b1cc2bc8aae 100644 --- a/packages/core/src/utils/vercel-ai/index.ts +++ b/packages/core/src/utils/vercel-ai/index.ts @@ -17,6 +17,7 @@ import { AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE, AI_RESPONSE_TEXT_ATTRIBUTE, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, + AI_SCHEMA_ATTRIBUTE, AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE, AI_TOOL_CALL_ARGS_ATTRIBUTE, AI_TOOL_CALL_ID_ATTRIBUTE, @@ -125,6 +126,8 @@ function processEndedVercelAiSpan(span: SpanJSON): void { renameAttributeKey(attributes, AI_TOOL_CALL_ARGS_ATTRIBUTE, 'gen_ai.tool.input'); renameAttributeKey(attributes, AI_TOOL_CALL_RESULT_ATTRIBUTE, 'gen_ai.tool.output'); + renameAttributeKey(attributes, AI_SCHEMA_ATTRIBUTE, 'gen_ai.request.schema'); + addProviderMetadataToAttributes(attributes); // Change attributes namespaced with `ai.X` to `vercel.ai.X`