From 6cd0c5cfdfb890856fd737cc9e53a411cbb06548 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 9 Jul 2026 23:18:06 +0200 Subject: [PATCH 1/5] feat(server-utils): Port SQS, SNS and Lambda aws-sdk extensions with trace propagation Ports the messaging service extensions from the OTel aws-sdk integration to the orchestrion channel integration and registers them in the service registry: - SQS: producer/consumer span kinds and names, `messaging.*` attributes, trace propagation into outgoing `MessageAttributes` (single and batch), and span links from received messages via the propagated headers - SNS: producer span kind/name for `Publish`, `messaging.*` and topic ARN attributes, trace propagation into `MessageAttributes` - Lambda: `faas.*` attributes for `Invoke`, trace propagation into the base64 `ClientContext` (respecting the 3583 byte cap) Propagation writes Sentry-native `sentry-trace`/`baggage` headers derived from the request span instead of OTel `propagation.inject`, and the SQS receive side reads them back with `propagationContextFromHeaders`. Part of #20946 Co-Authored-By: Claude Fable 5 --- .../tracing-channel/aws-sdk/aws-sdk.types.ts | 59 ++++++++ .../tracing-channel/aws-sdk/constants.ts | 14 ++ .../aws-sdk/services/MessageAttributes.ts | 83 +++++++++++ .../aws-sdk/services/ServicesExtensions.ts | 6 + .../aws-sdk/services/lambda.ts | 86 +++++++++++ .../tracing-channel/aws-sdk/services/sns.ts | 74 ++++++++++ .../tracing-channel/aws-sdk/services/sqs.ts | 136 ++++++++++++++++++ 7 files changed, 458 insertions(+) create mode 100644 packages/server-utils/src/integrations/tracing-channel/aws-sdk/aws-sdk.types.ts create mode 100644 packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/MessageAttributes.ts create mode 100644 packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/lambda.ts create mode 100644 packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sns.ts create mode 100644 packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sqs.ts diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/aws-sdk.types.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/aws-sdk.types.ts new file mode 100644 index 000000000000..bf290fc443b7 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/aws-sdk.types.ts @@ -0,0 +1,59 @@ +/* + * AWS SDK for JavaScript + * Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * This product includes software developed at + * Amazon Web Services, Inc. (http://aws.amazon.com/). + */ + +/* + These are slightly modified and simplified versions of the actual SQS/SNS types included + in the official distribution: + https://github.com/aws/aws-sdk-js/blob/master/clients/sqs.d.ts + These are brought here to avoid having users install the `aws-sdk` whenever they + require this instrumentation. +*/ + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface Blob {} +type Binary = Buffer | Uint8Array | Blob | string; + +// eslint-disable-next-line @typescript-eslint/no-namespace -- Prefer to contain the types copied over in one location +export namespace SNS { + interface MessageAttributeValue { + DataType: string; + StringValue?: string; + BinaryValue?: Binary; + } + + export type MessageAttributeMap = { [key: string]: MessageAttributeValue }; +} + +// eslint-disable-next-line @typescript-eslint/no-namespace -- Prefer to contain the types copied over in one location +export namespace SQS { + type StringList = string[]; + type BinaryList = Binary[]; + interface MessageAttributeValue { + StringValue?: string; + BinaryValue?: Binary; + StringListValues?: StringList; + BinaryListValues?: BinaryList; + DataType: string; + } + + export type MessageBodyAttributeMap = { + [key: string]: MessageAttributeValue; + }; + + type MessageSystemAttributeMap = { [key: string]: string }; + + export interface Message { + MessageId?: string; + ReceiptHandle?: string; + MD5OfBody?: string; + Body?: string; + Attributes?: MessageSystemAttributeMap; + MD5OfMessageAttributes?: string; + MessageAttributes?: MessageBodyAttributeMap; + } +} 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 9ec240793f12..b466c61437dc 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 @@ -46,3 +46,17 @@ export const DB_SYSTEM_VALUE_DYNAMODB = 'dynamodb'; export const ATTR_AWS_SECRETSMANAGER_SECRET_ARN = 'aws.secretsmanager.secret.arn'; export const ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN = 'aws.step_functions.activity.arn'; export const ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN = 'aws.step_functions.state_machine.arn'; + +// SNS +export const ATTR_AWS_SNS_TOPIC_ARN = 'aws.sns.topic.arn'; + +// Lambda (faas) +export const ATTR_FAAS_INVOKED_NAME = 'faas.invoked_name'; +export const ATTR_FAAS_INVOKED_PROVIDER = 'faas.invoked_provider'; +export const ATTR_FAAS_INVOKED_REGION = 'faas.invoked_region'; +export const ATTR_FAAS_EXECUTION = 'faas.execution'; + +// Messaging (obsolete OTel conventions kept for parity with the OTel integration) +export const ATTR_MESSAGING_DESTINATION = 'messaging.destination'; +export const ATTR_MESSAGING_DESTINATION_KIND = 'messaging.destination_kind'; +export const MESSAGING_DESTINATION_KIND_VALUE_TOPIC = 'topic'; diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/MessageAttributes.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/MessageAttributes.ts new file mode 100644 index 000000000000..fb040764d194 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/MessageAttributes.ts @@ -0,0 +1,83 @@ +import type { Span } from '@sentry/core'; +import { + debug, + dynamicSamplingContextToSentryBaggageHeader, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; +import { DEBUG_BUILD } from '../../../../debug-build'; +import type { SNS, SQS } from '../aws-sdk.types'; + +// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-quotas.html +export const MAX_MESSAGE_ATTRIBUTES = 10; + +// Sentry trace-propagation headers written into / read from AWS message attributes. +const SENTRY_TRACE_HEADER = 'sentry-trace'; +const BAGGAGE_HEADER = 'baggage'; +const PROPAGATION_FIELDS = [SENTRY_TRACE_HEADER, BAGGAGE_HEADER]; + +export interface AwsSdkContextObject { + [key: string]: { + StringValue?: string; + Value?: string; + }; +} + +/** Build the `sentry-trace`/`baggage` header pair carrying the span's trace context. */ +export function getPropagationHeaders(span: Span): Record { + const headers: Record = { + [SENTRY_TRACE_HEADER]: spanToTraceHeader(span), + }; + const baggage = dynamicSamplingContextToSentryBaggageHeader(getDynamicSamplingContextFromSpan(span)); + if (baggage) { + headers[BAGGAGE_HEADER] = baggage; + } + return headers; +} + +/** + * Inject the span's trace-propagation headers into an SQS/SNS message-attribute map, so the consumer + * can continue the trace. Respects the SQS 10-attribute quota. Mirrors the OTel integration's + * `injectPropagationContext`, but writes Sentry's `sentry-trace`/`baggage` instead of W3C headers. + */ +export function injectPropagationContext( + attributesMap: SQS.MessageBodyAttributeMap | SNS.MessageAttributeMap | undefined, + span: Span, +): SQS.MessageBodyAttributeMap | SNS.MessageAttributeMap { + const attributes = attributesMap ?? {}; + const headers = getPropagationHeaders(span); + const headerKeys = Object.keys(headers); + + if (Object.keys(attributes).length + headerKeys.length <= MAX_MESSAGE_ATTRIBUTES) { + for (const key of headerKeys) { + (attributes as AwsSdkContextObject)[key] = { DataType: 'String', StringValue: headers[key] } as any; + } + } else { + DEBUG_BUILD && + debug.warn( + '[orchestrion:aws-sdk] cannot set trace propagation on SQS/SNS message due to maximum amount of MessageAttributes', + ); + } + return attributes; +} + +/** Read the propagation headers back off a received SQS message, if present. */ +export function extractPropagationHeaders( + message: SQS.Message, +): { sentryTrace?: string; baggage?: string } | undefined { + const carrier = (message.MessageAttributes ?? {}) as AwsSdkContextObject; + const sentryTrace = carrier[SENTRY_TRACE_HEADER]?.StringValue ?? carrier[SENTRY_TRACE_HEADER]?.Value; + if (!sentryTrace) { + return undefined; + } + return { + sentryTrace, + baggage: carrier[BAGGAGE_HEADER]?.StringValue ?? carrier[BAGGAGE_HEADER]?.Value, + }; +} + +export function addPropagationFieldsToAttributeNames(messageAttributeNames: string[] = []): string[] { + return messageAttributeNames.length + ? Array.from(new Set([...messageAttributeNames, ...PROPAGATION_FIELDS])) + : [...PROPAGATION_FIELDS]; +} 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 b98d402c9078..b70b0f59bf6c 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 @@ -2,9 +2,12 @@ import type { Span } from '@sentry/core'; import type { NormalizedRequest, NormalizedResponse, RequestMetadata } from '../types'; import { DynamodbServiceExtension } from './dynamodb'; import { KinesisServiceExtension } from './kinesis'; +import { LambdaServiceExtension } from './lambda'; import { S3ServiceExtension } from './s3'; import { SecretsManagerServiceExtension } from './secretsmanager'; import type { ServiceExtension } from './ServiceExtension'; +import { SnsServiceExtension } from './sns'; +import { SqsServiceExtension } from './sqs'; import { StepFunctionsServiceExtension } from './stepfunctions'; export class ServicesExtensions implements ServiceExtension { @@ -13,7 +16,10 @@ export class ServicesExtensions implements ServiceExtension { private _services: Map = new Map([ ['SecretsManager', new SecretsManagerServiceExtension()], ['SFN', new StepFunctionsServiceExtension()], + ['SQS', new SqsServiceExtension()], + ['SNS', new SnsServiceExtension()], ['DynamoDB', new DynamodbServiceExtension()], + ['Lambda', new LambdaServiceExtension()], ['S3', new S3ServiceExtension()], ['Kinesis', new KinesisServiceExtension()], ]); diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/lambda.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/lambda.ts new file mode 100644 index 000000000000..133c87bdf725 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/lambda.ts @@ -0,0 +1,86 @@ +import type { Span } from '@sentry/core'; +import { debug, SPAN_KIND } from '@sentry/core'; +import { DEBUG_BUILD } from '../../../../debug-build'; +import { + ATTR_FAAS_EXECUTION, + ATTR_FAAS_INVOKED_NAME, + ATTR_FAAS_INVOKED_PROVIDER, + ATTR_FAAS_INVOKED_REGION, +} from '../constants'; +import type { NormalizedRequest, NormalizedResponse } from '../types'; +import { getPropagationHeaders } from './MessageAttributes'; +import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; + +const INVOKE_COMMAND = 'Invoke'; + +export class LambdaServiceExtension implements ServiceExtension { + public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { + const functionName = request.commandInput?.FunctionName; + + let spanAttributes: Record = {}; + let spanName: string | undefined; + + switch (request.commandName) { + case INVOKE_COMMAND: + spanAttributes = { + [ATTR_FAAS_INVOKED_NAME]: functionName, + [ATTR_FAAS_INVOKED_PROVIDER]: 'aws', + }; + if (request.region) { + spanAttributes[ATTR_FAAS_INVOKED_REGION] = request.region; + } + spanName = `${functionName} ${INVOKE_COMMAND}`; + break; + } + return { + spanAttributes, + spanKind: SPAN_KIND.CLIENT, + spanName, + }; + } + + public requestPostSpanHook(request: NormalizedRequest, span: Span): void { + if (request.commandName === INVOKE_COMMAND && request.commandInput) { + request.commandInput.ClientContext = injectLambdaPropagationContext(request.commandInput.ClientContext, span); + } + } + + public responseHook(response: NormalizedResponse, span: Span): void { + if (response.request.commandName === INVOKE_COMMAND) { + span.setAttribute(ATTR_FAAS_EXECUTION, response.requestId); + } + } +} + +function injectLambdaPropagationContext(clientContext: string | undefined, span: Span): string | undefined { + try { + const propagatedContext = getPropagationHeaders(span); + + const parsedClientContext = clientContext ? JSON.parse(Buffer.from(clientContext, 'base64').toString('utf8')) : {}; + + const updatedClientContext = { + ...parsedClientContext, + custom: { + ...parsedClientContext.custom, + ...propagatedContext, + }, + }; + + const encodedClientContext = Buffer.from(JSON.stringify(updatedClientContext)).toString('base64'); + + // The length of client context is capped at 3583 bytes of base64 encoded data + // (https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestSyntax) + if (encodedClientContext.length > 3583) { + DEBUG_BUILD && + debug.warn( + '[orchestrion:aws-sdk] cannot set trace propagation on lambda invoke parameters due to ClientContext length limitations.', + ); + return clientContext; + } + + return encodedClientContext; + } catch (e) { + DEBUG_BUILD && debug.log('[orchestrion:aws-sdk] failed to set trace propagation on lambda ClientContext', e); + return clientContext; + } +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sns.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sns.ts new file mode 100644 index 000000000000..4f50021cd1c2 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sns.ts @@ -0,0 +1,74 @@ +import type { Span, SpanKindValue } from '@sentry/core'; +import { SPAN_KIND } from '@sentry/core'; +import { MESSAGING_DESTINATION_NAME, MESSAGING_SYSTEM } from '@sentry/conventions/attributes'; +import { + ATTR_AWS_SNS_TOPIC_ARN, + ATTR_MESSAGING_DESTINATION, + ATTR_MESSAGING_DESTINATION_KIND, + MESSAGING_DESTINATION_KIND_VALUE_TOPIC, +} from '../constants'; +import type { NormalizedRequest, NormalizedResponse } from '../types'; +import { injectPropagationContext } from './MessageAttributes'; +import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; + +export class SnsServiceExtension implements ServiceExtension { + public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { + let spanKind: SpanKindValue = SPAN_KIND.CLIENT; + let spanName = `SNS ${request.commandName}`; + const spanAttributes: Record = { + [MESSAGING_SYSTEM]: 'aws.sns', + }; + + if (request.commandName === 'Publish') { + spanKind = SPAN_KIND.PRODUCER; + + spanAttributes[ATTR_MESSAGING_DESTINATION_KIND] = MESSAGING_DESTINATION_KIND_VALUE_TOPIC; + const { TopicArn, TargetArn, PhoneNumber } = request.commandInput; + const destinationName = extractDestinationName(TopicArn, TargetArn, PhoneNumber); + spanAttributes[ATTR_MESSAGING_DESTINATION] = destinationName; + spanAttributes[MESSAGING_DESTINATION_NAME] = TopicArn || TargetArn || PhoneNumber || 'unknown'; + + spanName = `${PhoneNumber ? 'phone_number' : destinationName} send`; + } + + const topicArn = request.commandInput?.TopicArn; + if (topicArn) { + spanAttributes[ATTR_AWS_SNS_TOPIC_ARN] = topicArn; + } + + return { + spanAttributes, + spanKind, + spanName, + }; + } + + public requestPostSpanHook(request: NormalizedRequest, span: Span): void { + if (request.commandName === 'Publish') { + const origMessageAttributes = request.commandInput.MessageAttributes ?? {}; + request.commandInput.MessageAttributes = injectPropagationContext(origMessageAttributes, span); + } + } + + public responseHook(response: NormalizedResponse, span: Span): void { + const topicArn = response.data?.TopicArn; + if (topicArn) { + span.setAttribute(ATTR_AWS_SNS_TOPIC_ARN, topicArn); + } + } +} + +function extractDestinationName(topicArn: string, targetArn: string, phoneNumber: string): string { + if (topicArn || targetArn) { + const arn = topicArn ?? targetArn; + try { + return arn.substring(arn.lastIndexOf(':') + 1); + } catch { + return arn; + } + } else if (phoneNumber) { + return phoneNumber; + } else { + return 'unknown'; + } +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sqs.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sqs.ts new file mode 100644 index 000000000000..27fb71dcb490 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sqs.ts @@ -0,0 +1,136 @@ +import type { Span, SpanKindValue } from '@sentry/core'; +import { propagationContextFromHeaders, SPAN_KIND } from '@sentry/core'; +import { + MESSAGING_BATCH_MESSAGE_COUNT, + MESSAGING_DESTINATION_NAME, + MESSAGING_MESSAGE_ID, + MESSAGING_OPERATION_TYPE, + MESSAGING_SYSTEM, + URL_FULL, +} from '@sentry/conventions/attributes'; +import type { SQS } from '../aws-sdk.types'; +import type { NormalizedRequest, NormalizedResponse } from '../types'; +import { + addPropagationFieldsToAttributeNames, + extractPropagationHeaders, + injectPropagationContext, +} from './MessageAttributes'; +import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; + +export class SqsServiceExtension implements ServiceExtension { + public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { + const queueUrl = extractQueueUrl(request.commandInput); + const queueName = extractQueueNameFromUrl(queueUrl); + let spanKind: SpanKindValue = SPAN_KIND.CLIENT; + let spanName: string | undefined; + + const spanAttributes: Record = { + [MESSAGING_SYSTEM]: 'aws_sqs', + [MESSAGING_DESTINATION_NAME]: queueName, + [URL_FULL]: queueUrl, + }; + + switch (request.commandName) { + case 'ReceiveMessage': + { + spanKind = SPAN_KIND.CONSUMER; + spanName = `${queueName} receive`; + spanAttributes[MESSAGING_OPERATION_TYPE] = 'receive'; + + request.commandInput.MessageAttributeNames = addPropagationFieldsToAttributeNames( + request.commandInput.MessageAttributeNames, + ); + } + break; + + case 'SendMessage': + case 'SendMessageBatch': + spanKind = SPAN_KIND.PRODUCER; + spanName = `${queueName} send`; + break; + } + + return { + spanAttributes, + spanKind, + spanName, + }; + } + + public requestPostSpanHook(request: NormalizedRequest, span: Span): void { + switch (request.commandName) { + case 'SendMessage': + { + const origMessageAttributes = request.commandInput.MessageAttributes ?? {}; + request.commandInput.MessageAttributes = injectPropagationContext(origMessageAttributes, span); + } + break; + + case 'SendMessageBatch': + { + const entries = request.commandInput?.Entries; + if (Array.isArray(entries)) { + entries.forEach((messageParams: { MessageAttributes: SQS.MessageBodyAttributeMap }) => { + messageParams.MessageAttributes = injectPropagationContext(messageParams.MessageAttributes ?? {}, span); + }); + } + } + break; + } + } + + public responseHook(response: NormalizedResponse, span: Span): void { + switch (response.request.commandName) { + case 'SendMessage': + span.setAttribute(MESSAGING_MESSAGE_ID, response?.data?.MessageId); + break; + + case 'SendMessageBatch': + break; + + case 'ReceiveMessage': { + const messages: SQS.Message[] = response?.data?.Messages || []; + + span.setAttribute(MESSAGING_BATCH_MESSAGE_COUNT, messages.length); + + for (const message of messages) { + const headers = extractPropagationHeaders(message); + if (!headers) { + continue; + } + + const { parentSpanId, traceId, sampled } = propagationContextFromHeaders( + headers.sentryTrace, + headers.baggage, + ); + if (traceId && parentSpanId) { + span.addLink({ + context: { + traceId, + spanId: parentSpanId, + traceFlags: sampled ? 1 : 0, + }, + attributes: { + [MESSAGING_MESSAGE_ID]: message.MessageId, + }, + }); + } + } + break; + } + } + } +} + +function extractQueueUrl(commandInput: Record): string { + return commandInput?.QueueUrl; +} + +function extractQueueNameFromUrl(queueUrl: string): string | undefined { + if (!queueUrl) return undefined; + + const segments = queueUrl.split('/'); + if (segments.length === 0) return undefined; + + return segments[segments.length - 1]; +} From 9cf25606c983c6b8ef426284efecd32bdbf556c8 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 10 Jul 2026 09:38:54 +0200 Subject: [PATCH 2/5] Remove as any cast and reuse CommandInput in messaging extensions --- .../tracing-channel/aws-sdk/services/MessageAttributes.ts | 4 +++- .../src/integrations/tracing-channel/aws-sdk/services/sqs.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/MessageAttributes.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/MessageAttributes.ts index fb040764d194..0387cdcb1f22 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/MessageAttributes.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/MessageAttributes.ts @@ -50,7 +50,9 @@ export function injectPropagationContext( if (Object.keys(attributes).length + headerKeys.length <= MAX_MESSAGE_ATTRIBUTES) { for (const key of headerKeys) { - (attributes as AwsSdkContextObject)[key] = { DataType: 'String', StringValue: headers[key] } as any; + // Index-assigning into the SQS/SNS map union needs one concrete map type; the written value + // shape is valid for both. + (attributes as SQS.MessageBodyAttributeMap)[key] = { DataType: 'String', StringValue: headers[key] }; } } else { DEBUG_BUILD && diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sqs.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sqs.ts index 27fb71dcb490..be2d65ed6da8 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sqs.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sqs.ts @@ -9,7 +9,7 @@ import { URL_FULL, } from '@sentry/conventions/attributes'; import type { SQS } from '../aws-sdk.types'; -import type { NormalizedRequest, NormalizedResponse } from '../types'; +import type { CommandInput, NormalizedRequest, NormalizedResponse } from '../types'; import { addPropagationFieldsToAttributeNames, extractPropagationHeaders, @@ -122,7 +122,7 @@ export class SqsServiceExtension implements ServiceExtension { } } -function extractQueueUrl(commandInput: Record): string { +function extractQueueUrl(commandInput: CommandInput): string { return commandInput?.QueueUrl; } From b9b6cf61937fa6574a79eddd64e4beb70a4d8779 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Sun, 12 Jul 2026 00:38:08 +0200 Subject: [PATCH 3/5] Use getTraceData and uniq from core for message propagation getTraceData({ span }) replaces the hand-rolled sentry-trace/baggage pair (matching the kafkajs/amqplib producer idiom and gaining its enabled/validity guards), callers pass the serialized headers in so batch sends compute them once, and addPropagationFieldsToAttributeNames uses core's uniq. --- .../aws-sdk/services/MessageAttributes.ts | 46 ++++++------------- .../aws-sdk/services/lambda.ts | 5 +- .../tracing-channel/aws-sdk/services/sns.ts | 4 +- .../tracing-channel/aws-sdk/services/sqs.ts | 14 ++++-- 4 files changed, 30 insertions(+), 39 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/MessageAttributes.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/MessageAttributes.ts index 0387cdcb1f22..80258914f788 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/MessageAttributes.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/MessageAttributes.ts @@ -1,10 +1,5 @@ -import type { Span } from '@sentry/core'; -import { - debug, - dynamicSamplingContextToSentryBaggageHeader, - getDynamicSamplingContextFromSpan, - spanToTraceHeader, -} from '@sentry/core'; +import type { SerializedTraceData } from '@sentry/core'; +import { debug, uniq } from '@sentry/core'; import { DEBUG_BUILD } from '../../../../debug-build'; import type { SNS, SQS } from '../aws-sdk.types'; @@ -23,36 +18,27 @@ export interface AwsSdkContextObject { }; } -/** Build the `sentry-trace`/`baggage` header pair carrying the span's trace context. */ -export function getPropagationHeaders(span: Span): Record { - const headers: Record = { - [SENTRY_TRACE_HEADER]: spanToTraceHeader(span), - }; - const baggage = dynamicSamplingContextToSentryBaggageHeader(getDynamicSamplingContextFromSpan(span)); - if (baggage) { - headers[BAGGAGE_HEADER] = baggage; - } - return headers; -} - /** - * Inject the span's trace-propagation headers into an SQS/SNS message-attribute map, so the consumer - * can continue the trace. Respects the SQS 10-attribute quota. Mirrors the OTel integration's - * `injectPropagationContext`, but writes Sentry's `sentry-trace`/`baggage` instead of W3C headers. + * Inject trace-propagation headers (from `getTraceData({ span })`) into an SQS/SNS message-attribute + * map, so the consumer can continue the trace. Respects the SQS 10-attribute quota. Mirrors the OTel + * integration's `injectPropagationContext`, but writes Sentry's `sentry-trace`/`baggage` instead of + * W3C headers. Callers pass the precomputed headers so batch sends serialize them only once. */ export function injectPropagationContext( attributesMap: SQS.MessageBodyAttributeMap | SNS.MessageAttributeMap | undefined, - span: Span, + traceData: SerializedTraceData, ): SQS.MessageBodyAttributeMap | SNS.MessageAttributeMap { const attributes = attributesMap ?? {}; - const headers = getPropagationHeaders(span); - const headerKeys = Object.keys(headers); + const headerKeys = Object.keys(traceData) as (keyof SerializedTraceData)[]; if (Object.keys(attributes).length + headerKeys.length <= MAX_MESSAGE_ATTRIBUTES) { for (const key of headerKeys) { - // Index-assigning into the SQS/SNS map union needs one concrete map type; the written value - // shape is valid for both. - (attributes as SQS.MessageBodyAttributeMap)[key] = { DataType: 'String', StringValue: headers[key] }; + const value = traceData[key]; + if (value) { + // Index-assigning into the SQS/SNS map union needs one concrete map type; the written value + // shape is valid for both. + (attributes as SQS.MessageBodyAttributeMap)[key] = { DataType: 'String', StringValue: value }; + } } } else { DEBUG_BUILD && @@ -79,7 +65,5 @@ export function extractPropagationHeaders( } export function addPropagationFieldsToAttributeNames(messageAttributeNames: string[] = []): string[] { - return messageAttributeNames.length - ? Array.from(new Set([...messageAttributeNames, ...PROPAGATION_FIELDS])) - : [...PROPAGATION_FIELDS]; + return uniq([...messageAttributeNames, ...PROPAGATION_FIELDS]); } diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/lambda.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/lambda.ts index 133c87bdf725..45862ee0da8d 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/lambda.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/lambda.ts @@ -1,5 +1,5 @@ import type { Span } from '@sentry/core'; -import { debug, SPAN_KIND } from '@sentry/core'; +import { debug, getTraceData, SPAN_KIND } from '@sentry/core'; import { DEBUG_BUILD } from '../../../../debug-build'; import { ATTR_FAAS_EXECUTION, @@ -8,7 +8,6 @@ import { ATTR_FAAS_INVOKED_REGION, } from '../constants'; import type { NormalizedRequest, NormalizedResponse } from '../types'; -import { getPropagationHeaders } from './MessageAttributes'; import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; const INVOKE_COMMAND = 'Invoke'; @@ -54,7 +53,7 @@ export class LambdaServiceExtension implements ServiceExtension { function injectLambdaPropagationContext(clientContext: string | undefined, span: Span): string | undefined { try { - const propagatedContext = getPropagationHeaders(span); + const propagatedContext = getTraceData({ span }); const parsedClientContext = clientContext ? JSON.parse(Buffer.from(clientContext, 'base64').toString('utf8')) : {}; diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sns.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sns.ts index 4f50021cd1c2..4a95192925a7 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sns.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sns.ts @@ -1,5 +1,5 @@ import type { Span, SpanKindValue } from '@sentry/core'; -import { SPAN_KIND } from '@sentry/core'; +import { getTraceData, SPAN_KIND } from '@sentry/core'; import { MESSAGING_DESTINATION_NAME, MESSAGING_SYSTEM } from '@sentry/conventions/attributes'; import { ATTR_AWS_SNS_TOPIC_ARN, @@ -46,7 +46,7 @@ export class SnsServiceExtension implements ServiceExtension { public requestPostSpanHook(request: NormalizedRequest, span: Span): void { if (request.commandName === 'Publish') { const origMessageAttributes = request.commandInput.MessageAttributes ?? {}; - request.commandInput.MessageAttributes = injectPropagationContext(origMessageAttributes, span); + request.commandInput.MessageAttributes = injectPropagationContext(origMessageAttributes, getTraceData({ span })); } } diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sqs.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sqs.ts index be2d65ed6da8..fce79dc38dad 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sqs.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sqs.ts @@ -1,5 +1,5 @@ import type { Span, SpanKindValue } from '@sentry/core'; -import { propagationContextFromHeaders, SPAN_KIND } from '@sentry/core'; +import { getTraceData, propagationContextFromHeaders, SPAN_KIND } from '@sentry/core'; import { MESSAGING_BATCH_MESSAGE_COUNT, MESSAGING_DESTINATION_NAME, @@ -62,7 +62,10 @@ export class SqsServiceExtension implements ServiceExtension { case 'SendMessage': { const origMessageAttributes = request.commandInput.MessageAttributes ?? {}; - request.commandInput.MessageAttributes = injectPropagationContext(origMessageAttributes, span); + request.commandInput.MessageAttributes = injectPropagationContext( + origMessageAttributes, + getTraceData({ span }), + ); } break; @@ -70,8 +73,13 @@ export class SqsServiceExtension implements ServiceExtension { { const entries = request.commandInput?.Entries; if (Array.isArray(entries)) { + // Serialized once; the headers are identical for every entry of the batch. + const traceData = getTraceData({ span }); entries.forEach((messageParams: { MessageAttributes: SQS.MessageBodyAttributeMap }) => { - messageParams.MessageAttributes = injectPropagationContext(messageParams.MessageAttributes ?? {}, span); + messageParams.MessageAttributes = injectPropagationContext( + messageParams.MessageAttributes ?? {}, + traceData, + ); }); } } From 9fe4309b25bf75746451a834ef663094d9d8473c Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 15 Jul 2026 10:51:13 +0200 Subject: [PATCH 4/5] Set Lambda faas.invoked_region from the backfilled region in the response hook --- .../tracing-channel/aws-sdk/services/lambda.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/lambda.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/lambda.ts index 45862ee0da8d..55866a825db6 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/lambda.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/lambda.ts @@ -25,9 +25,6 @@ export class LambdaServiceExtension implements ServiceExtension { [ATTR_FAAS_INVOKED_NAME]: functionName, [ATTR_FAAS_INVOKED_PROVIDER]: 'aws', }; - if (request.region) { - spanAttributes[ATTR_FAAS_INVOKED_REGION] = request.region; - } spanName = `${functionName} ${INVOKE_COMMAND}`; break; } @@ -47,6 +44,11 @@ export class LambdaServiceExtension implements ServiceExtension { public responseHook(response: NormalizedResponse, span: Span): void { if (response.request.commandName === INVOKE_COMMAND) { span.setAttribute(ATTR_FAAS_EXECUTION, response.requestId); + // Region resolves asynchronously after `requestPreSpanHook`, so it's backfilled onto the + // normalized request and read here (same timing as `cloud.region`). + if (response.request.region) { + span.setAttribute(ATTR_FAAS_INVOKED_REGION, response.request.region); + } } } } From a528fedf4ac6c4b54711e174ba25158518eaa544 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 15 Jul 2026 13:12:18 +0200 Subject: [PATCH 5/5] Mask SNS PhoneNumber in messaging.destination span attributes --- .../src/integrations/tracing-channel/aws-sdk/services/sns.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sns.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sns.ts index 4a95192925a7..075e42a9d095 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sns.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/sns.ts @@ -26,7 +26,7 @@ export class SnsServiceExtension implements ServiceExtension { const { TopicArn, TargetArn, PhoneNumber } = request.commandInput; const destinationName = extractDestinationName(TopicArn, TargetArn, PhoneNumber); spanAttributes[ATTR_MESSAGING_DESTINATION] = destinationName; - spanAttributes[MESSAGING_DESTINATION_NAME] = TopicArn || TargetArn || PhoneNumber || 'unknown'; + spanAttributes[MESSAGING_DESTINATION_NAME] = TopicArn || TargetArn || (PhoneNumber ? 'phone_number' : 'unknown'); spanName = `${PhoneNumber ? 'phone_number' : destinationName} send`; } @@ -67,7 +67,7 @@ function extractDestinationName(topicArn: string, targetArn: string, phoneNumber return arn; } } else if (phoneNumber) { - return phoneNumber; + return 'phone_number'; } else { return 'unknown'; }