From b1a3b32ecee6f16bf3f961ba586749eb9f49b5b6 Mon Sep 17 00:00:00 2001 From: lazerg Date: Thu, 29 Oct 2026 12:10:24 +0500 Subject: [PATCH 1/2] fix(server-utils): Require a parent span for aws-sdk DynamoDB operations Co-Authored-By: Claude Opus 5.5 --- .../aws-integration/scenario-no-parent.mjs | 35 +++++++++++++++ .../aws-serverless/aws-integration/test.ts | 44 ++++++++++++++++++- .../src/integrations/aws-sdk/index.ts | 7 ++- 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/scenario-no-parent.mjs diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/scenario-no-parent.mjs b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/scenario-no-parent.mjs new file mode 100644 index 000000000000..a5376dbe3f2a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/scenario-no-parent.mjs @@ -0,0 +1,35 @@ +import * as Sentry from '@sentry/aws-serverless'; +import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb'; +import nock from 'nock'; + +nock.disableNetConnect(); + +const region = 'us-east-1'; +const client = new DynamoDBClient({ + region, + credentials: { accessKeyId: 'aws-test-key', secretAccessKey: 'aws-test-secret' }, + maxAttempts: 1, +}); + +async function putItem(tableName) { + 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: tableName, Item: { id: { S: 'some-id' } } })); +} + +async function run() { + await putItem('before-parent'); + + const parent = Sentry.startInactiveSpan({ name: 'parent operation' }); + await Sentry.withActiveSpan(parent, () => putItem('inside-parent')); + + await putItem('after-parent'); + + // Flush every operation before the parent signals that span collection is complete. + await Sentry.flush(); + parent.end(); + await Sentry.flush(); +} + +run(); 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 f91142fc4e0b..1b38ae7c94a4 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 { SerializedStreamedSpanContainer } from '@sentry/core'; +import type { SerializedStreamedSpan, SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -270,5 +270,47 @@ describe('awsIntegration', () => { }, { additionalDependencies }, ); + + createEsmAndCjsTests( + __dirname, + 'scenario-no-parent.mjs', + 'instrument.mjs', + (createTestRunner, test) => { + test('only instruments DynamoDB operations with an active parent span', { timeout: 90_000 }, async () => { + const runner = createTestRunner().ignore('event'); + const dbSpans = new Map(); + const spansPromise = runner.collectStreamedSpans(spans => { + for (const span of spans) { + if (span.attributes['sentry.origin']?.value === DB_ORIGIN) { + dbSpans.set(span.span_id, span); + } + } + return spans.some(span => span.is_segment && span.name === 'parent operation'); + }); + + await runner.unordered().start().completed(); + + const spans = await spansPromise; + const parent = spans.find(span => span.is_segment && span.name === 'parent operation'); + expect(parent).toBeDefined(); + expect( + Array.from(dbSpans.values(), span => ({ + table: span.attributes['db.namespace']?.value, + parentSpanId: span.parent_span_id, + traceId: span.trace_id, + isSegment: span.is_segment, + })), + ).toEqual([ + { + table: 'inside-parent', + parentSpanId: parent?.span_id, + traceId: parent?.trace_id, + isSegment: false, + }, + ]); + }); + }, + { additionalDependencies }, + ); }); }); diff --git a/packages/server-utils/src/integrations/aws-sdk/index.ts b/packages/server-utils/src/integrations/aws-sdk/index.ts index 10951179dd04..dc316295b94a 100644 --- a/packages/server-utils/src/integrations/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/aws-sdk/index.ts @@ -1,6 +1,6 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn, Span } from '@sentry/core'; -import { defineIntegration, startInactiveSpan } from '@sentry/core'; +import { defineIntegration, getActiveSpan, startInactiveSpan } from '@sentry/core'; import { _AWS_REQUEST_ID as AWS_REQUEST_ID, AWS_REQUEST_EXTENDED_ID, @@ -98,6 +98,11 @@ function instrumentAwsSdk(servicesExtensions: ServicesExtensions): void { // constructor name (e.g. `S3Client` -> `S3`). `serviceId` is set for all AWS clients. removeSuffixFromStringIfExists(data.self?.constructor?.name || 'AWS', 'Client'); + // A DynamoDB span without a parent has no context; other services (e.g. SQS producers) can be root spans. + if (serviceName === 'DynamoDB' && !getActiveSpan()) { + return 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. // The default is assigned back onto the command (not kept detached) because service hooks From 60ba68c1657be983377efeba70a0bcdd747525ff Mon Sep 17 00:00:00 2001 From: lazerg Date: Thu, 29 Oct 2026 12:35:04 +0500 Subject: [PATCH 2/2] test(aws-serverless): Use conventions constants in DynamoDB parent span test Co-Authored-By: Claude Opus 5.5 --- .../suites/aws-serverless/aws-integration/test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 1b38ae7c94a4..afa960ec8284 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,3 +1,4 @@ +import { DB_NAMESPACE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { SerializedStreamedSpan, SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -281,7 +282,7 @@ describe('awsIntegration', () => { const dbSpans = new Map(); const spansPromise = runner.collectStreamedSpans(spans => { for (const span of spans) { - if (span.attributes['sentry.origin']?.value === DB_ORIGIN) { + if (span.attributes[SENTRY_ORIGIN]?.value === DB_ORIGIN) { dbSpans.set(span.span_id, span); } } @@ -295,7 +296,7 @@ describe('awsIntegration', () => { expect(parent).toBeDefined(); expect( Array.from(dbSpans.values(), span => ({ - table: span.attributes['db.namespace']?.value, + table: span.attributes[DB_NAMESPACE]?.value, parentSpanId: span.parent_span_id, traceId: span.trace_id, isSegment: span.is_segment,