Skip to content

Commit cb232a7

Browse files
committed
fix(aws-serverless): Only start root span in Sentry wrapper if Otel didn't wrap handler
1 parent a91f2e6 commit cb232a7

File tree

5 files changed

+78
-17
lines changed

5 files changed

+78
-17
lines changed

dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/lambda-function.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const Sentry = require('@sentry/aws-serverless');
33
const http = require('http');
44

55
async function handle() {
6-
await Sentry.startSpan({ name: 'aws-lambda-layer-test-txn', op: 'test' }, async () => {
6+
await Sentry.startSpan({ name: 'manual-span', op: 'test' }, async () => {
77
await new Promise(resolve => {
88
http.get('http://example.com', res => {
99
res.on('data', d => {
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
const { handle } = require('./lambda-function');
2-
handle();
2+
const event = {};
3+
const context = {
4+
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
5+
functionName: 'my-lambda',
6+
};
7+
handle(event, context);

dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ child_process.execSync('node ./src/run-lambda.js', {
44
stdio: 'inherit',
55
env: {
66
...process.env,
7-
LAMBDA_TASK_ROOT: '/var/task',
7+
// On AWS, LAMBDA_TASK_ROOT is usually /var/task but for testing, we set it to the CWD to correctly apply our handler
8+
LAMBDA_TASK_ROOT: process.cwd(),
89
_HANDLER: 'src/lambda-function.handle',
910

1011
NODE_OPTIONS: '--require @sentry/aws-serverless/dist/awslambda-auto',

dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
44

55
test('Lambda layer SDK bundle sends events', async ({ request }) => {
66
const transactionEventPromise = waitForTransaction('aws-serverless-lambda-layer-cjs', transactionEvent => {
7-
return transactionEvent?.transaction === 'aws-lambda-layer-test-txn';
7+
return transactionEvent?.transaction === 'my-lambda';
88
});
99

1010
// Waiting for 1s here because attaching the listener for events in `waitForTransaction` is not synchronous
@@ -24,17 +24,46 @@ test('Lambda layer SDK bundle sends events', async ({ request }) => {
2424
const transactionEvent = await transactionEventPromise;
2525

2626
// shows the SDK sent a transaction
27-
expect(transactionEvent.transaction).toEqual('aws-lambda-layer-test-txn');
27+
expect(transactionEvent.transaction).toEqual('my-lambda'); // name should be the function name
28+
expect(transactionEvent.contexts?.trace).toEqual({
29+
data: {
30+
'sentry.sample_rate': 1,
31+
'sentry.source': 'custom',
32+
'sentry.origin': 'auto.otel.aws-lambda',
33+
'cloud.account.id': '123453789012',
34+
'faas.id': 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
35+
'otel.kind': 'SERVER',
36+
},
37+
origin: 'auto.otel.aws-lambda',
38+
span_id: expect.any(String),
39+
status: 'ok',
40+
trace_id: expect.any(String),
41+
});
42+
43+
expect(transactionEvent.spans).toHaveLength(2);
2844

2945
// shows that the Otel Http instrumentation is working
30-
expect(transactionEvent.spans).toHaveLength(1);
31-
expect(transactionEvent.spans![0]).toMatchObject({
32-
data: expect.objectContaining({
33-
'sentry.op': 'http.client',
34-
'sentry.origin': 'auto.http.otel.http',
35-
url: 'http://example.com/',
46+
expect(transactionEvent.spans).toContainEqual(
47+
expect.objectContaining({
48+
data: expect.objectContaining({
49+
'sentry.op': 'http.client',
50+
'sentry.origin': 'auto.http.otel.http',
51+
url: 'http://example.com/',
52+
}),
53+
description: 'GET http://example.com/',
54+
op: 'http.client',
3655
}),
37-
description: 'GET http://example.com/',
38-
op: 'http.client',
39-
});
56+
);
57+
58+
// shows that the manual span creation is working
59+
expect(transactionEvent.spans).toContainEqual(
60+
expect.objectContaining({
61+
data: expect.objectContaining({
62+
'sentry.op': 'test',
63+
'sentry.origin': 'manual',
64+
}),
65+
description: 'manual-span',
66+
op: 'test',
67+
}),
68+
);
4069
});

packages/aws-serverless/src/sdk.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ import { isString, logger } from '@sentry/utils';
2020
import type { Context, Handler } from 'aws-lambda';
2121
import { performance } from 'perf_hooks';
2222

23-
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
23+
import {
24+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
25+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
26+
getActiveSpan,
27+
spanToJSON,
28+
} from '@sentry/core';
2429

2530
import { DEBUG_BUILD } from './debug-build';
2631
import { awsIntegration } from './integration/aws';
@@ -320,15 +325,20 @@ export function wrapHandler<TEvent, TResult>(
320325
throw e;
321326
} finally {
322327
clearTimeout(timeoutWarningTimer);
323-
span?.end();
328+
if (span && span.isRecording()) {
329+
span.end();
330+
}
324331
await flush(options.flushTimeout).catch(e => {
325332
DEBUG_BUILD && logger.error(e);
326333
});
327334
}
328335
return rv;
329336
}
330337

331-
if (options.startTrace) {
338+
// Only start a trace and root span if the handler is not already wrapped by Otel instrumentation
339+
// Otherwise, we create two root spans (one from otel, one from our wrapper).
340+
// If Otel instrumentation didn't work or was filtered by users, we still want to trace the handler.
341+
if (options.startTrace && !isWrappedByOtel(handler)) {
332342
const eventWithHeaders = event as { headers?: { [key: string]: string } };
333343

334344
const sentryTrace =
@@ -361,3 +371,19 @@ export function wrapHandler<TEvent, TResult>(
361371
});
362372
};
363373
}
374+
375+
/**
376+
* Checks if Otel's AWSLambda instrumentation successfully wrapped the handler.
377+
* Check taken from @opentelemetry/core
378+
*/
379+
function isWrappedByOtel(
380+
// eslint-disable-next-line @typescript-eslint/ban-types
381+
handler: Function & { __original?: unknown; __unwrap?: unknown; __wrapped?: boolean },
382+
): boolean {
383+
return (
384+
typeof handler === 'function' &&
385+
typeof handler.__original === 'function' &&
386+
typeof handler.__unwrap === 'function' &&
387+
handler.__wrapped === true
388+
);
389+
}

0 commit comments

Comments
 (0)