From 4d5d90ccfd441d4b7bfd84bb32629de62b98436a Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Sat, 3 Oct 2026 13:05:18 +0200 Subject: [PATCH] test(node): port AWS integration tests to span streaming Co-Authored-By: GPT-6 --- .../aws-integration-streamed/instrument.mjs | 10 - .../aws-integration-streamed/scenario.mjs | 183 ----------- .../aws-integration-streamed/test.ts | 249 --------------- .../aws-integration/instrument.mjs | 1 - .../aws-serverless/aws-integration/test.ts | 286 ++++++++++-------- 5 files changed, 162 insertions(+), 567 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/instrument.mjs delete mode 100644 dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/scenario.mjs delete mode 100644 dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/instrument.mjs b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/instrument.mjs deleted file mode 100644 index abfa58bbff9f..000000000000 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/instrument.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import * as Sentry from '@sentry/aws-serverless'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - transport: loggingTransport, - traceLifecycle: 'stream', -}); diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/scenario.mjs b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/scenario.mjs deleted file mode 100644 index 624a10e9523b..000000000000 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/scenario.mjs +++ /dev/null @@ -1,183 +0,0 @@ -import * as Sentry from '@sentry/aws-serverless'; -import { createHash } from 'crypto'; -import { DynamoDBClient, PutItemCommand, QueryCommand } from '@aws-sdk/client-dynamodb'; -import { KinesisClient, PutRecordCommand } from '@aws-sdk/client-kinesis'; -import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'; -import { S3 } from '@aws-sdk/client-s3'; -import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; -import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn'; -import { PublishCommand, SNSClient } from '@aws-sdk/client-sns'; -import { ReceiveMessageCommand, SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs'; -// The Kinesis client defaults to an HTTP/2 request handler, which `nock` cannot intercept. -// Force the HTTP/1 handler so the request is mocked instead of hitting real AWS. -import { NodeHttpHandler } from '@smithy/node-http-handler'; -import nock from 'nock'; - -nock.disableNetConnect(); - -const region = 'us-east-1'; -const credentials = { accessKeyId: 'aws-test-key', secretAccessKey: 'aws-test-secret' }; - -async function s3() { - const s3Client = new S3({ region, credentials, maxAttempts: 1 }); - const host = `https://ot-demo-test.s3.${region}.amazonaws.com`; - - nock(host).put('/aws-ot-s3-test-object.txt?x-id=PutObject').reply(200, 'test'); - await s3Client.putObject({ Bucket: 'ot-demo-test', Key: 'aws-ot-s3-test-object.txt' }); - - nock(host).get('/aws-ot-s3-test-object.txt?x-id=GetObject').reply(200, 'contents'); - const getResult = await s3Client.getObject({ Bucket: 'ot-demo-test', Key: 'aws-ot-s3-test-object.txt' }); - await getResult.Body?.transformToString(); - - nock(host) - .get('/missing-object.txt?x-id=GetObject') - .reply( - 404, - 'NoSuchKeyThe specified key does not exist.', - { 'content-type': 'application/xml' }, - ); - try { - await s3Client.getObject({ Bucket: 'ot-demo-test', Key: 'missing-object.txt' }); - } catch { - // expected - } -} - -async function dynamodb() { - const client = new DynamoDBClient({ region, credentials, maxAttempts: 1 }); - - nock(`https://dynamodb.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({}), { 'content-type': 'application/x-amz-json-1.0' }); - await client.send(new PutItemCommand({ TableName: 'my-table', Item: { id: { S: 'some-id' } } })); - - nock(`https://dynamodb.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ Items: [{ id: { S: 'some-id' } }], Count: 1, ScannedCount: 1 }), { - 'content-type': 'application/x-amz-json-1.0', - }); - await client.send( - new QueryCommand({ - TableName: 'my-table', - KeyConditionExpression: 'id = :id', - ExpressionAttributeValues: { ':id': { S: 'some-id' } }, - }), - ); -} - -async function sqs() { - const client = new SQSClient({ region, credentials, maxAttempts: 1 }); - const queueUrl = `https://sqs.${region}.amazonaws.com/123456789012/my-queue`; - const messageBody = 'Hello from Sentry'; - const md5 = createHash('md5').update(messageBody).digest('hex'); - - nock(`https://sqs.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ MessageId: 'message-id-1', MD5OfMessageBody: md5 }), { - 'content-type': 'application/x-amz-json-1.0', - }); - await client.send(new SendMessageCommand({ QueueUrl: queueUrl, MessageBody: messageBody })); - - nock(`https://sqs.${region}.amazonaws.com`) - .post('/') - .reply( - 200, - JSON.stringify({ - Messages: [{ MessageId: 'message-id-2', Body: messageBody, MD5OfBody: md5, ReceiptHandle: 'handle' }], - }), - { 'content-type': 'application/x-amz-json-1.0' }, - ); - await client.send(new ReceiveMessageCommand({ QueueUrl: queueUrl })); -} - -async function sns() { - const client = new SNSClient({ region, credentials, maxAttempts: 1 }); - - nock(`https://sns.${region}.amazonaws.com`) - .post('/') - .reply( - 200, - 'message-id-1request-id-1', - { 'content-type': 'text/xml' }, - ); - await client.send(new PublishCommand({ TopicArn: 'arn:aws:sns:us-east-1:123456789012:my-topic', Message: 'Hello' })); - - // Publish to a platform endpoint, whose ARN ends in a per-device id. - nock(`https://sns.${region}.amazonaws.com`) - .post('/') - .reply( - 200, - 'message-id-2request-id-2', - { 'content-type': 'text/xml' }, - ); - await client.send( - new PublishCommand({ - TargetArn: 'arn:aws:sns:us-east-1:123456789012:endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', - Message: 'Hello', - }), - ); -} - -async function lambda() { - const client = new LambdaClient({ region, credentials, maxAttempts: 1 }); - - nock(`https://lambda.${region}.amazonaws.com`) - .post('/2015-03-31/functions/my-function/invocations') - .reply(200, JSON.stringify({ result: 'ok' }), { - 'content-type': 'application/json', - 'x-amzn-requestid': 'request-id-1', - }); - await client.send(new InvokeCommand({ FunctionName: 'my-function' })); -} - -async function kinesis() { - const client = new KinesisClient({ region, credentials, maxAttempts: 1, requestHandler: new NodeHttpHandler() }); - - nock(`https://kinesis.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ SequenceNumber: '1', ShardId: 'shardId-000000000000' }), { - 'content-type': 'application/x-amz-json-1.1', - }); - await client.send( - new PutRecordCommand({ StreamName: 'my-stream', Data: Buffer.from('data'), PartitionKey: 'partition-key' }), - ); -} - -async function secretsmanager() { - const client = new SecretsManagerClient({ region, credentials, maxAttempts: 1 }); - const secretArn = 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-abc'; - - nock(`https://secretsmanager.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ ARN: secretArn, Name: 'my-secret', SecretString: 'secret-value' }), { - 'content-type': 'application/x-amz-json-1.1', - }); - await client.send(new GetSecretValueCommand({ SecretId: secretArn })); -} - -async function stepfunctions() { - const client = new SFNClient({ region, credentials, maxAttempts: 1 }); - const stateMachineArn = 'arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine'; - - nock(`https://states.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ executionArn: `${stateMachineArn}:execution-1`, startDate: 1 }), { - 'content-type': 'application/x-amz-json-1.0', - }); - await client.send(new StartExecutionCommand({ stateMachineArn, input: '{}' })); -} - -async function run() { - await Sentry.startSpan({ name: 'Test Service Span' }, async () => { - await s3(); - await dynamodb(); - await sqs(); - await sns(); - await lambda(); - await kinesis(); - await secretsmanager(); - await stepfunctions(); - }); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts deleted file mode 100644 index b4974f86ef2e..000000000000 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts +++ /dev/null @@ -1,249 +0,0 @@ -import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { afterAll, describe, expect } from 'vitest'; -import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; - -// See the non-streamed `aws-integration` suite: only the origin differs between the OTel and -// orchestrion diagnostics-channel runs. -const ORIGIN = 'auto.aws.aws_sdk'; - -// The aws-sdk instrumentation creates spans by patching the underlying smithy middleware stack. The -// patch target differs between aws-sdk versions, so we run the exact same assertions against both: -// - the current aws-sdk (default, resolved from the workspace) which routes through `@smithy/core` >= 3.24.0 -// - aws-sdk 3.1041.0 pinned together with the pre-`@smithy/core` stack (`@smithy/middleware-stack`) -const LEGACY_AWS_SDK_DEPENDENCIES = { - '@aws-sdk/client-dynamodb': '3.1041.0', - '@aws-sdk/client-kinesis': '3.1041.0', - '@aws-sdk/client-lambda': '3.1041.0', - '@aws-sdk/client-s3': '3.1041.0', - '@aws-sdk/client-secrets-manager': '3.1041.0', - '@aws-sdk/client-sfn': '3.1041.0', - '@aws-sdk/client-sns': '3.1041.0', - '@aws-sdk/client-sqs': '3.1041.0', - // Pin the smithy layer to the pre-`@smithy/core` versions, otherwise the 3.1041.0 clients still - // resolve `@smithy/smithy-client` >= 4.13 (which routes through `@smithy/core` >= 3.24.0). - '@smithy/smithy-client': '4.12.13', - '@smithy/core': '3.23.17', - '@smithy/middleware-stack': '4.2.14', - '@smithy/node-http-handler': '4.7.8', -}; - -/** - * Asserts the transaction contains one span per instrumented aws-sdk service. Each service is checked - * with its own `expect` so a failure points at the specific service rather than the whole transaction. - */ -function assertAwsServiceSpans(spanCcontainer: SerializedStreamedSpanContainer): void { - const spans = spanCcontainer.items; - - const expectSpan = ( - label: string, - expected: Record, - find?: (item: SerializedStreamedSpanContainer['items'][number]) => boolean, - ): void => { - const matches = spans.filter(item => item.name === expected.name); - const span = find ? matches.find(find) : matches[0]; - expect(span, label).toMatchObject(expected); - }; - - const segmentSpan = spans.find(item => item.is_segment); - - expect(segmentSpan?.name).toBe('Test Service Span'); - - // S3 - PutObject (success) - expectSpan('S3.PutObject', { - name: 'S3.PutObject', - status: 'ok', - attributes: expect.objectContaining({ - 'sentry.origin': { value: ORIGIN, type: 'string' }, - 'sentry.op': { value: 'rpc', type: 'string' }, - 'rpc.system': { value: 'aws-api', type: 'string' }, - 'rpc.method': { value: 'PutObject', type: 'string' }, - 'rpc.service': { value: 'S3', type: 'string' }, - 'cloud.region': { value: 'us-east-1', type: 'string' }, - 'aws.s3.bucket': { value: 'ot-demo-test', type: 'string' }, - 'sentry.kind': { value: 'client', type: 'string' }, - }), - }); - - // S3 - GetObject (success) - expectSpan( - 'S3.GetObject (success)', - { - name: 'S3.GetObject', - status: 'ok', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'GetObject', type: 'string' }, - 'rpc.service': { value: 'S3', type: 'string' }, - 'aws.s3.bucket': { value: 'ot-demo-test', type: 'string' }, - }), - }, - // Two spans share the name `S3.GetObject`; disambiguate by HTTP status code. - item => item.attributes['http.response.status_code']?.value === 200, - ); - - // S3 - GetObject (errored, missing key) - expectSpan( - 'S3.GetObject (error)', - { - name: 'S3.GetObject', - status: 'error', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'GetObject', type: 'string' }, - 'rpc.service': { value: 'S3', type: 'string' }, - }), - }, - item => item.attributes['http.response.status_code']?.value === 404, - ); - - // DynamoDB - PutItem - expectSpan('DynamoDB.PutItem', { - name: 'DynamoDB.PutItem', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'PutItem', type: 'string' }, - 'rpc.service': { value: 'DynamoDB', type: 'string' }, - 'db.system.name': { value: 'dynamodb', type: 'string' }, - 'db.namespace': { value: 'my-table', type: 'string' }, - 'db.operation.name': { value: 'PutItem', type: 'string' }, - 'aws.dynamodb.table_names': { value: ['my-table'], type: 'array' }, - }), - }); - - // DynamoDB - Query - expectSpan('DynamoDB.Query', { - name: 'DynamoDB.Query', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'Query', type: 'string' }, - 'db.operation.name': { value: 'Query', type: 'string' }, - 'aws.dynamodb.count': { value: 1, type: 'integer' }, - 'aws.dynamodb.scanned_count': { value: 1, type: 'integer' }, - }), - }); - - // SQS - SendMessage (producer) - expectSpan('SQS SendMessage', { - name: 'send my-queue', - attributes: expect.objectContaining({ - 'sentry.op': { value: 'queue.publish', type: 'string' }, - 'rpc.method': { value: 'SendMessage', type: 'string' }, - 'rpc.service': { value: 'SQS', type: 'string' }, - 'messaging.system': { value: 'aws_sqs', type: 'string' }, - 'messaging.destination.name': { value: 'my-queue', type: 'string' }, - 'url.full': { value: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue', type: 'string' }, - 'messaging.message.id': { value: 'message-id-1', type: 'string' }, - 'sentry.kind': { value: 'producer', type: 'string' }, - }), - }); - - // SQS - ReceiveMessage (consumer) - expectSpan('SQS ReceiveMessage', { - name: 'receive my-queue', - attributes: expect.objectContaining({ - 'sentry.op': { value: 'queue.receive', type: 'string' }, - 'rpc.method': { value: 'ReceiveMessage', type: 'string' }, - 'messaging.system': { value: 'aws_sqs', type: 'string' }, - 'messaging.operation.type': { value: 'receive', type: 'string' }, - 'messaging.batch.message_count': { value: 1, type: 'integer' }, - 'sentry.kind': { value: 'consumer', type: 'string' }, - }), - }); - - // SNS - Publish (producer) - expectSpan('SNS Publish', { - name: 'send my-topic', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'Publish', type: 'string' }, - 'rpc.service': { value: 'SNS', type: 'string' }, - 'messaging.system': { value: 'aws.sns', type: 'string' }, - 'sentry.op': { value: 'queue.publish', type: 'string' }, - 'messaging.destination': { value: 'my-topic', type: 'string' }, - 'aws.sns.topic.arn': { value: 'arn:aws:sns:us-east-1:123456789012:my-topic', type: 'string' }, - 'sentry.kind': { value: 'producer', type: 'string' }, - }), - }); - - // The ARN suffix is a per-device id, so the streamed name drops the destination. - expectSpan('SNS Publish (platform endpoint)', { - name: 'send', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'Publish', type: 'string' }, - 'rpc.service': { value: 'SNS', type: 'string' }, - 'sentry.op': { value: 'queue.publish', type: 'string' }, - 'messaging.destination': { value: 'endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', type: 'string' }, - 'messaging.destination.name': { - value: 'arn:aws:sns:us-east-1:123456789012:endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', - type: 'string', - }, - }), - }); - - // Lambda - Invoke - expectSpan('Lambda Invoke', { - name: 'my-function Invoke', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'Invoke', type: 'string' }, - 'rpc.service': { value: 'Lambda', type: 'string' }, - 'faas.invoked_name': { value: 'my-function', type: 'string' }, - 'faas.invoked_provider': { value: 'aws', type: 'string' }, - 'faas.invocation_id': { value: 'request-id-1', type: 'string' }, - }), - }); - - // Kinesis - PutRecord - expectSpan('Kinesis.PutRecord', { - name: 'Kinesis.PutRecord', - status: 'ok', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'PutRecord', type: 'string' }, - 'rpc.service': { value: 'Kinesis', type: 'string' }, - 'aws.kinesis.stream.name': { value: 'my-stream', type: 'string' }, - }), - }); - - // SecretsManager - GetSecretValue - expectSpan('SecretsManager.GetSecretValue', { - name: 'SecretsManager.GetSecretValue', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'GetSecretValue', type: 'string' }, - 'rpc.service': { value: 'SecretsManager', type: 'string' }, - 'aws.secretsmanager.secret.arn': { - value: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-abc', - type: 'string', - }, - }), - }); - - // StepFunctions - StartExecution - expectSpan('StepFunctions.StartExecution', { - name: 'SFN.StartExecution', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'StartExecution', type: 'string' }, - 'rpc.service': { value: 'SFN', type: 'string' }, - 'aws.step_functions.state_machine.arn': { - value: 'arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine', - type: 'string', - }, - }), - }); -} - -describe('awsIntegration (streamed)', () => { - afterAll(() => { - cleanupChildProcesses(); - }); - - describe.each([ - { label: 'latest', additionalDependencies: undefined }, - { label: 'v3.1041.0 (@smithy/middleware-stack)', additionalDependencies: LEGACY_AWS_SDK_DEPENDENCIES }, - ])('aws-sdk $label', ({ additionalDependencies }) => { - createEsmAndCjsTests( - __dirname, - 'scenario.mjs', - 'instrument.mjs', - (createTestRunner, test) => { - test('auto-instruments aws-sdk service operations', { timeout: 90_000 }, async () => { - await createTestRunner().ignore('event').expect({ span: assertAwsServiceSpans }).start().completed(); - }); - }, - { additionalDependencies }, - ); - }); -}); diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/instrument.mjs b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/instrument.mjs index d6a9efde7bae..fe1c5c47983f 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/aws-serverless'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts index cac71f20ad6e..e12bf1ecaff2 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts @@ -1,4 +1,4 @@ -import type { TransactionEvent } from '@sentry/core'; +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -29,189 +29,216 @@ const LEGACY_AWS_SDK_DEPENDENCIES = { }; /** - * Asserts the transaction contains one span per instrumented aws-sdk service. Each service is checked - * with its own `expect` so a failure points at the specific service rather than the whole transaction. + * Asserts the collected spans include one span per instrumented aws-sdk service. Each service is checked + * with its own `expect` so a failure points at the specific service rather than the whole trace. */ -function assertAwsServiceSpans(transaction: TransactionEvent): void { - const spans = transaction.spans ?? []; - - const expectSpan = (label: string, expected: Record): void => { - expect(spans, `expected an aws-sdk span for "${label}"`).toContainEqual(expect.objectContaining(expected)); +function assertAwsServiceSpans(spans: SerializedStreamedSpanContainer['items']): void { + const expectSpan = ( + label: string, + expected: Record, + find?: (item: SerializedStreamedSpanContainer['items'][number]) => boolean, + ): void => { + const matches = spans.filter(item => item.name === expected.name); + const span = find ? matches.find(find) : matches[0]; + expect(span, label).toMatchObject(expected); }; - expect(transaction.transaction).toBe('Test Transaction'); + const segmentSpan = spans.find(item => item.is_segment); + + expect(segmentSpan?.name).toBe('Test Transaction'); // S3 - PutObject (success) expectSpan('S3.PutObject', { - description: 'S3.PutObject', - op: 'rpc', - origin: ORIGIN, + name: 'S3.PutObject', status: 'ok', - data: expect.objectContaining({ - 'sentry.origin': ORIGIN, - 'sentry.op': 'rpc', - 'rpc.system': 'aws-api', - 'rpc.method': 'PutObject', - 'rpc.service': 'S3', - 'cloud.region': 'us-east-1', - 'aws.s3.bucket': 'ot-demo-test', - 'sentry.kind': 'client', + attributes: expect.objectContaining({ + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'sentry.op': { value: 'rpc', type: 'string' }, + 'rpc.system': { value: 'aws-api', type: 'string' }, + 'rpc.method': { value: 'PutObject', type: 'string' }, + 'rpc.service': { value: 'S3', type: 'string' }, + 'cloud.region': { value: 'us-east-1', type: 'string' }, + 'aws.s3.bucket': { value: 'ot-demo-test', type: 'string' }, + 'sentry.kind': { value: 'client', type: 'string' }, }), }); // S3 - GetObject (success) - expectSpan('S3.GetObject (success)', { - description: 'S3.GetObject', - op: 'rpc', - origin: ORIGIN, - status: 'ok', - data: expect.objectContaining({ 'rpc.method': 'GetObject', 'rpc.service': 'S3', 'aws.s3.bucket': 'ot-demo-test' }), - }); + expectSpan( + 'S3.GetObject (success)', + { + name: 'S3.GetObject', + status: 'ok', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'GetObject', type: 'string' }, + 'rpc.service': { value: 'S3', type: 'string' }, + 'aws.s3.bucket': { value: 'ot-demo-test', type: 'string' }, + }), + }, + // Two spans share the name `S3.GetObject`; disambiguate by HTTP status code. + item => item.attributes['http.response.status_code']?.value === 200, + ); // S3 - GetObject (errored, missing key) - expectSpan('S3.GetObject (error)', { - description: 'S3.GetObject', - op: 'rpc', - origin: ORIGIN, - status: 'internal_error', - data: expect.objectContaining({ 'rpc.method': 'GetObject', 'rpc.service': 'S3' }), - }); + expectSpan( + 'S3.GetObject (error)', + { + name: 'S3.GetObject', + status: 'error', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'GetObject', type: 'string' }, + 'rpc.service': { value: 'S3', type: 'string' }, + }), + }, + item => item.attributes['http.response.status_code']?.value === 404, + ); // DynamoDB - PutItem expectSpan('DynamoDB.PutItem', { - description: 'DynamoDB.PutItem', - op: 'db', - origin: ORIGIN, - data: expect.objectContaining({ - 'sentry.op': 'db', - 'rpc.method': 'PutItem', - 'rpc.service': 'DynamoDB', - 'db.system.name': 'dynamodb', - 'db.namespace': 'my-table', - 'db.operation.name': 'PutItem', - 'aws.dynamodb.table_names': ['my-table'], + name: 'DynamoDB.PutItem', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'db', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'PutItem', type: 'string' }, + 'rpc.service': { value: 'DynamoDB', type: 'string' }, + 'db.system.name': { value: 'dynamodb', type: 'string' }, + 'db.namespace': { value: 'my-table', type: 'string' }, + 'db.operation.name': { value: 'PutItem', type: 'string' }, + 'aws.dynamodb.table_names': { value: ['my-table'], type: 'array' }, }), }); // DynamoDB - Query expectSpan('DynamoDB.Query', { - description: 'DynamoDB.Query', - op: 'db', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'Query', - 'db.operation.name': 'Query', - 'aws.dynamodb.count': 1, - 'aws.dynamodb.scanned_count': 1, + name: 'DynamoDB.Query', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'db', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'Query', type: 'string' }, + 'db.operation.name': { value: 'Query', type: 'string' }, + 'aws.dynamodb.count': { value: 1, type: 'integer' }, + 'aws.dynamodb.scanned_count': { value: 1, type: 'integer' }, }), }); // SQS - SendMessage (producer) expectSpan('SQS SendMessage', { - description: 'my-queue send', - op: 'queue.publish', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'SendMessage', - 'rpc.service': 'SQS', - 'messaging.system': 'aws_sqs', - 'messaging.destination.name': 'my-queue', - 'url.full': 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue', - 'messaging.message.id': 'message-id-1', - 'sentry.kind': 'producer', + name: 'send my-queue', + attributes: expect.objectContaining({ + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'sentry.op': { value: 'queue.publish', type: 'string' }, + 'rpc.method': { value: 'SendMessage', type: 'string' }, + 'rpc.service': { value: 'SQS', type: 'string' }, + 'messaging.system': { value: 'aws_sqs', type: 'string' }, + 'messaging.destination.name': { value: 'my-queue', type: 'string' }, + 'url.full': { value: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue', type: 'string' }, + 'messaging.message.id': { value: 'message-id-1', type: 'string' }, + 'sentry.kind': { value: 'producer', type: 'string' }, }), }); // SQS - ReceiveMessage (consumer) expectSpan('SQS ReceiveMessage', { - description: 'my-queue receive', - op: 'queue.receive', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'ReceiveMessage', - 'messaging.system': 'aws_sqs', - 'messaging.operation.type': 'receive', - 'messaging.batch.message_count': 1, - 'sentry.kind': 'consumer', + name: 'receive my-queue', + attributes: expect.objectContaining({ + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'sentry.op': { value: 'queue.receive', type: 'string' }, + 'rpc.method': { value: 'ReceiveMessage', type: 'string' }, + 'messaging.system': { value: 'aws_sqs', type: 'string' }, + 'messaging.operation.type': { value: 'receive', type: 'string' }, + 'messaging.batch.message_count': { value: 1, type: 'integer' }, + 'sentry.kind': { value: 'consumer', type: 'string' }, }), }); // SNS - Publish (producer) expectSpan('SNS Publish', { - description: 'my-topic send', - op: 'queue.publish', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'Publish', - 'rpc.service': 'SNS', - 'messaging.system': 'aws.sns', - 'messaging.destination': 'my-topic', - 'aws.sns.topic.arn': 'arn:aws:sns:us-east-1:123456789012:my-topic', - 'sentry.kind': 'producer', + name: 'send my-topic', + attributes: expect.objectContaining({ + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'Publish', type: 'string' }, + 'rpc.service': { value: 'SNS', type: 'string' }, + 'messaging.system': { value: 'aws.sns', type: 'string' }, + 'sentry.op': { value: 'queue.publish', type: 'string' }, + 'messaging.destination': { value: 'my-topic', type: 'string' }, + 'aws.sns.topic.arn': { value: 'arn:aws:sns:us-east-1:123456789012:my-topic', type: 'string' }, + 'sentry.kind': { value: 'producer', type: 'string' }, }), }); - // Without span streaming the name keeps the raw ARN suffix, including the per-device id. + // The ARN suffix is a per-device id, so the streamed name drops the destination. expectSpan('SNS Publish (platform endpoint)', { - description: 'endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3 send', - op: 'queue.publish', - data: expect.objectContaining({ - 'rpc.method': 'Publish', - 'rpc.service': 'SNS', - 'messaging.destination': 'endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', - 'messaging.destination.name': - 'arn:aws:sns:us-east-1:123456789012:endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', + name: 'send', + attributes: expect.objectContaining({ + 'rpc.method': { value: 'Publish', type: 'string' }, + 'rpc.service': { value: 'SNS', type: 'string' }, + 'sentry.op': { value: 'queue.publish', type: 'string' }, + 'messaging.destination': { value: 'endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', type: 'string' }, + 'messaging.destination.name': { + value: 'arn:aws:sns:us-east-1:123456789012:endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', + type: 'string', + }, }), }); // Lambda - Invoke expectSpan('Lambda Invoke', { - description: 'my-function Invoke', - op: 'rpc', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'Invoke', - 'rpc.service': 'Lambda', - 'faas.invoked_name': 'my-function', - 'faas.invoked_provider': 'aws', - 'faas.invocation_id': 'request-id-1', + name: 'my-function Invoke', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'Invoke', type: 'string' }, + 'rpc.service': { value: 'Lambda', type: 'string' }, + 'faas.invoked_name': { value: 'my-function', type: 'string' }, + 'faas.invoked_provider': { value: 'aws', type: 'string' }, + 'faas.invocation_id': { value: 'request-id-1', type: 'string' }, }), }); // Kinesis - PutRecord expectSpan('Kinesis.PutRecord', { - description: 'Kinesis.PutRecord', - op: 'rpc', - origin: ORIGIN, + name: 'Kinesis.PutRecord', status: 'ok', - data: expect.objectContaining({ - 'rpc.method': 'PutRecord', - 'rpc.service': 'Kinesis', - 'aws.kinesis.stream.name': 'my-stream', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'PutRecord', type: 'string' }, + 'rpc.service': { value: 'Kinesis', type: 'string' }, + 'aws.kinesis.stream.name': { value: 'my-stream', type: 'string' }, }), }); // SecretsManager - GetSecretValue expectSpan('SecretsManager.GetSecretValue', { - description: 'SecretsManager.GetSecretValue', - op: 'rpc', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'GetSecretValue', - 'rpc.service': 'SecretsManager', - 'aws.secretsmanager.secret.arn': 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-abc', + name: 'SecretsManager.GetSecretValue', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'GetSecretValue', type: 'string' }, + 'rpc.service': { value: 'SecretsManager', type: 'string' }, + 'aws.secretsmanager.secret.arn': { + value: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-abc', + type: 'string', + }, }), }); // StepFunctions - StartExecution expectSpan('StepFunctions.StartExecution', { - description: 'SFN.StartExecution', - op: 'rpc', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'StartExecution', - 'rpc.service': 'SFN', - 'aws.step_functions.state_machine.arn': 'arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine', + name: 'SFN.StartExecution', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'StartExecution', type: 'string' }, + 'rpc.service': { value: 'SFN', type: 'string' }, + 'aws.step_functions.state_machine.arn': { + value: 'arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine', + type: 'string', + }, }), }); } @@ -231,7 +258,18 @@ describe('awsIntegration', () => { 'instrument.mjs', (createTestRunner, test) => { test('auto-instruments aws-sdk service operations', { timeout: 90_000 }, async () => { - await createTestRunner().ignore('event').expect({ transaction: assertAwsServiceSpans }).start().completed(); + const spans: SerializedStreamedSpanContainer['items'] = []; + await createTestRunner() + .ignore('event') + .unordered() + .expect({ + span: container => { + spans.push(...container.items); + assertAwsServiceSpans(spans); + }, + }) + .start() + .completed(); }); }, { additionalDependencies },