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 4e7593d5f1d2..9ec240793f12 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 @@ -15,3 +15,34 @@ 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'; export const AWS_REQUEST_EXTENDED_ID = 'aws.request.extended_id'; +export const AWS_S3_BUCKET = 'aws.s3.bucket'; +export const AWS_KINESIS_STREAM_NAME = 'aws.kinesis.stream.name'; + +// DynamoDB +export const ATTR_AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS = 'aws.dynamodb.attribute_definitions'; +export const ATTR_AWS_DYNAMODB_CONSISTENT_READ = 'aws.dynamodb.consistent_read'; +export const ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY = 'aws.dynamodb.consumed_capacity'; +export const ATTR_AWS_DYNAMODB_COUNT = 'aws.dynamodb.count'; +export const ATTR_AWS_DYNAMODB_EXCLUSIVE_START_TABLE = 'aws.dynamodb.exclusive_start_table'; +export const ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES = 'aws.dynamodb.global_secondary_indexes'; +export const ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES = 'aws.dynamodb.global_secondary_index_updates'; +export const ATTR_AWS_DYNAMODB_INDEX_NAME = 'aws.dynamodb.index_name'; +export const ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS = 'aws.dynamodb.item_collection_metrics'; +export const ATTR_AWS_DYNAMODB_LIMIT = 'aws.dynamodb.limit'; +export const ATTR_AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES = 'aws.dynamodb.local_secondary_indexes'; +export const ATTR_AWS_DYNAMODB_PROJECTION = 'aws.dynamodb.projection'; +export const ATTR_AWS_DYNAMODB_PROVISIONED_READ_CAPACITY = 'aws.dynamodb.provisioned_read_capacity'; +export const ATTR_AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY = 'aws.dynamodb.provisioned_write_capacity'; +export const ATTR_AWS_DYNAMODB_SCANNED_COUNT = 'aws.dynamodb.scanned_count'; +export const ATTR_AWS_DYNAMODB_SCAN_FORWARD = 'aws.dynamodb.scan_forward'; +export const ATTR_AWS_DYNAMODB_SEGMENT = 'aws.dynamodb.segment'; +export const ATTR_AWS_DYNAMODB_SELECT = 'aws.dynamodb.select'; +export const ATTR_AWS_DYNAMODB_TABLE_COUNT = 'aws.dynamodb.table_count'; +export const ATTR_AWS_DYNAMODB_TABLE_NAMES = 'aws.dynamodb.table_names'; +export const ATTR_AWS_DYNAMODB_TOTAL_SEGMENTS = 'aws.dynamodb.total_segments'; +export const DB_SYSTEM_VALUE_DYNAMODB = 'dynamodb'; + +// SecretsManager / StepFunctions +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'; 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 25959f753eb2..315faa17b04b 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 @@ -44,16 +44,6 @@ interface AwsV3Command { 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; - } -} - // `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 { @@ -83,8 +73,10 @@ const _awsChannelIntegration = (() => { return; } - const getSpan = (data: AwsSendChannelContext): Span | undefined => - safe(() => { + // Everything in here (and in `deferSpanEnd` below) runs inside the tracingChannel machinery + // wrapping the user's `send` call, so a throw must never escape: it would break the AWS call. + const getSpan = (data: AwsSendChannelContext): Span | undefined => { + try { const command = data.arguments[0] as AwsV3Command | undefined; const commandName = command?.constructor?.name; if (!command || !commandName) { @@ -126,7 +118,7 @@ const _awsChannelIntegration = (() => { // 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 + // throw bubbling into the enclosing catch would discard it without ending it (a leaked // open span). let regionResult: string | Promise | undefined; try { @@ -151,11 +143,20 @@ const _awsChannelIntegration = (() => { 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. - safe(() => servicesExtensions.requestPostSpanHook(normalizedRequest, span)); + // `send` proceeds, so the mutated `commandInput` is used to build the request. Guarded + // separately so a throw can't discard the already-started span via the outer catch. + try { + servicesExtensions.requestPostSpanHook(normalizedRequest, span); + } catch (error) { + DEBUG_BUILD && debug.warn('[orchestrion:aws-sdk] error in request post-span hook', error); + } return span; - }); + } catch (error) { + DEBUG_BUILD && debug.warn('[orchestrion:aws-sdk] error building span', error); + return undefined; + } + }; const opts: TracingChannelLifeCycleOptions = { deferSpanEnd({ span, data, end }) { @@ -168,8 +169,9 @@ 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(() => { + // `ResponseMetadata` shape (`any`-valued, see `setMetadataAttributes`). Guarded so an + // enrichment throw can't escape into the user's `send` (see `getSpan`). + try { if (failed) { const err = data.error as | { $metadata?: Record; RequestId?: string; extendedRequestId?: string } @@ -183,19 +185,20 @@ const _awsChannelIntegration = (() => { httpStatusCode: errMetadata?.httpStatusCode, extendedRequestId: err?.extendedRequestId ?? errMetadata?.extendedRequestId, }); - return; - } - - const output = data.result as { $metadata?: Record } | undefined; - setMetadataAttributes(span, output?.$metadata); + } else { + 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); - }); + const normalizedResponse: NormalizedResponse = { + data: output, + request: normalizedRequest, + requestId: output?.$metadata?.requestId, + }; + servicesExtensions.responseHook(normalizedResponse, span); + } + } catch (error) { + DEBUG_BUILD && debug.warn('[orchestrion:aws-sdk] error enriching span', error); + } // 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. 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 2cc308150307..fa67d828c957 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 @@ -1,6 +1,11 @@ import type { Span } from '@sentry/core'; import type { NormalizedRequest, NormalizedResponse, RequestMetadata } from '../types'; +import { DynamodbServiceExtension } from './dynamodb'; +import { KinesisServiceExtension } from './kinesis'; +import { S3ServiceExtension } from './s3'; +import { SecretsManagerServiceExtension } from './secretsmanager'; import type { ServiceExtension } from './ServiceExtension'; +import { StepFunctionsServiceExtension } from './stepfunctions'; export class ServicesExtensions implements ServiceExtension { private _services: Map; @@ -9,6 +14,11 @@ export class ServicesExtensions implements ServiceExtension { // 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(); + this._services.set('SecretsManager', new SecretsManagerServiceExtension()); + this._services.set('SFN', new StepFunctionsServiceExtension()); + this._services.set('DynamoDB', new DynamodbServiceExtension()); + this._services.set('S3', new S3ServiceExtension()); + this._services.set('Kinesis', new KinesisServiceExtension()); } public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/dynamodb.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/dynamodb.ts new file mode 100644 index 000000000000..d74d47fc98a2 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/dynamodb.ts @@ -0,0 +1,186 @@ +import type { Span } from '@sentry/core'; +import { SPAN_KIND } from '@sentry/core'; +import { DB_NAME, DB_OPERATION, DB_SYSTEM } from '@sentry/conventions/attributes'; +import { + ATTR_AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS, + ATTR_AWS_DYNAMODB_CONSISTENT_READ, + ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY, + ATTR_AWS_DYNAMODB_COUNT, + ATTR_AWS_DYNAMODB_EXCLUSIVE_START_TABLE, + ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES, + ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES, + ATTR_AWS_DYNAMODB_INDEX_NAME, + ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS, + ATTR_AWS_DYNAMODB_LIMIT, + ATTR_AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES, + ATTR_AWS_DYNAMODB_PROJECTION, + ATTR_AWS_DYNAMODB_PROVISIONED_READ_CAPACITY, + ATTR_AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY, + ATTR_AWS_DYNAMODB_SCAN_FORWARD, + ATTR_AWS_DYNAMODB_SCANNED_COUNT, + ATTR_AWS_DYNAMODB_SEGMENT, + ATTR_AWS_DYNAMODB_SELECT, + ATTR_AWS_DYNAMODB_TABLE_COUNT, + ATTR_AWS_DYNAMODB_TABLE_NAMES, + ATTR_AWS_DYNAMODB_TOTAL_SEGMENTS, + DB_SYSTEM_VALUE_DYNAMODB, +} from '../constants'; +import type { NormalizedRequest, NormalizedResponse } from '../types'; +import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; + +function toArray(values: T | T[]): T[] { + return Array.isArray(values) ? values : [values]; +} + +export class DynamodbServiceExtension implements ServiceExtension { + public requestPreSpanHook(normalizedRequest: NormalizedRequest): RequestMetadata { + const operation = normalizedRequest.commandName; + const tableName = normalizedRequest.commandInput?.TableName; + + const spanAttributes: Record = {}; + + /* oxlint-disable typescript/no-deprecated -- old-semconv db.* attributes, matched to the OTel aws-sdk integration */ + spanAttributes[DB_SYSTEM] = DB_SYSTEM_VALUE_DYNAMODB; + spanAttributes[DB_NAME] = tableName; + spanAttributes[DB_OPERATION] = operation; + /* oxlint-enable typescript/no-deprecated */ + + // `RequestItems` is undefined when no table names are returned; its keys are the table names. + if (normalizedRequest.commandInput?.TableName) { + // Necessary for commands with only 1 table name (e.g. CreateTable). Attribute is `TableName`, not keys of `RequestItems`. + spanAttributes[ATTR_AWS_DYNAMODB_TABLE_NAMES] = [normalizedRequest.commandInput.TableName]; + } else if (normalizedRequest.commandInput?.RequestItems) { + spanAttributes[ATTR_AWS_DYNAMODB_TABLE_NAMES] = Object.keys(normalizedRequest.commandInput.RequestItems); + } + + if (operation === 'CreateTable' || operation === 'UpdateTable') { + // only check for ProvisionedThroughput since ReadCapacityUnits and WriteCapacityUnits are required attributes + if (normalizedRequest.commandInput?.ProvisionedThroughput) { + spanAttributes[ATTR_AWS_DYNAMODB_PROVISIONED_READ_CAPACITY] = + normalizedRequest.commandInput.ProvisionedThroughput.ReadCapacityUnits; + spanAttributes[ATTR_AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY] = + normalizedRequest.commandInput.ProvisionedThroughput.WriteCapacityUnits; + } + } + + if (operation === 'GetItem' || operation === 'Scan' || operation === 'Query') { + if (normalizedRequest.commandInput?.ConsistentRead) { + spanAttributes[ATTR_AWS_DYNAMODB_CONSISTENT_READ] = normalizedRequest.commandInput.ConsistentRead; + } + } + + if (operation === 'Query' || operation === 'Scan') { + if (normalizedRequest.commandInput?.ProjectionExpression) { + spanAttributes[ATTR_AWS_DYNAMODB_PROJECTION] = normalizedRequest.commandInput.ProjectionExpression; + } + } + + if (operation === 'CreateTable') { + if (normalizedRequest.commandInput?.GlobalSecondaryIndexes) { + spanAttributes[ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES] = toArray( + normalizedRequest.commandInput.GlobalSecondaryIndexes, + ).map((x: unknown) => JSON.stringify(x)); + } + + if (normalizedRequest.commandInput?.LocalSecondaryIndexes) { + spanAttributes[ATTR_AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES] = toArray( + normalizedRequest.commandInput.LocalSecondaryIndexes, + ).map((x: unknown) => JSON.stringify(x)); + } + } + + if (operation === 'ListTables' || operation === 'Query' || operation === 'Scan') { + if (normalizedRequest.commandInput?.Limit) { + spanAttributes[ATTR_AWS_DYNAMODB_LIMIT] = normalizedRequest.commandInput.Limit; + } + } + + if (operation === 'ListTables') { + if (normalizedRequest.commandInput?.ExclusiveStartTableName) { + spanAttributes[ATTR_AWS_DYNAMODB_EXCLUSIVE_START_TABLE] = + normalizedRequest.commandInput.ExclusiveStartTableName; + } + } + + if (operation === 'Query') { + if (normalizedRequest.commandInput?.ScanIndexForward) { + spanAttributes[ATTR_AWS_DYNAMODB_SCAN_FORWARD] = normalizedRequest.commandInput.ScanIndexForward; + } + + if (normalizedRequest.commandInput?.IndexName) { + spanAttributes[ATTR_AWS_DYNAMODB_INDEX_NAME] = normalizedRequest.commandInput.IndexName; + } + + if (normalizedRequest.commandInput?.Select) { + spanAttributes[ATTR_AWS_DYNAMODB_SELECT] = normalizedRequest.commandInput.Select; + } + } + + if (operation === 'Scan') { + if (normalizedRequest.commandInput?.Segment) { + spanAttributes[ATTR_AWS_DYNAMODB_SEGMENT] = normalizedRequest.commandInput?.Segment; + } + + if (normalizedRequest.commandInput?.TotalSegments) { + spanAttributes[ATTR_AWS_DYNAMODB_TOTAL_SEGMENTS] = normalizedRequest.commandInput?.TotalSegments; + } + + if (normalizedRequest.commandInput?.IndexName) { + spanAttributes[ATTR_AWS_DYNAMODB_INDEX_NAME] = normalizedRequest.commandInput.IndexName; + } + + if (normalizedRequest.commandInput?.Select) { + spanAttributes[ATTR_AWS_DYNAMODB_SELECT] = normalizedRequest.commandInput.Select; + } + } + + if (operation === 'UpdateTable') { + if (normalizedRequest.commandInput?.AttributeDefinitions) { + spanAttributes[ATTR_AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS] = toArray( + normalizedRequest.commandInput.AttributeDefinitions, + ).map((x: unknown) => JSON.stringify(x)); + } + + if (normalizedRequest.commandInput?.GlobalSecondaryIndexUpdates) { + spanAttributes[ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES] = toArray( + normalizedRequest.commandInput.GlobalSecondaryIndexUpdates, + ).map((x: unknown) => JSON.stringify(x)); + } + } + + return { + spanAttributes, + spanKind: SPAN_KIND.CLIENT, + // Matches what the exporter infers from `db.system` for the OTel DynamoDB spans. + spanOp: 'db', + }; + } + + public responseHook(response: NormalizedResponse, span: Span): void { + if (response.data?.ConsumedCapacity) { + span.setAttribute( + ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY, + toArray(response.data.ConsumedCapacity).map((x: unknown) => JSON.stringify(x)), + ); + } + + if (response.data?.ItemCollectionMetrics) { + span.setAttribute( + ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS, + toArray(response.data.ItemCollectionMetrics).map((x: unknown) => JSON.stringify(x)), + ); + } + + if (response.data?.TableNames) { + span.setAttribute(ATTR_AWS_DYNAMODB_TABLE_COUNT, response.data?.TableNames.length); + } + + if (response.data?.Count) { + span.setAttribute(ATTR_AWS_DYNAMODB_COUNT, response.data?.Count); + } + + if (response.data?.ScannedCount) { + span.setAttribute(ATTR_AWS_DYNAMODB_SCANNED_COUNT, response.data?.ScannedCount); + } + } +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/kinesis.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/kinesis.ts new file mode 100644 index 000000000000..aee76d924bdb --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/kinesis.ts @@ -0,0 +1,20 @@ +import { SPAN_KIND } from '@sentry/core'; +import { AWS_KINESIS_STREAM_NAME } from '../constants'; +import type { NormalizedRequest } from '../types'; +import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; + +export class KinesisServiceExtension implements ServiceExtension { + public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { + const streamName = request.commandInput?.StreamName; + const spanAttributes: Record = {}; + + if (streamName) { + spanAttributes[AWS_KINESIS_STREAM_NAME] = streamName; + } + + return { + spanAttributes, + spanKind: SPAN_KIND.CLIENT, + }; + } +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/s3.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/s3.ts new file mode 100644 index 000000000000..1719c8e77d62 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/s3.ts @@ -0,0 +1,20 @@ +import { SPAN_KIND } from '@sentry/core'; +import { AWS_S3_BUCKET } from '../constants'; +import type { NormalizedRequest } from '../types'; +import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; + +export class S3ServiceExtension implements ServiceExtension { + public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { + const bucketName = request.commandInput?.Bucket; + const spanAttributes: Record = {}; + + if (bucketName) { + spanAttributes[AWS_S3_BUCKET] = bucketName; + } + + return { + spanAttributes, + spanKind: SPAN_KIND.CLIENT, + }; + } +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/secretsmanager.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/secretsmanager.ts new file mode 100644 index 000000000000..e10378815ffe --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/secretsmanager.ts @@ -0,0 +1,27 @@ +import type { Span } from '@sentry/core'; +import { SPAN_KIND } from '@sentry/core'; +import { ATTR_AWS_SECRETSMANAGER_SECRET_ARN } from '../constants'; +import type { NormalizedRequest, NormalizedResponse } from '../types'; +import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; + +export class SecretsManagerServiceExtension implements ServiceExtension { + public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { + const secretId = request.commandInput?.SecretId; + const spanAttributes: Record = {}; + if (typeof secretId === 'string' && secretId.startsWith('arn:aws:secretsmanager:')) { + spanAttributes[ATTR_AWS_SECRETSMANAGER_SECRET_ARN] = secretId; + } + + return { + spanAttributes, + spanKind: SPAN_KIND.CLIENT, + }; + } + + public responseHook(response: NormalizedResponse, span: Span): void { + const secretArn = response.data?.ARN; + if (secretArn) { + span.setAttribute(ATTR_AWS_SECRETSMANAGER_SECRET_ARN, secretArn); + } + } +} diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/stepfunctions.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/stepfunctions.ts new file mode 100644 index 000000000000..b08f452288b7 --- /dev/null +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/stepfunctions.ts @@ -0,0 +1,25 @@ +import { SPAN_KIND } from '@sentry/core'; +import { ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN, ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN } from '../constants'; +import type { NormalizedRequest } from '../types'; +import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; + +export class StepFunctionsServiceExtension implements ServiceExtension { + public requestPreSpanHook(request: NormalizedRequest): RequestMetadata { + const stateMachineArn = request.commandInput?.stateMachineArn; + const activityArn = request.commandInput?.activityArn; + const spanAttributes: Record = {}; + + if (stateMachineArn) { + spanAttributes[ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN] = stateMachineArn; + } + + if (activityArn) { + spanAttributes[ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN] = activityArn; + } + + return { + spanAttributes, + spanKind: SPAN_KIND.CLIENT, + }; + } +}