From acd34bc120a4cfe1937a27089cf0f547c82d249d Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 9 Oct 2026 10:11:43 +0200 Subject: [PATCH] ref(server-utils): Share Orchestrion channel context Co-Authored-By: GPT-6 --- .../integrations/orchestrion-subscriber.ts | 35 +++++++-------- .../orchestrion-subscriber.test.ts | 39 ++++++---------- .../server/integrations/tracing-channel.ts | 38 +++++++--------- packages/remix/src/v3/assetServer.ts | 5 +-- packages/remix/src/v3/server/instrument.ts | 44 +++++++++---------- packages/server-utils/src/index.ts | 1 + .../server-utils/src/integrations/amqplib.ts | 16 ++----- .../src/integrations/anthropic.ts | 12 ++--- .../src/integrations/aws-sdk/index.ts | 6 +-- .../src/integrations/dataloader.ts | 10 +---- .../src/integrations/express/types.ts | 8 ++-- .../src/integrations/firebase/functions.ts | 11 +---- .../integrations/firebase/instrumentation.ts | 12 ++--- .../src/integrations/generic-pool.ts | 7 +-- .../src/integrations/google-genai.ts | 15 ++----- .../src/integrations/graphql/index.ts | 14 ++---- .../src/integrations/hapi/index.ts | 6 +-- .../src/integrations/hono/honoIntegration.ts | 11 +++-- .../src/integrations/kafkajs/index.ts | 6 +-- .../server-utils/src/integrations/knex.ts | 16 ++----- .../src/integrations/koa/index.ts | 11 ++--- .../src/integrations/langchain.ts | 23 +++------- .../src/integrations/langgraph.ts | 21 +++------ .../src/integrations/lru-memoizer.ts | 7 +-- .../src/integrations/mastra-classifier.ts | 16 ++----- .../server-utils/src/integrations/mastra.ts | 21 +++------ .../src/integrations/mcp-server.ts | 10 ++--- .../server-utils/src/integrations/mistral.ts | 14 ++---- .../src/integrations/mongodb/index.ts | 4 +- .../src/integrations/mongoose/index.ts | 7 +-- .../server-utils/src/integrations/mysql.ts | 8 +--- .../src/integrations/mysql2/index.ts | 9 +--- .../src/integrations/openai-compatible.ts | 16 ++----- .../server-utils/src/integrations/openai.ts | 17 ++----- .../src/integrations/pi-durable.ts | 20 +++------ .../src/integrations/postgres-js.ts | 11 +++-- .../server-utils/src/integrations/postgres.ts | 8 +--- .../src/integrations/prisma/orchestrion.ts | 7 +-- .../src/integrations/redis/index.ts | 33 +++++++------- .../redis/ioredis-channel-subscriber.ts | 10 ++--- .../server-utils/src/integrations/tedious.ts | 7 ++- .../server-utils/src/integrations/typesafe.ts | 15 +------ .../vercel-ai-orchestrion-subscriber.ts | 18 +++----- .../server-utils/src/orchestrion/types.ts | 7 +++ .../integrations/mastra/error-capture.test.ts | 3 +- .../integrations/postgres/postgres.test.ts | 23 +++++----- 46 files changed, 228 insertions(+), 430 deletions(-) create mode 100644 packages/server-utils/src/orchestrion/types.ts diff --git a/packages/nestjs/src/integrations/orchestrion-subscriber.ts b/packages/nestjs/src/integrations/orchestrion-subscriber.ts index e2ac9e7524f2..53e21dda735f 100644 --- a/packages/nestjs/src/integrations/orchestrion-subscriber.ts +++ b/packages/nestjs/src/integrations/orchestrion-subscriber.ts @@ -1,5 +1,6 @@ import * as diagnosticsChannel from 'node:diagnostics_channel'; import { debug, startInactiveSpan, waitForTracingChannelBinding } from '@sentry/core'; +import type { OrchestrionChannelContext } from '@sentry/server-utils'; import { bindTracingChannelToSpan } from '@sentry/server-utils'; import { nestjsChannels as CHANNELS } from '@sentry/server-utils/orchestrion/config'; import { DEBUG_BUILD } from '../debug-build'; @@ -21,18 +22,6 @@ import { getAppCreationSpanOptions, wrapRequestContextHandler, wrapRouteHandler const NOOP = (): void => {}; -/** - * The orchestrion tracing-channel context. `arguments` is the live call args - * array; `result` is the return value, which an `end` handler may reassign to - * substitute it (`traceSync`/`tracePromise` always return `ctx.result`). - */ -interface ChannelContext { - arguments: unknown[]; - moduleVersion?: string; - result?: unknown; - error?: unknown; -} - /** * Subscribe to a decorator channel (`Injectable`/`Catch`). * @@ -42,7 +31,7 @@ interface ChannelContext { * spans later. */ function subscribeDecoratorChannel(channelName: string, patch: (target: T) => void): void { - diagnosticsChannel.tracingChannel(channelName).subscribe({ + diagnosticsChannel.tracingChannel(channelName).subscribe({ start(data) { const target = data.arguments?.[0] as T | undefined; if (target) { @@ -90,8 +79,11 @@ function makeProcessorDecorator(original: AnyFn, queueName: string | undefined): * whatever `end` leaves there. `wrap` receives the original decorator and the * channel context (for the factory's args, e.g. the BullMQ queue name). */ -function subscribeFactoryDecorator(channelName: string, wrap: (decorator: AnyFn, data: ChannelContext) => AnyFn): void { - diagnosticsChannel.tracingChannel(channelName).subscribe({ +function subscribeFactoryDecorator( + channelName: string, + wrap: (decorator: AnyFn, data: OrchestrionChannelContext) => AnyFn, +): void { + diagnosticsChannel.tracingChannel(channelName).subscribe({ start: NOOP, end(data) { const decorator = data.result; @@ -134,10 +126,13 @@ export function subscribeToNestChannels(): void { // calls below stay synchronous because the decorator channels fire at // module-load time, which a deferred subscription could miss. waitForTracingChannelBinding(() => { - bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(CHANNELS.NESTJS_APP_CREATION), data => { - const moduleCls = data.arguments?.[0] as { name?: string } | undefined; - return startInactiveSpan(getAppCreationSpanOptions(data.moduleVersion, moduleCls?.name)); - }); + bindTracingChannelToSpan( + diagnosticsChannel.tracingChannel(CHANNELS.NESTJS_APP_CREATION), + data => { + const moduleCls = data.arguments?.[0] as { name?: string } | undefined; + return startInactiveSpan(getAppCreationSpanOptions(data.moduleVersion, moduleCls?.name)); + }, + ); }); // request_context + request_handler. `RouterExecutionContext.create` @@ -146,7 +141,7 @@ export function subscribeToNestChannels(): void { // (-> handler span per call) and `end` reassigns `data.result` to // replace the returned handler (-> request_context span per request). const routerMeta = new WeakMap(); - diagnosticsChannel.tracingChannel(CHANNELS.NESTJS_ROUTER_CONTEXT).subscribe({ + diagnosticsChannel.tracingChannel(CHANNELS.NESTJS_ROUTER_CONTEXT).subscribe({ start(data) { const instance = data.arguments?.[0] as { constructor?: { name?: string } } | undefined; const callback = data.arguments?.[1]; diff --git a/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts b/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts index 04d6d5598c63..37409a4312d1 100644 --- a/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts +++ b/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts @@ -15,6 +15,7 @@ import { spanToJSON, } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { OrchestrionChannelContext } from '@sentry/server-utils'; import { nestjsChannels as CHANNELS } from '@sentry/server-utils/orchestrion/config'; import { subscribeToNestChannels } from '../../src/integrations/orchestrion-subscriber'; @@ -94,13 +95,6 @@ function installTestAsyncContextStrategy(): void { }); } -interface NestFactoryCreateData { - arguments: unknown[]; - moduleVersion?: string; - result?: unknown; - error?: unknown; -} - describe('NestJS orchestrion subscriber: app_creation', () => { afterEach(() => { setAsyncContextStrategy(undefined); @@ -112,12 +106,12 @@ describe('NestJS orchestrion subscriber: app_creation', () => { // `data._sentrySpan` function captureSpan(): { getSpan: () => Span | undefined } { let span: Span | undefined; - const grab = (data: NestFactoryCreateData): void => { + const grab = (data: OrchestrionChannelContext): void => { span ??= (data as { _sentrySpan?: Span })._sentrySpan; }; // The raw node `tracingChannel` type wants all five handlers; only // `end`/`asyncEnd` carry the bound span by the time it settles. - tracingChannel(CHANNELS.NESTJS_APP_CREATION).subscribe({ + tracingChannel(CHANNELS.NESTJS_APP_CREATION).subscribe({ start: () => undefined, asyncStart: () => undefined, asyncEnd: grab, @@ -133,7 +127,7 @@ describe('NestJS orchestrion subscriber: app_creation', () => { subscribeToNestChannels(); const { getSpan } = captureSpan(); - const channel = tracingChannel(CHANNELS.NESTJS_APP_CREATION); + const channel = tracingChannel(CHANNELS.NESTJS_APP_CREATION); class AppModule {} await channel.tracePromise(async () => ({ app: true }), { arguments: [AppModule], moduleVersion: '10.4.1' }); @@ -160,7 +154,7 @@ describe('NestJS orchestrion subscriber: app_creation', () => { subscribeToNestChannels(); const { getSpan } = captureSpan(); - const channel = tracingChannel(CHANNELS.NESTJS_APP_CREATION); + const channel = tracingChannel(CHANNELS.NESTJS_APP_CREATION); await channel.tracePromise(async () => ({ app: true }), { arguments: [] }); @@ -173,13 +167,6 @@ describe('NestJS orchestrion subscriber: app_creation', () => { type AnyFn = (this: unknown, ...args: unknown[]) => unknown; -interface RouterCreateData { - arguments: unknown[]; - moduleVersion?: string; - result?: unknown; - error?: unknown; -} - describe('NestJS orchestrion subscriber: request_context / request_handler', () => { afterEach(() => { setAsyncContextStrategy(undefined); @@ -195,10 +182,10 @@ describe('NestJS orchestrion subscriber: request_context / request_handler', () instance: object, callback: AnyFn, moduleVersion: string | undefined, - makeHandler: (data: RouterCreateData) => AnyFn, + makeHandler: (data: OrchestrionChannelContext) => AnyFn, ): { effectiveHandler: AnyFn; wrappedCallback: AnyFn } { - const channel = tracingChannel(CHANNELS.NESTJS_ROUTER_CONTEXT); - const data: RouterCreateData = { arguments: [instance, callback], moduleVersion }; + const channel = tracingChannel(CHANNELS.NESTJS_ROUTER_CONTEXT); + const data: OrchestrionChannelContext = { arguments: [instance, callback], moduleVersion }; channel.traceSync(() => makeHandler(data), data); return { effectiveHandler: data.result as AnyFn, wrappedCallback: data.arguments[1] as AnyFn }; } @@ -340,7 +327,7 @@ describe('NestJS orchestrion subscriber: @Injectable (middleware/guard/pipe/inte // Fire the @Injectable channel against `target` (as if its decorator arrow // ran), so the subscriber's `start` patches `target.prototype`. function applyInjectable(target: object): void { - tracingChannel<{ arguments: unknown[] }>(CHANNELS.NESTJS_INJECTABLE).traceSync(() => undefined, { + tracingChannel(CHANNELS.NESTJS_INJECTABLE).traceSync(() => undefined, { arguments: [target], }); } @@ -643,7 +630,7 @@ describe('NestJS orchestrion subscriber: @Catch (exception filter)', () => { }); function applyCatch(target: object): void { - tracingChannel<{ arguments: unknown[] }>(CHANNELS.NESTJS_CATCH).traceSync(() => undefined, { + tracingChannel(CHANNELS.NESTJS_CATCH).traceSync(() => undefined, { arguments: [target], }); } @@ -697,7 +684,7 @@ describe('NestJS orchestrion subscriber: @Catch (exception filter)', () => { // @Catch first. Because the two passes use separate patched-flags, both // must wrap their own methods regardless of which channel fires first. function fireInjectable(target: object): void { - tracingChannel<{ arguments: unknown[] }>(CHANNELS.NESTJS_INJECTABLE).traceSync(() => undefined, { + tracingChannel(CHANNELS.NESTJS_INJECTABLE).traceSync(() => undefined, { arguments: [target], }); } @@ -801,8 +788,8 @@ describe('NestJS orchestrion subscriber: schedule / event / bullmq', () => { // to the factory's return (our `originalDecorator`), then the subscriber's // `end` reassigns `data.result`. Returns effective (wrapped) decorator. function driveFactory(channelName: string, factoryArgs: unknown[], originalDecorator: AnyFn): AnyFn { - const data: { arguments: unknown[]; result?: unknown } = { arguments: factoryArgs }; - tracingChannel<{ arguments: unknown[]; result?: unknown }>(channelName).traceSync(() => originalDecorator, data); + const data: OrchestrionChannelContext = { arguments: factoryArgs }; + tracingChannel(channelName).traceSync(() => originalDecorator, data); return data.result as AnyFn; } diff --git a/packages/remix/src/server/integrations/tracing-channel.ts b/packages/remix/src/server/integrations/tracing-channel.ts index 6ab1645ca503..8e93813a19dd 100644 --- a/packages/remix/src/server/integrations/tracing-channel.ts +++ b/packages/remix/src/server/integrations/tracing-channel.ts @@ -14,6 +14,7 @@ import { waitForTracingChannelBinding, filterCollectedUrl, } from '@sentry/core'; +import type { OrchestrionChannelContext } from '@sentry/server-utils'; import { bindTracingChannelToSpan } from '@sentry/server-utils'; import { SENTRY_SEGMENT_NAME_SOURCE, @@ -41,17 +42,6 @@ const NOOP = (): void => {}; const MATCH_ROUTE_ID = 'match.route.id'; const MATCH_PARAMS = 'match.params'; -/** - * The shape orchestrion's transform attaches to a tracing-channel `context` object. Documented here - * rather than imported because orchestrion's runtime doesn't export it. - */ -interface ChannelContext { - // The live `arguments` of the wrapped call. - arguments: unknown[]; - result?: unknown; - error?: unknown; -} - // `callRouteLoader`/`callRouteAction` receive a single options object as `arguments[0]`. interface RouteCallParams { request?: Request; @@ -61,7 +51,7 @@ interface RouteCallParams { // The in-flight form-data read, started at span start (before the action consumes the body) so it // overlaps the action's execution rather than starting after it settles. -interface ActionChannelContext extends ChannelContext { +interface ActionChannelContext extends OrchestrionChannelContext { _sentryFormData?: Promise; } @@ -140,7 +130,7 @@ function enrichActiveSpanWithRoute(result: unknown): void { } function subscribeRequestHandler(): void { - bindTracingChannelToSpan( + bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(remixChannels.REMIX_REQUEST_HANDLER), data => { const requestAttributes = getRequestAttributes(data.arguments[0]); @@ -178,19 +168,21 @@ function subscribeRequestHandler(): void { function subscribeMatchServerRoutes(): void { // `matchServerRoutes` is synchronous, so only the `end` event carries a result; the rest are // no-ops. `subscribe` types demand a handler for each channel. - diagnosticsChannel.tracingChannel(remixChannels.REMIX_MATCH_SERVER_ROUTES).subscribe({ - start: NOOP, - end(data) { - enrichActiveSpanWithRoute(data.result); - }, - asyncStart: NOOP, - asyncEnd: NOOP, - error: NOOP, - }); + diagnosticsChannel + .tracingChannel(remixChannels.REMIX_MATCH_SERVER_ROUTES) + .subscribe({ + start: NOOP, + end(data) { + enrichActiveSpanWithRoute(data.result); + }, + asyncStart: NOOP, + asyncEnd: NOOP, + error: NOOP, + }); } function subscribeCallRouteLoader(): void { - bindTracingChannelToSpan( + bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(remixChannels.REMIX_CALL_ROUTE_LOADER), data => { const params = (data.arguments[0] ?? {}) as RouteCallParams; diff --git a/packages/remix/src/v3/assetServer.ts b/packages/remix/src/v3/assetServer.ts index 88ce5060662a..2c1c1bd649f2 100644 --- a/packages/remix/src/v3/assetServer.ts +++ b/packages/remix/src/v3/assetServer.ts @@ -1,5 +1,6 @@ import * as diagnosticsChannel from 'node:diagnostics_channel'; import { consoleSandbox } from '@sentry/core'; +import type { OrchestrionChannelContext } from '@sentry/server-utils'; import { remixV3Channels } from '@sentry/server-utils/orchestrion/config'; import { addDebugIdToSourceMap, findDebugId, getDebugId, injectDebugIdSnippet } from './debugId'; import { getShimUrl, isOrchestrionLoader, orchestrionLoader, resolveShimPath } from './orchestrionLoader'; @@ -39,9 +40,7 @@ interface AssetServer { fetch: (request: Request) => Promise; } -interface CreateAssetServerContext { - arguments: unknown[]; - result?: unknown; +interface CreateAssetServerContext extends OrchestrionChannelContext { _sentryHideSourceMaps?: boolean; } diff --git a/packages/remix/src/v3/server/instrument.ts b/packages/remix/src/v3/server/instrument.ts index 2fee25db560b..f348c9ae01b0 100644 --- a/packages/remix/src/v3/server/instrument.ts +++ b/packages/remix/src/v3/server/instrument.ts @@ -1,6 +1,7 @@ import * as diagnosticsChannel from 'node:diagnostics_channel'; import { createMultiMatcher } from 'remix/route-pattern/match'; import { consoleSandbox } from '@sentry/core'; +import type { OrchestrionChannelContext } from '@sentry/server-utils'; import { remixV3Channels } from '@sentry/server-utils/orchestrion/config'; import type { MatcherLike, RequestListenerOptionsLike, RouterOptionsLike } from '../types'; @@ -9,11 +10,6 @@ import { sentryRemixMiddleware } from './middleware'; const NOOP = (): void => {}; -/** The call's arguments, as orchestrion's transform attaches them to a tracing channel context. */ -interface ChannelContext { - arguments: unknown[]; -} - // Marks an options object already injected into, so the same mutation cannot be applied twice. const INJECTED = Symbol.for('SentryRemixV3Injected'); @@ -41,22 +37,24 @@ export function instrumentRemixV3(): void { } function subscribeToCreateRouter(): void { - diagnosticsChannel.tracingChannel(remixV3Channels.REMIX_V3_CREATE_ROUTER).subscribe({ - start(data) { - // Node rethrows anything this handler throws as an uncaught exception, which would kill an app - // that runs fine without Sentry. Frozen options, a non-writable property and a `route-pattern` - // copy that cannot build a matcher all reach here, so the router is left uninstrumented instead. - try { - injectRouterMiddleware(ensureOptions(data.arguments)); - } catch { - // Ignored on purpose. - } - }, - end: NOOP, - asyncStart: NOOP, - asyncEnd: NOOP, - error: NOOP, - }); + diagnosticsChannel + .tracingChannel(remixV3Channels.REMIX_V3_CREATE_ROUTER) + .subscribe({ + start(data) { + // Node rethrows anything this handler throws as an uncaught exception, which would kill an app + // that runs fine without Sentry. Frozen options, a non-writable property and a `route-pattern` + // copy that cannot build a matcher all reach here, so the router is left uninstrumented instead. + try { + injectRouterMiddleware(ensureOptions(data.arguments)); + } catch { + // Ignored on purpose. + } + }, + end: NOOP, + asyncStart: NOOP, + asyncEnd: NOOP, + error: NOOP, + }); } /** @@ -66,7 +64,9 @@ function subscribeToCreateRouter(): void { */ function subscribeToCreateRequestListener(): void { diagnosticsChannel - .tracingChannel(remixV3Channels.REMIX_V3_CREATE_REQUEST_LISTENER) + .tracingChannel( + remixV3Channels.REMIX_V3_CREATE_REQUEST_LISTENER, + ) .subscribe({ start(data) { try { diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 446812f8eed5..22b7470fa7b7 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -8,6 +8,7 @@ import { attachHapiErrorHandler as _attachHapiErrorHandler } from './integration export { bindTracingChannelToSpan } from './tracing-channel'; export type { TracingChannelPayloadWithSpan } from './tracing-channel'; export type { InstrumentationConfig } from './orchestrion/apmTypes'; +export type { OrchestrionChannelContext } from './orchestrion/types'; // Runtime target of the bundler-injected module snippet. The snippet imports it // from this entry alongside the module's subscriber factory (see // `orchestrion/bundler/moduleInjectedTransform.ts`); it is a plain runtime diff --git a/packages/server-utils/src/integrations/amqplib.ts b/packages/server-utils/src/integrations/amqplib.ts index 9b227d7e0e2c..1275fbb1d8a2 100644 --- a/packages/server-utils/src/integrations/amqplib.ts +++ b/packages/server-utils/src/integrations/amqplib.ts @@ -28,6 +28,7 @@ import { SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { QUEUE_PROCESS, QUEUE_PUBLISH } from '@sentry/conventions/op'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { amqplibModuleNames } from '../orchestrion/config/amqplib'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; import { CHANNELS } from '../orchestrion/channels'; @@ -121,12 +122,8 @@ interface ChannelLike { * The shape orchestrion's transform attaches to the tracing-channel `context`. Documented here rather * than imported because orchestrion's runtime doesn't export it. */ -interface AmqpChannelContext { - // The live args array passed to the wrapped call. - arguments: unknown[]; +interface AmqpChannelContext extends OrchestrionChannelContext { self?: ChannelLike; - result?: unknown; - error?: unknown; } interface AmqpDispatchContext extends AmqpChannelContext { @@ -135,11 +132,6 @@ interface AmqpDispatchContext extends AmqpChannelContext { _sentryNoAck?: boolean; } -interface AmqpConnectContext { - arguments?: unknown[]; - result?: unknown; -} - const NOOP = (): void => {}; const _amqplibIntegration = (() => { @@ -302,11 +294,11 @@ function subscribeSettle(): void { /** Captures connection attributes on the connection object for span-time reads via `channel.connection`. */ function subscribeConnect(): void { - const channel = diagnosticsChannel.tracingChannel(CHANNELS.AMQPLIB_CONNECT); + const channel = diagnosticsChannel.tracingChannel(CHANNELS.AMQPLIB_CONNECT); // A `start` subscriber is required for orchestrion to wrap the callback-style `connect` at all. channel.start.subscribe(NOOP); channel.asyncEnd.subscribe(message => { - const data = message as AmqpConnectContext; + const data = message as OrchestrionChannelContext; const conn = data.result as ConnectionLike | undefined; if (!conn || typeof conn !== 'object') { return; diff --git a/packages/server-utils/src/integrations/anthropic.ts b/packages/server-utils/src/integrations/anthropic.ts index d14d3cbb2f6a..a45bbf02496f 100644 --- a/packages/server-utils/src/integrations/anthropic.ts +++ b/packages/server-utils/src/integrations/anthropic.ts @@ -14,6 +14,7 @@ import { wrapApiPromiseResponse } from '../ai/core/apiPromise'; import { addPrivateRequestAttributes, addResponseAttributes, extractRequestAttributes } from '../ai/anthropic-ai'; import { instrumentAsyncIterableStream, instrumentMessageStream } from '../ai/anthropic-ai/streaming'; import type { AnthropicAiOptions, AnthropicAiResponse } from '../ai/anthropic-ai/types'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { anthropicAiModuleNames } from '../orchestrion/config/anthropic-ai'; @@ -37,11 +38,6 @@ const INSTRUMENTED_CHANNELS = [ type StreamMode = (typeof INSTRUMENTED_CHANNELS)[number]['stream']; -interface AnthropicChannelContext { - arguments: unknown[]; - result?: unknown; -} - const _anthropicAIIntegration = ((options: AnthropicAiOptions = {}) => { return { name: INTEGRATION_NAME, @@ -54,7 +50,7 @@ const _anthropicAIIntegration = ((options: AnthropicAiOptions = {}) => { function instrumentAnthropic(options: AnthropicAiOptions): void { for (const { channel, operation, stream } of INSTRUMENTED_CHANNELS) { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channel), + diagnosticsChannel.tracingChannel(channel), data => createGenAiSpan(data, operation, options, stream), { beforeSpanEnd: (span, data) => { @@ -85,7 +81,7 @@ function instrumentAnthropic(options: AnthropicAiOptions): void { * Returning `undefined` opts the payload out so no span is opened. */ function createGenAiSpan( - data: AnthropicChannelContext, + data: OrchestrionChannelContext, operation: string, options: AnthropicAiOptions, stream: StreamMode, @@ -191,7 +187,7 @@ function isMessageStream(value: unknown): value is MessageStreamEmitter { */ function wrapStreamResult( span: Span, - data: AnthropicChannelContext, + data: OrchestrionChannelContext, stream: StreamMode, options: AnthropicAiOptions, ): boolean { diff --git a/packages/server-utils/src/integrations/aws-sdk/index.ts b/packages/server-utils/src/integrations/aws-sdk/index.ts index 25a75931deb7..95b38b41eece 100644 --- a/packages/server-utils/src/integrations/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/aws-sdk/index.ts @@ -10,6 +10,7 @@ import { SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { RPC } from '@sentry/conventions/op'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { awsSdkModuleNames } from '../../orchestrion/config/aws-sdk'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -26,11 +27,8 @@ const INTEGRATION_NAME = 'Aws' as const; // The context orchestrion's transform attaches to the channel: `arguments` is the live args of the // wrapped `Client.prototype.send` call (`[command, ...]`), `self` the client, `result`/`error` the // settled value. The `_sentry*` fields are stashed by us across the call's lifecycle. -interface AwsSendChannelContext { - arguments: unknown[]; +interface AwsSendChannelContext extends OrchestrionChannelContext { self?: { config?: AwsClientConfig; constructor?: { name?: string } }; - result?: unknown; - error?: unknown; _sentryNormalizedRequest?: NormalizedRequest; _sentryRequestMetadata?: RequestMetadata; _sentryRegion?: { settled: boolean; promise: Promise }; diff --git a/packages/server-utils/src/integrations/dataloader.ts b/packages/server-utils/src/integrations/dataloader.ts index 89decb7338e9..5fb50a577325 100644 --- a/packages/server-utils/src/integrations/dataloader.ts +++ b/packages/server-utils/src/integrations/dataloader.ts @@ -21,6 +21,7 @@ import { waitForTracingChannelBinding, } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import type { ChannelName } from '../orchestrion/channels'; import { CHANNELS } from '../orchestrion/channels'; import type { TracingChannelPayloadWithSpan } from '../tracing-channel'; @@ -63,15 +64,8 @@ interface DataLoaderInstance { _batch?: DataLoaderBatch | null; } -/** - * The shape orchestrion's transform attaches to the tracing-channel `context`. Documented here rather - * than imported because orchestrion's runtime doesn't export it. - */ -interface DataLoaderChannelContext { - arguments: unknown[]; +interface DataLoaderChannelContext extends OrchestrionChannelContext { self?: DataLoaderInstance; - result?: unknown; - error?: unknown; } // Marks a wrapped `batchLoadFn` so a re-used loader (or a double construct) isn't wrapped twice. diff --git a/packages/server-utils/src/integrations/express/types.ts b/packages/server-utils/src/integrations/express/types.ts index 5536f524e849..ea1d979a18af 100644 --- a/packages/server-utils/src/integrations/express/types.ts +++ b/packages/server-utils/src/integrations/express/types.ts @@ -1,3 +1,4 @@ +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import type { Span } from '@sentry/core'; export type ExpressLayerType = 'router' | 'middleware' | 'request_handler'; @@ -40,13 +41,11 @@ export interface ExpressResponse { * for this layer by `bindTracingChannelToSpan`, and `error` is present on the * channel's `error` event. */ -export interface HandleChannelContext { +export interface HandleChannelContext extends OrchestrionChannelContext { self?: ExpressLayer; - arguments?: unknown[]; _sentryCleanup?: () => void; _sentryStoredLayer?: boolean; _sentrySpan?: Span; - error?: unknown; } /** @@ -55,9 +54,8 @@ export interface HandleChannelContext { * the last entry in `stack`) and `arguments` are the registration args (the * first of which is the path pattern). */ -export interface RegistrationChannelContext { +export interface RegistrationChannelContext extends OrchestrionChannelContext { self?: { stack?: ExpressLayer[] }; - arguments?: unknown[]; } /** An Express error carrying an optional HTTP status, in the various shapes middleware use. */ diff --git a/packages/server-utils/src/integrations/firebase/functions.ts b/packages/server-utils/src/integrations/firebase/functions.ts index 75fd439a3cd6..1ad3d8c03f90 100644 --- a/packages/server-utils/src/integrations/firebase/functions.ts +++ b/packages/server-utils/src/integrations/firebase/functions.ts @@ -1,3 +1,4 @@ +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { FAAS_NAME, FAAS_TRIGGER, @@ -26,14 +27,6 @@ const WRAPPED = '__sentryFirebaseWrapped'; type Handler = (this: unknown, ...args: unknown[]) => unknown; -interface FunctionsChannelContext { - // The live args of the `onX(...)` registration call. firebase-functions accepts either - // `onX(handler)` or `onX(documentOrOptions, handler)`, so the handler is `arguments[0]` when it's a - // function, otherwise `arguments[1]`. Mutating the entry here swaps in the wrapped handler. - arguments: unknown[]; - self?: unknown; -} - /** * Rewrap the handler argument of a firebase-functions `onX(...)` registration so the returned cloud * function opens a `SERVER` span (and error boundary) each time it's invoked. Runs as the tracing @@ -42,7 +35,7 @@ interface FunctionsChannelContext { * The registration call itself is trivial and synchronous, so — unlike the firestore path — this does * not bind a span to the channel; it only uses the channel as an injection point. */ -export function wrapFunctionsRegistration(data: FunctionsChannelContext, triggerType: string): void { +export function wrapFunctionsRegistration(data: OrchestrionChannelContext, triggerType: string): void { const args = data.arguments; if (!Array.isArray(args) || args.length === 0) { return; diff --git a/packages/server-utils/src/integrations/firebase/instrumentation.ts b/packages/server-utils/src/integrations/firebase/instrumentation.ts index 5fcadfd2ae45..b10534aea30a 100644 --- a/packages/server-utils/src/integrations/firebase/instrumentation.ts +++ b/packages/server-utils/src/integrations/firebase/instrumentation.ts @@ -1,4 +1,5 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan, safeChannelCallback } from '../../tracing-channel'; import type { FirestoreReference } from './firestore-types'; @@ -7,13 +8,6 @@ import { wrapFunctionsRegistration } from './functions'; // The context orchestrion's transform attaches to each firestore channel: `arguments` is the live args // of the wrapped `addDoc`/`getDocs`/`setDoc`/`deleteDoc` call, `arguments[0]` the reference. -interface FirestoreChannelContext { - arguments: unknown[]; - self?: unknown; - result?: unknown; - error?: unknown; -} - // The firestore operations, keyed by channel. `useParent` mirrors the OTel integration: `setDoc`/ // `deleteDoc` take a *document* reference but the span is named after its parent *collection*. const FIRESTORE_OPERATIONS: Array<{ channel: string; spanName: string; useParent: boolean }> = [ @@ -43,7 +37,7 @@ const NOOP = (): void => {}; export function instrumentFirebase() { for (const { channel, spanName, useParent } of FIRESTORE_OPERATIONS) { - bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channel), data => + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channel), data => safeChannelCallback(() => { const reference = data.arguments[0] as FirestoreReference | undefined; if (!reference) { @@ -61,7 +55,7 @@ export function instrumentFirebase() { // span inside that wrapper. The other lifecycle events are irrelevant, so no-op them. diagnosticsChannel.tracingChannel(channel).subscribe({ start: data => - safeChannelCallback(() => wrapFunctionsRegistration(data as { arguments: unknown[] }, triggerType)), + safeChannelCallback(() => wrapFunctionsRegistration(data as OrchestrionChannelContext, triggerType)), end: NOOP, asyncStart: NOOP, asyncEnd: NOOP, diff --git a/packages/server-utils/src/integrations/generic-pool.ts b/packages/server-utils/src/integrations/generic-pool.ts index f5236f5a364c..ca27d9bd29ff 100644 --- a/packages/server-utils/src/integrations/generic-pool.ts +++ b/packages/server-utils/src/integrations/generic-pool.ts @@ -3,6 +3,7 @@ import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, startInactiveSpan } from '@sentry/core'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { genericPoolModuleNames } from '../orchestrion/config/generic-pool'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; @@ -12,10 +13,6 @@ import { bindTracingChannelToSpan } from '../tracing-channel'; // 'GenericPool' integration is omitted from the default set. const INTEGRATION_NAME = 'GenericPool' as const; -interface GenericPoolAcquireContext { - arguments: unknown[]; -} - const _genericPoolIntegration = (() => { return { name: INTEGRATION_NAME, @@ -35,7 +32,7 @@ export const genericPoolIntegration = defineIntegration(_genericPoolIntegration) function instrumentGenericPool(): void { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.GENERIC_POOL_ACQUIRE), + diagnosticsChannel.tracingChannel(CHANNELS.GENERIC_POOL_ACQUIRE), () => startInactiveSpan({ name: 'generic-pool.acquire', diff --git a/packages/server-utils/src/integrations/google-genai.ts b/packages/server-utils/src/integrations/google-genai.ts index b191c503df5b..846ba9ae40cf 100644 --- a/packages/server-utils/src/integrations/google-genai.ts +++ b/packages/server-utils/src/integrations/google-genai.ts @@ -19,6 +19,7 @@ import { } from '../ai/google-genai'; import { instrumentStream } from '../ai/google-genai/streaming'; import type { GoogleGenAIOptions, GoogleGenAIResponse } from '../ai/google-genai/types'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { googleGenAiModuleNames } from '../orchestrion/config/google-genai'; @@ -37,14 +38,6 @@ const INSTRUMENTED_CHANNELS = [ { channel: CHANNELS.GOOGLE_GENAI_CHAT, operation: 'chat' }, ] as const; -interface GoogleGenAIChannelContext { - arguments: unknown[]; - // The transform stashes the call's `this` here, which chat methods need since the model lives on - // the `Chat` instance (`this.model`/`this.modelVersion`), not in the call arguments. - self?: unknown; - result?: unknown; -} - const _googleGenAIIntegration = ((options: GoogleGenAIOptions = {}) => { return { name: INTEGRATION_NAME, @@ -57,7 +50,7 @@ const _googleGenAIIntegration = ((options: GoogleGenAIOptions = {}) => { function instrumentGoogleGenai(options: GoogleGenAIOptions): void { for (const { channel, operation } of INSTRUMENTED_CHANNELS) { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channel), + diagnosticsChannel.tracingChannel(channel), data => createGenAiSpan(data, operation, options), { beforeSpanEnd: (span, data) => { @@ -81,7 +74,7 @@ function instrumentGoogleGenai(options: GoogleGenAIOptions): void { * Returning `undefined` opts the payload out so no span is opened. */ function createGenAiSpan( - data: GoogleGenAIChannelContext, + data: OrchestrionChannelContext, operation: string, options: GoogleGenAIOptions, ): Span | undefined { @@ -143,7 +136,7 @@ function isAsyncIterable(value: unknown): value is AsyncIterableStream { * For a stream we patch `result[Symbol.asyncIterator]` in place so `instrumentStream` ends the * span when iteration finishes. */ -function wrapStreamResult(span: Span, data: GoogleGenAIChannelContext, options: GoogleGenAIOptions): boolean { +function wrapStreamResult(span: Span, data: OrchestrionChannelContext, options: GoogleGenAIOptions): boolean { const result = data.result; if (!isAsyncIterable(result)) { return false; diff --git a/packages/server-utils/src/integrations/graphql/index.ts b/packages/server-utils/src/integrations/graphql/index.ts index e0397b1ac769..b217b84e45f5 100644 --- a/packages/server-utils/src/integrations/graphql/index.ts +++ b/packages/server-utils/src/integrations/graphql/index.ts @@ -2,6 +2,7 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, waitForTracingChannelBinding } from '@sentry/core'; import { subscribeGraphqlDiagnosticChannels, type GraphQLOptions } from './graphql-dc-subscriber'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { graphqlModuleNames } from '../../orchestrion/config/graphql'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -20,13 +21,6 @@ const INTEGRATION_NAME = 'Graphql' as const; // The context orchestrion's transform attaches to each channel: `arguments` is the live args of the // wrapped call, `result` the settled return value. -interface GraphqlChannelContext { - arguments: unknown[]; - self?: unknown; - result?: unknown; - error?: unknown; -} - function getOptionsWithDefaults(options: GraphQLOptions): GraphqlResolvedConfig { return { ignoreResolveSpans: options.ignoreResolveSpans !== false, @@ -51,18 +45,18 @@ const _graphqlIntegration = ((options: GraphQLOptions = {}) => { }) satisfies IntegrationFn; function instrumentGraphql(config: GraphqlResolvedConfig, getConfig: () => GraphqlResolvedConfig): void { - bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(CHANNELS.GRAPHQL_PARSE), () => + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(CHANNELS.GRAPHQL_PARSE), () => safeChannelCallback(() => startParseSpan()), ); bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.GRAPHQL_VALIDATE), + diagnosticsChannel.tracingChannel(CHANNELS.GRAPHQL_VALIDATE), data => safeChannelCallback(() => startValidateSpan(data.arguments[1])), { beforeSpanEnd: (span, data) => safeChannelCallback(() => finalizeValidateSpan(span, data.result)) }, ); bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.GRAPHQL_EXECUTE), + diagnosticsChannel.tracingChannel(CHANNELS.GRAPHQL_EXECUTE), data => safeChannelCallback(() => startExecuteSpan(data.arguments, data.self, config, getConfig)), { beforeSpanEnd: (span, data) => safeChannelCallback(() => finalizeExecuteSpan(span, data.result)) }, ); diff --git a/packages/server-utils/src/integrations/hapi/index.ts b/packages/server-utils/src/integrations/hapi/index.ts index e7b8af943694..a4e83be91e1d 100644 --- a/packages/server-utils/src/integrations/hapi/index.ts +++ b/packages/server-utils/src/integrations/hapi/index.ts @@ -1,6 +1,7 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { hapiModuleNames } from '../../orchestrion/config/hapi'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -46,8 +47,7 @@ interface HapiIntegrationOptions { * the hapi server instance: the root server has `self.realm.plugin === undefined`, * while a plugin's clone server exposes the registering plugin's name there. */ -interface HapiChannelContext { - arguments: unknown[]; +interface HapiChannelContext extends OrchestrionChannelContext { self?: { realm?: { plugin?: string } }; } @@ -55,7 +55,7 @@ interface HapiChannelContext { * The `start`/`initialize` channel `context` shape: `self` is the live server * we attach the auto-registered error listener to. */ -interface HapiServerContext { +interface HapiServerContext extends OrchestrionChannelContext { self?: HapiServer; } diff --git a/packages/server-utils/src/integrations/hono/honoIntegration.ts b/packages/server-utils/src/integrations/hono/honoIntegration.ts index d62e949125ef..c2b504db2dab 100644 --- a/packages/server-utils/src/integrations/hono/honoIntegration.ts +++ b/packages/server-utils/src/integrations/hono/honoIntegration.ts @@ -6,6 +6,7 @@ import type { IntegrationFn } from '@sentry/core'; import { debug, defineIntegration, startInactiveSpan } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import type { Env, GetConnInfo, Hono, MiddlewareHandler } from './honoTypes'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { honoModuleNames } from '../../orchestrion/config/hono'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -82,8 +83,6 @@ export function honoMiddleware(app: Hono, options: HonoIntegra }); } -type ChannelArgs = { arguments: unknown[] }; - // A Hono `matchResult[0]` entry: `[[handler, routeMeta], paramIndexMap]` — `compose` runs the // handler (`entry[0][0]`) and the `matchedRoutes` getter reads `routeMeta` (`entry[0][1]`). // oxlint-disable-next-line typescript/no-explicit-any @@ -100,7 +99,7 @@ type MatchedHandlerEntry = [[any, any], any]; * Running per request (no module-scope state) is what lets this work on Cloudflare. */ function injectHonoInstrumentation( - message: ChannelArgs, + message: OrchestrionChannelContext, requestMiddleware: MiddlewareHandler, injectedHandlerLists: WeakSet, ): void { @@ -155,7 +154,7 @@ function injectHonoInstrumentation( */ function instrumentInternalRequests(): void { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.HONO_REQUEST), + diagnosticsChannel.tracingChannel(CHANNELS.HONO_REQUEST), data => { // Alongside the manual middleware, the instance `app.request` Proxy has already opened this // span and is calling through — returning `undefined` (no span) avoids nesting a duplicate. @@ -202,9 +201,9 @@ function instrumentHono(options: HonoIntegrationOptions): void { // The Context constructor's `end` fires synchronously inside `new Context()`, before `#dispatch` // reads `matchResult[0].length`, so the injected middleware is in place for the same request. - diagnosticsChannel.tracingChannel(CHANNELS.HONO_CONTEXT).end.subscribe(message => { + diagnosticsChannel.tracingChannel(CHANNELS.HONO_CONTEXT).end.subscribe(message => { safeChannelCallback(() => - injectHonoInstrumentation(message as ChannelArgs, requestMiddleware, injectedHandlerLists), + injectHonoInstrumentation(message as OrchestrionChannelContext, requestMiddleware, injectedHandlerLists), ); }); diff --git a/packages/server-utils/src/integrations/kafkajs/index.ts b/packages/server-utils/src/integrations/kafkajs/index.ts index 5160dedf80ca..0bc54f12f892 100644 --- a/packages/server-utils/src/integrations/kafkajs/index.ts +++ b/packages/server-utils/src/integrations/kafkajs/index.ts @@ -2,6 +2,7 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { TracingChannelSubscribers } from 'node:diagnostics_channel'; import type { IntegrationFn, Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { kafkajsModuleNames } from '../../orchestrion/config/kafkajs'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -14,16 +15,15 @@ import type { ConsumerRunConfig, ProducerBatch } from './types'; const INTEGRATION_NAME = 'Kafka' as const; /** The tracing-channel context the transform attaches around `messageProducer.js`'s `sendBatch`. */ -interface SendBatchChannelContext { +interface SendBatchChannelContext extends OrchestrionChannelContext { // `arguments[0]` is the `{ topicMessages }` batch (kafkajs normalizes `send` into `sendBatch`). arguments: [ProducerBatch?, ...unknown[]]; - error?: unknown; // The producer spans opened at `start`, ended on `asyncEnd` (and marked errored on `error`). _sentrySpans?: Span[]; } /** The tracing-channel context the transform attaches around `consumer/index.js`'s `run`. */ -interface ConsumerRunChannelContext { +interface ConsumerRunChannelContext extends OrchestrionChannelContext { // `arguments[0]` is the `run(config)` config whose `eachMessage`/`eachBatch` we swap in place. arguments: [ConsumerRunConfig?, ...unknown[]]; } diff --git a/packages/server-utils/src/integrations/knex.ts b/packages/server-utils/src/integrations/knex.ts index 781d26aa70c8..bdc3d4dac16f 100644 --- a/packages/server-utils/src/integrations/knex.ts +++ b/packages/server-utils/src/integrations/knex.ts @@ -31,6 +31,7 @@ import { } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import { DEBUG_BUILD } from '../debug-build'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { sanitizeSqlQueryWithSummary, toSqlDialect } from '../utils/sql'; @@ -89,21 +90,10 @@ interface KnexRunner { } /** Context orchestrion attaches to the `orchestrion:knex:query` channel (wrapping `Runner.query`). */ -interface KnexQueryChannelContext { +interface KnexQueryChannelContext extends OrchestrionChannelContext { // `arguments[0]` is the query object (`{ sql, method, bindings }`). arguments: [KnexQuery?]; self?: KnexRunner; - moduleVersion?: string; - result?: unknown; - error?: unknown; -} - -/** Context orchestrion attaches to the `Client.queryBuilder`/`schemaBuilder`/`raw` channels. */ -interface KnexBuilderChannelContext { - arguments: unknown[]; - self?: unknown; - // The builder returned by the wrapped method. - result?: KnexBuilder; } const _knexIntegration = (() => { @@ -135,7 +125,7 @@ const _knexIntegration = (() => { */ function subscribeBuilder(channelName: string): void { diagnosticsChannel.tracingChannel(channelName).end.subscribe(message => { - const builder = (message as KnexBuilderChannelContext).result; + const builder = (message as OrchestrionChannelContext).result; if (!builder || typeof builder !== 'object' || parentSpanSymbol in builder) { return; } diff --git a/packages/server-utils/src/integrations/koa/index.ts b/packages/server-utils/src/integrations/koa/index.ts index e3f1c10690c0..d2bba675abfe 100644 --- a/packages/server-utils/src/integrations/koa/index.ts +++ b/packages/server-utils/src/integrations/koa/index.ts @@ -15,6 +15,7 @@ import { import { CODE_FUNCTION_NAME, HTTP_ROUTE, KOA_TYPE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { MIDDLEWARE, ROUTER } from '@sentry/conventions/op'; import { DEBUG_BUILD } from '../../debug-build'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { koaModuleNames } from '../../orchestrion/config/koa'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -66,12 +67,8 @@ interface KoaMiddlewareMetadata { name: string; } -interface KoaUseContext { - arguments: unknown[]; -} - /** The `callback` channel `context` shape: `self` is the live app to attach the error listener to. */ -interface KoaCallbackContext { +interface KoaCallbackContext extends OrchestrionChannelContext { self?: KoaApp; } @@ -96,7 +93,7 @@ const _koaIntegration = ((options: KoaIntegrationOptions = {}) => { function instrumentKoa(ignoreLayersType: KoaLayerType[]): void { diagnosticsChannel.tracingChannel(CHANNELS.KOA_USE).subscribe({ start(rawCtx) { - handleUse(rawCtx as KoaUseContext, ignoreLayersType); + handleUse(rawCtx as OrchestrionChannelContext, ignoreLayersType); }, end() {}, asyncStart() {}, @@ -123,7 +120,7 @@ function instrumentKoa(ignoreLayersType: KoaLayerType[]): void { }); } -function handleUse(ctx: KoaUseContext, ignoreLayersType: KoaLayerType[]): void { +function handleUse(ctx: OrchestrionChannelContext, ignoreLayersType: KoaLayerType[]): void { const middleware = ctx.arguments[0]; if (typeof middleware === 'function') { ctx.arguments[0] = patchUse(middleware as KoaMiddleware, ignoreLayersType); diff --git a/packages/server-utils/src/integrations/langchain.ts b/packages/server-utils/src/integrations/langchain.ts index c10b9c79b6ae..54492e5f2b4f 100644 --- a/packages/server-utils/src/integrations/langchain.ts +++ b/packages/server-utils/src/integrations/langchain.ts @@ -16,6 +16,7 @@ import { import { MISTRAL_INTEGRATION_NAME } from '../ai/mistral/constants'; import { OPENAI_INTEGRATION_NAME } from '../ai/openai/constants'; import { GROQ_INTEGRATION_NAME } from './groq'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { langchainEmbeddingsChannels } from '../orchestrion/config/langchain'; import { bindTracingChannelToSpan } from '../tracing-channel'; @@ -38,17 +39,7 @@ const SKIPPED_PROVIDERS = [ ]; // The chat-model channels carry the live args array of `invoke(input, options)` / `_streamIterator(input, options)`. -interface RunnableChannelContext { - arguments: unknown[]; - self?: unknown; -} - // The embeddings channels carry the instance (`self`) and the `embedQuery(text)` / `embedDocuments(texts)` args. -interface EmbeddingsChannelContext { - self?: unknown; - arguments: unknown[]; -} - // Registered lazily on the first LangChain call (not at `setupOnce`) so a direct provider call made // before any LangChain call still gets its own span — matches the OTel patch-on-import timing. It // also stops the underlying SDK from double-instrumenting embeddings, whose `embedQuery`/ @@ -83,7 +74,7 @@ function instrumentChatModels(options: LangChainOptions): void { // callback dispatch then creates the spans, exactly as in the OTel path, so no span is opened // here — a `start` subscriber (which also makes orchestrion wrap the function) is enough. const injectHandler = (message: unknown): void => { - const args = (message as RunnableChannelContext).arguments; + const args = (message as OrchestrionChannelContext).arguments; if (!Array.isArray(args)) { return; } @@ -107,7 +98,7 @@ function instrumentChatModels(options: LangChainOptions): void { }; for (const channelName of [CHANNELS.LANGCHAIN_CHAT_MODEL_INVOKE, CHANNELS.LANGCHAIN_CHAT_MODEL_STREAM]) { - diagnosticsChannel.tracingChannel(channelName).start.subscribe(message => { + diagnosticsChannel.tracingChannel(channelName).start.subscribe(message => { markProvidersSkipped(); injectHandler(message); }); @@ -115,10 +106,10 @@ function instrumentChatModels(options: LangChainOptions): void { // `TypeSafeClassifier` calls Jev with `fetch`, not through a provider SDK, so nothing needs skipping. diagnosticsChannel - .tracingChannel(CHANNELS.LANGCHAIN_TYPESAFE_CLASSIFIER_INVOKE) + .tracingChannel(CHANNELS.LANGCHAIN_TYPESAFE_CLASSIFIER_INVOKE) .start.subscribe(message => { injectHandler(message); - const { self, arguments: args } = message as RunnableChannelContext; + const { self, arguments: args } = message as OrchestrionChannelContext; recordTypeSafeClassifierState(self, args?.[0]); }); } @@ -128,13 +119,13 @@ function instrumentChatModels(options: LangChainOptions): void { // still marks failed on error) and do not capture them. function instrumentEmbeddings(options: LangChainOptions): void { for (const channelName of langchainEmbeddingsChannels) { - bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => createEmbeddingsSpan(data, options), ); } } -function createEmbeddingsSpan(data: EmbeddingsChannelContext, options: LangChainOptions): Span { +function createEmbeddingsSpan(data: OrchestrionChannelContext, options: LangChainOptions): Span { // `embedQuery`/`embedDocuments` call the provider SDK internally, so skip that SDK's own // instrumentation before its channel fires (the producer runs at the embeddings channel's `start`). markProvidersSkipped(); diff --git a/packages/server-utils/src/integrations/langgraph.ts b/packages/server-utils/src/integrations/langgraph.ts index 5a41a8296aa3..d9d5bd863144 100644 --- a/packages/server-utils/src/integrations/langgraph.ts +++ b/packages/server-utils/src/integrations/langgraph.ts @@ -8,6 +8,7 @@ import { LANGGRAPH_INTEGRATION_NAME } from '../ai/langgraph/constants'; import type { CompiledGraph, LangGraphOptions } from '../ai/langgraph/types'; import { extractAgentNameFromParams, extractLLMFromParams, wrapToolsWithSpans } from '../ai/langgraph/utils'; import { DEBUG_BUILD } from '../debug-build'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { langgraphModuleNames } from '../orchestrion/config/langgraph'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; @@ -16,16 +17,6 @@ import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation // deduplicated out of the default set. const INTEGRATION_NAME = LANGGRAPH_INTEGRATION_NAME; -interface CompileChannelContext { - arguments: unknown[]; - result?: unknown; -} - -interface CreateReactAgentChannelContext { - arguments: unknown[]; - result?: unknown; -} - // `createReactAgent` compiles a `StateGraph` internally. When set, the compile subscriber skips that // nested graph so its `invoke` is wrapped once (by the createReactAgent handler), not twice. let insideCreateReactAgent = false; @@ -49,18 +40,18 @@ function instrumentLanggraph(options: LangGraphOptions): void { // StateGraph.compile returns synchronously; wrap the returned graph's `invoke` at `end`. diagnosticsChannel - .tracingChannel(CHANNELS.LANGGRAPH_STATE_GRAPH_COMPILE) + .tracingChannel(CHANNELS.LANGGRAPH_STATE_GRAPH_COMPILE) .end.subscribe(message => { if (insideCreateReactAgent) { return; } - const { arguments: args, result } = message as CompileChannelContext; + const { arguments: args, result } = message as OrchestrionChannelContext; wrapCompiledGraphInvoke(result, getFirstArgObject(args) ?? {}, resolvedOptions, null, sentryHandler); }); // createReactAgent only wraps tools and the returned graph's `invoke`. Tools are wrapped at // `start` (before the agent runs), invoke at `end`. - const reactAgentChannel = diagnosticsChannel.tracingChannel( + const reactAgentChannel = diagnosticsChannel.tracingChannel( CHANNELS.LANGGRAPH_CREATE_REACT_AGENT, ); reactAgentChannel.start.subscribe(message => { @@ -70,7 +61,7 @@ function instrumentLanggraph(options: LangGraphOptions): void { // stay off during this call's nested compile. Tool wrapping is guarded for the same reason. insideCreateReactAgent = true; try { - const { arguments: args } = message as CreateReactAgentChannelContext; + const { arguments: args } = message as OrchestrionChannelContext; const params = getFirstArgObject(args); if (params && Array.isArray(params.tools) && params.tools.length > 0) { wrapToolsWithSpans(params.tools, resolvedOptions, extractAgentNameFromParams(args) ?? undefined); @@ -81,7 +72,7 @@ function instrumentLanggraph(options: LangGraphOptions): void { }); reactAgentChannel.end.subscribe(message => { insideCreateReactAgent = false; - const { arguments: args, result } = message as CreateReactAgentChannelContext; + const { arguments: args, result } = message as OrchestrionChannelContext; const agentName = extractAgentNameFromParams(args) ?? undefined; const compileOptions = agentName ? { name: agentName } : {}; wrapCompiledGraphInvoke(result, compileOptions, resolvedOptions, extractLLMFromParams(args), sentryHandler); diff --git a/packages/server-utils/src/integrations/lru-memoizer.ts b/packages/server-utils/src/integrations/lru-memoizer.ts index 6bb2b8acbd53..8b4ec5c0391b 100644 --- a/packages/server-utils/src/integrations/lru-memoizer.ts +++ b/packages/server-utils/src/integrations/lru-memoizer.ts @@ -1,6 +1,7 @@ import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { lruMemoizerModuleNames } from '../orchestrion/config/lru-memoizer'; @@ -10,10 +11,6 @@ import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation // 'LruMemoizer' integration is omitted from the default set. const INTEGRATION_NAME = 'LruMemoizer' as const; -interface LruMemoizerLoadContext { - arguments: unknown[]; -} - const _lruMemoizerIntegration = (() => { return { name: INTEGRATION_NAME, @@ -25,7 +22,7 @@ const _lruMemoizerIntegration = (() => { function instrumentLruMemoizer(): void { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.LRU_MEMOIZER_LOAD), + diagnosticsChannel.tracingChannel(CHANNELS.LRU_MEMOIZER_LOAD), // We only want the helper's caller-context restore for the callback lru-memoizer fires from a detached `setImmediate`. () => undefined, ); diff --git a/packages/server-utils/src/integrations/mastra-classifier.ts b/packages/server-utils/src/integrations/mastra-classifier.ts index d25434bb2c9d..5db546336779 100644 --- a/packages/server-utils/src/integrations/mastra-classifier.ts +++ b/packages/server-utils/src/integrations/mastra-classifier.ts @@ -3,25 +3,17 @@ import { isObjectLike } from '@sentry/core'; import type { ClassifierEvaluationCall } from '../ai/mastra/classifier-evaluation'; import { processClassifierEvaluationData, setStartingClassifierEvaluation } from '../ai/mastra/classifier-evaluation'; import { getEvaluationInputMessages, getEvaluationOutputMessages } from '../ai/core/utils'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { safeChannelCallback } from '../tracing-channel'; import * as diagnosticsChannel from '../utils/diagnosticsChannel'; -interface ClassifierEvaluateChannelContext { - // `evaluate({ state, questions? })`; `self` is the `Classifier`. - arguments: unknown[]; - self?: unknown; - result?: unknown; -} - /** * Add the evaluated state, questions and answers of a `Classifier.evaluate()` call to the exporter's * `classifier_evaluation` span, which Mastra leaves without input or output. */ export function recordClassifierEvaluations(): void { - const channel = diagnosticsChannel.tracingChannel( - CHANNELS.MASTRA_CLASSIFIER_EVALUATE, - ); + const channel = diagnosticsChannel.tracingChannel(CHANNELS.MASTRA_CLASSIFIER_EVALUATE); const calls = new WeakMap(); channel.start.subscribe(message => { @@ -39,13 +31,13 @@ export function recordClassifierEvaluations(): void { safeChannelCallback(() => { const call = calls.get(message as object); if (call) { - finishClassifierEvaluation(call, message as ClassifierEvaluateChannelContext); + finishClassifierEvaluation(call, message as OrchestrionChannelContext); } }); }); } -function finishClassifierEvaluation(call: ClassifierEvaluationCall, message: ClassifierEvaluateChannelContext): void { +function finishClassifierEvaluation(call: ClassifierEvaluationCall, message: OrchestrionChannelContext): void { call.settled = true; const { span, recordInputs, recordOutputs } = call; if (!span) { diff --git a/packages/server-utils/src/integrations/mastra.ts b/packages/server-utils/src/integrations/mastra.ts index 1f541fe83303..126afd052d15 100644 --- a/packages/server-utils/src/integrations/mastra.ts +++ b/packages/server-utils/src/integrations/mastra.ts @@ -22,6 +22,7 @@ import type { MastraExporterOptions } from '../ai/mastra'; import { getSentrySpanForMastraId } from '../ai/mastra/span-registry'; import type { MastraObservabilityExporter } from '../ai/mastra/types'; import { DEBUG_BUILD } from '../debug-build'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { mastraModuleNames } from '../orchestrion/config/mastra'; import { recordClassifierEvaluations } from './mastra-classifier'; @@ -45,16 +46,6 @@ interface MastraInstance { observability?: { getDefaultInstance?: () => MastraObservabilityInstance | undefined }; } -interface ConstructorChannelContext { - arguments: unknown[]; - self?: unknown; -} - -interface ExecuteWithContextChannelContext { - // `executeWithContext({ span, fn })` — the first arg carries the Mastra AISpan. - arguments: unknown[]; -} - /** Mastra AISpan → the id the exporter keys its Sentry span on. */ function mastraSpanId(span: unknown): string | undefined { if (!isObjectLike(span)) { @@ -89,9 +80,9 @@ const _mastraIntegration = ((options: MastraOptions = {}) => { }) satisfies IntegrationFn; function instrumentExporter(options: MastraOptions): void { - diagnosticsChannel.tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.subscribe(message => { + diagnosticsChannel.tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.subscribe(message => { safeChannelCallback(() => { - const { self } = message as ConstructorChannelContext; + const { self } = message as OrchestrionChannelContext; attachExporter(self, options); }); }); @@ -109,10 +100,10 @@ function instrumentExporter(options: MastraOptions): void { */ function captureExecuteWithContextErrors(): void { diagnosticsChannel - .tracingChannel(CHANNELS.MASTRA_EXECUTE_WITH_CONTEXT) + .tracingChannel(CHANNELS.MASTRA_EXECUTE_WITH_CONTEXT) .error.subscribe(message => { safeChannelCallback(() => { - const data = message as ExecuteWithContextChannelContext & { error: unknown }; + const data = message as OrchestrionChannelContext; captureMastraError(data.error, (data.arguments as unknown[] | undefined)?.[0]); }); }); @@ -168,7 +159,7 @@ function captureMastraError(error: unknown, params: unknown): void { */ function instrumentExecuteWithContext(): void { bindSpanToChannelStore( - diagnosticsChannel.tracingChannel(CHANNELS.MASTRA_EXECUTE_WITH_CONTEXT), + diagnosticsChannel.tracingChannel(CHANNELS.MASTRA_EXECUTE_WITH_CONTEXT), data => { const params = (data.arguments as unknown[] | undefined)?.[0]; const id = isObjectLike(params) ? mastraSpanId(params.span) : undefined; diff --git a/packages/server-utils/src/integrations/mcp-server.ts b/packages/server-utils/src/integrations/mcp-server.ts index 8f3a75f1a0f1..d3a4823cf389 100644 --- a/packages/server-utils/src/integrations/mcp-server.ts +++ b/packages/server-utils/src/integrations/mcp-server.ts @@ -2,6 +2,7 @@ import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { wrapMcpServerWithSentry } from '@sentry/core/server'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { mcpServerModuleNames } from '../orchestrion/config/mcp-server'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; @@ -9,11 +10,6 @@ import { safeChannelCallback } from '../tracing-channel'; const INTEGRATION_NAME = 'McpServer'; -interface ConstructorChannelContext { - arguments: unknown[]; - self?: unknown; -} - const _mcpServerIntegration = (() => { return { name: INTEGRATION_NAME, @@ -34,9 +30,9 @@ function subscribe(): void { // wrap's own `WeakSet` guard makes a later manual call on the same instance a no-op, so the two // paths coexist safely. for (const channel of [CHANNELS.MCP_SERVER_V2_CONSTRUCTOR, CHANNELS.MCP_SERVER_V1_CONSTRUCTOR]) { - diagnosticsChannel.tracingChannel(channel).end.subscribe(message => { + diagnosticsChannel.tracingChannel(channel).end.subscribe(message => { safeChannelCallback(() => { - const { self } = message as ConstructorChannelContext; + const { self } = message as OrchestrionChannelContext; if (self) { wrapMcpServerWithSentry(self); } diff --git a/packages/server-utils/src/integrations/mistral.ts b/packages/server-utils/src/integrations/mistral.ts index f70d026cf358..f916321bd31c 100644 --- a/packages/server-utils/src/integrations/mistral.ts +++ b/packages/server-utils/src/integrations/mistral.ts @@ -8,6 +8,7 @@ import { MISTRAL_INTEGRATION_NAME, MISTRAL_ORIGIN } from '../ai/mistral/constant import { instrumentEventStream } from '../ai/mistral/streaming'; import type { MistralOptions } from '../ai/mistral/types'; import { addResponseAttributes, getSpanName } from '../ai/mistral/utils'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { mistralModuleNames } from '../orchestrion/config/mistral'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; @@ -25,15 +26,6 @@ const INSTRUMENTED_CHANNELS = [ { channel: CHANNELS.MISTRAL_AGENTS_STREAM, operation: 'invoke_agent', streaming: true }, ] as const; -/** - * The context orchestrion shares across the tracing-channel lifecycle hooks: `arguments` is the live - * args array passed to the SDK method, and Node's `tracingChannel` attaches `result` when it settles. - */ -interface MistralChannelContext { - arguments: unknown[]; - result?: unknown; -} - const _mistralAIIntegration = ((options: MistralOptions = {}) => { return { name: INTEGRATION_NAME, @@ -46,7 +38,7 @@ const _mistralAIIntegration = ((options: MistralOptions = {}) => { function instrumentMistral(options: MistralOptions): void { for (const { channel, operation, streaming } of INSTRUMENTED_CHANNELS) { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channel), + diagnosticsChannel.tracingChannel(channel), data => createGenAiSpan(data, operation, streaming, options), { beforeSpanEnd: (span, data) => { @@ -65,7 +57,7 @@ function instrumentMistral(options: MistralOptions): void { * Returning `undefined` opts the payload out so no span is opened. */ function createGenAiSpan( - data: MistralChannelContext, + data: OrchestrionChannelContext, operation: string, streaming: boolean, options: MistralOptions, diff --git a/packages/server-utils/src/integrations/mongodb/index.ts b/packages/server-utils/src/integrations/mongodb/index.ts index 6f492a575a6b..593638025c6f 100644 --- a/packages/server-utils/src/integrations/mongodb/index.ts +++ b/packages/server-utils/src/integrations/mongodb/index.ts @@ -3,6 +3,7 @@ import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import type { MongodbNamespace, MongoV3Topology } from './mongodb-span'; import { getV3CommandOperation, getV3SpanAttributes, getV4SpanAttributes, startMongoSpan } from './mongodb-span'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import { mongodbModuleNames } from '../../orchestrion/config/mongodb'; @@ -16,9 +17,8 @@ const ORIGIN = 'auto.db.mongo'; * what orchestrion's transform attaches to a channel context: * `self` is the `this`, plus args. */ -interface MongoChannelContext { +interface MongoChannelContext extends OrchestrionChannelContext { self?: { address?: string }; - arguments?: unknown[]; } // Details extracted from a v3 wireprotocol call's arguments to build its span. diff --git a/packages/server-utils/src/integrations/mongoose/index.ts b/packages/server-utils/src/integrations/mongoose/index.ts index 4e00635939c1..62b7ead8b720 100644 --- a/packages/server-utils/src/integrations/mongoose/index.ts +++ b/packages/server-utils/src/integrations/mongoose/index.ts @@ -4,6 +4,7 @@ import { defineIntegration, getActiveSpan, waitForTracingChannelBinding } from ' import { subscribeMongooseDiagnosticChannels } from './mongoose-dc-subscriber'; import type { MongooseLegacyCollection } from './mongoose-legacy-span'; import { startMongooseLegacySpan } from './mongoose-legacy-span'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { MONGOOSE_CONTEXT_CAPTURE_CHANNELS } from '../../orchestrion/config/mongoose'; import type { SentryTracingChannel } from '../../tracing-channel'; @@ -38,12 +39,8 @@ interface MongooseDocument { * The shape orchestrion's transform attaches to the tracing-channel context * object. `self` is the `this` of the traced method */ -interface MongooseChannelContext { +interface MongooseChannelContext extends OrchestrionChannelContext { self?: object; - arguments?: unknown[]; - result?: unknown; - error?: unknown; - moduleVersion?: string; } // The active span captured when a query/aggregate was *built*, keyed by the diff --git a/packages/server-utils/src/integrations/mysql.ts b/packages/server-utils/src/integrations/mysql.ts index e4049b65d30a..d3e8010cb1ba 100644 --- a/packages/server-utils/src/integrations/mysql.ts +++ b/packages/server-utils/src/integrations/mysql.ts @@ -23,6 +23,7 @@ import { startInactiveSpan, } from '@sentry/core'; import { sanitizeSqlQueryWithSummary } from '../utils/sql'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { mysqlModuleNames } from '../orchestrion/config/mysql'; @@ -42,13 +43,8 @@ const DB_SYSTEM_NAME_VALUE_MYSQL = 'mysql' as const; * The shape orchestrion's transform attaches to the tracing-channel `context` object. Documented here * rather than imported because orchestrion's runtime doesn't export it. */ -interface MysqlQueryChannelContext { - // The live args array passed to the wrapped `connection.query` call; `arguments[0]` is the SQL. - arguments: unknown[]; +interface MysqlQueryChannelContext extends OrchestrionChannelContext { self?: MysqlConnection; - moduleVersion?: string; - result?: unknown; - error?: unknown; // The caller's scope, captured at `start` and replayed onto the streamed `Query` emitter (see below). _sentryCallerScope?: Scope; } diff --git a/packages/server-utils/src/integrations/mysql2/index.ts b/packages/server-utils/src/integrations/mysql2/index.ts index b8f6501a26ba..5f337d21a41d 100644 --- a/packages/server-utils/src/integrations/mysql2/index.ts +++ b/packages/server-utils/src/integrations/mysql2/index.ts @@ -10,6 +10,7 @@ import { } from '@sentry/core'; import { sanitizeSqlQueryWithSummary } from '../../utils/sql'; import { subscribeMysql2DiagnosticChannels } from './mysql2-dc-subscriber'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import type { ChannelName } from '../../orchestrion/channels'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan } from '../../tracing-channel'; @@ -38,14 +39,8 @@ const DB_SYSTEM_VALUE_MYSQL = 'mysql'; * object. Documented here rather than imported because orchestrion's runtime * doesn't export it. */ -interface Mysql2QueryChannelContext { - // The live args array passed to the wrapped `query`/`execute` call: - // `arguments[0]` is the SQL (a string, `Query`, or `{ sql, values }`), - // `arguments[1]` is the values array or a callback. - arguments: unknown[]; +interface Mysql2QueryChannelContext extends OrchestrionChannelContext { self?: Mysql2Connection; - result?: unknown; - error?: unknown; } interface Mysql2ConnectionConfig { diff --git a/packages/server-utils/src/integrations/openai-compatible.ts b/packages/server-utils/src/integrations/openai-compatible.ts index 2c23c711b60f..48bacae5bcf8 100644 --- a/packages/server-utils/src/integrations/openai-compatible.ts +++ b/packages/server-utils/src/integrations/openai-compatible.ts @@ -13,6 +13,7 @@ import { addRequestAttributes, extractRequestAttributes } from '../ai/openai'; import { instrumentStream } from '../ai/openai/streaming'; import type { OpenAiOptions } from '../ai/openai/types'; import { addResponseAttributes } from '../ai/openai/utils'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; import { bindTracingChannelToSpan } from '../tracing-channel'; @@ -33,15 +34,6 @@ export interface OpenAiCompatibleProvider { channels: { chat: string; embeddings: string }; } -/** - * The context orchestrion shares across the tracing-channel lifecycle hooks: `arguments` is the live args - * array passed to `Completions.create(body, options)`, and `result` holds its returned `APIPromise`. - */ -interface OpenAiCompatibleChannelContext { - arguments: unknown[]; - result?: unknown; -} - /** * Builds a diagnostics-channel integration for an OpenAI-compatible provider SDK. It subscribes to the * `orchestrion::{chat,embeddings}` channels injected into the SDK's `create` methods, so it @@ -60,7 +52,7 @@ export function createOpenAiCompatibleIntegration(channel), + diagnosticsChannel.tracingChannel(channel), data => createGenAiSpan(data, operation, provider, options), { beforeSpanEnd: (span, data) => { @@ -97,7 +89,7 @@ export function createOpenAiCompatibleIntegration { return { name: INTEGRATION_NAME, @@ -53,7 +44,7 @@ const _openAIIntegration = ((options: OpenAiOptions = {}) => { function instrumentOpenai(options: OpenAiOptions): void { for (const { channel, operation } of INSTRUMENTED_CHANNELS) { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channel), + diagnosticsChannel.tracingChannel(channel), data => createGenAiSpan(data, operation, options), { beforeSpanEnd: (span, data) => { @@ -80,7 +71,7 @@ function instrumentOpenai(options: OpenAiOptions): void { * Build the span for an instrumented `create` call. * Returning `undefined` opts the payload out so no span is opened. */ -function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, options: OpenAiOptions): Span | undefined { +function createGenAiSpan(data: OrchestrionChannelContext, operation: string, options: OpenAiOptions): Span | undefined { // When another provider (e.g. LangChain) is driving the SDK, it records the spans itself and marks this // provider as skipped; skip here to avoid double spans. if (_INTERNAL_shouldSkipAiProviderWrapping(INTEGRATION_NAME)) { @@ -126,7 +117,7 @@ function isAsyncIterable(value: unknown): value is AsyncIterableStream { * to hand span-ending ownership to `instrumentStream`; `false` for non-streaming/errored results, which end * via the normal `beforeSpanEnd` path. */ -function wrapStreamResult(span: Span, data: OpenAiChatChannelContext, options: OpenAiOptions): boolean { +function wrapStreamResult(span: Span, data: OrchestrionChannelContext, options: OpenAiOptions): boolean { const result = data.result; if (!isAsyncIterable(result)) { return false; diff --git a/packages/server-utils/src/integrations/pi-durable.ts b/packages/server-utils/src/integrations/pi-durable.ts index b5b115ec6c7b..a9bd9e1b6a99 100644 --- a/packages/server-utils/src/integrations/pi-durable.ts +++ b/packages/server-utils/src/integrations/pi-durable.ts @@ -7,19 +7,11 @@ import { PI_DURABLE_INTEGRATION_NAME } from '../ai/pi-durable/constants'; import { markBuiltInTool } from '../ai/pi-durable/tools'; import type { PiHarnessOptions } from '../ai/pi-durable/types'; import { DEBUG_BUILD } from '../debug-build'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { piDurableModuleNames } from '../orchestrion/config/pi-durable'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; -interface HarnessOpenChannelContext { - arguments: unknown[]; - result?: unknown; -} - -interface CodingToolChannelContext { - result?: unknown; -} - const _piDurableIntegration = ((options: PiDurableOptions = {}) => { return { name: PI_DURABLE_INTEGRATION_NAME, @@ -33,10 +25,10 @@ const _piDurableIntegration = ((options: PiDurableOptions = {}) => { }) satisfies IntegrationFn; function instrumentPiDurable(options: PiDurableOptions): void { - const harnessOpen = diagnosticsChannel.tracingChannel(CHANNELS.PI_DURABLE_HARNESS_OPEN); + const harnessOpen = diagnosticsChannel.tracingChannel(CHANNELS.PI_DURABLE_HARNESS_OPEN); harnessOpen.start.subscribe(message => { - const args = (message as HarnessOpenChannelContext).arguments; + const args = (message as OrchestrionChannelContext).arguments; try { if (args && isObjectLike(args[1])) { args[1] = instrumentPiDurableHarnessOptions(args[1] as PiHarnessOptions, options); @@ -47,7 +39,7 @@ function instrumentPiDurable(options: PiDurableOptions): void { }); harnessOpen.asyncEnd.subscribe(message => { - const { arguments: args, result } = message as HarnessOpenChannelContext; + const { arguments: args, result } = message as OrchestrionChannelContext; try { if (args && isObjectLike(args[1])) { endRunsOnClose(args[1], result); @@ -58,8 +50,8 @@ function instrumentPiDurable(options: PiDurableOptions): void { }); diagnosticsChannel - .tracingChannel(CHANNELS.PI_DURABLE_CODING_TOOL) - .end.subscribe(message => markBuiltInTool((message as CodingToolChannelContext).result)); + .tracingChannel(CHANNELS.PI_DURABLE_CODING_TOOL) + .end.subscribe(message => markBuiltInTool((message as OrchestrionChannelContext).result)); } /** diff --git a/packages/server-utils/src/integrations/postgres-js.ts b/packages/server-utils/src/integrations/postgres-js.ts index 5d3b865cd20d..0ac51daa8f54 100644 --- a/packages/server-utils/src/integrations/postgres-js.ts +++ b/packages/server-utils/src/integrations/postgres-js.ts @@ -28,6 +28,7 @@ import { _reconstructQuery, type PostgresConnectionContext, } from './postgresjs'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { postgresJsModuleNames } from '../orchestrion/config/postgres'; @@ -81,11 +82,9 @@ interface PostgresQuery { reject?: (...args: unknown[]) => unknown; } -interface PostgresJsQueryContext { - arguments?: unknown[]; +interface PostgresJsQueryContext extends OrchestrionChannelContext { self?: PostgresQuery; - result?: unknown; - error?: unknown; + [SPAN_ENDED]?: boolean; } // A connection object -> its resolved context, populated on the `connection` @@ -178,7 +177,7 @@ function wrapQuerySettlement(data: PostgresJsQueryContext, span: Span, sanitized // Claim ownership of ending the span up front, so `deferSpanEnd` defers to the // wrapper even if `span.end()` below throws. const markEnded = (): void => { - (data as Record)[SPAN_ENDED] = true; + data[SPAN_ENDED] = true; }; const originalResolve = query.resolve; @@ -332,7 +331,7 @@ function instrumentPostgresJs(options: PostgresJsIntegrationOptions): void { deferSpanEnd({ data }) { // `handle` is async: its promise settles on dispatch (asyncEnd), long // before the query does. The resolve/reject wrappers own the ending. - if ((data as Record)[SPAN_ENDED]) { + if (data[SPAN_ENDED]) { return true; // wrappers already ended it } if ('error' in data) { diff --git a/packages/server-utils/src/integrations/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index 5d93ac17769f..c1a2980943db 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -23,6 +23,7 @@ import { startInactiveSpan, } from '@sentry/core'; import { sanitizeSqlQueryWithSummary } from '../utils/sql'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { pgModuleNames } from '../orchestrion/config/pg'; @@ -59,12 +60,7 @@ const SPAN_POOL_CONNECT = 'pg-pool.connect'; * The shape orchestrion's transform attaches to the tracing-channel `context`. Documented here rather * than imported because orchestrion's runtime doesn't export it. */ -interface PgChannelContext { - // The live args array passed to the wrapped `query`/`connect` call. - arguments: unknown[]; - self?: unknown; - result?: unknown; - error?: unknown; +interface PgChannelContext extends OrchestrionChannelContext { // The caller's scope, captured at `start` and replayed onto a streamed `Submittable` emitter (see below). _sentryCallerScope?: Scope; } diff --git a/packages/server-utils/src/integrations/prisma/orchestrion.ts b/packages/server-utils/src/integrations/prisma/orchestrion.ts index 656b7cf8fc75..de209f694d71 100644 --- a/packages/server-utils/src/integrations/prisma/orchestrion.ts +++ b/packages/server-utils/src/integrations/prisma/orchestrion.ts @@ -3,6 +3,7 @@ import type { Span } from '@sentry/core'; import { getActiveSpan, getRootSpan, spanIsIgnored, startInactiveSpan, withActiveSpan } from '@sentry/core'; import { DB_COLLECTION_NAME, DB_OPERATION_NAME, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { prismaChannels, PRISMA_LAZY_TERMINALS } from '../../orchestrion/config/prisma'; import type { TracingChannelLifeCycleOptions } from '../../tracing-channel'; import { bindTracingChannelToSpan, safeChannelCallback } from '../../tracing-channel'; @@ -20,12 +21,8 @@ interface PrismaCollection { tableName?: unknown; } -interface PrismaTerminalChannelContext { - arguments: unknown[]; +interface PrismaTerminalChannelContext extends OrchestrionChannelContext { self?: PrismaCollection; - moduleVersion?: string; - result?: unknown; - error?: unknown; } // Prisma's `AsyncIterableResult`: `await`, `.then()` and `.first()` all funnel through `toArray()`; only diff --git a/packages/server-utils/src/integrations/redis/index.ts b/packages/server-utils/src/integrations/redis/index.ts index 4140156cee57..7176a14b57ec 100644 --- a/packages/server-utils/src/integrations/redis/index.ts +++ b/packages/server-utils/src/integrations/redis/index.ts @@ -21,6 +21,7 @@ import { withActiveSpan, waitForTracingChannelBinding, } from '@sentry/core'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { getRedisQueryNaming } from './redis-span-name'; import { defaultDbStatementSerializer } from './redis-statement-serializer'; @@ -67,13 +68,6 @@ interface NodeRedisClient { options?: NodeRedisClientOptions; } -interface CommandContext { - arguments?: unknown[]; - self?: unknown; - result?: unknown; - error?: unknown; -} - function endSpan(span: Span, err: unknown): void { if (err) { span.setStatus({ code: SPAN_STATUS_ERROR, message: err instanceof Error ? err.message : String(err) }); @@ -140,7 +134,7 @@ function startCommandSpan( // Settles via `command_obj.callback`, not the sync return — so instead of // `bindTracingChannelToSpan` we open the span in `start`, wrap the callback to end it, and end on `error` for sync throws. function subscribeLegacyRedisCommand(cacheOptions: RedisCacheOptions): void { - const channel = diagnosticsChannel.tracingChannel(CHANNELS.REDIS_COMMAND); + const channel = diagnosticsChannel.tracingChannel(CHANNELS.REDIS_COMMAND); const noop = (): void => {}; channel.subscribe({ end: noop, @@ -170,7 +164,7 @@ function subscribeLegacyRedisCommand(cacheOptions: RedisCacheOptions): void { attributes[SERVER_PORT] = client.connection_options.port; } const span = startCommandSpan(command.command, command.args ?? [], attributes, cacheOptions); - (data as CommandContext & { _sentrySpan?: Span })._sentrySpan = span; + (data as OrchestrionChannelContext & { _sentrySpan?: Span })._sentrySpan = span; const parentSpan = getActiveSpan(); command.callback = function (this: unknown, err: Error | null | undefined, reply: unknown) { @@ -185,7 +179,7 @@ function subscribeLegacyRedisCommand(cacheOptions: RedisCacheOptions): void { }, error(data) { // Synchronous throw: the wrapped callback never fires, so end here instead. - const span = (data as CommandContext & { _sentrySpan?: Span })._sentrySpan; + const span = (data as OrchestrionChannelContext & { _sentrySpan?: Span })._sentrySpan; if (span) { endSpan(span, data.error); } @@ -197,10 +191,10 @@ function subscribeLegacyRedisCommand(cacheOptions: RedisCacheOptions): void { function bindNodeRedisCommandChannel( channelName: string, - getWireArgs: (data: CommandContext) => Array | undefined, + getWireArgs: (data: OrchestrionChannelContext) => Array | undefined, cacheOptions: RedisCacheOptions, ): void { - const channel = diagnosticsChannel.tracingChannel(channelName); + const channel = diagnosticsChannel.tracingChannel(channelName); bindTracingChannelToSpan( channel, data => { @@ -223,14 +217,14 @@ function bindNodeRedisCommandChannel( } // `sendCommand(args, options)` — `args` are already the wire arguments. -function getSendCommandArgs(data: CommandContext): Array | undefined { +function getSendCommandArgs(data: OrchestrionChannelContext): Array | undefined { const args = data.arguments?.[0]; return Array.isArray(args) ? (args as Array) : undefined; } // `commandsExecutor(command, jsArgs)` — derive the wire arguments the same way // `@redis/client` does internally, via `command.transformArguments`. -function getExecutorArgs(data: CommandContext): Array | undefined { +function getExecutorArgs(data: OrchestrionChannelContext): Array | undefined { const command = data.arguments?.[0] as RedisCommandDefinition | undefined; const jsArgs = data.arguments?.[1]; if (typeof command?.transformArguments !== 'function' || !Array.isArray(jsArgs)) { @@ -244,7 +238,9 @@ function getExecutorArgs(data: CommandContext): Array | undefin } function bindNodeRedisConnectChannel(): void { - const channel = diagnosticsChannel.tracingChannel(CHANNELS.NODE_REDIS_CONNECT); + const channel = diagnosticsChannel.tracingChannel( + CHANNELS.NODE_REDIS_CONNECT, + ); bindTracingChannelToSpan(channel, data => { const options = (data.self as NodeRedisClient | undefined)?.options; return startInactiveSpan({ @@ -261,8 +257,11 @@ function bindNodeRedisConnectChannel(): void { // Batch (multi/pipeline): one span per `exec`. Batched commands bypass `sendCommand`, so // the executor's `ctx.arguments[0]` (the queued commands) gives the batch size. Span shape // mirrors the native `node-redis:batch` span (see `redis-dc-subscriber.ts`). -function bindNodeRedisBatchChannel(channelName: string, getOperation: (data: CommandContext) => string): void { - const channel = diagnosticsChannel.tracingChannel(channelName); +function bindNodeRedisBatchChannel( + channelName: string, + getOperation: (data: OrchestrionChannelContext) => string, +): void { + const channel = diagnosticsChannel.tracingChannel(channelName); bindTracingChannelToSpan(channel, data => { const commands = data.arguments?.[0]; const size = Array.isArray(commands) ? commands.length : undefined; diff --git a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts index 0e91d59c03bb..d918d080f749 100644 --- a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts @@ -12,6 +12,7 @@ import { import { DB_QUERY, DB } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; import { startInactiveSpan } from '@sentry/core'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import type { RedisCacheOptions } from './redis-cache'; @@ -33,15 +34,10 @@ interface RedisClientLike { options?: { host?: string; port?: number }; } -interface IORedisCommandContext { - arguments?: unknown[]; +interface IORedisCommandContext extends OrchestrionChannelContext { self?: RedisClientLike; - result?: unknown; - error?: unknown; } -type IORedisConnectContext = Omit; - function getConnectionOptions(self: RedisClientLike | undefined): { host?: string; port?: number } { return { host: self?.options?.host, port: self?.options?.port }; } @@ -111,7 +107,7 @@ export function instrumentIoredis(options: RedisCacheOptions): void { const commandChannel = diagnosticsChannel.tracingChannel( CHANNELS.IOREDIS_COMMAND, ); - const connectChannel = diagnosticsChannel.tracingChannel( + const connectChannel = diagnosticsChannel.tracingChannel( CHANNELS.IOREDIS_CONNECT, ); diff --git a/packages/server-utils/src/integrations/tedious.ts b/packages/server-utils/src/integrations/tedious.ts index 21ff279dd13f..0878dc5e3118 100644 --- a/packages/server-utils/src/integrations/tedious.ts +++ b/packages/server-utils/src/integrations/tedious.ts @@ -25,6 +25,7 @@ import { SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { tediousModuleNames } from '../orchestrion/config/tedious'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; @@ -66,16 +67,14 @@ interface TediousRequest extends EventEmitter { } /** Context orchestrion attaches to the query channels (wrapping the `Connection` request methods). */ -interface TediousQueryChannelContext { +interface TediousQueryChannelContext extends OrchestrionChannelContext { // `arguments[0]` is the `Request` (or `BulkLoad` for `execBulkLoad`), both `EventEmitter`s. arguments: [TediousRequest?, ...unknown[]]; self?: TediousConnection; - moduleVersion?: string; } /** Context orchestrion attaches to the `Connection.connect` channel. */ -interface TediousConnectChannelContext { - arguments: unknown[]; +interface TediousConnectChannelContext extends OrchestrionChannelContext { self?: TediousConnection; } diff --git a/packages/server-utils/src/integrations/typesafe.ts b/packages/server-utils/src/integrations/typesafe.ts index 594f9df7a068..9575e1206435 100644 --- a/packages/server-utils/src/integrations/typesafe.ts +++ b/packages/server-utils/src/integrations/typesafe.ts @@ -5,23 +5,12 @@ import type { GenAiOptions } from '../ai/core/utils'; import { resolveAIRecordingOptions } from '../ai/core/utils'; import { addResponseAttributes, onSystemOneResponse, startEvaluateSpan } from '../ai/typesafe'; import { TYPESAFE_INTEGRATION_NAME } from '../ai/typesafe/constants'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { typesafeModuleNames } from '../orchestrion/config/typesafe'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; import { bindTracingChannelToSpan } from '../tracing-channel'; -/** - * The context orchestrion shares across the tracing-channel lifecycle hooks: `arguments` is the live - * args array passed to `systemOne(request, options)`, `self` the `TypeSafeClient`, and `result` the - * returned `APIPromise` (replaced by the parsed response body once it arrives). `Sync` tracing keeps - * orchestrion from calling `.then()` on it, see `orchestrion/config/typesafe.ts`. - */ -interface TypeSafeChannelContext { - arguments: unknown[]; - self?: unknown; - result?: unknown; -} - const _typesafeIntegration = ((options: GenAiOptions = {}) => { return { name: TYPESAFE_INTEGRATION_NAME, @@ -33,7 +22,7 @@ const _typesafeIntegration = ((options: GenAiOptions = {}) => { function instrumentTypeSafe(options: GenAiOptions): void { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.TYPESAFE_SYSTEM_ONE), + diagnosticsChannel.tracingChannel(CHANNELS.TYPESAFE_SYSTEM_ONE), data => startEvaluateSpan(data.arguments?.[0], data.self, resolveAIRecordingOptions(options).recordInputs), { beforeSpanEnd: (span, data) => { diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts index daf4ee24584c..2a59de79b4f8 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts @@ -9,6 +9,7 @@ import { withActiveSpan, } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan, type TracingChannelPayloadWithSpan } from '../../tracing-channel'; import { @@ -56,13 +57,6 @@ import { asString, isReadableStream, tapModelCallStream } from './util'; * double-counts tool spans. */ -/** Shape orchestrion's transform attaches to the tracing-channel context. */ -interface OrchestrionContext { - arguments: unknown[]; - result?: unknown; - error?: unknown; -} - /** Builds the normalized message for a channel from the wrapped call's first-arg options. */ type MessageBuilder = (options: Record, telemetry: Record) => VercelAiChannelMessage; @@ -227,13 +221,13 @@ function bindOperation( build: MessageBuilder, options: VercelAiChannelOptions, ): void { - const channel = tracingChannel(channelName); + const channel = tracingChannel(channelName); // Build the operation span from the wrapped call's first argument and track it (so a model call can // resolve it as its parent). `bindTracingChannelToSpan` calls this once at channel `start` and makes // the returned span the active async context for the operation's duration — that active span is what // `resolveModelCallParent` reads. It also sets `data._sentrySpan`, so we don't here. - const buildOperationSpan = (data: TracingChannelPayloadWithSpan): Span | undefined => { + const buildOperationSpan = (data: TracingChannelPayloadWithSpan): Span | undefined => { const callOptions = isObjectLike(data.arguments[0]) ? data.arguments[0] : {}; const telemetry = isObjectLike(callOptions.experimental_telemetry) ? callOptions.experimental_telemetry : {}; // `isEnabled === false` means the user opted out — emit no span. But `isEnabled` is also `false` on @@ -288,7 +282,7 @@ function bindOperation( bindTracingChannelToSpan( channel, - (data: TracingChannelPayloadWithSpan) => buildOperationSpan(data), + (data: TracingChannelPayloadWithSpan) => buildOperationSpan(data), { beforeSpanEnd: (span, data) => { const message = messages.get(data); @@ -334,7 +328,7 @@ function bindOperation( * anything that isn't a streamed `streamText` result, so the helper ends the span as usual. */ function deferStreamTextOperationEnd( - data: TracingChannelPayloadWithSpan, + data: TracingChannelPayloadWithSpan, end: (error?: unknown) => void, ): boolean { if (messages.get(data)?.type !== 'streamText' || 'error' in data || !isStreamingResult(data.result)) { @@ -404,7 +398,7 @@ function subscribeResolveLanguageModel( channelName: string, options: VercelAiChannelOptions, ): void { - tracingChannel(channelName).subscribe({ + tracingChannel(channelName).subscribe({ end(rawCtx) { const ctx = rawCtx; if (!isObjectLike(ctx.result)) { diff --git a/packages/server-utils/src/orchestrion/types.ts b/packages/server-utils/src/orchestrion/types.ts new file mode 100644 index 000000000000..b30376cb44fb --- /dev/null +++ b/packages/server-utils/src/orchestrion/types.ts @@ -0,0 +1,7 @@ +export interface OrchestrionChannelContext { + arguments: unknown[]; + self?: unknown; + result?: unknown; + error?: unknown; + moduleVersion?: string; +} diff --git a/packages/server-utils/test/integrations/mastra/error-capture.test.ts b/packages/server-utils/test/integrations/mastra/error-capture.test.ts index d0bd26dd7335..2ee0f7e368c3 100644 --- a/packages/server-utils/test/integrations/mastra/error-capture.test.ts +++ b/packages/server-utils/test/integrations/mastra/error-capture.test.ts @@ -1,3 +1,4 @@ +import type { OrchestrionChannelContext } from '../../../src/orchestrion/types'; import { tracingChannel } from 'node:diagnostics_channel'; import { GLOBAL_OBJ, setCurrentClient } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -5,7 +6,7 @@ import { mastraIntegration } from '../../../src/integrations/mastra'; import { CHANNELS } from '../../../src/orchestrion/channels'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; -const channel = tracingChannel<{ error: unknown; arguments: unknown[] }>(CHANNELS.MASTRA_EXECUTE_WITH_CONTEXT); +const channel = tracingChannel(CHANNELS.MASTRA_EXECUTE_WITH_CONTEXT); describe('mastraIntegration error capture', () => { let client: TestClient; diff --git a/packages/server-utils/test/integrations/postgres/postgres.test.ts b/packages/server-utils/test/integrations/postgres/postgres.test.ts index 376edf60c883..db64ef9e137c 100644 --- a/packages/server-utils/test/integrations/postgres/postgres.test.ts +++ b/packages/server-utils/test/integrations/postgres/postgres.test.ts @@ -1,3 +1,4 @@ +import type { OrchestrionChannelContext } from '../../../src/orchestrion/types'; import { AsyncLocalStorage } from 'node:async_hooks'; import { tracingChannel } from 'node:diagnostics_channel'; import type { Client, Scope, Span } from '@sentry/core'; @@ -73,11 +74,6 @@ function makeSpan(): Span { return { end: vi.fn(), setStatus: vi.fn(), setAttributes: vi.fn() } as unknown as Span; } -interface ChannelContext { - arguments: unknown[]; - self?: unknown; -} - describe('postgresIntegration', () => { let startInactiveSpanSpy: MockInstance; let getActiveSpanSpy: MockInstance; @@ -113,7 +109,10 @@ describe('postgresIntegration', () => { const CONNECTION = { database: 'tests', host: 'localhost', port: 5432, user: 'tim' }; it('query: builds a `pg.query` span with db attributes and the orchestrion origin', async () => { - const ctx: ChannelContext = { arguments: ['SELECT * FROM "User"'], self: { connectionParameters: CONNECTION } }; + const ctx: OrchestrionChannelContext = { + arguments: ['SELECT * FROM "User"'], + self: { connectionParameters: CONNECTION }, + }; await tracingChannel(CHANNELS.PG_QUERY).tracePromise(async () => ({ rows: [] }), ctx); @@ -138,7 +137,7 @@ describe('postgresIntegration', () => { }); it('query: records the prepared-statement name as `db.postgresql.plan`', async () => { - const ctx: ChannelContext = { + const ctx: OrchestrionChannelContext = { arguments: [{ name: 'select-user-by-email', text: 'SELECT * FROM "User" WHERE "email" = $1', values: ['x'] }], self: { connectionParameters: CONNECTION }, }; @@ -159,7 +158,7 @@ describe('postgresIntegration', () => { }); it('query: sets error status and ends the span when the query rejects', async () => { - const ctx: ChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; + const ctx: OrchestrionChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; await expect( tracingChannel(CHANNELS.PG_QUERY).tracePromise(async () => { @@ -172,7 +171,7 @@ describe('postgresIntegration', () => { }); it('connect: builds a `pg.connect` span with no origin (defaults to manual)', async () => { - const ctx: ChannelContext = { arguments: [], self: { connectionParameters: CONNECTION } }; + const ctx: OrchestrionChannelContext = { arguments: [], self: { connectionParameters: CONNECTION } }; await tracingChannel(CHANNELS.PG_CONNECT).tracePromise(async () => undefined, ctx); @@ -193,7 +192,7 @@ describe('postgresIntegration', () => { }); it('pool connect: builds a `pg-pool.connect` span with masked connection string + pool attributes', async () => { - const ctx: ChannelContext = { + const ctx: OrchestrionChannelContext = { arguments: [], self: { options: { @@ -229,7 +228,7 @@ describe('postgresIntegration', () => { }); it('pool connect: falls back to the explicit `port` when the connection string omits it (OTel parity)', async () => { - const ctx: ChannelContext = { + const ctx: OrchestrionChannelContext = { arguments: [], self: { options: { @@ -254,7 +253,7 @@ describe('postgresIntegration', () => { it('requireParentSpan: does not create a span when there is no active span', async () => { getActiveSpanSpy.mockReturnValue(undefined); - const ctx: ChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; + const ctx: OrchestrionChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; await tracingChannel(CHANNELS.PG_QUERY).tracePromise(async () => ({ rows: [] }), ctx);