Skip to content

Commit eadfd61

Browse files
mydeaLms24
andauthored
ref(serverless): Use startSpanManual() instead of startTransaction() (#9970)
Also refactor it to use `continueTrace`. Note that this already uses the "new" way of `startSpanManual` which ignores the `finish` function. --------- Co-authored-by: Lukas Stracke <[email protected]>
1 parent 2c0b6d3 commit eadfd61

File tree

7 files changed

+287
-414
lines changed

7 files changed

+287
-414
lines changed

packages/serverless/src/awslambda.ts

+61-46
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@ import { hostname } from 'os';
33
import { basename, resolve } from 'path';
44
import { types } from 'util';
55
/* eslint-disable max-lines */
6-
import type { Scope } from '@sentry/node';
7-
import * as Sentry from '@sentry/node';
8-
import { captureException, captureMessage, flush, getCurrentHub, withScope } from '@sentry/node';
9-
import type { Integration, SdkMetadata } from '@sentry/types';
10-
import { isString, logger, tracingContextFromHeaders } from '@sentry/utils';
6+
import type { NodeOptions, Scope } from '@sentry/node';
7+
import { SDK_VERSION } from '@sentry/node';
8+
import {
9+
captureException,
10+
captureMessage,
11+
continueTrace,
12+
defaultIntegrations as nodeDefaultIntegrations,
13+
flush,
14+
getCurrentScope,
15+
init as initNode,
16+
startSpanManual,
17+
withScope,
18+
} from '@sentry/node';
19+
import type { Integration, SdkMetadata, Span } from '@sentry/types';
20+
import { isString, logger } from '@sentry/utils';
1121
// NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil
1222
import type { Context, Handler } from 'aws-lambda';
1323
import { performance } from 'perf_hooks';
@@ -55,9 +65,9 @@ export interface WrapperOptions {
5565
startTrace: boolean;
5666
}
5767

58-
export const defaultIntegrations: Integration[] = [...Sentry.defaultIntegrations, new AWSServices({ optional: true })];
68+
export const defaultIntegrations: Integration[] = [...nodeDefaultIntegrations, new AWSServices({ optional: true })];
5969

60-
interface AWSLambdaOptions extends Sentry.NodeOptions {
70+
interface AWSLambdaOptions extends NodeOptions {
6171
/**
6272
* Internal field that is set to `true` when init() is called by the Sentry AWS Lambda layer.
6373
*
@@ -66,7 +76,9 @@ interface AWSLambdaOptions extends Sentry.NodeOptions {
6676
}
6777

6878
/**
69-
* @see {@link Sentry.init}
79+
* Initializes the Sentry AWS Lambda SDK.
80+
*
81+
* @param options Configuration options for the SDK, @see {@link AWSLambdaOptions}.
7082
*/
7183
export function init(options: AWSLambdaOptions = {}): void {
7284
const opts = {
@@ -81,13 +93,13 @@ export function init(options: AWSLambdaOptions = {}): void {
8193
packages: [
8294
{
8395
name: 'npm:@sentry/serverless',
84-
version: Sentry.SDK_VERSION,
96+
version: SDK_VERSION,
8597
},
8698
],
87-
version: Sentry.SDK_VERSION,
99+
version: SDK_VERSION,
88100
};
89101

90-
Sentry.init(opts);
102+
initNode(opts);
91103
}
92104

93105
/** */
@@ -290,44 +302,13 @@ export function wrapHandler<TEvent, TResult>(
290302
}, timeoutWarningDelay) as unknown as NodeJS.Timeout;
291303
}
292304

293-
const hub = getCurrentHub();
294-
295-
let transaction: Sentry.Transaction | undefined;
296-
if (options.startTrace) {
297-
const eventWithHeaders = event as { headers?: { [key: string]: string } };
298-
299-
const sentryTrace =
300-
eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])
301-
? eventWithHeaders.headers['sentry-trace']
302-
: undefined;
303-
const baggage = eventWithHeaders.headers?.baggage;
304-
const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders(
305-
sentryTrace,
306-
baggage,
307-
);
308-
Sentry.getCurrentScope().setPropagationContext(propagationContext);
309-
310-
transaction = hub.startTransaction({
311-
name: context.functionName,
312-
op: 'function.aws.lambda',
313-
origin: 'auto.function.serverless',
314-
...traceparentData,
315-
metadata: {
316-
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
317-
source: 'component',
318-
},
319-
});
320-
}
305+
async function processResult(span?: Span): Promise<TResult> {
306+
const scope = getCurrentScope();
321307

322-
return withScope(async scope => {
323308
let rv: TResult;
324309
try {
325310
enhanceScopeWithEnvironmentData(scope, context, START_TIME);
326-
if (options.startTrace) {
327-
enhanceScopeWithTransactionData(scope, context);
328-
// We put the transaction on the scope so users can attach children to it
329-
scope.setSpan(transaction);
330-
}
311+
331312
rv = await asyncHandler(event, context);
332313

333314
// We manage lambdas that use Promise.allSettled by capturing the errors of failed promises
@@ -342,12 +323,46 @@ export function wrapHandler<TEvent, TResult>(
342323
throw e;
343324
} finally {
344325
clearTimeout(timeoutWarningTimer);
345-
transaction?.end();
326+
span?.end();
346327
await flush(options.flushTimeout).catch(e => {
347328
DEBUG_BUILD && logger.error(e);
348329
});
349330
}
350331
return rv;
332+
}
333+
334+
if (options.startTrace) {
335+
const eventWithHeaders = event as { headers?: { [key: string]: string } };
336+
337+
const sentryTrace =
338+
eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])
339+
? eventWithHeaders.headers['sentry-trace']
340+
: undefined;
341+
const baggage = eventWithHeaders.headers?.baggage;
342+
343+
const continueTraceContext = continueTrace({ sentryTrace, baggage });
344+
345+
return startSpanManual(
346+
{
347+
name: context.functionName,
348+
op: 'function.aws.lambda',
349+
origin: 'auto.function.serverless',
350+
...continueTraceContext,
351+
metadata: {
352+
...continueTraceContext.metadata,
353+
source: 'component',
354+
},
355+
},
356+
span => {
357+
enhanceScopeWithTransactionData(getCurrentScope(), context);
358+
359+
return processResult(span);
360+
},
361+
);
362+
}
363+
364+
return withScope(async () => {
365+
return processResult(undefined);
351366
});
352367
};
353368
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { captureException, flush, getCurrentHub, getCurrentScope } from '@sentry/node';
1+
import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node';
22
import { isThenable, logger } from '@sentry/utils';
33

44
import { DEBUG_BUILD } from '../debug-build';
@@ -21,7 +21,6 @@ export function wrapCloudEventFunction(
2121
return proxyFunction(fn, f => domainify(_wrapCloudEventFunction(f, wrapOptions)));
2222
}
2323

24-
/** */
2524
function _wrapCloudEventFunction(
2625
fn: CloudEventFunction | CloudEventFunctionWithCallback,
2726
wrapOptions: Partial<CloudEventFunctionWrapperOptions> = {},
@@ -31,63 +30,59 @@ function _wrapCloudEventFunction(
3130
...wrapOptions,
3231
};
3332
return (context, callback) => {
34-
const hub = getCurrentHub();
33+
return startSpanManual(
34+
{
35+
name: context.type || '<unknown>',
36+
op: 'function.gcp.cloud_event',
37+
origin: 'auto.function.serverless.gcp_cloud_event',
38+
metadata: { source: 'component' },
39+
},
40+
span => {
41+
const scope = getCurrentScope();
42+
scope.setContext('gcp.function.context', { ...context });
3543

36-
const transaction = hub.startTransaction({
37-
name: context.type || '<unknown>',
38-
op: 'function.gcp.cloud_event',
39-
origin: 'auto.function.serverless.gcp_cloud_event',
40-
metadata: { source: 'component' },
41-
}) as ReturnType<typeof hub.startTransaction> | undefined;
44+
const newCallback = domainify((...args: unknown[]) => {
45+
if (args[0] !== null && args[0] !== undefined) {
46+
captureException(args[0], scope => markEventUnhandled(scope));
47+
}
48+
span?.end();
4249

43-
// getCurrentHub() is expected to use current active domain as a carrier
44-
// since functions-framework creates a domain for each incoming request.
45-
// So adding of event processors every time should not lead to memory bloat.
46-
const scope = getCurrentScope();
47-
scope.setContext('gcp.function.context', { ...context });
48-
// We put the transaction on the scope so users can attach children to it
49-
scope.setSpan(transaction);
50-
51-
const newCallback = domainify((...args: unknown[]) => {
52-
if (args[0] !== null && args[0] !== undefined) {
53-
captureException(args[0], scope => markEventUnhandled(scope));
54-
}
55-
transaction?.end();
56-
57-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
58-
flush(options.flushTimeout)
59-
.then(null, e => {
60-
DEBUG_BUILD && logger.error(e);
61-
})
62-
.then(() => {
63-
callback(...args);
50+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
51+
flush(options.flushTimeout)
52+
.then(null, e => {
53+
DEBUG_BUILD && logger.error(e);
54+
})
55+
.then(() => {
56+
callback(...args);
57+
});
6458
});
65-
});
6659

67-
if (fn.length > 1) {
68-
let fnResult;
69-
try {
70-
fnResult = (fn as CloudEventFunctionWithCallback)(context, newCallback);
71-
} catch (err) {
72-
captureException(err, scope => markEventUnhandled(scope));
73-
throw err;
74-
}
60+
if (fn.length > 1) {
61+
let fnResult;
62+
try {
63+
fnResult = (fn as CloudEventFunctionWithCallback)(context, newCallback);
64+
} catch (err) {
65+
captureException(err, scope => markEventUnhandled(scope));
66+
throw err;
67+
}
7568

76-
if (isThenable(fnResult)) {
77-
fnResult.then(null, err => {
78-
captureException(err, scope => markEventUnhandled(scope));
79-
throw err;
80-
});
81-
}
69+
if (isThenable(fnResult)) {
70+
fnResult.then(null, err => {
71+
captureException(err, scope => markEventUnhandled(scope));
72+
throw err;
73+
});
74+
}
8275

83-
return fnResult;
84-
}
76+
return fnResult;
77+
}
8578

86-
return Promise.resolve()
87-
.then(() => (fn as CloudEventFunction)(context))
88-
.then(
89-
result => newCallback(null, result),
90-
err => newCallback(err, undefined),
91-
);
79+
return Promise.resolve()
80+
.then(() => (fn as CloudEventFunction)(context))
81+
.then(
82+
result => newCallback(null, result),
83+
err => newCallback(err, undefined),
84+
);
85+
},
86+
);
9287
};
9388
}

0 commit comments

Comments
 (0)