|
1 | | -import type { Integration, Options, Scope } from '@sentry/core'; |
2 | | -import { applySdkMetadata, consoleSandbox, debug, getSDKSource } from '@sentry/core'; |
| 1 | +import type { Integration, Options } from '@sentry/core'; |
| 2 | +import { applySdkMetadata, debug, getSDKSource } from '@sentry/core'; |
3 | 3 | import type { NodeClient, NodeOptions } from '@sentry/node'; |
4 | | -import { |
5 | | - captureException, |
6 | | - captureMessage, |
7 | | - flush, |
8 | | - getCurrentScope, |
9 | | - getDefaultIntegrationsWithoutPerformance, |
10 | | - initWithoutDefaultIntegrations, |
11 | | - withScope, |
12 | | -} from '@sentry/node'; |
13 | | -import type { Context, Handler } from 'aws-lambda'; |
| 4 | +import { getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations } from '@sentry/node'; |
| 5 | +import type { Handler } from 'aws-lambda'; |
14 | 6 | import { existsSync } from 'fs'; |
15 | 7 | import { basename, resolve } from 'path'; |
16 | | -import { performance } from 'perf_hooks'; |
17 | | -import { types } from 'util'; |
18 | 8 | import { DEBUG_BUILD } from './debug-build'; |
19 | 9 | import { awsIntegration } from './integration/aws'; |
20 | 10 | import { awsLambdaIntegration } from './integration/awslambda'; |
21 | | -import { markEventUnhandled } from './utils'; |
| 11 | +import { wrapHandler } from './wrappers/wrap-handler'; |
22 | 12 |
|
23 | | -const { isPromise } = types; |
24 | | - |
25 | | -// https://www.npmjs.com/package/aws-lambda-consumer |
26 | | -type SyncHandler<T extends Handler> = ( |
27 | | - event: Parameters<T>[0], |
28 | | - context: Parameters<T>[1], |
29 | | - callback: Parameters<T>[2], |
30 | | -) => void; |
| 13 | +export { wrapHandler }; |
31 | 14 |
|
32 | 15 | export type AsyncHandler<T extends Handler> = ( |
33 | 16 | event: Parameters<T>[0], |
@@ -89,25 +72,6 @@ function tryRequire<T>(taskRoot: string, subdir: string, mod: string): T { |
89 | 72 | return require(require.resolve(mod, { paths: [taskRoot, subdir] })); |
90 | 73 | } |
91 | 74 |
|
92 | | -/** */ |
93 | | -function isPromiseAllSettledResult<T>(result: T[]): boolean { |
94 | | - return result.every( |
95 | | - v => |
96 | | - Object.prototype.hasOwnProperty.call(v, 'status') && |
97 | | - (Object.prototype.hasOwnProperty.call(v, 'value') || Object.prototype.hasOwnProperty.call(v, 'reason')), |
98 | | - ); |
99 | | -} |
100 | | - |
101 | | -type PromiseSettledResult<T> = { status: 'rejected' | 'fulfilled'; reason?: T }; |
102 | | - |
103 | | -/** */ |
104 | | -function getRejectedReasons<T>(results: PromiseSettledResult<T>[]): T[] { |
105 | | - return results.reduce((rejected: T[], result) => { |
106 | | - if (result.status === 'rejected' && result.reason) rejected.push(result.reason); |
107 | | - return rejected; |
108 | | - }, []); |
109 | | -} |
110 | | - |
111 | 75 | /** */ |
112 | 76 | export function tryPatchHandler(taskRoot: string, handlerPath: string): void { |
113 | 77 | type HandlerBag = HandlerModule | Handler | null | undefined; |
@@ -159,161 +123,3 @@ export function tryPatchHandler(taskRoot: string, handlerPath: string): void { |
159 | 123 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
160 | 124 | (mod as HandlerModule)[functionName!] = wrapHandler(obj); |
161 | 125 | } |
162 | | - |
163 | | -/** |
164 | | - * Tries to invoke context.getRemainingTimeInMillis if not available returns 0 |
165 | | - * Some environments use AWS lambda but don't support this function |
166 | | - * @param context |
167 | | - */ |
168 | | -function tryGetRemainingTimeInMillis(context: Context): number { |
169 | | - return typeof context.getRemainingTimeInMillis === 'function' ? context.getRemainingTimeInMillis() : 0; |
170 | | -} |
171 | | - |
172 | | -/** |
173 | | - * Adds additional information from the environment and AWS Context to the Sentry Scope. |
174 | | - * |
175 | | - * @param scope Scope that should be enhanced |
176 | | - * @param context AWS Lambda context that will be used to extract some part of the data |
177 | | - * @param startTime performance.now() when wrapHandler was invoked |
178 | | - */ |
179 | | -function enhanceScopeWithEnvironmentData(scope: Scope, context: Context, startTime: number): void { |
180 | | - scope.setContext('aws.lambda', { |
181 | | - aws_request_id: context.awsRequestId, |
182 | | - function_name: context.functionName, |
183 | | - function_version: context.functionVersion, |
184 | | - invoked_function_arn: context.invokedFunctionArn, |
185 | | - execution_duration_in_millis: performance.now() - startTime, |
186 | | - remaining_time_in_millis: tryGetRemainingTimeInMillis(context), |
187 | | - 'sys.argv': process.argv, |
188 | | - }); |
189 | | - |
190 | | - scope.setContext('aws.cloudwatch.logs', { |
191 | | - log_group: context.logGroupName, |
192 | | - log_stream: context.logStreamName, |
193 | | - url: `https://console.aws.amazon.com/cloudwatch/home?region=${ |
194 | | - process.env.AWS_REGION |
195 | | - }#logsV2:log-groups/log-group/${encodeURIComponent(context.logGroupName)}/log-events/${encodeURIComponent( |
196 | | - context.logStreamName, |
197 | | - )}?filterPattern="${context.awsRequestId}"`, |
198 | | - }); |
199 | | -} |
200 | | - |
201 | | -/** |
202 | | - * Wraps a lambda handler adding it error capture and tracing capabilities. |
203 | | - * |
204 | | - * @param handler Handler |
205 | | - * @param options Options |
206 | | - * @returns Handler |
207 | | - */ |
208 | | -export function wrapHandler<TEvent, TResult>( |
209 | | - handler: Handler<TEvent, TResult>, |
210 | | - wrapOptions: Partial<WrapperOptions> = {}, |
211 | | -): Handler<TEvent, TResult> { |
212 | | - const START_TIME = performance.now(); |
213 | | - |
214 | | - // eslint-disable-next-line deprecation/deprecation |
215 | | - if (typeof wrapOptions.startTrace !== 'undefined') { |
216 | | - consoleSandbox(() => { |
217 | | - // eslint-disable-next-line no-console |
218 | | - console.warn( |
219 | | - 'The `startTrace` option is deprecated and will be removed in a future major version. If you want to disable tracing, set `SENTRY_TRACES_SAMPLE_RATE` to `0.0`.', |
220 | | - ); |
221 | | - }); |
222 | | - } |
223 | | - |
224 | | - const options: WrapperOptions = { |
225 | | - flushTimeout: 2000, |
226 | | - callbackWaitsForEmptyEventLoop: false, |
227 | | - captureTimeoutWarning: true, |
228 | | - timeoutWarningLimit: 500, |
229 | | - captureAllSettledReasons: false, |
230 | | - startTrace: true, // TODO(v11): Remove this option. Set to true here to satisfy the type, but has no effect. |
231 | | - ...wrapOptions, |
232 | | - }; |
233 | | - |
234 | | - let timeoutWarningTimer: NodeJS.Timeout; |
235 | | - |
236 | | - // AWSLambda is like Express. It makes a distinction about handlers based on its last argument |
237 | | - // async (event) => async handler |
238 | | - // async (event, context) => async handler |
239 | | - // (event, context, callback) => sync handler |
240 | | - // Nevertheless whatever option is chosen by user, we convert it to async handler. |
241 | | - const asyncHandler: AsyncHandler<typeof handler> = |
242 | | - handler.length > 2 |
243 | | - ? (event, context) => |
244 | | - new Promise((resolve, reject) => { |
245 | | - const rv = (handler as SyncHandler<typeof handler>)(event, context, (error, result) => { |
246 | | - if (error === null || error === undefined) { |
247 | | - resolve(result!); // eslint-disable-line @typescript-eslint/no-non-null-assertion |
248 | | - } else { |
249 | | - reject(error); |
250 | | - } |
251 | | - }) as unknown; |
252 | | - |
253 | | - // This should never happen, but still can if someone writes a handler as |
254 | | - // `async (event, context, callback) => {}` |
255 | | - if (isPromise(rv)) { |
256 | | - void (rv as Promise<NonNullable<TResult>>).then(resolve, reject); |
257 | | - } |
258 | | - }) |
259 | | - : (handler as AsyncHandler<typeof handler>); |
260 | | - |
261 | | - return async (event, context) => { |
262 | | - context.callbackWaitsForEmptyEventLoop = options.callbackWaitsForEmptyEventLoop; |
263 | | - |
264 | | - // In seconds. You cannot go any more granular than this in AWS Lambda. |
265 | | - const configuredTimeout = Math.ceil(tryGetRemainingTimeInMillis(context) / 1000); |
266 | | - const configuredTimeoutMinutes = Math.floor(configuredTimeout / 60); |
267 | | - const configuredTimeoutSeconds = configuredTimeout % 60; |
268 | | - |
269 | | - const humanReadableTimeout = |
270 | | - configuredTimeoutMinutes > 0 |
271 | | - ? `${configuredTimeoutMinutes}m${configuredTimeoutSeconds}s` |
272 | | - : `${configuredTimeoutSeconds}s`; |
273 | | - |
274 | | - // When `callbackWaitsForEmptyEventLoop` is set to false, which it should when using `captureTimeoutWarning`, |
275 | | - // we don't have a guarantee that this message will be delivered. Because of that, we don't flush it. |
276 | | - if (options.captureTimeoutWarning) { |
277 | | - const timeoutWarningDelay = tryGetRemainingTimeInMillis(context) - options.timeoutWarningLimit; |
278 | | - |
279 | | - timeoutWarningTimer = setTimeout(() => { |
280 | | - withScope(scope => { |
281 | | - scope.setTag('timeout', humanReadableTimeout); |
282 | | - captureMessage(`Possible function timeout: ${context.functionName}`, 'warning'); |
283 | | - }); |
284 | | - }, timeoutWarningDelay) as unknown as NodeJS.Timeout; |
285 | | - } |
286 | | - |
287 | | - async function processResult(): Promise<TResult> { |
288 | | - const scope = getCurrentScope(); |
289 | | - |
290 | | - let rv: TResult; |
291 | | - try { |
292 | | - enhanceScopeWithEnvironmentData(scope, context, START_TIME); |
293 | | - |
294 | | - rv = await asyncHandler(event, context); |
295 | | - |
296 | | - // We manage lambdas that use Promise.allSettled by capturing the errors of failed promises |
297 | | - if (options.captureAllSettledReasons && Array.isArray(rv) && isPromiseAllSettledResult(rv)) { |
298 | | - const reasons = getRejectedReasons(rv); |
299 | | - reasons.forEach(exception => { |
300 | | - captureException(exception, scope => markEventUnhandled(scope, 'auto.function.aws-serverless.promise')); |
301 | | - }); |
302 | | - } |
303 | | - } catch (e) { |
304 | | - captureException(e, scope => markEventUnhandled(scope, 'auto.function.aws-serverless.handler')); |
305 | | - throw e; |
306 | | - } finally { |
307 | | - clearTimeout(timeoutWarningTimer); |
308 | | - await flush(options.flushTimeout).catch(e => { |
309 | | - DEBUG_BUILD && debug.error(e); |
310 | | - }); |
311 | | - } |
312 | | - return rv; |
313 | | - } |
314 | | - |
315 | | - return withScope(async () => { |
316 | | - return processResult(); |
317 | | - }); |
318 | | - }; |
319 | | -} |
0 commit comments