-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
WIP: Implement orchestrion-based instrumentation for vercel-ai v6 #21658
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import * as api from '@opentelemetry/api'; | ||
| import type { Scope, Span, withActiveSpan as defaultWithActiveSpan } from '@sentry/core'; | ||
| import type { Scope, withActiveSpan as defaultWithActiveSpan } from '@sentry/core'; | ||
| import { getDefaultCurrentScope, getDefaultIsolationScope, setAsyncContextStrategy } from '@sentry/core'; | ||
| import { | ||
| SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY, | ||
|
|
@@ -12,20 +12,23 @@ import { getContextFromScope, getScopesFromContext } from './utils/contextData'; | |
| import { getActiveSpan } from './utils/getActiveSpan'; | ||
| import { getTraceData } from './utils/getTraceData'; | ||
| import { suppressTracing } from './utils/suppressTracing'; | ||
| import { getAsyncLocalStorage } from './asyncLocalStorageContextManager'; | ||
|
|
||
| interface ContextApi { | ||
| _getContextManager(): { | ||
| getAsyncLocalStorageLookup(): { | ||
| asyncLocalStorage: unknown; | ||
| }; | ||
| }; | ||
| _getContextManager(): | ||
| | undefined | ||
| | { | ||
| getAsyncLocalStorageLookup(): { | ||
| asyncLocalStorage: unknown; | ||
| }; | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the async context strategy to use follow the OTEL context under the hood. | ||
| * We handle forking a hub inside of our custom OTEL Context Manager (./otelContextManager.ts) | ||
| */ | ||
| export function setOpenTelemetryContextAsyncContextStrategy(): void { | ||
| export function setOpenTelemetryContextAsyncContextStrategy(options?: { skipOpenTelemetrySetup?: boolean }): void { | ||
| function getScopes(): CurrentScopes { | ||
| const ctx = api.context.active(); | ||
| const scopes = getScopesFromContext(ctx); | ||
|
|
@@ -117,13 +120,27 @@ export function setOpenTelemetryContextAsyncContextStrategy(): void { | |
| // than the OTEL one - but this is OK for here, as we now we'll only have OTEL spans passed around | ||
| withActiveSpan: withActiveSpan as typeof defaultWithActiveSpan, | ||
| getTracingChannelBinding: () => { | ||
| // Default case: by default we can just access the async local storage instance here | ||
| // this will work no matter if this called before or after the Otel ContextManager was setup | ||
| if (!options?.skipOpenTelemetrySetup) { | ||
| const asyncLocalStorage = getAsyncLocalStorage(); | ||
|
|
||
| return { | ||
| asyncLocalStorage, | ||
| getStoreWithActiveSpan: span => api.trace.setSpan(api.context.active(), span), | ||
| }; | ||
| } | ||
|
|
||
| // Else, if we have a custom context manager, we need to access it via the context manager | ||
| // this may not be available yet, if this is called before the Otel ContextManager was setup | ||
| // in this case, we need to return undefined and retry later, hoping that the setup works by then | ||
| try { | ||
| const contextManager = (api.context as unknown as ContextApi)._getContextManager(); | ||
| const lookup = contextManager.getAsyncLocalStorageLookup(); | ||
| const asyncLocalStorage = contextManager?.getAsyncLocalStorageLookup().asyncLocalStorage; | ||
|
|
||
| return { | ||
| asyncLocalStorage: lookup.asyncLocalStorage, | ||
| getStoreWithActiveSpan: (span: Span) => api.trace.setSpan(api.context.active(), span as api.Span), | ||
| asyncLocalStorage, | ||
| getStoreWithActiveSpan: span => api.trace.setSpan(api.context.active(), span as api.Span), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty binding skips retryMedium Severity With Reviewed by Cursor Bugbot for commit d05d40e. Configure here. |
||
| }; | ||
| } catch { | ||
| return undefined; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import type { IntegrationFn } from '@sentry/core'; | ||
| import { defineIntegration, waitForTracingChannelBinding } from '@sentry/core'; | ||
| import { vercelAiIntegration as baseVercelAiIntegration } from '../../vercel-ai'; | ||
| import * as dc from 'node:diagnostics_channel'; | ||
| import { subscribeVercelAiOrchestrionChannels } from '../../vercel-ai/vercel-ai-orchestrion-v6-subscriber'; | ||
|
|
||
| type VercelAiOptions = Parameters<typeof baseVercelAiIntegration>[0]; | ||
|
|
||
| // 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 = ((options: VercelAiOptions = {}) => { | ||
| const parentIntegration = baseVercelAiIntegration(options); | ||
|
|
||
| return { | ||
| name: 'VercelAI' as const, | ||
| beforeSetup(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() { | ||
| parentIntegration?.setupOnce?.(); | ||
|
|
||
| // Bail if this is not available | ||
| if (!dc.tracingChannel) { | ||
| return; | ||
| } | ||
|
|
||
| waitForTracingChannelBinding(() => { | ||
| subscribeVercelAiOrchestrionChannels(dc.tracingChannel, options); | ||
| }); | ||
| }, | ||
| }; | ||
| }) satisfies IntegrationFn; | ||
|
|
||
| /** | ||
| * Auto-instrument the `ai` SDK. Supported are: | ||
| * - v7 via native `ai:telemetry` tracing channel | ||
| * - v6 via orchestrion `orchestrion:ai:*` channels | ||
| */ | ||
| export const vercelAiChannelIntegration = defineIntegration(_vercelAiChannelIntegration); |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Timer missing safeUnref call
Low Severity
waitForTracingChannelBindingschedules asetTimeoutretry in@sentry/corewithout callingsafeUnref(orunref), which can keep short-lived Node processes alive until the timer fires.Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit d05d40e. Configure here.