From 50a27209120ed5332d8ea4fb941dcb4e10282ad0 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 9 Jul 2026 23:18:26 +0200 Subject: [PATCH 1/5] feat(aws-serverless): Use orchestrion aws-sdk integration under diagnostics-channel opt-in Wires the orchestrion aws-sdk channel integration into `@sentry/aws-serverless`, where the OTel `Aws` integration ships as a default today. `@sentry/node` exposes a reusable `applyDiagnosticsChannelInjectionIntegrations` helper (extracted from its `getDefaultIntegrations`), and `@sentry/aws-serverless` uses it to swap the OTel `Aws` integration for the channel version when the app opts in via `experimentalUseDiagnosticsChannelInjection()` (now re-exported from the aws-serverless SDK). No change when the opt-in isn't used. The existing `aws-integration` node-integration-tests assert the swapped origin via `isOrchestrionEnabled()`, so both the OTel and diagnostics-channel paths are covered by the same suites across both smithy stacks and ESM/CJS. Part of #20946 Co-Authored-By: Claude Fable 5 --- .../scripts/consistentExports.ts | 3 ++ .../aws-integration-streamed/test.ts | 7 +++- .../aws-serverless/aws-integration/test.ts | 32 +++++++++++-------- packages/aws-serverless/src/index.ts | 2 ++ packages/aws-serverless/src/init.ts | 13 ++++++-- packages/node/src/index.ts | 1 + packages/node/src/sdk/index.ts | 24 ++++++++++++++ 7 files changed, 65 insertions(+), 17 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index 14dedc28363d..7e0873355c65 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -31,6 +31,9 @@ const NODE_EXPORTS_IGNORE = [ 'diagnosticsChannelInjectionIntegrations', // Companion to the above two, same reasoning (Next.js re-exports it via `export * from '@sentry/node'`) 'isDiagnosticsChannelInjectionEnabled', + // Helper for SDKs that build their own default-integration set (e.g. aws-serverless) + // to apply the diagnostics-channel integration swap; not surfaced elsewhere. + 'applyDiagnosticsChannelInjectionIntegrations', // Internal helper only needed within integrations (e.g. bunRuntimeMetricsIntegration) '_INTERNAL_normalizeCollectionInterval', ]; diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts index 48e81b01b9db..8df60402cafc 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts @@ -1,7 +1,12 @@ import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; +import { isOrchestrionEnabled } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; +// See the non-streamed `aws-integration` suite: only the origin differs between the OTel and +// orchestrion diagnostics-channel runs. +const ORIGIN = isOrchestrionEnabled() ? 'auto.aws.orchestrion.aws-sdk' : 'auto.otel.aws'; + // The aws-sdk instrumentation creates spans by patching the underlying smithy middleware stack. The // patch target differs between aws-sdk versions, so we run the exact same assertions against both: // - the current aws-sdk (default, resolved from the workspace) which routes through `@smithy/core` >= 3.24.0 @@ -49,7 +54,7 @@ function assertAwsServiceSpans(spanCcontainer: SerializedStreamedSpanContainer): name: 'S3.PutObject', status: 'ok', attributes: expect.objectContaining({ - 'sentry.origin': { value: 'auto.otel.aws', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, 'sentry.op': { value: 'rpc', type: 'string' }, 'rpc.system': { value: 'aws-api', type: 'string' }, 'rpc.method': { value: 'PutObject', type: 'string' }, diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts index 2da3a170a11c..9b13b58516f3 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts @@ -1,7 +1,13 @@ import type { TransactionEvent } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; +import { isOrchestrionEnabled } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; +// The suite runs twice on CI: once with the OTel `Aws` integration (default) and once with the +// orchestrion diagnostics-channel integration auto-injected (`INJECT_ORCHESTRION`). Both emit the +// same spans; only the origin differs. +const ORIGIN = isOrchestrionEnabled() ? 'auto.aws.orchestrion.aws-sdk' : 'auto.otel.aws'; + // The aws-sdk instrumentation creates spans by patching the underlying smithy middleware stack. The // patch target differs between aws-sdk versions, so we run the exact same assertions against both: // - the current aws-sdk (default, resolved from the workspace) which routes through `@smithy/core` >= 3.24.0 @@ -40,10 +46,10 @@ function assertAwsServiceSpans(transaction: TransactionEvent): void { expectSpan('S3.PutObject', { description: 'S3.PutObject', op: 'rpc', - origin: 'auto.otel.aws', + origin: ORIGIN, status: 'ok', data: expect.objectContaining({ - 'sentry.origin': 'auto.otel.aws', + 'sentry.origin': ORIGIN, 'sentry.op': 'rpc', 'rpc.system': 'aws-api', 'rpc.method': 'PutObject', @@ -58,7 +64,7 @@ function assertAwsServiceSpans(transaction: TransactionEvent): void { expectSpan('S3.GetObject (success)', { description: 'S3.GetObject', op: 'rpc', - origin: 'auto.otel.aws', + origin: ORIGIN, status: 'ok', data: expect.objectContaining({ 'rpc.method': 'GetObject', 'rpc.service': 'S3', 'aws.s3.bucket': 'ot-demo-test' }), }); @@ -67,7 +73,7 @@ function assertAwsServiceSpans(transaction: TransactionEvent): void { expectSpan('S3.GetObject (error)', { description: 'S3.GetObject', op: 'rpc', - origin: 'auto.otel.aws', + origin: ORIGIN, status: 'internal_error', data: expect.objectContaining({ 'rpc.method': 'GetObject', 'rpc.service': 'S3' }), }); @@ -76,7 +82,7 @@ function assertAwsServiceSpans(transaction: TransactionEvent): void { expectSpan('DynamoDB.PutItem', { description: 'DynamoDB.PutItem', op: 'db', - origin: 'auto.otel.aws', + origin: ORIGIN, data: expect.objectContaining({ 'sentry.op': 'db', 'rpc.method': 'PutItem', @@ -92,7 +98,7 @@ function assertAwsServiceSpans(transaction: TransactionEvent): void { expectSpan('DynamoDB.Query', { description: 'DynamoDB.Query', op: 'db', - origin: 'auto.otel.aws', + origin: ORIGIN, data: expect.objectContaining({ 'rpc.method': 'Query', 'db.operation': 'Query', @@ -105,7 +111,7 @@ function assertAwsServiceSpans(transaction: TransactionEvent): void { expectSpan('SQS SendMessage', { description: 'my-queue send', op: 'rpc', - origin: 'auto.otel.aws', + origin: ORIGIN, data: expect.objectContaining({ 'rpc.method': 'SendMessage', 'rpc.service': 'SQS', @@ -121,7 +127,7 @@ function assertAwsServiceSpans(transaction: TransactionEvent): void { expectSpan('SQS ReceiveMessage', { description: 'my-queue receive', op: 'rpc', - origin: 'auto.otel.aws', + origin: ORIGIN, data: expect.objectContaining({ 'rpc.method': 'ReceiveMessage', 'messaging.system': 'aws_sqs', @@ -135,7 +141,7 @@ function assertAwsServiceSpans(transaction: TransactionEvent): void { expectSpan('SNS Publish', { description: 'my-topic send', op: 'rpc', - origin: 'auto.otel.aws', + origin: ORIGIN, data: expect.objectContaining({ 'rpc.method': 'Publish', 'rpc.service': 'SNS', @@ -150,7 +156,7 @@ function assertAwsServiceSpans(transaction: TransactionEvent): void { expectSpan('Lambda Invoke', { description: 'my-function Invoke', op: 'rpc', - origin: 'auto.otel.aws', + origin: ORIGIN, data: expect.objectContaining({ 'rpc.method': 'Invoke', 'rpc.service': 'Lambda', @@ -164,7 +170,7 @@ function assertAwsServiceSpans(transaction: TransactionEvent): void { expectSpan('Kinesis.PutRecord', { description: 'Kinesis.PutRecord', op: 'rpc', - origin: 'auto.otel.aws', + origin: ORIGIN, status: 'ok', data: expect.objectContaining({ 'rpc.method': 'PutRecord', @@ -177,7 +183,7 @@ function assertAwsServiceSpans(transaction: TransactionEvent): void { expectSpan('SecretsManager.GetSecretValue', { description: 'SecretsManager.GetSecretValue', op: 'rpc', - origin: 'auto.otel.aws', + origin: ORIGIN, data: expect.objectContaining({ 'rpc.method': 'GetSecretValue', 'rpc.service': 'SecretsManager', @@ -189,7 +195,7 @@ function assertAwsServiceSpans(transaction: TransactionEvent): void { expectSpan('StepFunctions.StartExecution', { description: 'SFN.StartExecution', op: 'rpc', - origin: 'auto.otel.aws', + origin: ORIGIN, data: expect.objectContaining({ 'rpc.method': 'StartExecution', 'rpc.service': 'SFN', diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 71222eb0a6a6..2078044d64a8 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -167,6 +167,8 @@ export { metrics, spanStreamingIntegration, withStreamedSpan, + experimentalUseDiagnosticsChannelInjection, + diagnosticsChannelInjectionIntegrations, } from '@sentry/node'; export { diff --git a/packages/aws-serverless/src/init.ts b/packages/aws-serverless/src/init.ts index fe069d7ff6f5..afdbf3a6c1dd 100644 --- a/packages/aws-serverless/src/init.ts +++ b/packages/aws-serverless/src/init.ts @@ -1,7 +1,11 @@ import type { Integration, Options } from '@sentry/core'; import { applySdkMetadata, debug, getSDKSource } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations } from '@sentry/node'; +import { + applyDiagnosticsChannelInjectionIntegrations, + getDefaultIntegrationsWithoutPerformance, + initWithoutDefaultIntegrations, +} from '@sentry/node'; import { envToBool } from '@sentry/node-core'; import { DEBUG_BUILD } from './debug-build'; import { awsIntegration } from './integration/aws'; @@ -49,8 +53,11 @@ function shouldDisableLayerExtensionForProxy(): boolean { */ // NOTE: in awslambda-auto.ts, we also call the original `getDefaultIntegrations` from `@sentry/node` to load performance integrations. // If at some point we need to filter a node integration out for good, we need to make sure to also filter it out there. -export function getDefaultIntegrations(_options: Options): Integration[] { - return [...getDefaultIntegrationsWithoutPerformance(), awsIntegration(), awsLambdaIntegration()]; +export function getDefaultIntegrations(options: Options): Integration[] { + const integrations = [...getDefaultIntegrationsWithoutPerformance(), awsIntegration(), awsLambdaIntegration()]; + // If the app opted into diagnostics-channel injection, swap the OTel `Aws` integration for its + // channel-based equivalent (registered in `@sentry/server-utils`). No-op otherwise. + return applyDiagnosticsChannelInjectionIntegrations(integrations, options); } export interface AwsServerlessOptions extends NodeOptions { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 61c80f44794f..1ad92fd21b8e 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -46,6 +46,7 @@ export { getDefaultIntegrations, getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations, + applyDiagnosticsChannelInjectionIntegrations, } from './sdk'; export { experimentalUseDiagnosticsChannelInjection, diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 77c3c3bc6fcc..2aa79765e4c1 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -40,6 +40,30 @@ export function getDefaultIntegrations(options: Options): Integration[] { ]; } +/** + * When the app opted into diagnostics-channel injection (via + * `experimentalUseDiagnosticsChannelInjection()`) AND span recording is enabled, swap the + * channel-based integrations in place of their OTel equivalents so the two don't both instrument the + * same library. Otherwise returns `integrations` unchanged. + * + * `_init` applies the same swap to `defaultIntegrations`, but SDKs that seed their integrations + * through the user `integrations` option instead (e.g. the `@sentry/aws-serverless` Lambda layer + * entry) never hit that path, so they call this directly from their own `getDefaultIntegrations`. + */ +export function applyDiagnosticsChannelInjectionIntegrations( + integrations: Integration[], + options: Options, +): Integration[] { + if (isDiagnosticsChannelInjectionEnabled() && hasSpansEnabled(options)) { + const diagnosticsChannelInjection = resolveDiagnosticsChannelInjection(); + if (diagnosticsChannelInjection) { + const replaced = new Set(diagnosticsChannelInjection.replacedOtelIntegrationNames); + return [...integrations.filter(i => !replaced.has(i.name)), ...diagnosticsChannelInjection.integrations]; + } + } + return integrations; +} + /** * Initialize Sentry for Node. */ From a8c696990284ef956b393489f40f053c4004f090 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Jul 2026 00:44:33 +0200 Subject: [PATCH 2/5] Update expected origin to auto.aws.orchestrion.aws_sdk --- .../suites/aws-serverless/aws-integration-streamed/test.ts | 2 +- .../suites/aws-serverless/aws-integration/test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts index 8df60402cafc..1ae30b461d86 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts @@ -5,7 +5,7 @@ import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runn // See the non-streamed `aws-integration` suite: only the origin differs between the OTel and // orchestrion diagnostics-channel runs. -const ORIGIN = isOrchestrionEnabled() ? 'auto.aws.orchestrion.aws-sdk' : 'auto.otel.aws'; +const ORIGIN = isOrchestrionEnabled() ? 'auto.aws.orchestrion.aws_sdk' : 'auto.otel.aws'; // The aws-sdk instrumentation creates spans by patching the underlying smithy middleware stack. The // patch target differs between aws-sdk versions, so we run the exact same assertions against both: diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts index 9b13b58516f3..449db5ad7b65 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts @@ -6,7 +6,7 @@ import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runn // The suite runs twice on CI: once with the OTel `Aws` integration (default) and once with the // orchestrion diagnostics-channel integration auto-injected (`INJECT_ORCHESTRION`). Both emit the // same spans; only the origin differs. -const ORIGIN = isOrchestrionEnabled() ? 'auto.aws.orchestrion.aws-sdk' : 'auto.otel.aws'; +const ORIGIN = isOrchestrionEnabled() ? 'auto.aws.orchestrion.aws_sdk' : 'auto.otel.aws'; // The aws-sdk instrumentation creates spans by patching the underlying smithy middleware stack. The // patch target differs between aws-sdk versions, so we run the exact same assertions against both: From 2b8bcc5ded72890e22bb7124aff37663dc670fc0 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Sun, 12 Jul 2026 00:39:18 +0200 Subject: [PATCH 3/5] Document that the channel-injection helper appends the full channel set --- packages/aws-serverless/src/init.ts | 6 ++++-- packages/node/src/sdk/index.ts | 13 ++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/aws-serverless/src/init.ts b/packages/aws-serverless/src/init.ts index afdbf3a6c1dd..9d815a304a22 100644 --- a/packages/aws-serverless/src/init.ts +++ b/packages/aws-serverless/src/init.ts @@ -55,8 +55,10 @@ function shouldDisableLayerExtensionForProxy(): boolean { // If at some point we need to filter a node integration out for good, we need to make sure to also filter it out there. export function getDefaultIntegrations(options: Options): Integration[] { const integrations = [...getDefaultIntegrationsWithoutPerformance(), awsIntegration(), awsLambdaIntegration()]; - // If the app opted into diagnostics-channel injection, swap the OTel `Aws` integration for its - // channel-based equivalent (registered in `@sentry/server-utils`). No-op otherwise. + // If the app opted into diagnostics-channel injection, the OTel `Aws` integration is swapped for + // its channel-based equivalent AND the full channel-integration set is appended (mysql, postgres, + // express, ...), giving opted-in apps performance coverage this SDK's defaults otherwise omit. + // No-op otherwise. return applyDiagnosticsChannelInjectionIntegrations(integrations, options); } diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 2aa79765e4c1..760ae63b9696 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -42,13 +42,20 @@ export function getDefaultIntegrations(options: Options): Integration[] { /** * When the app opted into diagnostics-channel injection (via - * `experimentalUseDiagnosticsChannelInjection()`) AND span recording is enabled, swap the - * channel-based integrations in place of their OTel equivalents so the two don't both instrument the - * same library. Otherwise returns `integrations` unchanged. + * `experimentalUseDiagnosticsChannelInjection()`) AND span recording is enabled, drop the OTel + * integrations that have a channel-based replacement and append the FULL channel-integration set, + * so the two never both instrument the same library. Otherwise returns `integrations` unchanged. * * `_init` applies the same swap to `defaultIntegrations`, but SDKs that seed their integrations * through the user `integrations` option instead (e.g. the `@sentry/aws-serverless` Lambda layer * entry) never hit that path, so they call this directly from their own `getDefaultIntegrations`. + * + * Note the asymmetry: appended channel integrations are not limited to ones whose OTel counterpart + * was in `integrations`. For `@sentry/node` that makes no difference (the incoming list carries the + * whole OTel performance set), but a caller with a narrower list (e.g. `@sentry/aws-serverless`) + * gains channel coverage for libraries it never shipped OTel integrations for. Channel integrations + * produce nothing but spans, so this is gated on span recording. Exported so SDKs that build their + * own default-integration set can apply the same logic instead of duplicating it. */ export function applyDiagnosticsChannelInjectionIntegrations( integrations: Integration[], From aff4bcb5facc833a9edcbd6f3672dc6a31d74f71 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 14 Jul 2026 17:29:34 +0200 Subject: [PATCH 4/5] Re-enable orchestrion for aws integration tests now that all services are registered --- .../suites/aws-serverless/aws-integration-streamed/test.ts | 5 +---- .../suites/aws-serverless/aws-integration/test.ts | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts index 1ae30b461d86..a5e4ea13011a 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts @@ -226,10 +226,7 @@ describe('awsIntegration (streamed)', () => { await createTestRunner().ignore('event').expect({ span: assertAwsServiceSpans }).start().completed(); }); }, - // The orchestrion aws-sdk channel integration has no service extensions yet (empty registry), - // so it can't emit the service-specific attributes asserted here. Stay on the OTel path until - // the service extensions land in a follow-up. - { additionalDependencies, injectOrchestrion: false }, + { additionalDependencies }, ); }); }); diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts index 449db5ad7b65..22d84e5ffb3a 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts @@ -222,10 +222,7 @@ describe('awsIntegration', () => { await createTestRunner().ignore('event').expect({ transaction: assertAwsServiceSpans }).start().completed(); }); }, - // The orchestrion aws-sdk channel integration has no service extensions yet (empty registry), - // so it can't emit the service-specific attributes asserted here. Stay on the OTel path until - // the service extensions land in a follow-up. - { additionalDependencies, injectOrchestrion: false }, + { additionalDependencies }, ); }); }); From 4294c76d064b5a8530f0cd3b54d465d245c59bfb Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 15 Jul 2026 10:56:20 +0200 Subject: [PATCH 5/5] Honor SENTRY_TRACES_SAMPLE_RATE in the diagnostics-channel injection gate --- .../src/sdk/diagnosticsChannelInjection.ts | 2 ++ packages/node/src/sdk/index.ts | 22 +++++++++++-- .../sdk/diagnosticsChannelInjection.test.ts | 33 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/node/src/sdk/diagnosticsChannelInjection.ts b/packages/node/src/sdk/diagnosticsChannelInjection.ts index c152d1177f56..131a16392061 100644 --- a/packages/node/src/sdk/diagnosticsChannelInjection.ts +++ b/packages/node/src/sdk/diagnosticsChannelInjection.ts @@ -35,6 +35,8 @@ let cached: DiagnosticsChannelInjection | undefined; */ export function setDiagnosticsChannelInjectionLoader(load: () => DiagnosticsChannelInjection): void { loader = load; + // Invalidate the memoized result so a newly set loader resolves fresh. + cached = undefined; } /** Whether `experimentalUseDiagnosticsChannelInjection()` was called. */ diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 760ae63b9696..bf5e9089c519 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -16,6 +16,19 @@ import { } from './diagnosticsChannelInjection'; import { initOpenTelemetry } from './initOtel'; +/** + * Whether span recording is enabled, including the `SENTRY_TRACES_SAMPLE_RATE` env var that node-core + * resolves later. `hasSpansEnabled` only inspects the options object, so on init paths that enable + * tracing purely through the env var (e.g. the `@sentry/aws-serverless` Lambda auto-init) it would + * otherwise report `false` and skip the diagnostics-channel injection swap and hook registration. + */ +function hasSpansEnabledIncludingEnv(options: Options): boolean { + return ( + hasSpansEnabled(options) || + (options.tracesSampleRate == null && !options.tracesSampler && !!process.env.SENTRY_TRACES_SAMPLE_RATE) + ); +} + /** * Get default integrations, excluding performance. */ @@ -61,7 +74,7 @@ export function applyDiagnosticsChannelInjectionIntegrations( integrations: Integration[], options: Options, ): Integration[] { - if (isDiagnosticsChannelInjectionEnabled() && hasSpansEnabled(options)) { + if (isDiagnosticsChannelInjectionEnabled() && hasSpansEnabledIncludingEnv(options)) { const diagnosticsChannelInjection = resolveDiagnosticsChannelInjection(); if (diagnosticsChannelInjection) { const replaced = new Set(diagnosticsChannelInjection.replacedOtelIntegrationNames); @@ -90,9 +103,12 @@ function _init( // EXPERIMENTAL: diagnostics-channel injection, opted into via // `experimentalUseDiagnosticsChannelInjection()`. Gated on span recording to // match the OTel integrations it replaces. With tracing off there are no - // channel subscribers, so injecting is pointless work. + // channel subscribers, so injecting is pointless work. The env var is included + // because node-core resolves `SENTRY_TRACES_SAMPLE_RATE` later, and without it + // the auto-init path (pre-built `integrations`) would skip `register()` and + // never install the module hooks, silently emitting no spans. const diagnosticsChannelInjection = - isDiagnosticsChannelInjectionEnabled() && hasSpansEnabled(options) + isDiagnosticsChannelInjectionEnabled() && hasSpansEnabledIncludingEnv(options) ? resolveDiagnosticsChannelInjection() : undefined; diff --git a/packages/node/test/sdk/diagnosticsChannelInjection.test.ts b/packages/node/test/sdk/diagnosticsChannelInjection.test.ts index b1412904e33c..b72d4ab54f94 100644 --- a/packages/node/test/sdk/diagnosticsChannelInjection.test.ts +++ b/packages/node/test/sdk/diagnosticsChannelInjection.test.ts @@ -114,4 +114,37 @@ describe('diagnostics-channel injection integration swap', () => { expect(channelNoDefaults.setupOnce).not.toHaveBeenCalled(); }); + + it('swaps integrations and registers hooks when tracing is enabled only via SENTRY_TRACES_SAMPLE_RATE', () => { + // The auto-init path (e.g. `@sentry/aws-serverless`) enables tracing through the env var, not the + // options object. `hasSpansEnabled(options)` alone reports false there, so the swap and + // `register()` must still fire from the env-derived rate or spans silently never emit. + const channelEnvMysql = mockIntegration('EnvMysql'); + const register = vi.fn(); + setDiagnosticsChannelInjectionLoader(() => ({ + integrations: [channelEnvMysql], + replacedOtelIntegrationNames: ['EnvMysql'], + register, + detect: vi.fn(), + })); + + const otelEnvMysql = mockIntegration('EnvMysql'); + const http = mockIntegration('EnvHttp'); + + vi.stubEnv('SENTRY_TRACES_SAMPLE_RATE', '1'); + try { + init({ + dsn: PUBLIC_DSN, + skipOpenTelemetrySetup: true, + defaultIntegrations: [otelEnvMysql, http], + }); + } finally { + vi.unstubAllEnvs(); + } + + expect(otelEnvMysql.setupOnce).not.toHaveBeenCalled(); + expect(channelEnvMysql.setupOnce).toHaveBeenCalledTimes(1); + expect(http.setupOnce).toHaveBeenCalledTimes(1); + expect(register).toHaveBeenCalledTimes(1); + }); });