From 3376abe640a26823df2d1e5c8bcb7dc9898fc43d Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Thu, 13 Aug 2026 15:54:42 +0200 Subject: [PATCH 1/3] fix(server-utils)!: Register channel integrations on versions with native channels --- .../cloudflare-vercelai-v7/package.json | 9 +++++---- .../cloudflare-vercelai-v7/vite.config.mts | 10 ++++++++++ packages/cloudflare/src/sdk.ts | 7 +------ .../config/channel-integration-definitions.ts | 9 +++++---- .../src/orchestrion/config/ioredis.ts | 5 +++++ .../src/orchestrion/config/mongoose.ts | 5 +++++ .../src/orchestrion/config/mysql2.ts | 5 +++++ .../src/orchestrion/config/redis.ts | 5 +++++ .../orchestrion/config/registration-only.ts | 18 ++++++++++++++++++ .../src/orchestrion/config/vercel-ai.ts | 4 ++++ 10 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/vite.config.mts create mode 100644 packages/server-utils/src/orchestrion/config/registration-only.ts diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json index a5d574038322..9608a75db97d 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json @@ -3,9 +3,8 @@ "version": "0.0.0", "private": true, "scripts": { - "deploy": "wrangler deploy", - "dev": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", - "build": "wrangler deploy --dry-run", + "dev": "vite dev", + "build": "vite build", "typecheck": "tsc --noEmit", "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod", @@ -17,11 +16,13 @@ "ai": "^7.0.0" }, "devDependencies": { + "@cloudflare/vite-plugin": "1.52.0", "@playwright/test": "~1.56.0", "@cloudflare/workers-types": "^4.20260426.0", "@sentry-internal/test-utils": "link:../../../test-utils", "typescript": "^5.5.2", - "wrangler": "4.86.0", + "vite": "7.3.2", + "wrangler": "^4.86.0", "ws": "^8.18.3" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/vite.config.mts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/vite.config.mts new file mode 100644 index 000000000000..2d826cc05f61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/vite.config.mts @@ -0,0 +1,10 @@ +import { cloudflare } from '@cloudflare/vite-plugin'; +import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + cloudflare(), + sentryCloudflareVitePlugin(), + ], +}); diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts index f96cbdc0626f..c07a5ec019a0 100644 --- a/packages/cloudflare/src/sdk.ts +++ b/packages/cloudflare/src/sdk.ts @@ -1,5 +1,4 @@ import type { Integration } from '@sentry/core'; -import { vercelAIIntegration } from './integrations/tracing/vercelai'; import { getBaseDefaultIntegrations, initWithDefaultIntegrations } from './baseSdk'; import type { CloudflareClient, CloudflareOptions } from './client'; @@ -11,11 +10,7 @@ import type { CloudflareClient, CloudflareOptions } from './client'; * `getBaseDefaultIntegrations`. */ export function getDefaultIntegrations(options: CloudflareOptions): Integration[] { - return [ - ...getBaseDefaultIntegrations(options), - // Subscribes to the `ai` SDK's native `node:diagnostics_channel` telemetry channel. - vercelAIIntegration(), - ]; + return getBaseDefaultIntegrations(options); } /** diff --git a/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts b/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts index df7400104578..446ce552a434 100644 --- a/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts +++ b/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts @@ -1,8 +1,8 @@ /** - * Build-time metadata mapping each instrumented package (orchestrion - * `module.name`) to the channel-subscriber integration that consumes the - * channels injected into it — by the `exportName` it is published under from - * `@sentry/server-utils/orchestrion`. + * Build-time metadata mapping each channel-subscriber integration — by the + * `exportName` it is published under from `@sentry/server-utils/orchestrion` — + * to the instrumented packages (orchestrion `module.name`) whose channels it + * consumes. * * Kept in a separate, factory-free module on purpose: the module-injected * transform (reachable from every orchestrion bundler plugin) reads this to @@ -20,6 +20,7 @@ export const CHANNEL_INTEGRATION_DEFINITIONS = [ { exportName: 'postgresJsIntegration', modules: ['postgres'] }, { exportName: 'mysqlIntegration', modules: ['mysql'] }, { exportName: 'mysql2Integration', modules: ['mysql2'] }, + { exportName: 'mongooseIntegration', modules: ['mongoose'] }, { exportName: 'genericPoolIntegration', modules: ['generic-pool'] }, { exportName: 'lruMemoizerIntegration', modules: ['lru-memoizer'] }, { exportName: 'openAIIntegration', modules: ['openai'] }, diff --git a/packages/server-utils/src/orchestrion/config/ioredis.ts b/packages/server-utils/src/orchestrion/config/ioredis.ts index edaa7f525985..c01465a7d8c4 100644 --- a/packages/server-utils/src/orchestrion/config/ioredis.ts +++ b/packages/server-utils/src/orchestrion/config/ioredis.ts @@ -1,7 +1,12 @@ import type { InstrumentationConfig } from '..'; import { getModuleNames } from './module-names'; +import { registrationOnly } from './registration-only'; export const ioredisConfig = [ + registrationOnly( + { name: 'ioredis', versionRange: '>=5.11.0', filePath: 'built/Redis.js' }, + { className: 'Redis', methodName: 'connect' }, + ), // ioredis `<5.11.0` (>=5.11.0 publishes its own `ioredis:*` diagnostics_channel) ...['lib/redis.js', 'built/redis.js', 'built/redis/index.js'].flatMap((filePath): InstrumentationConfig[] => [ { diff --git a/packages/server-utils/src/orchestrion/config/mongoose.ts b/packages/server-utils/src/orchestrion/config/mongoose.ts index 2a9c8c1ee4eb..d08cc1166424 100644 --- a/packages/server-utils/src/orchestrion/config/mongoose.ts +++ b/packages/server-utils/src/orchestrion/config/mongoose.ts @@ -1,5 +1,6 @@ import type { InstrumentationConfig } from '..'; import { getModuleNames } from './module-names'; +import { registrationOnly } from './registration-only'; // mongoose >= 9.7.0 publishes via its own `node:diagnostics_channel` tracing channels (handled by // `subscribeMongooseDiagnosticChannels`), so this transform is gated to `< 9.7.0` to avoid emitting @@ -37,6 +38,10 @@ const CONTEXT_CAPTURE_QUERY_METHODS = [ ] as const; export const mongooseConfig = [ + registrationOnly( + { name: 'mongoose', versionRange: '>=9.7.0', filePath: 'lib/query.js' }, + { expressionName: 'estimatedDocumentCount' }, + ), // Query execution // the span for most read/write operations. `op`, collection and model are // read off the `Query` at exec time. diff --git a/packages/server-utils/src/orchestrion/config/mysql2.ts b/packages/server-utils/src/orchestrion/config/mysql2.ts index 0dcb5e7067e4..d636f49ae060 100644 --- a/packages/server-utils/src/orchestrion/config/mysql2.ts +++ b/packages/server-utils/src/orchestrion/config/mysql2.ts @@ -1,5 +1,6 @@ import type { InstrumentationConfig } from '..'; import { getModuleNames } from './module-names'; +import { registrationOnly } from './registration-only'; // Ports `@opentelemetry/instrumentation-mysql2` (which patches `query`/`execute` on the connection // prototype) to orchestrion channel injection. @@ -21,6 +22,10 @@ import { getModuleNames } from './module-names'; // guard) — so `Auto` would crash streamed queries. `Callback` leaves that shape untouched (a rare, // row-streaming use that consumes the emitter's events), the tradeoff being it isn't traced. export const mysql2Config = [ + registrationOnly( + { name: 'mysql2', versionRange: '>=3.20.0', filePath: 'lib/base/connection.js' }, + { className: 'BaseConnection', methodName: 'pause' }, + ), { channelName: 'query', module: { name: 'mysql2', versionRange: '>=1.4.2 <3.11.5', filePath: 'lib/connection.js' }, diff --git a/packages/server-utils/src/orchestrion/config/redis.ts b/packages/server-utils/src/orchestrion/config/redis.ts index 7d5cc56839e4..8c8c42648ac9 100644 --- a/packages/server-utils/src/orchestrion/config/redis.ts +++ b/packages/server-utils/src/orchestrion/config/redis.ts @@ -1,7 +1,12 @@ import type { InstrumentationConfig } from '..'; import { getModuleNames } from './module-names'; +import { registrationOnly } from './registration-only'; export const redisConfig = [ + registrationOnly( + { name: '@redis/client', versionRange: '>=5.12.0', filePath: 'dist/lib/client/index.js' }, + { className: 'RedisClient', methodName: 'connect' }, + ), // redis `>=2.6.0 <4` (standalone `redis`). `internal_send_command` is an // anonymous prototype assignment (`expressionName`); it settles via the nested // `command_obj.callback`, so `kind: 'Sync'` and the subscriber wraps that callback. diff --git a/packages/server-utils/src/orchestrion/config/registration-only.ts b/packages/server-utils/src/orchestrion/config/registration-only.ts new file mode 100644 index 000000000000..dbdc42d811ea --- /dev/null +++ b/packages/server-utils/src/orchestrion/config/registration-only.ts @@ -0,0 +1,18 @@ +import type { InstrumentationConfig } from '..'; + +/** + * Build a registration-only config for one file of a library. + * + * `functionQuery` takes the same shapes as a normal config (`functionName`, + * `expressionName`, or `className` + `methodName`); the `kind` is fixed. + */ +export function registrationOnly( + module: { name: string; versionRange: string; filePath: string }, + functionQuery: { functionName?: string; expressionName?: string; className?: string; methodName?: string }, +): InstrumentationConfig { + return { + channelName: 'module-registration', + module, + functionQuery: { ...functionQuery, kind: 'Sync' }, + } as InstrumentationConfig; +} diff --git a/packages/server-utils/src/orchestrion/config/vercel-ai.ts b/packages/server-utils/src/orchestrion/config/vercel-ai.ts index 6aa86beba558..2025a17fce7d 100644 --- a/packages/server-utils/src/orchestrion/config/vercel-ai.ts +++ b/packages/server-utils/src/orchestrion/config/vercel-ai.ts @@ -1,7 +1,11 @@ import type { InstrumentationConfig } from '..'; import { getModuleNames } from './module-names'; +import { registrationOnly } from './registration-only'; export const vercelAiConfig = [ + ...['dist/index.js', 'dist/index.mjs'].map(filePath => + registrationOnly({ name: 'ai', versionRange: '>=7.0.0', filePath }, { functionName: 'embedMany' }), + ), // Vercel AI v6: mirror the v7 native `ai:telemetry` channel by injecting // channels into the top-level entry points. `resolveLanguageModel` is wrapped // not to span it, but so the subscriber can monkey-patch `doGenerate`/ From ddde71b4e9d4fe20d0b14f729f46307934428362 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Thu, 13 Aug 2026 16:15:24 +0200 Subject: [PATCH 2/3] fixup! fix(server-utils)!: Register channel integrations on versions with native channels --- .../v6 => vite/diagnostics-channel/vercelai-6}/index.ts | 0 .../v6 => vite/diagnostics-channel/vercelai-6}/test.ts | 4 ++-- .../vite/diagnostics-channel/vercelai-6/vite.config.mts | 7 +++++++ .../diagnostics-channel/vercelai-6}/wrangler.jsonc | 0 .../test-applications/cloudflare-vercelai-v7/package.json | 1 + .../cloudflare-vercelai-v7/playwright.config.ts | 2 +- .../cloudflare-vercelai-v7/vite.config.mts | 5 +---- .../test-applications/cloudflare-vercelai-v7/wrangler.toml | 5 +++++ 8 files changed, 17 insertions(+), 7 deletions(-) rename dev-packages/cloudflare-integration-tests/suites/{tracing/vercelai/v6 => vite/diagnostics-channel/vercelai-6}/index.ts (100%) rename dev-packages/cloudflare-integration-tests/suites/{tracing/vercelai/v6 => vite/diagnostics-channel/vercelai-6}/test.ts (98%) create mode 100644 dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/vercelai-6/vite.config.mts rename dev-packages/cloudflare-integration-tests/suites/{tracing/vercelai/v6 => vite/diagnostics-channel/vercelai-6}/wrangler.jsonc (100%) diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/index.ts b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/vercelai-6/index.ts similarity index 100% rename from dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/index.ts rename to dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/vercelai-6/index.ts diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/test.ts b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/vercelai-6/test.ts similarity index 98% rename from dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/test.ts rename to dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/vercelai-6/test.ts index 65fb16e9ab1e..556f5babbe80 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/vercelai-6/test.ts @@ -31,7 +31,7 @@ it('captures a transaction with Vercel AI v6 spans via @sentry/cloudflare vercel is_segment: false, attributes: expect.objectContaining({ 'sentry.op': { type: 'string', value: 'gen_ai.invoke_agent' }, - 'sentry.origin': { type: 'string', value: 'auto.vercelai.otel' }, + 'sentry.origin': { type: 'string', value: 'auto.vercelai.channel' }, [GEN_AI_OPERATION_NAME]: { type: 'string', value: 'invoke_agent' }, [GEN_AI_USAGE_INPUT_TOKENS]: { type: 'integer', value: 10 }, [GEN_AI_USAGE_OUTPUT_TOKENS]: { type: 'integer', value: 20 }, @@ -49,7 +49,7 @@ it('captures a transaction with Vercel AI v6 spans via @sentry/cloudflare vercel is_segment: false, attributes: expect.objectContaining({ 'sentry.op': { type: 'string', value: 'gen_ai.generate_content' }, - 'sentry.origin': { type: 'string', value: 'auto.vercelai.otel' }, + 'sentry.origin': { type: 'string', value: 'auto.vercelai.channel' }, [GEN_AI_OPERATION_NAME]: { type: 'string', value: 'generate_content' }, }), }, diff --git a/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/vercelai-6/vite.config.mts b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/vercelai-6/vite.config.mts new file mode 100644 index 000000000000..cee0e8c0b4b8 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/vercelai-6/vite.config.mts @@ -0,0 +1,7 @@ +import { cloudflare } from '@cloudflare/vite-plugin'; +import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [cloudflare(), sentryCloudflareVitePlugin({ autoInstrumentation: false })], +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/vercelai-6/wrangler.jsonc similarity index 100% rename from dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/wrangler.jsonc rename to dev-packages/cloudflare-integration-tests/suites/vite/diagnostics-channel/vercelai-6/wrangler.jsonc diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json index 9608a75db97d..f557f00e891e 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "vite dev", "build": "vite build", + "preview": "vite preview --port 38787", "typecheck": "tsc --noEmit", "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/playwright.config.ts index 59a70bd39838..3761c194df43 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/playwright.config.ts @@ -9,7 +9,7 @@ const APP_PORT = 38787; const config = getPlaywrightConfig( { - startCommand: `pnpm dev --port ${APP_PORT}`, + startCommand: `pnpm preview`, port: APP_PORT, }, { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/vite.config.mts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/vite.config.mts index 2d826cc05f61..005f4448f6cb 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/vite.config.mts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/vite.config.mts @@ -3,8 +3,5 @@ import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [ - cloudflare(), - sentryCloudflareVitePlugin(), - ], + plugins: [cloudflare(), sentryCloudflareVitePlugin()], }); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/wrangler.toml index 9e33c22a28ed..b416a58dd9ab 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/wrangler.toml +++ b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/wrangler.toml @@ -3,3 +3,8 @@ name = "cloudflare-vercelai-v7-compat" main = "src/index.ts" compatibility_date = "2026-04-26" compatibility_flags = ["nodejs_compat"] + +# `vite preview` takes no `--var` flag, so the DSN is declared here and read from +# the process environment the Playwright web server inherits. +[secrets] +required = ["E2E_TEST_DSN"] From f0bb82fd8359e7dbdd0c50d7fc78b86632eec88f Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 20 Aug 2026 16:57:54 +0200 Subject: [PATCH 3/3] Only inject registration code. Don't inject tracing channels. --- .../bundler/moduleInjectedTransform.ts | 34 ++++++++++++++--- .../src/orchestrion/config/ioredis.ts | 5 +-- .../src/orchestrion/config/mongoose.ts | 5 +-- .../src/orchestrion/config/mysql2.ts | 5 +-- .../src/orchestrion/config/redis.ts | 5 +-- .../orchestrion/config/registration-only.ts | 32 +++++++++++----- .../src/orchestrion/config/vercel-ai.ts | 2 +- .../moduleInjectedTransform.test.ts | 38 ++++++++++++++++++- 8 files changed, 91 insertions(+), 35 deletions(-) diff --git a/packages/server-utils/src/orchestrion/bundler/moduleInjectedTransform.ts b/packages/server-utils/src/orchestrion/bundler/moduleInjectedTransform.ts index 781ba5fd8a0c..d6acd04f8559 100644 --- a/packages/server-utils/src/orchestrion/bundler/moduleInjectedTransform.ts +++ b/packages/server-utils/src/orchestrion/bundler/moduleInjectedTransform.ts @@ -1,6 +1,7 @@ import type { CustomTransform } from '../apmTypes'; import { parse } from 'meriyah'; import { subscriberExportForModule } from '../config/channel-integration-definitions'; +import { MODULE_REGISTRATION_TRANSFORM } from '../config/registration-only'; // Tracks Program nodes we already injected into, so a package with several // instrumented files (or several configs pointing at one file) is injected only @@ -75,6 +76,10 @@ function moduleInjectedSnippet( * per file. Requires `@apm-js-collab/code-transformer` >= 0.18.1, where * built-ins dispatch through the override map and expose the originals on * `state.transforms.defaults`. + * + * Also carries the registration-only operator, which splices the same snippet + * without any channel injection — for library versions whose tracing channels + * are native. */ export function moduleInjectedTransforms( // A function is read per injected file — the webpack/Turbopack loader uses it @@ -83,15 +88,12 @@ export function moduleInjectedTransforms( // importing file's location). importSpecifier?: string | (() => string | undefined), ): Record { - const injectModuleInjected: CustomTransform = (state, program, parent, ancestry) => { - const { moduleType, module, transforms } = state as { + const spliceModuleInjected = (state: unknown, program: unknown): void => { + const { moduleType, module } = state as { moduleType?: string; module?: { name?: string }; - transforms: { defaults: { tracingChannelImport: CustomTransform } }; }; - transforms.defaults.tracingChannelImport(state, program, parent, ancestry); - const node = program as ProgramNode; if (injectedPrograms.has(node)) { return; @@ -116,5 +118,25 @@ export function moduleInjectedTransforms( node.body.splice(directiveIndex + 1, 0, ...statements); }; - return { tracingChannelImport: injectModuleInjected }; + const injectModuleInjected: CustomTransform = (state, program, parent, ancestry) => { + const { transforms } = state as { + transforms: { defaults: { tracingChannelImport: CustomTransform } }; + }; + + transforms.defaults.tracingChannelImport(state, program, parent, ancestry); + spliceModuleInjected(state, program); + }; + + // Operator for registration-only configs (`transform` field, see + // `config/registration-only.ts`): dispatched INSTEAD of `traceSync`, so no + // channel is declared and no function is wrapped. Their `astQuery: 'Program'` + // matches only the file root, so `node` is the Program itself. + const injectRegistrationOnly: CustomTransform = (state, node) => { + spliceModuleInjected(state, node); + }; + + return { + tracingChannelImport: injectModuleInjected, + [MODULE_REGISTRATION_TRANSFORM]: injectRegistrationOnly, + }; } diff --git a/packages/server-utils/src/orchestrion/config/ioredis.ts b/packages/server-utils/src/orchestrion/config/ioredis.ts index c01465a7d8c4..d64eb8536441 100644 --- a/packages/server-utils/src/orchestrion/config/ioredis.ts +++ b/packages/server-utils/src/orchestrion/config/ioredis.ts @@ -3,10 +3,7 @@ import { getModuleNames } from './module-names'; import { registrationOnly } from './registration-only'; export const ioredisConfig = [ - registrationOnly( - { name: 'ioredis', versionRange: '>=5.11.0', filePath: 'built/Redis.js' }, - { className: 'Redis', methodName: 'connect' }, - ), + registrationOnly({ name: 'ioredis', versionRange: '>=5.11.0', filePath: 'built/Redis.js' }), // ioredis `<5.11.0` (>=5.11.0 publishes its own `ioredis:*` diagnostics_channel) ...['lib/redis.js', 'built/redis.js', 'built/redis/index.js'].flatMap((filePath): InstrumentationConfig[] => [ { diff --git a/packages/server-utils/src/orchestrion/config/mongoose.ts b/packages/server-utils/src/orchestrion/config/mongoose.ts index d08cc1166424..b2533d01855f 100644 --- a/packages/server-utils/src/orchestrion/config/mongoose.ts +++ b/packages/server-utils/src/orchestrion/config/mongoose.ts @@ -38,10 +38,7 @@ const CONTEXT_CAPTURE_QUERY_METHODS = [ ] as const; export const mongooseConfig = [ - registrationOnly( - { name: 'mongoose', versionRange: '>=9.7.0', filePath: 'lib/query.js' }, - { expressionName: 'estimatedDocumentCount' }, - ), + registrationOnly({ name: 'mongoose', versionRange: '>=9.7.0', filePath: 'lib/query.js' }), // Query execution // the span for most read/write operations. `op`, collection and model are // read off the `Query` at exec time. diff --git a/packages/server-utils/src/orchestrion/config/mysql2.ts b/packages/server-utils/src/orchestrion/config/mysql2.ts index d636f49ae060..edbbb3aa2984 100644 --- a/packages/server-utils/src/orchestrion/config/mysql2.ts +++ b/packages/server-utils/src/orchestrion/config/mysql2.ts @@ -22,10 +22,7 @@ import { registrationOnly } from './registration-only'; // guard) — so `Auto` would crash streamed queries. `Callback` leaves that shape untouched (a rare, // row-streaming use that consumes the emitter's events), the tradeoff being it isn't traced. export const mysql2Config = [ - registrationOnly( - { name: 'mysql2', versionRange: '>=3.20.0', filePath: 'lib/base/connection.js' }, - { className: 'BaseConnection', methodName: 'pause' }, - ), + registrationOnly({ name: 'mysql2', versionRange: '>=3.20.0', filePath: 'lib/base/connection.js' }), { channelName: 'query', module: { name: 'mysql2', versionRange: '>=1.4.2 <3.11.5', filePath: 'lib/connection.js' }, diff --git a/packages/server-utils/src/orchestrion/config/redis.ts b/packages/server-utils/src/orchestrion/config/redis.ts index 8c8c42648ac9..4fc95fba6ef5 100644 --- a/packages/server-utils/src/orchestrion/config/redis.ts +++ b/packages/server-utils/src/orchestrion/config/redis.ts @@ -3,10 +3,7 @@ import { getModuleNames } from './module-names'; import { registrationOnly } from './registration-only'; export const redisConfig = [ - registrationOnly( - { name: '@redis/client', versionRange: '>=5.12.0', filePath: 'dist/lib/client/index.js' }, - { className: 'RedisClient', methodName: 'connect' }, - ), + registrationOnly({ name: '@redis/client', versionRange: '>=5.12.0', filePath: 'dist/lib/client/index.js' }), // redis `>=2.6.0 <4` (standalone `redis`). `internal_send_command` is an // anonymous prototype assignment (`expressionName`); it settles via the nested // `command_obj.callback`, so `kind: 'Sync'` and the subscriber wraps that callback. diff --git a/packages/server-utils/src/orchestrion/config/registration-only.ts b/packages/server-utils/src/orchestrion/config/registration-only.ts index dbdc42d811ea..336b44b9e62f 100644 --- a/packages/server-utils/src/orchestrion/config/registration-only.ts +++ b/packages/server-utils/src/orchestrion/config/registration-only.ts @@ -1,18 +1,30 @@ import type { InstrumentationConfig } from '..'; /** - * Build a registration-only config for one file of a library. - * - * `functionQuery` takes the same shapes as a normal config (`functionName`, - * `expressionName`, or `className` + `methodName`); the `kind` is fixed. + * Name of the custom transform (see `bundler/moduleInjectedTransform.ts`) that + * splices only the module-injected registration snippet — no channel machinery. + * Prefixed so it can never shadow a built-in operator (`traceSync`, + * `tracingChannelImport`, ...), which the custom-transforms map overrides by + * name. */ -export function registrationOnly( - module: { name: string; versionRange: string; filePath: string }, - functionQuery: { functionName?: string; expressionName?: string; className?: string; methodName?: string }, -): InstrumentationConfig { +export const MODULE_REGISTRATION_TRANSFORM = 'sentryModuleRegistration'; + +/** + * Build a registration-only config for one file of a library whose tracing + * channels are native (published by the library itself): no channels are + * injected, but transforming the file registers the module's channel-subscriber + * integration at evaluation time. `astQuery: 'Program'` matches the file root + * unconditionally, so no anchor function inside the library needs to exist. + */ +export function registrationOnly(module: { + name: string; + versionRange: string; + filePath: string; +}): InstrumentationConfig { return { channelName: 'module-registration', module, - functionQuery: { ...functionQuery, kind: 'Sync' }, - } as InstrumentationConfig; + astQuery: 'Program', + transform: MODULE_REGISTRATION_TRANSFORM, + }; } diff --git a/packages/server-utils/src/orchestrion/config/vercel-ai.ts b/packages/server-utils/src/orchestrion/config/vercel-ai.ts index 2025a17fce7d..bac7f35cae00 100644 --- a/packages/server-utils/src/orchestrion/config/vercel-ai.ts +++ b/packages/server-utils/src/orchestrion/config/vercel-ai.ts @@ -4,7 +4,7 @@ import { registrationOnly } from './registration-only'; export const vercelAiConfig = [ ...['dist/index.js', 'dist/index.mjs'].map(filePath => - registrationOnly({ name: 'ai', versionRange: '>=7.0.0', filePath }, { functionName: 'embedMany' }), + registrationOnly({ name: 'ai', versionRange: '>=7.0.0', filePath }), ), // Vercel AI v6: mirror the v7 native `ai:telemetry` channel by injecting // channels into the top-level entry points. `resolveLanguageModel` is wrapped diff --git a/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts b/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts index 57817b569a08..b45e5761dbe9 100644 --- a/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts +++ b/packages/server-utils/test/orchestrion/moduleInjectedTransform.test.ts @@ -12,9 +12,9 @@ import { orchestrionTransformOptions } from '../../src/orchestrion/bundler/optio // The code transformer reads the instrumented package's version from its // on-disk `package.json`, so each test package needs a real directory. -function makePackage(root: string, name: string, version: string, type?: 'module' | 'commonjs'): void { +function makePackage(root: string, name: string, version: string, type?: 'module' | 'commonjs', subdir = 'lib'): void { const dir = join(root, 'node_modules', name); - mkdirSync(join(dir, 'lib'), { recursive: true }); + mkdirSync(join(dir, subdir), { recursive: true }); writeFileSync(join(dir, 'package.json'), JSON.stringify({ name, version, ...(type ? { type } : {}) })); } @@ -44,6 +44,8 @@ describe('module-injected transform', () => { makePackage(root, 'mysql', '2.18.1', 'commonjs'); makePackage(root, 'pg', '8.11.0', 'module'); makePackage(root, 'my-lib', '1.0.0', 'commonjs'); + makePackage(root, 'ioredis', '5.11.0', 'commonjs', 'built'); + makePackage(root, 'ai', '7.0.0', 'module', 'dist'); }); afterAll(() => { @@ -124,6 +126,38 @@ describe('module-injected transform', () => { expect(calls).toHaveLength(1); }); + it('injects only the registration snippet for a CJS version with native channels', () => { + const t = createCodeTransformer(orchestrionTransformOptions({})); + // No anchor function present — registration must not depend on any library + // internals existing (`astQuery: 'Program'` matches the file root). + const code = "'use strict';\nclass Redis {}\nmodule.exports = Redis;\n"; + const result = t.transform(code, join(root, 'node_modules/ioredis/built/Redis.js')); + + expect(result).not.toBeNull(); + expect(result!.code).toMatch( + /const\s*\{\s*orchestrionModuleInjected,\s*redisIntegration\s*\}\s*=\s*require\(["']@sentry\/server-utils\/orchestrion["']\)/, + ); + expect(result!.code).toContain('orchestrionModuleInjected("ioredis", redisIntegration)'); + // The library publishes its own channels, so nothing else is injected: no + // diagnostics_channel import, no channel declaration, no function wrapper. + expect(result!.code).not.toContain('diagnostics_channel'); + expect(result!.code).not.toContain('tr_ch_apm'); + expect(result!.code).toContain('class Redis'); + }); + + it('injects only the registration snippet for an ESM version with native channels', () => { + const t = createCodeTransformer(orchestrionTransformOptions({})); + const result = t.transform('export const embed = () => {};\n', join(root, 'node_modules/ai/dist/index.mjs')); + + expect(result).not.toBeNull(); + expect(result!.code).toMatch( + /import\s*\{\s*orchestrionModuleInjected,\s*vercelAIIntegration\s*\}\s*from\s*["']@sentry\/server-utils\/orchestrion["']/, + ); + expect(result!.code).toContain('orchestrionModuleInjected("ai", vercelAIIntegration)'); + expect(result!.code).not.toContain('diagnostics_channel'); + expect(result!.code).not.toContain('tr_ch_apm'); + }); + it('honors a custom import specifier (Turbopack passes an absolute path)', () => { const t = createCodeTransformer({ ...orchestrionTransformOptions({}),