|
| 1 | +/** |
| 2 | + * Auto-register `@ai-sdk/otel` so AI SDK 7 emits OpenTelemetry spans into the |
| 3 | + * Trigger.dev run trace with no customer setup. |
| 4 | + * |
| 5 | + * AI SDK 6 emitted spans from `ai` core, so `experimental_telemetry` (set by |
| 6 | + * `chat.toStreamTextOptions({ telemetry })`) was enough. v7 moved span emission |
| 7 | + * into the separate `@ai-sdk/otel` adapter, so on v7 `experimental_telemetry` |
| 8 | + * alone produces nothing until an integration is registered. We register it once |
| 9 | + * per worker process at chat.agent run boot. `@ai-sdk/otel` writes to the global |
| 10 | + * OpenTelemetry tracer, which is the same provider the Trigger worker installs |
| 11 | + * (the `@opentelemetry/api` global is a `globalThis` singleton keyed by major |
| 12 | + * version, so the separate copies still share it), so spans land in the trace. |
| 13 | + * |
| 14 | + * Fully guarded and best-effort — telemetry must never break a run: |
| 15 | + * - `registerTelemetry` only exists in v7 `ai` (no-op on v5/v6). |
| 16 | + * - `@ai-sdk/otel` is an OPTIONAL peer. The specifier is computed so the task |
| 17 | + * bundler doesn't hard-require it (v5/v6 users never install it). |
| 18 | + * - We detect an already-registered `@ai-sdk/otel` integration and skip, so a |
| 19 | + * customer (or a library they import) that registers it themselves doesn't |
| 20 | + * get duplicate spans. `registerTelemetry` is append-only, so without this |
| 21 | + * guard a second integration would double every span. |
| 22 | + * - To disable our auto-register entirely (e.g. you register `@ai-sdk/otel` |
| 23 | + * yourself after this boot, or via a custom integration our detection can't |
| 24 | + * see), set the env var `TRIGGER_AI_SDK_OTEL_AUTOREGISTER=0`. |
| 25 | + */ |
| 26 | +let registration: Promise<void> | null = null; |
| 27 | + |
| 28 | +/** Registers the AI SDK OTel integration once per process. Safe to call on every run. */ |
| 29 | +export function ensureAiSdkTelemetry(): Promise<void> { |
| 30 | + if (!registration) { |
| 31 | + registration = register(); |
| 32 | + } |
| 33 | + return registration; |
| 34 | +} |
| 35 | + |
| 36 | +async function register(): Promise<void> { |
| 37 | + try { |
| 38 | + if (isAutoRegisterDisabled()) { |
| 39 | + return; // opted out via TRIGGER_AI_SDK_OTEL_AUTOREGISTER |
| 40 | + } |
| 41 | + const aiMod: any = await import("ai"); |
| 42 | + if (typeof aiMod.registerTelemetry !== "function") { |
| 43 | + return; // v5 / v6 — `ai` core emits spans itself, nothing to wire. |
| 44 | + } |
| 45 | + // Computed specifier keeps the optional peer out of static bundler |
| 46 | + // resolution; resolves at runtime only when the customer installed it. |
| 47 | + const otelSpecifier = ["@ai-sdk", "otel"].join("/"); |
| 48 | + const otelMod: any = await import(otelSpecifier).catch(() => null); |
| 49 | + if (typeof otelMod?.OpenTelemetry !== "function") { |
| 50 | + return; // optional peer not installed |
| 51 | + } |
| 52 | + if (hasAiSdkOtelIntegration(otelMod.OpenTelemetry)) { |
| 53 | + return; // already registered by the customer or a library they import |
| 54 | + } |
| 55 | + aiMod.registerTelemetry(new otelMod.OpenTelemetry()); |
| 56 | + } catch { |
| 57 | + // never throw from telemetry setup |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +function isAutoRegisterDisabled(): boolean { |
| 62 | + const value = process.env.TRIGGER_AI_SDK_OTEL_AUTOREGISTER?.toLowerCase(); |
| 63 | + return value === "0" || value === "false"; |
| 64 | +} |
| 65 | + |
| 66 | +/** |
| 67 | + * True if an `@ai-sdk/otel` integration is already in v7's global telemetry |
| 68 | + * registry (`globalThis.AI_SDK_TELEMETRY_INTEGRATIONS`, a documented public |
| 69 | + * global that `registerTelemetry` appends to). `instanceof` matches a same-copy |
| 70 | + * registration; the constructor-name fallback catches a separate copy of |
| 71 | + * `@ai-sdk/otel`. |
| 72 | + */ |
| 73 | +function hasAiSdkOtelIntegration(OpenTelemetry: any): boolean { |
| 74 | + const integrations = (globalThis as any).AI_SDK_TELEMETRY_INTEGRATIONS; |
| 75 | + if (!Array.isArray(integrations)) { |
| 76 | + return false; |
| 77 | + } |
| 78 | + return integrations.some( |
| 79 | + (integration: any) => |
| 80 | + (typeof OpenTelemetry === "function" && integration instanceof OpenTelemetry) || |
| 81 | + integration?.constructor?.name === "OpenTelemetry" |
| 82 | + ); |
| 83 | +} |
0 commit comments