Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,7 @@ jobs:
needs: [job_get_metadata, job_build]
if: needs.job_build.outputs.changed_node_integration == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-24.04
timeout-minutes: 15
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
} from '../../../../../../packages/core/src/tracing/ai/gen-ai-attributes';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner';
import { isOrchestrionEnabled } from '../../../../utils';

/**
* Helper to match a typed attribute value in a SerializedStreamedSpan.
Expand All @@ -29,7 +30,7 @@ function attr(value: unknown) {
return expect.objectContaining({ value });
}

describe('Vercel AI integration (streaming)', () => {
describe.skipIf(isOrchestrionEnabled())('Vercel AI integration (streaming v4)', () => {
afterAll(() => {
cleanupChildProcesses();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
} from '../../../../../../packages/core/src/tracing/ai/gen-ai-attributes';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner';
import { isOrchestrionEnabled } from '../../../../utils';

/**
* Helper to match a typed attribute value in a SerializedStreamedSpan.
Expand All @@ -29,11 +30,13 @@ function attr(value: unknown) {
return expect.objectContaining({ value });
}

describe('Vercel AI integration (streaming, V6)', () => {
describe('Vercel AI integration (streaming, v6)', () => {
afterAll(() => {
cleanupChildProcesses();
});

const origin = isOrchestrionEnabled() ? 'auto.vercelai.channel' : 'auto.vercelai.otel';

const EXPECTED_SPANS_DEFAULT_PII_FALSE = {
items: expect.arrayContaining([
// First span - invoke_agent for simple generateText
Expand All @@ -48,10 +51,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: attr(30),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
'vercel.ai.pipeline.name': attr('generateText'),
'vercel.ai.streaming': attr(false),
'vercel.ai.request.headers.user-agent': expect.objectContaining({ value: expect.any(String) }),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
// Second span - generate_content for simple generateText
Expand All @@ -66,9 +66,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: attr(30),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('generate_content'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
'vercel.ai.pipeline.name': attr('generateText.doGenerate'),
'vercel.ai.streaming': attr(false),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
// Third span - invoke_agent for explicit telemetry generateText
Expand All @@ -82,7 +80,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: attr(30),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
// Fourth span - tool call invoke_agent
Expand All @@ -96,7 +94,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: attr(40),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
// Fifth span - tool call generate_content
Expand All @@ -110,7 +108,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: attr(40),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('generate_content'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
// Sixth span - execute_tool
Expand All @@ -124,7 +122,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_TOOL_TYPE_ATTRIBUTE]: attr('function'),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('execute_tool'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
]),
Expand All @@ -149,9 +147,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: attr(30),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
'vercel.ai.pipeline.name': attr('generateText'),
'vercel.ai.streaming': attr(false),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
// Second span - generate_content with input/output messages
Expand All @@ -170,7 +166,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: attr(30),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('generate_content'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
// Third span - explicit telemetry invoke_agent with messages
Expand All @@ -188,7 +184,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: attr(30),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
// Fourth span - tool call invoke_agent with messages (V6: no text part, only tool_call)
Expand All @@ -208,7 +204,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: attr(40),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
// Fifth span - tool call generate_content with available_tools
Expand All @@ -225,7 +221,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: attr(40),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('generate_content'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
// Sixth span - execute_tool with description and input/output
Expand All @@ -241,7 +237,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_TOOL_TYPE_ATTRIBUTE]: attr('function'),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('execute_tool'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
]),
Expand All @@ -254,7 +250,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
attributes: expect.objectContaining({
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
expect.objectContaining({
Expand All @@ -267,7 +263,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: attr(40),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('generate_content'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
expect.objectContaining({
Expand All @@ -279,7 +275,7 @@ describe('Vercel AI integration (streaming, V6)', () => {
[GEN_AI_TOOL_TYPE_ATTRIBUTE]: attr('function'),
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: attr('execute_tool'),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr('auto.vercelai.otel'),
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin),
}),
}),
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import {
GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
} from '../../../../../packages/core/src/tracing/ai/gen-ai-attributes';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
import { isOrchestrionEnabled } from '../../../utils';

describe('Vercel AI integration', () => {
describe.skipIf(isOrchestrionEnabled())('Vercel AI integration (v4)', () => {
afterAll(() => {
cleanupChildProcesses();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import {
GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
} from '../../../../../../packages/core/src/tracing/ai/gen-ai-attributes';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner';
import { isOrchestrionEnabled } from '../../../../utils';

describe('Vercel AI integration (V5)', () => {
describe.skipIf(isOrchestrionEnabled())('Vercel AI integration (v5)', () => {
afterAll(() => {
cleanupChildProcesses();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as Sentry from '@sentry/node';
import { streamText } from 'ai';
import { MockLanguageModelV3, simulateReadableStream } from 'ai/test';

function makeStreamModel(text) {
return new MockLanguageModelV3({
doStream: async () => ({
stream: simulateReadableStream({
chunks: [
{ type: 'stream-start', warnings: [] },
{ type: 'text-start', id: '0' },
{ type: 'text-delta', id: '0', delta: text },
{ type: 'text-end', id: '0' },
{
type: 'finish',
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 },
},
},
],
}),
}),
});
}

async function consume(result) {
for await (const _part of result.fullStream) {
void _part;
}
}

async function run() {
// A single model instance shared by two *concurrent* streamText calls. The shared model carries
// only a single captured-parent slot, so naive parent tracking could attribute a model call to
// whichever operation resolved the model last. Each `generate_content` (doStream) must land under
// its own `invoke_agent`, and both invoke_agents under `main`.
const sharedModel = makeStreamModel('shared!');

await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
// Start both operations before consuming either, so both resolve the shared model first.
const streams = [
streamText({
experimental_telemetry: { isEnabled: true },
model: sharedModel,
prompt: 'Concurrent stream A?',
}),
streamText({
experimental_telemetry: { isEnabled: true },
model: sharedModel,
prompt: 'Concurrent stream B?',
}),
];

await Promise.all(streams.map(consume));
});
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as Sentry from '@sentry/node';
import { embed, embedMany } from 'ai';
import { MockEmbeddingModelV3 } from 'ai/test';

async function run() {
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
// Single embedding
await embed({
model: new MockEmbeddingModelV3({
doEmbed: async () => ({
embeddings: [[0.1, 0.2, 0.3]],
usage: { tokens: 10 },
}),
}),
value: 'Embedding test!',
});

// Multiple embeddings
await embedMany({
model: new MockEmbeddingModelV3({
maxEmbeddingsPerCall: 5,
doEmbed: async () => ({
embeddings: [
[0.1, 0.2, 0.3],
[0.4, 0.5, 0.6],
],
usage: { tokens: 20 },
}),
}),
values: ['First input', 'Second input'],
});
});
}

run();
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ async function run() {
prompt: 'Where is the first span?',
});

// This span should have input and output prompts attached because telemetry is explicitly enabled.
// Vercel AI treats this as recording inputs and outputs by default
// but in v7, we do not have access to this, so we do not override data collection settings for this
// Recording and inputs explicitly enabled
await generateText({
experimental_telemetry: { isEnabled: true },
experimental_telemetry: { isEnabled: true, recordInputs: true, recordOutputs: true },
model: new MockLanguageModelV3({
doGenerate: async () => ({
finishReason: { unified: 'stop', raw: 'stop' },
Expand Down
Loading
Loading