From 59d90df91d13ddb87153c89d7f5f6aa0440a3ab9 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 15 Jul 2026 20:17:26 +0200 Subject: [PATCH] feat(aws-serverless): Use orchestrion aws-sdk integration under diagnostics-channel opt-in --- .../scripts/consistentExports.ts | 3 ++ .../aws-integration-streamed/test.ts | 12 +++--- .../aws-serverless/aws-integration/test.ts | 37 ++++++++++--------- packages/aws-serverless/src/index.ts | 2 + packages/aws-serverless/src/init.ts | 15 ++++++-- packages/node/src/index.ts | 1 + packages/node/src/sdk/index.ts | 31 ++++++++++++++++ 7 files changed, 76 insertions(+), 25 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..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 @@ -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' }, @@ -221,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 2da3a170a11c..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 @@ -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', @@ -216,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 }, ); }); }); 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..9d815a304a22 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,13 @@ 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, 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); } 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..760ae63b9696 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -40,6 +40,37 @@ export function getDefaultIntegrations(options: Options): Integration[] { ]; } +/** + * When the app opted into diagnostics-channel injection (via + * `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[], + 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. */