From 9747e2e3691edb5c64eff4ee3234277d700b0ccb Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 9 Jul 2026 23:15:39 +0200 Subject: [PATCH 01/11] feat(server-utils): Add orchestrion aws-sdk channel integration core Adds the core of an OTel-free, orchestrion diagnostics-channel implementation of the aws-sdk (v3) instrumentation to `@sentry/server-utils`. The integration subscribes to the `orchestrion::send` channels the transform injects into the smithy `Client.prototype.send` (`@smithy/core`, `@smithy/smithy-client`, `@aws-sdk/smithy-client`) and emits a client rpc span per command (`rpc.system`/`rpc.method`/`rpc.service`, `cloud.region`, request id metadata), with a distinct `auto.aws.orchestrion.aws-sdk` origin. The per-service extension registry (span names, messaging/db/gen_ai attributes, trace propagation) starts empty; the services are ported one group at a time in follow-up PRs, mirroring the OTel integration's ServiceExtension contract. Registered in `channelIntegrations`, so it is reachable via `@sentry/node`'s `experimentalUseDiagnosticsChannelInjection()`. The `@sentry/aws-serverless` swap follows at the top of the stack. Part of #20946 Co-Authored-By: Claude Fable 5 --- .oxlintrc.base.json | 11 + .../tracing-channel/aws-sdk/constants.ts | 17 ++ .../tracing-channel/aws-sdk/index.ts | 205 ++++++++++++++++++ .../aws-sdk/services/ServiceExtension.ts | 16 ++ .../aws-sdk/services/ServicesExtensions.ts | 31 +++ .../tracing-channel/aws-sdk/services/index.ts | 1 + .../tracing-channel/aws-sdk/types.ts | 31 +++ .../tracing-channel/aws-sdk/utils.ts | 31 +++ .../server-utils/src/orchestrion/channels.ts | 2 + .../src/orchestrion/config/aws-sdk.ts | 34 +++ .../src/orchestrion/config/index.ts | 2 + .../server-utils/src/orchestrion/index.ts | 3 + 12 files changed, 384 insertions(+) create mode 100644 packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts create mode 100644 packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts create mode 100644 packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServiceExtension.ts create mode 100644 packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts create mode 100644 packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/index.ts create mode 100644 packages/server-utils/src/integrations/tracing-channel/aws-sdk/types.ts create mode 100644 packages/server-utils/src/integrations/tracing-channel/aws-sdk/utils.ts create mode 100644 packages/server-utils/src/orchestrion/config/aws-sdk.ts diff --git a/.oxlintrc.base.json b/.oxlintrc.base.json index dd6ed529bc95..aba1665cd578 100644 --- a/.oxlintrc.base.json +++ b/.oxlintrc.base.json @@ -147,6 +147,17 @@ "no-param-reassign": "off" } }, + { + "files": ["**/integrations/tracing-channel/aws-sdk/**/*.ts"], + "rules": { + "typescript/no-unsafe-member-access": "off", + "typescript/no-explicit-any": "off", + "typescript/no-this-alias": "off", + "max-lines": "off", + "complexity": "off", + "no-param-reassign": "off" + } + }, { "files": ["**/integrations/tracing/redis/vendored/**/*.ts"], "rules": { diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts new file mode 100644 index 000000000000..27d886365ab5 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts @@ -0,0 +1,17 @@ +/** + * AWS-specific span attribute names used by the aws-sdk channel integration. + * + * These mirror the constants the OTel `@opentelemetry/instrumentation-aws-sdk` emits (some are + * unstable/obsolete OTel semantic conventions with no `ATTR_*` export in + * `@opentelemetry/semantic-conventions`), inlined here so the integration stays free of OTel deps. + * Standard conventions (`rpc.*`, `db.*`, `messaging.*`, `gen_ai.*`, ...) come from + * `@sentry/conventions/attributes` directly. + */ + +/** The span origin every aws-sdk channel span carries, mirroring the uniform OTel `auto.otel.aws`. */ +export const AWS_SDK_ORIGIN = 'auto.aws.orchestrion.aws-sdk'; + +export const ATTR_RPC_SYSTEM = 'rpc.system'; +export const CLOUD_REGION = 'cloud.region'; +export const AWS_REQUEST_ID = 'aws.request.id'; +export const AWS_REQUEST_EXTENDED_ID = 'aws.request.extended_id'; diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts new file mode 100644 index 000000000000..bc862a96bc29 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts @@ -0,0 +1,205 @@ +import * as diagnosticsChannel from 'node:diagnostics_channel'; +import type { IntegrationFn, Span } from '@sentry/core'; +import { + debug, + defineIntegration, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SPAN_KIND, + startInactiveSpan, + waitForTracingChannelBinding, +} from '@sentry/core'; +import { HTTP_STATUS_CODE } from '@sentry/conventions/attributes'; +import { DEBUG_BUILD } from '../../../debug-build'; +import { CHANNELS } from '../../../orchestrion/channels'; +import type { TracingChannelLifeCycleOptions } from '../../../tracing-channel'; +import { bindTracingChannelToSpan } from '../../../tracing-channel'; +import { AWS_REQUEST_EXTENDED_ID, AWS_REQUEST_ID, AWS_SDK_ORIGIN, CLOUD_REGION } from './constants'; +import { ServicesExtensions } from './services'; +import type { NormalizedRequest, NormalizedResponse, RequestMetadata } from './types'; +import { extractAttributesFromNormalizedRequest, normalizeV3Request, removeSuffixFromStringIfExists } from './utils'; + +// Same name as the OTel `Aws` integration by design, so enabling injection swaps this in for it. +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[]; + self?: { config?: AwsClientConfig; constructor?: { name?: string } }; + result?: unknown; + error?: unknown; + _sentryNormalizedRequest?: NormalizedRequest; + _sentryRequestMetadata?: RequestMetadata; +} + +interface AwsClientConfig { + serviceId?: string; + region?: () => string | Promise | undefined; +} + +interface AwsV3Command { + input?: Record; + constructor?: { name?: string }; +} + +/** Runs a span-building callback so a throw inside it can never break the user's aws-sdk call. */ +function safe(fn: () => T): T | undefined { + try { + return fn(); + } catch (error) { + DEBUG_BUILD && debug.warn('[orchestrion:aws-sdk] error building span', error); + return undefined; + } +} + +function setMetadataAttributes(span: Span, metadata: Record | undefined): void { + if (!metadata) { + return; + } + if (metadata.requestId) { + span.setAttribute(AWS_REQUEST_ID, metadata.requestId); + } + if (metadata.httpStatusCode) { + // oxlint-disable-next-line typescript/no-deprecated + span.setAttribute(HTTP_STATUS_CODE, metadata.httpStatusCode); + } + if (metadata.extendedRequestId) { + span.setAttribute(AWS_REQUEST_EXTENDED_ID, metadata.extendedRequestId); + } +} + +const _awsChannelIntegration = (() => { + const servicesExtensions = new ServicesExtensions(); + + return { + name: INTEGRATION_NAME, + setupOnce() { + // `tracingChannel` is unavailable before Node 18.19 so do nothing in that case. + if (!diagnosticsChannel.tracingChannel) { + return; + } + + const getSpan = (data: AwsSendChannelContext): Span | undefined => + safe(() => { + const command = data.arguments[0] as AwsV3Command | undefined; + const commandInput = command?.input; + const commandName = command?.constructor?.name; + if (!command || !commandName || !commandInput) { + // Not a recognizable v3 command call; leave the active context untouched. + return undefined; + } + + const clientConfig = data.self?.config; + const serviceName = + clientConfig?.serviceId ?? + // `clientName` isn't available at the `send` boundary; fall back to the client's + // constructor name (e.g. `S3Client` -> `S3`). `serviceId` is set for all AWS clients. + removeSuffixFromStringIfExists(data.self?.constructor?.name || 'AWS', 'Client'); + + const normalizedRequest = normalizeV3Request(serviceName, commandName, commandInput, undefined); + const requestMetadata = servicesExtensions.requestPreSpanHook(normalizedRequest); + + const span = startInactiveSpan({ + name: requestMetadata.spanName ?? `${normalizedRequest.serviceName}.${normalizedRequest.commandName}`, + kind: requestMetadata.spanKind ?? SPAN_KIND.CLIENT, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: AWS_SDK_ORIGIN, + ...extractAttributesFromNormalizedRequest(normalizedRequest), + ...requestMetadata.spanAttributes, + }, + }); + + data._sentryNormalizedRequest = normalizedRequest; + data._sentryRequestMetadata = requestMetadata; + + // `region` resolves asynchronously; set it on the span (still open until `send` settles) + // and backfill it on the normalized request once available. + Promise.resolve(clientConfig?.region?.()) + .then(region => { + if (region) { + normalizedRequest.region = region; + span.setAttribute(CLOUD_REGION, region); + } + }) + .catch(() => { + // Nothing to do; continue without a region. + }); + + // Inject trace-propagation headers into outgoing messages (SQS/SNS/Lambda). Runs before + // `send` proceeds, so the mutated `commandInput` is used to build the request. + safe(() => servicesExtensions.requestPostSpanHook(normalizedRequest, span)); + + return span; + }); + + const opts: TracingChannelLifeCycleOptions = { + deferSpanEnd({ span, data }) { + const normalizedRequest = data._sentryNormalizedRequest; + const requestMetadata = data._sentryRequestMetadata; + if (!normalizedRequest) { + return false; + } + + const failed = 'error' in data; + + safe(() => { + if (failed) { + const err = data.error as { $metadata?: Record; RequestId?: string } | undefined; + setMetadataAttributes(span, { requestId: err?.RequestId, ...err?.$metadata }); + return; + } + + const output = data.result as { $metadata?: Record } | undefined; + setMetadataAttributes(span, output?.$metadata); + + const normalizedResponse: NormalizedResponse = { + data: output, + request: normalizedRequest, + requestId: output?.$metadata?.requestId, + }; + servicesExtensions.responseHook(normalizedResponse, span); + }); + + // Streaming responses end the span when their wrapped stream is consumed (see + // bedrock-runtime); the helper must not end it on `send` settling. Errors always end here. + return !!requestMetadata?.isStream && !failed; + }, + }; + + // The AWS SDK's `Client.prototype.send` lives in different smithy packages across versions; the + // transform injects one channel per package. Only the package hosting the app's client fires, so + // subscribing to all of them is safe and never double-instruments a single call. + const awsSendChannels = [ + CHANNELS.AWS_SMITHY_CORE_SEND, + CHANNELS.AWS_SMITHY_CLIENT_SEND, + CHANNELS.AWS_SDK_SMITHY_CLIENT_SEND, + ] as const; + + DEBUG_BUILD && debug.log(`[orchestrion:aws-sdk] subscribing to channels "${awsSendChannels.join('", "')}"`); + + waitForTracingChannelBinding(() => { + for (const channelName of awsSendChannels) { + bindTracingChannelToSpan( + diagnosticsChannel.tracingChannel(channelName), + getSpan, + opts, + ); + } + }); + }, + }; +}) satisfies IntegrationFn; + +/** + * EXPERIMENTAL — orchestrion-driven aws-sdk (v3) integration. + * + * Subscribes to the `orchestrion:@smithy/smithy-client:send` (and equivalent) diagnostics_channel + * the orchestrion code transform injects into the AWS SDK's smithy `Client.prototype.send`, emitting + * spans identical to the OTel `@opentelemetry/instrumentation-aws-sdk` integration (with a distinct + * `auto.aws.orchestrion.aws-sdk` origin). Requires the orchestrion runtime hook or bundler plugin — + * wire it up via `experimentalUseDiagnosticsChannelInjection()`. + * + * @experimental + */ +export const awsChannelIntegration = defineIntegration(_awsChannelIntegration); diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServiceExtension.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServiceExtension.ts new file mode 100644 index 000000000000..1ca4577fd1fb --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServiceExtension.ts @@ -0,0 +1,16 @@ +import type { Span } from '@sentry/core'; +import type { NormalizedRequest, NormalizedResponse, RequestMetadata } from '../types'; + +export type { RequestMetadata }; + +export interface ServiceExtension { + // called before the request is sent, and before the span is started + requestPreSpanHook: (request: NormalizedRequest) => RequestMetadata; + + // called before the request is sent, and after the span is started. `span` is the started span, + // used to derive trace-propagation headers injected into outgoing messages. + requestPostSpanHook?: (request: NormalizedRequest, span: Span) => void; + + // called after the response is received. If a value is returned, it replaces the response output. + responseHook?: (response: NormalizedResponse, span: Span) => any | undefined; +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts new file mode 100644 index 000000000000..8c921c318e6f --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts @@ -0,0 +1,31 @@ +import type { Span } from '@sentry/core'; +import type { NormalizedRequest, NormalizedResponse, RequestMetadata } from '../types'; +import type { ServiceExtension } from './ServiceExtension'; + +export class ServicesExtensions implements ServiceExtension { + private _services: Map; + + public constructor() { + // Per-service extensions, keyed by the client's `serviceId` (e.g. `'S3'`). Services without a + // registered extension still get the base rpc span from the subscriber. + this._services = new Map(); + } + + public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { + const serviceExtension = this._services.get(request.serviceName); + if (!serviceExtension) { + return {}; + } + return serviceExtension.requestPreSpanHook(request); + } + + public requestPostSpanHook(request: NormalizedRequest, span: Span): void { + const serviceExtension = this._services.get(request.serviceName); + serviceExtension?.requestPostSpanHook?.(request, span); + } + + public responseHook(response: NormalizedResponse, span: Span): any | undefined { + const serviceExtension = this._services.get(response.request.serviceName); + return serviceExtension?.responseHook?.(response, span); + } +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/index.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/index.ts new file mode 100644 index 000000000000..ef438e065534 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/index.ts @@ -0,0 +1 @@ +export { ServicesExtensions } from './ServicesExtensions'; diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/types.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/types.ts new file mode 100644 index 000000000000..d46e813eacce --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/types.ts @@ -0,0 +1,31 @@ +import type { SpanKindValue } from '@sentry/core'; + +export type CommandInput = Record; + +/** + * These are normalized request and response. They organize the relevant data in one interface which + * can be processed in a uniform manner in the per-service hooks. + */ +export interface NormalizedRequest { + serviceName: string; + commandName: string; + commandInput: CommandInput; + region?: string; +} + +export interface NormalizedResponse { + data: any; + request: NormalizedRequest; + requestId?: string; +} + +/** Span metadata a per-service extension returns for the subscriber to build the span from. */ +export interface RequestMetadata { + // If true, then the response is a stream so the subscriber must not end the span when `send` settles. + // The service extension ends the span itself, generally by wrapping the stream and ending after it is + // consumed. + isStream?: boolean; + spanAttributes?: Record; + spanKind?: SpanKindValue; + spanName?: string; +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/utils.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/utils.ts new file mode 100644 index 000000000000..4af89127f6f4 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/utils.ts @@ -0,0 +1,31 @@ +import { RPC_METHOD, RPC_SERVICE } from '@sentry/conventions/attributes'; +import { ATTR_RPC_SYSTEM, CLOUD_REGION } from './constants'; +import type { NormalizedRequest } from './types'; + +export function removeSuffixFromStringIfExists(str: string, suffixToRemove: string): string { + const suffixLength = suffixToRemove.length; + return str?.slice(-suffixLength) === suffixToRemove ? str.slice(0, str.length - suffixLength) : str; +} + +export function normalizeV3Request( + serviceName: string, + commandNameWithSuffix: string, + commandInput: Record, + region: string | undefined, +): NormalizedRequest { + return { + serviceName: serviceName?.replace(/\s+/g, ''), + commandName: removeSuffixFromStringIfExists(commandNameWithSuffix, 'Command'), + commandInput, + region, + }; +} + +export function extractAttributesFromNormalizedRequest(normalizedRequest: NormalizedRequest): Record { + return { + [ATTR_RPC_SYSTEM]: 'aws-api', + [RPC_METHOD]: normalizedRequest.commandName, + [RPC_SERVICE]: normalizedRequest.serviceName, + [CLOUD_REGION]: normalizedRequest.region, + }; +} diff --git a/packages/server-utils/src/orchestrion/channels.ts b/packages/server-utils/src/orchestrion/channels.ts index 3d56b43b29d8..bec38119b1d8 100644 --- a/packages/server-utils/src/orchestrion/channels.ts +++ b/packages/server-utils/src/orchestrion/channels.ts @@ -13,6 +13,7 @@ import { redisChannels } from './config/redis'; import { expressChannels } from './config/express'; import { graphqlChannels } from './config/graphql'; import { kafkajsChannels } from './config/kafkajs'; +import { awsSdkChannels } from './config/aws-sdk'; /** * Fully-qualified `diagnostics_channel` names that orchestrion publishes to. @@ -43,6 +44,7 @@ export const CHANNELS = { ...expressChannels, ...graphqlChannels, ...kafkajsChannels, + ...awsSdkChannels, } as const; export type ChannelName = (typeof CHANNELS)[keyof typeof CHANNELS]; diff --git a/packages/server-utils/src/orchestrion/config/aws-sdk.ts b/packages/server-utils/src/orchestrion/config/aws-sdk.ts new file mode 100644 index 000000000000..6b84ef73368f --- /dev/null +++ b/packages/server-utils/src/orchestrion/config/aws-sdk.ts @@ -0,0 +1,34 @@ +import type { InstrumentationConfig } from '@apm-js-collab/code-transformer'; + +// The AWS SDK (v3) routes every command through the smithy `Client.prototype.send` method. Which +// package hosts that `Client` class changed across versions, so we target all of them; only the one +// the app's client actually extends is ever invoked. +// +// - `@smithy/core` >= 3.24.0: the `Client` class moved into the `client` submodule bundle. +// - `@smithy/smithy-client`: the `Client` class for aws-sdk v3.363.0+ (pre-`@smithy/core` stack). +// - `@aws-sdk/smithy-client`: the `Client` class for older aws-sdk v3 releases. +// +// `send` is `async send(command, options)` (returns a promise), so `kind: 'Async'`. +export const awsSdkConfig = [ + { + channelName: 'send', + module: { name: '@smithy/core', versionRange: '>=3.24.0', filePath: 'dist-cjs/submodules/client/index.js' }, + functionQuery: { className: 'Client', methodName: 'send', kind: 'Async' }, + }, + { + channelName: 'send', + module: { name: '@smithy/smithy-client', versionRange: '>=1.0.3', filePath: 'dist-cjs/index.js' }, + functionQuery: { className: 'Client', methodName: 'send', kind: 'Async' }, + }, + { + channelName: 'send', + module: { name: '@aws-sdk/smithy-client', versionRange: '^3.1.0', filePath: 'dist-cjs/index.js' }, + functionQuery: { className: 'Client', methodName: 'send', kind: 'Async' }, + }, +] satisfies InstrumentationConfig[]; + +export const awsSdkChannels = { + AWS_SMITHY_CORE_SEND: 'orchestrion:@smithy/core:send', + AWS_SMITHY_CLIENT_SEND: 'orchestrion:@smithy/smithy-client:send', + AWS_SDK_SMITHY_CLIENT_SEND: 'orchestrion:@aws-sdk/smithy-client:send', +} as const; diff --git a/packages/server-utils/src/orchestrion/config/index.ts b/packages/server-utils/src/orchestrion/config/index.ts index b0e4d3e8b6d1..4dead6822e99 100644 --- a/packages/server-utils/src/orchestrion/config/index.ts +++ b/packages/server-utils/src/orchestrion/config/index.ts @@ -15,6 +15,7 @@ import { redisConfig } from './redis'; import { expressConfig } from './express'; import { graphqlConfig } from './graphql'; import { kafkajsConfig } from './kafkajs'; +import { awsSdkConfig } from './aws-sdk'; export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [ ...mysqlConfig, @@ -32,6 +33,7 @@ export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [ ...expressConfig, ...graphqlConfig, ...kafkajsConfig, + ...awsSdkConfig, ]; /** diff --git a/packages/server-utils/src/orchestrion/index.ts b/packages/server-utils/src/orchestrion/index.ts index 1933978787fc..f38962a1abdb 100644 --- a/packages/server-utils/src/orchestrion/index.ts +++ b/packages/server-utils/src/orchestrion/index.ts @@ -1,5 +1,6 @@ import { amqplibChannelIntegration } from '../integrations/tracing-channel/amqplib'; import { anthropicChannelIntegration } from '../integrations/tracing-channel/anthropic'; +import { awsChannelIntegration } from '../integrations/tracing-channel/aws-sdk'; import { googleGenAIChannelIntegration } from '../integrations/tracing-channel/google-genai'; import { graphqlChannelIntegration, @@ -20,6 +21,7 @@ export { detectOrchestrionSetup, isOrchestrionInjected } from './detect'; export { amqplibChannelIntegration, anthropicChannelIntegration, + awsChannelIntegration, googleGenAIChannelIntegration, graphqlChannelIntegration, hapiChannelIntegration, @@ -69,4 +71,5 @@ export const channelIntegrations = { expressIntegration: expressChannelIntegration, graphqlIntegration: graphqlDiagnosticsChannelIntegration, kafkajsIntegration: kafkajsChannelIntegration, + awsIntegration: awsChannelIntegration, } as const; From 19006ed1f2f6e24b3547f7967b4fb556340c471d Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 9 Jul 2026 23:32:35 +0200 Subject: [PATCH 02/11] Set sentry.op explicitly on aws-sdk channel spans Peer channel integrations (mysql, ioredis, anthropic) set the op at span start. Default to rpc, matching what the exporter infers from rpc.service, and let service extensions override it via RequestMetadata.spanOp where inference yields a different op. --- .../src/integrations/tracing-channel/aws-sdk/index.ts | 3 +++ .../src/integrations/tracing-channel/aws-sdk/types.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts index bc862a96bc29..3e07a5d5deeb 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts @@ -103,6 +103,9 @@ const _awsChannelIntegration = (() => { const span = startInactiveSpan({ name: requestMetadata.spanName ?? `${normalizedRequest.serviceName}.${normalizedRequest.commandName}`, kind: requestMetadata.spanKind ?? SPAN_KIND.CLIENT, + // `rpc` matches what the exporter infers from `rpc.service` for the OTel aws-sdk spans; + // service extensions override it where inference yields a different op (DynamoDB: `db`). + op: requestMetadata.spanOp ?? 'rpc', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: AWS_SDK_ORIGIN, ...extractAttributesFromNormalizedRequest(normalizedRequest), diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/types.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/types.ts index d46e813eacce..493e9dfa15c8 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/types.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/types.ts @@ -28,4 +28,6 @@ export interface RequestMetadata { spanAttributes?: Record; spanKind?: SpanKindValue; spanName?: string; + // Overrides the default `rpc` span op (e.g. `db` for DynamoDB). + spanOp?: string; } From 8220e0e54323eca3342ed4a327e0db7b8cf72244 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 9 Jul 2026 23:42:22 +0200 Subject: [PATCH 03/11] Make responseHook contract explicit about in-place response mutation The return-value override from the vendored OTel path cannot work through a tracing channel: subscribers cannot replace the value the caller's promise resolves with, so the subscriber rightly ignores any return value. Type responseHook as void and document that response changes (e.g. wrapping a stream) must mutate response.data in place, which is what the extensions do. --- .../tracing-channel/aws-sdk/services/ServiceExtension.ts | 7 +++++-- .../tracing-channel/aws-sdk/services/ServicesExtensions.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServiceExtension.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServiceExtension.ts index 1ca4577fd1fb..5920f89f3346 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServiceExtension.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServiceExtension.ts @@ -11,6 +11,9 @@ export interface ServiceExtension { // used to derive trace-propagation headers injected into outgoing messages. requestPostSpanHook?: (request: NormalizedRequest, span: Span) => void; - // called after the response is received. If a value is returned, it replaces the response output. - responseHook?: (response: NormalizedResponse, span: Span) => any | undefined; + // Called after the response is received. Unlike the OTel middleware patch, a tracing-channel + // subscriber cannot replace the value the caller's promise resolves with (`data.result` is not + // writable through the channel), so extensions that need to alter the response, e.g. to wrap a + // stream, must mutate `response.data` in place. + responseHook?: (response: NormalizedResponse, span: Span) => void; } diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts index 8c921c318e6f..2cc308150307 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServicesExtensions.ts @@ -24,8 +24,8 @@ export class ServicesExtensions implements ServiceExtension { serviceExtension?.requestPostSpanHook?.(request, span); } - public responseHook(response: NormalizedResponse, span: Span): any | undefined { + public responseHook(response: NormalizedResponse, span: Span): void { const serviceExtension = this._services.get(response.request.serviceName); - return serviceExtension?.responseHook?.(response, span); + serviceExtension?.responseHook?.(response, span); } } From a8a601f065df08113a707237d9f858a544e4f21f Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 9 Jul 2026 23:48:45 +0200 Subject: [PATCH 04/11] Document why in-place response mutation is safe through the channel --- .../tracing-channel/aws-sdk/services/ServiceExtension.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServiceExtension.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServiceExtension.ts index 5920f89f3346..071f9005b8f0 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServiceExtension.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/ServiceExtension.ts @@ -12,8 +12,11 @@ export interface ServiceExtension { requestPostSpanHook?: (request: NormalizedRequest, span: Span) => void; // Called after the response is received. Unlike the OTel middleware patch, a tracing-channel - // subscriber cannot replace the value the caller's promise resolves with (`data.result` is not - // writable through the channel), so extensions that need to alter the response, e.g. to wrap a - // stream, must mutate `response.data` in place. + // subscriber cannot replace the value the caller's promise resolves with: the injected settle + // handler returns the captured result, not `ctx.result`. It does however publish `asyncEnd` + // synchronously BEFORE the caller's continuations run, and `response.data` is the same object the + // caller receives, so extensions that need to alter the response (e.g. wrap a stream) must mutate + // `response.data` in place; the mutation is guaranteed to be visible to the caller. Same idiom as + // the vercel-ai subscribers' `result.stream` tap. responseHook?: (response: NormalizedResponse, span: Span) => void; } From 6e0ed9c5ea4c743fa10776b6c8efd76fcf34ba85 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Jul 2026 00:07:57 +0200 Subject: [PATCH 05/11] Hold span end for pending region resolution The region provider resolves asynchronously while send proceeds; when send settled first (e.g. an early failure) the fire-and-forget backfill hit an already-ended span and cloud.region was lost. deferSpanEnd now keeps the span open until the region promise settles. In the common path the SDK awaits the region internally before sending, so the promise is long settled and nothing changes. --- .../tracing-channel/aws-sdk/index.ts | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts index 3e07a5d5deeb..e6527b278eb9 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts @@ -31,6 +31,7 @@ interface AwsSendChannelContext { error?: unknown; _sentryNormalizedRequest?: NormalizedRequest; _sentryRequestMetadata?: RequestMetadata; + _sentryRegion?: { settled: boolean; promise: Promise }; } interface AwsClientConfig { @@ -116,9 +117,12 @@ const _awsChannelIntegration = (() => { data._sentryNormalizedRequest = normalizedRequest; data._sentryRequestMetadata = requestMetadata; - // `region` resolves asynchronously; set it on the span (still open until `send` settles) - // and backfill it on the normalized request once available. - Promise.resolve(clientConfig?.region?.()) + // `region` resolves asynchronously while `send` proceeds (a channel subscriber cannot delay + // the traced call the way the OTel middleware does). Backfill it onto the span and the + // normalized request once available; `deferSpanEnd` holds the span open until this settles + // so `cloud.region` cannot be lost when `send` settles first (e.g. an early failure). + const regionHolder = { settled: false, promise: Promise.resolve() }; + regionHolder.promise = Promise.resolve(clientConfig?.region?.()) .then(region => { if (region) { normalizedRequest.region = region; @@ -127,7 +131,11 @@ const _awsChannelIntegration = (() => { }) .catch(() => { // Nothing to do; continue without a region. + }) + .finally(() => { + regionHolder.settled = true; }); + data._sentryRegion = regionHolder; // Inject trace-propagation headers into outgoing messages (SQS/SNS/Lambda). Runs before // `send` proceeds, so the mutated `commandInput` is used to build the request. @@ -137,7 +145,7 @@ const _awsChannelIntegration = (() => { }); const opts: TracingChannelLifeCycleOptions = { - deferSpanEnd({ span, data }) { + deferSpanEnd({ span, data, end }) { const normalizedRequest = data._sentryNormalizedRequest; const requestMetadata = data._sentryRequestMetadata; if (!normalizedRequest) { @@ -166,7 +174,21 @@ const _awsChannelIntegration = (() => { // Streaming responses end the span when their wrapped stream is consumed (see // bedrock-runtime); the helper must not end it on `send` settling. Errors always end here. - return !!requestMetadata?.isStream && !failed; + if (requestMetadata?.isStream && !failed) { + return true; + } + + // Normally the region settles long before `send` does (the SDK awaits it internally to + // build the endpoint), but when `send` settles first (e.g. an early failure) hold the span + // open until the region backfill lands. The error status was already applied by the + // helper's `error` subscriber, so a plain `end()` suffices. + const region = data._sentryRegion; + if (region && !region.settled) { + void region.promise.then(() => end()); + return true; + } + + return false; }, }; From 70c9d09523d684a1c182c218c5c735f084ae6a21 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Jul 2026 00:13:45 +0200 Subject: [PATCH 06/11] Read extendedRequestId off the error object with metadata fallback --- .../integrations/tracing-channel/aws-sdk/index.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts index e6527b278eb9..993bef8cf762 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts @@ -156,8 +156,18 @@ const _awsChannelIntegration = (() => { safe(() => { if (failed) { - const err = data.error as { $metadata?: Record; RequestId?: string } | undefined; - setMetadataAttributes(span, { requestId: err?.RequestId, ...err?.$metadata }); + const err = data.error as + | { $metadata?: Record; RequestId?: string; extendedRequestId?: string } + | undefined; + const errMetadata = err?.$metadata; + // Like the OTel path, read RequestId/extendedRequestId off the error itself, with + // `$metadata` (which smithy service errors also carry) as the fallback. A spread won't + // do: `$metadata` includes these keys with `undefined` values, clobbering the fallback. + setMetadataAttributes(span, { + requestId: err?.RequestId ?? errMetadata?.requestId, + httpStatusCode: errMetadata?.httpStatusCode, + extendedRequestId: err?.extendedRequestId ?? errMetadata?.extendedRequestId, + }); return; } From a36b11dc30a2979ba1c6417d09872c2fc76fdd5e Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Jul 2026 00:31:12 +0200 Subject: [PATCH 07/11] Use CLOUD_REGION from @sentry/conventions --- .../src/integrations/tracing-channel/aws-sdk/constants.ts | 6 +++--- .../src/integrations/tracing-channel/aws-sdk/index.ts | 4 ++-- .../src/integrations/tracing-channel/aws-sdk/utils.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts index 27d886365ab5..cc4e5905c3c9 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts @@ -4,14 +4,14 @@ * These mirror the constants the OTel `@opentelemetry/instrumentation-aws-sdk` emits (some are * unstable/obsolete OTel semantic conventions with no `ATTR_*` export in * `@opentelemetry/semantic-conventions`), inlined here so the integration stays free of OTel deps. - * Standard conventions (`rpc.*`, `db.*`, `messaging.*`, `gen_ai.*`, ...) come from - * `@sentry/conventions/attributes` directly. + * Attributes that exist in `@sentry/conventions/attributes` are imported from there instead; + * TODO(aws-sdk): the active attributes below are being added to sentry-conventions and should move + * to `@sentry/conventions/attributes` imports once a release containing them ships. */ /** The span origin every aws-sdk channel span carries, mirroring the uniform OTel `auto.otel.aws`. */ export const AWS_SDK_ORIGIN = 'auto.aws.orchestrion.aws-sdk'; export const ATTR_RPC_SYSTEM = 'rpc.system'; -export const CLOUD_REGION = 'cloud.region'; export const AWS_REQUEST_ID = 'aws.request.id'; export const AWS_REQUEST_EXTENDED_ID = 'aws.request.extended_id'; diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts index 993bef8cf762..49f4f19eea42 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts @@ -8,12 +8,12 @@ import { startInactiveSpan, waitForTracingChannelBinding, } from '@sentry/core'; -import { HTTP_STATUS_CODE } from '@sentry/conventions/attributes'; +import { CLOUD_REGION, HTTP_STATUS_CODE } from '@sentry/conventions/attributes'; import { DEBUG_BUILD } from '../../../debug-build'; import { CHANNELS } from '../../../orchestrion/channels'; import type { TracingChannelLifeCycleOptions } from '../../../tracing-channel'; import { bindTracingChannelToSpan } from '../../../tracing-channel'; -import { AWS_REQUEST_EXTENDED_ID, AWS_REQUEST_ID, AWS_SDK_ORIGIN, CLOUD_REGION } from './constants'; +import { AWS_REQUEST_EXTENDED_ID, AWS_REQUEST_ID, AWS_SDK_ORIGIN } from './constants'; import { ServicesExtensions } from './services'; import type { NormalizedRequest, NormalizedResponse, RequestMetadata } from './types'; import { extractAttributesFromNormalizedRequest, normalizeV3Request, removeSuffixFromStringIfExists } from './utils'; diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/utils.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/utils.ts index 4af89127f6f4..997d87acaa4f 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/utils.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/utils.ts @@ -1,5 +1,5 @@ -import { RPC_METHOD, RPC_SERVICE } from '@sentry/conventions/attributes'; -import { ATTR_RPC_SYSTEM, CLOUD_REGION } from './constants'; +import { CLOUD_REGION, RPC_METHOD, RPC_SERVICE } from '@sentry/conventions/attributes'; +import { ATTR_RPC_SYSTEM } from './constants'; import type { NormalizedRequest } from './types'; export function removeSuffixFromStringIfExists(str: string, suffixToRemove: string): string { From 4eced1d3d7d7b77b15b1725ae816886cf5187213 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Jul 2026 00:44:19 +0200 Subject: [PATCH 08/11] Use underscore in span origin: auto.aws.orchestrion.aws_sdk Span origins only allow letters, digits, underscores, and dots per segment; matches the auto.ai.orchestrion.google_genai precedent. --- .../src/integrations/tracing-channel/aws-sdk/constants.ts | 2 +- .../src/integrations/tracing-channel/aws-sdk/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts index cc4e5905c3c9..4e7593d5f1d2 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts @@ -10,7 +10,7 @@ */ /** The span origin every aws-sdk channel span carries, mirroring the uniform OTel `auto.otel.aws`. */ -export const AWS_SDK_ORIGIN = 'auto.aws.orchestrion.aws-sdk'; +export const AWS_SDK_ORIGIN = 'auto.aws.orchestrion.aws_sdk'; export const ATTR_RPC_SYSTEM = 'rpc.system'; export const AWS_REQUEST_ID = 'aws.request.id'; diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts index 49f4f19eea42..8d406afef532 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts @@ -232,7 +232,7 @@ const _awsChannelIntegration = (() => { * Subscribes to the `orchestrion:@smithy/smithy-client:send` (and equivalent) diagnostics_channel * the orchestrion code transform injects into the AWS SDK's smithy `Client.prototype.send`, emitting * spans identical to the OTel `@opentelemetry/instrumentation-aws-sdk` integration (with a distinct - * `auto.aws.orchestrion.aws-sdk` origin). Requires the orchestrion runtime hook or bundler plugin — + * `auto.aws.orchestrion.aws_sdk` origin). Requires the orchestrion runtime hook or bundler plugin — * wire it up via `experimentalUseDiagnosticsChannelInjection()`. * * @experimental From 1227294b6f58d98b9c4eecd3c30812e1f34b30cd Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Jul 2026 00:54:51 +0200 Subject: [PATCH 09/11] Guard region provider call against synchronous throws The span is already started at that point; a sync throw bubbling into the enclosing safe wrapper would discard the span without ending it, leaking an open span for that AWS call. --- .../integrations/tracing-channel/aws-sdk/index.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts index 8d406afef532..9a0b1c425de2 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts @@ -121,8 +121,18 @@ const _awsChannelIntegration = (() => { // the traced call the way the OTel middleware does). Backfill it onto the span and the // normalized request once available; `deferSpanEnd` holds the span open until this settles // so `cloud.region` cannot be lost when `send` settles first (e.g. an early failure). + // + // The provider call is guarded separately: the span is already started, so a synchronous + // throw bubbling into the enclosing `safe` would discard it without ending it (a leaked + // open span). + let regionResult: string | Promise | undefined; + try { + regionResult = clientConfig?.region?.(); + } catch { + // Nothing to do; continue without a region. + } const regionHolder = { settled: false, promise: Promise.resolve() }; - regionHolder.promise = Promise.resolve(clientConfig?.region?.()) + regionHolder.promise = Promise.resolve(regionResult) .then(region => { if (region) { normalizedRequest.region = region; From 92b04fc9571e034f61ef84754d2efed5993018be Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Jul 2026 01:01:55 +0200 Subject: [PATCH 10/11] Trace commands constructed without an input object Commands with all-optional members can be constructed without an input (e.g. new ListBucketsCommand()); the OTel path traces those, so default the command input to an empty object instead of skipping the span. All service extensions already read the input defensively. --- .../src/integrations/tracing-channel/aws-sdk/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts index 9a0b1c425de2..33f6c1915442 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts @@ -84,9 +84,8 @@ const _awsChannelIntegration = (() => { const getSpan = (data: AwsSendChannelContext): Span | undefined => safe(() => { const command = data.arguments[0] as AwsV3Command | undefined; - const commandInput = command?.input; const commandName = command?.constructor?.name; - if (!command || !commandName || !commandInput) { + if (!command || !commandName) { // Not a recognizable v3 command call; leave the active context untouched. return undefined; } @@ -98,7 +97,9 @@ const _awsChannelIntegration = (() => { // constructor name (e.g. `S3Client` -> `S3`). `serviceId` is set for all AWS clients. removeSuffixFromStringIfExists(data.self?.constructor?.name || 'AWS', 'Client'); - const normalizedRequest = normalizeV3Request(serviceName, commandName, commandInput, undefined); + // Commands with all-optional members can be constructed without an input (`new + // ListBucketsCommand()`); the OTel path traces those too, so default rather than bail. + const normalizedRequest = normalizeV3Request(serviceName, commandName, command.input ?? {}, undefined); const requestMetadata = servicesExtensions.requestPreSpanHook(normalizedRequest); const span = startInactiveSpan({ From 9510b15c9f96ac5fe113e2de7847c98b21871a68 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Jul 2026 09:37:17 +0200 Subject: [PATCH 11/11] Document and narrow any usage in core aws-sdk files --- .../src/integrations/tracing-channel/aws-sdk/index.ts | 4 ++++ .../src/integrations/tracing-channel/aws-sdk/types.ts | 3 +++ .../src/integrations/tracing-channel/aws-sdk/utils.ts | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts index 33f6c1915442..25959f753eb2 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/index.ts @@ -54,6 +54,8 @@ function safe(fn: () => T): T | undefined { } } +// `metadata` is smithy's `ResponseMetadata`, read off the untyped channel result/error (`any` for the +// same reason as `CommandInput`, see types.ts). function setMetadataAttributes(span: Span, metadata: Record | undefined): void { if (!metadata) { return; @@ -165,6 +167,8 @@ const _awsChannelIntegration = (() => { const failed = 'error' in data; + // The channel `result`/`error` are untyped; the `$metadata` casts below name smithy's + // `ResponseMetadata` shape (`any`-valued, see `setMetadataAttributes`). safe(() => { if (failed) { const err = data.error as diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/types.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/types.ts index 493e9dfa15c8..93d1ebade30d 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/types.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/types.ts @@ -1,5 +1,7 @@ import type { SpanKindValue } from '@sentry/core'; +// Command inputs are service-specific shapes from hundreds of AWS APIs; typing them would require +// depending on the `@aws-sdk/*` client types. The per-service hooks read fields defensively instead. export type CommandInput = Record; /** @@ -14,6 +16,7 @@ export interface NormalizedRequest { } export interface NormalizedResponse { + // The command output, shaped per service/command (see `CommandInput` on why this isn't typed). data: any; request: NormalizedRequest; requestId?: string; diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/utils.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/utils.ts index 997d87acaa4f..4f8e771ba819 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/utils.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/utils.ts @@ -1,6 +1,6 @@ import { CLOUD_REGION, RPC_METHOD, RPC_SERVICE } from '@sentry/conventions/attributes'; import { ATTR_RPC_SYSTEM } from './constants'; -import type { NormalizedRequest } from './types'; +import type { CommandInput, NormalizedRequest } from './types'; export function removeSuffixFromStringIfExists(str: string, suffixToRemove: string): string { const suffixLength = suffixToRemove.length; @@ -10,7 +10,7 @@ export function removeSuffixFromStringIfExists(str: string, suffixToRemove: stri export function normalizeV3Request( serviceName: string, commandNameWithSuffix: string, - commandInput: Record, + commandInput: CommandInput, region: string | undefined, ): NormalizedRequest { return {