Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

if (process.env.USE_ORCHESTRION) {
Sentry.experimentalUseDiagnosticsChannelInjection();
}

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

if (process.env.USE_ORCHESTRION) {
Sentry.experimentalUseDiagnosticsChannelInjection();
}

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/r
describe.each([
['6', {}, '^6.0.0'],
['7', {}, '7.0.0-beta.179'],
['7', { USE_ORCHESTRION: '1' }, '7.0.0-beta.179'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing v6 orchestrion E2E coverage

Low Severity

This feature adds orchestrion-based Vercel AI v6 instrumentation, but the integration matrix only enables USE_ORCHESTRION for AI v7. AI v6 with orchestrion—the main new subscriber path—is not exercised by the E2E suite.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 3417464. Configure here.

])('Vercel AI integration (version %s, env: %o)', (_, env: { USE_ORCHESTRION?: string }, vercelAiVersion: string) => {
afterAll(() => {
cleanupChildProcesses();
Expand Down
1 change: 1 addition & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { genericPoolIntegration } from './integrations/tracing/genericPool';
export { dataloaderIntegration } from './integrations/tracing/dataloader';
export { amqplibIntegration } from './integrations/tracing/amqplib';
export { vercelAIIntegration } from './integrations/tracing/vercelai';
export { vercelAiChannelIntegration } from './integrations/tracing/vercelai/channel-integration';
export { openAIIntegration } from './integrations/tracing/openai';
export { anthropicAIIntegration } from './integrations/tracing/anthropic-ai';
export { googleGenAIIntegration } from './integrations/tracing/google-genai';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Client, IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { tracingChannel as otelTracingChannel } from '@sentry/opentelemetry/tracing-channel';
import { subscribeVercelAiOrchestrionChannels, subscribeVercelAiTracingChannel } from '@sentry/server-utils';
import { INTEGRATION_NAME } from './constants';

// In channel-based (orchestrion) mode we emit our own `gen_ai.*` spans from the
// diagnostics channels. The `ai` SDK still emits its own native OpenTelemetry
// spans whenever the user enables `experimental_telemetry`, which would be
// duplicates. Every native `ai` span carries an `ai.operationId` attribute
// (e.g. `ai.generateText`, `ai.generateText.doGenerate`, `ai.toolCall`) at span
// start, whereas our channel spans use `vercel.ai.operationId` — so we drop the
// native ones up front via `ignoreSpans`, before any vercel-ai processing runs.
const NATIVE_VERCEL_AI_SPANS = { attributes: { 'ai.operationId': /^ai\./ } };

const _vercelAiChannelIntegration = (() => {
return {
name: INTEGRATION_NAME,
beforeSetup(client: Client) {
// Ensure we drop spans emitted by ai v6 or below
// To avoid double-instrumentation - in this scenario, we only want to rely on our own spans
const options = client.getOptions();
options.ignoreSpans = [...(options.ignoreSpans || []), NATIVE_VERCEL_AI_SPANS];
},
setupOnce() {
// v7: subscribe to the `ai` SDK's native `ai:telemetry` tracing channel.
// No-op on versions that don't publish to it, so it is always safe to call.
// The factory needs the Sentry OTel context manager, which `initOpenTelemetry()`
// registers after `setupOnce`, so defer a tick.
void Promise.resolve().then(() => subscribeVercelAiTracingChannel(otelTracingChannel));

// v6: there is no native channel — orchestrion injects `orchestrion:ai:*`
// channels which this adapter consumes via the same span core. Inert when
// orchestrion isn't active or on `ai` >= 7.
subscribeVercelAiOrchestrionChannels();
},
};
}) satisfies IntegrationFn;

/**
* Adds Sentry tracing instrumentation for the [ai](https://www.npmjs.com/package/ai) library via
* diagnostics channels — the channel-based counterpart to the OpenTelemetry `vercelAIIntegration`,
* used when diagnostics-channel injection is opted into.
*/
export const vercelAiChannelIntegration = defineIntegration(_vercelAiChannelIntegration);
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { mysqlChannelIntegration, detectOrchestrionSetup } from '@sentry/server-
import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register';
import type { DiagnosticsChannelInjection } from './diagnosticsChannelInjection';
import { setDiagnosticsChannelInjectionLoader } from './diagnosticsChannelInjection';
import { vercelAiChannelIntegration } from '../integrations/tracing/vercelai/channel-integration';

/**
* EXPERIMENTAL: opt into diagnostics-channel-based auto-instrumentation.
Expand Down Expand Up @@ -38,8 +39,8 @@ import { setDiagnosticsChannelInjectionLoader } from './diagnosticsChannelInject
export function experimentalUseDiagnosticsChannelInjection(): void {
setDiagnosticsChannelInjectionLoader(
(): DiagnosticsChannelInjection => ({
integrations: [mysqlChannelIntegration()],
replacedOtelIntegrationNames: ['Mysql'],
integrations: [mysqlChannelIntegration(), vercelAiChannelIntegration()],
replacedOtelIntegrationNames: ['Mysql', 'VercelAI'],
register: registerDiagnosticsChannelInjection,
detect: detectOrchestrionSetup,
}),
Expand Down
1 change: 1 addition & 0 deletions packages/server-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export type {
RedisTracingChannelSubscribers,
} from './redis/redis-dc-subscriber';
export { subscribeVercelAiTracingChannel } from './vercel-ai/vercel-ai-dc-subscriber';
export { subscribeVercelAiOrchestrionChannels } from './vercel-ai/vercel-ai-orchestrion-v6-subscriber';
12 changes: 12 additions & 0 deletions packages/server-utils/src/orchestrion/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
*/
export const CHANNELS = {
MYSQL_QUERY: 'orchestrion:mysql:query',
// Vercel AI (`ai`) v6: orchestrion injects these so the same channel-based
// integration that consumes `ai`'s native `ai:telemetry` channel (v7) can
// also instrument v6. Each maps to a top-level function in `ai`'s bundle.
VERCEL_AI_GENERATE_TEXT: 'orchestrion:ai:generateText',
VERCEL_AI_STREAM_TEXT: 'orchestrion:ai:streamText',
VERCEL_AI_EMBED: 'orchestrion:ai:embed',
VERCEL_AI_EXECUTE_TOOL_CALL: 'orchestrion:ai:executeToolCall',
// `resolveLanguageModel` is the single chokepoint every model call flows
// through; we wrap it to monkey-patch `doGenerate`/`doStream` on the returned
// model (the model-call site itself is an inline call with no injectable
// definition).
VERCEL_AI_RESOLVE_LANGUAGE_MODEL: 'orchestrion:ai:resolveLanguageModel',
} as const;

export type ChannelName = (typeof CHANNELS)[keyof typeof CHANNELS];
25 changes: 25 additions & 0 deletions packages/server-utils/src/orchestrion/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ import type { InstrumentationConfig } from '@apm-js-collab/code-transformer';
* `channelName` here is the unprefixed suffix; the actual diagnostics_channel
* name is `orchestrion:${module.name}:${channelName}` (see `channels.ts`).
*/
/**
* `ai` ships a single bundled entry per module system, so each instrumented
* function needs one config entry per file (the app loads whichever matches its
* module system). This expands a single target into both.
*/
function vercelAiV6Entries(channelName: string, functionName: string, kind: 'Async' | 'Sync'): InstrumentationConfig[] {
return ['dist/index.js', 'dist/index.mjs'].map(filePath => ({
channelName,
module: { name: 'ai', versionRange: '>=6.0.0 <7.0.0', filePath },
functionQuery: { functionName, kind },
}));
}

export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [
{
channelName: 'query',
Expand All @@ -32,6 +45,18 @@ export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [
// attach `'end'`/`'error'` listeners that finish the span.
functionQuery: { expressionName: 'query', kind: 'Auto' },
},
// Vercel AI v6: mirror the v7 native `ai:telemetry` channel by injecting
// channels into the top-level entry points. `resolveLanguageModel` is wrapped
// not to span it, but so the subscriber can monkey-patch `doGenerate`/
// `doStream` on the returned model (the only way to span the model call,
// which is an inline call with no injectable definition in `ai`).
// `streamText` returns its result synchronously (streaming is lazy), so it's
// `Sync`; the subscriber ends the span off the result's usage promise.
...vercelAiV6Entries('generateText', 'generateText', 'Async'),
...vercelAiV6Entries('streamText', 'streamText', 'Sync'),
...vercelAiV6Entries('embed', 'embed', 'Async'),
...vercelAiV6Entries('executeToolCall', 'executeToolCall', 'Async'),
...vercelAiV6Entries('resolveLanguageModel', 'resolveLanguageModel', 'Sync'),
];

/**
Expand Down
Loading
Loading