Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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();
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { SerializedStreamedSpanContainer } from '@sentry/core';
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';

Expand Down Expand Up @@ -270,5 +271,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<string, SerializedStreamedSpan>();
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 },
);
});
});
7 changes: 6 additions & 1 deletion packages/server-utils/src/integrations/aws-sdk/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
Expand Down