diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index b5aa9200b524..e8847fc7b212 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -14,7 +14,7 @@ import { performance } from 'perf_hooks'; import { types } from 'util'; import { AWSServices } from './awsservices'; -import { serverlessEventProcessor } from './utils'; +import { markEventUnhandled, serverlessEventProcessor } from './utils'; export * from '@sentry/node'; @@ -312,11 +312,11 @@ export function wrapHandler( if (options.captureAllSettledReasons && Array.isArray(rv) && isPromiseAllSettledResult(rv)) { const reasons = getRejectedReasons(rv); reasons.forEach(exception => { - captureException(exception); + captureException(exception, scope => markEventUnhandled(scope)); }); } } catch (e) { - captureException(e); + captureException(e, scope => markEventUnhandled(scope)); throw e; } finally { clearTimeout(timeoutWarningTimer); diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index 83725ffbb840..a0d843e71abe 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -1,7 +1,7 @@ import { captureException, flush, getCurrentHub } from '@sentry/node'; import { isThenable, logger } from '@sentry/utils'; -import { domainify, proxyFunction } from '../utils'; +import { domainify, markEventUnhandled, proxyFunction } from '../utils'; import type { CloudEventFunction, CloudEventFunctionWithCallback, WrapperOptions } from './general'; export type CloudEventFunctionWrapperOptions = WrapperOptions; @@ -50,7 +50,7 @@ function _wrapCloudEventFunction( const newCallback = domainify((...args: unknown[]) => { if (args[0] !== null && args[0] !== undefined) { - captureException(args[0]); + captureException(args[0], scope => markEventUnhandled(scope)); } transaction?.finish(); @@ -68,13 +68,13 @@ function _wrapCloudEventFunction( try { fnResult = (fn as CloudEventFunctionWithCallback)(context, newCallback); } catch (err) { - captureException(err); + captureException(err, scope => markEventUnhandled(scope)); throw err; } if (isThenable(fnResult)) { fnResult.then(null, err => { - captureException(err); + captureException(err, scope => markEventUnhandled(scope)); throw err; }); } diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index e2342d1fe905..9c98fcb8c485 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -1,7 +1,7 @@ import { captureException, flush, getCurrentHub } from '@sentry/node'; import { isThenable, logger } from '@sentry/utils'; -import { domainify, proxyFunction } from '../utils'; +import { domainify, markEventUnhandled, proxyFunction } from '../utils'; import type { EventFunction, EventFunctionWithCallback, WrapperOptions } from './general'; export type EventFunctionWrapperOptions = WrapperOptions; @@ -52,7 +52,7 @@ function _wrapEventFunction const newCallback = domainify((...args: unknown[]) => { if (args[0] !== null && args[0] !== undefined) { - captureException(args[0]); + captureException(args[0], scope => markEventUnhandled(scope)); } transaction?.finish(); @@ -72,13 +72,13 @@ function _wrapEventFunction try { fnResult = (fn as EventFunctionWithCallback)(data, context, newCallback); } catch (err) { - captureException(err); + captureException(err, scope => markEventUnhandled(scope)); throw err; } if (isThenable(fnResult)) { fnResult.then(null, err => { - captureException(err); + captureException(err, scope => markEventUnhandled(scope)); throw err; }); } diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index 1c265fe9fb64..eea492bb8dab 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -2,7 +2,7 @@ import type { AddRequestDataToEventOptions } from '@sentry/node'; import { captureException, flush, getCurrentHub } from '@sentry/node'; import { isString, isThenable, logger, stripUrlQueryAndFragment, tracingContextFromHeaders } from '@sentry/utils'; -import { domainify, proxyFunction } from './../utils'; +import { domainify, markEventUnhandled, proxyFunction } from './../utils'; import type { HttpFunction, WrapperOptions } from './general'; // TODO (v8 / #5257): Remove this whole old/new business and just use the new stuff @@ -122,13 +122,13 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial markEventUnhandled(scope)); throw err; } if (isThenable(fnResult)) { fnResult.then(null, err => { - captureException(err); + captureException(err, scope => markEventUnhandled(scope)); throw err; }); } diff --git a/packages/serverless/src/utils.ts b/packages/serverless/src/utils.ts index ae1f4b987ffb..69e28ab3a823 100644 --- a/packages/serverless/src/utils.ts +++ b/packages/serverless/src/utils.ts @@ -1,5 +1,6 @@ import { runWithAsyncContext } from '@sentry/core'; import type { Event } from '@sentry/node'; +import type { Scope } from '@sentry/types'; import { addExceptionMechanism } from '@sentry/utils'; /** @@ -55,3 +56,15 @@ export function proxyFunction R>( return new Proxy(source, handler); } + +/** + * Marks an event as unhandled by adding a span processor to the passed scope. + */ +export function markEventUnhandled(scope: Scope): Scope { + scope.addEventProcessor(event => { + addExceptionMechanism(event, { handled: false }); + return event; + }); + + return scope; +} diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index 53770927c4a5..e03d17bfd14b 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -1,6 +1,7 @@ // 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 // eslint-disable-next-line import/no-unresolved import * as SentryNode from '@sentry/node'; +import type { Event } from '@sentry/types'; // eslint-disable-next-line import/no-unresolved import type { Callback, Handler } from 'aws-lambda'; @@ -175,8 +176,8 @@ describe('AWSLambda', () => { ]); const wrappedHandler = wrapHandler(handler, { flushTimeout: 1337, captureAllSettledReasons: true }); await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(SentryNode.captureException).toHaveBeenNthCalledWith(1, error); - expect(SentryNode.captureException).toHaveBeenNthCalledWith(2, error2); + expect(SentryNode.captureException).toHaveBeenNthCalledWith(1, error, expect.any(Function)); + expect(SentryNode.captureException).toHaveBeenNthCalledWith(2, error2, expect.any(Function)); expect(SentryNode.captureException).toBeCalledTimes(2); }); }); @@ -229,7 +230,7 @@ describe('AWSLambda', () => { // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); - expect(SentryNode.captureException).toBeCalledWith(error); + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); @@ -308,7 +309,7 @@ describe('AWSLambda', () => { // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); - expect(SentryNode.captureException).toBeCalledWith(e); + expect(SentryNode.captureException).toBeCalledWith(e, expect.any(Function)); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); @@ -375,7 +376,7 @@ describe('AWSLambda', () => { // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); - expect(SentryNode.captureException).toBeCalledWith(error); + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); @@ -457,7 +458,7 @@ describe('AWSLambda', () => { // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); - expect(SentryNode.captureException).toBeCalledWith(error); + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); @@ -465,6 +466,34 @@ describe('AWSLambda', () => { }); }); + test('marks the captured error as unhandled', async () => { + expect.assertions(3); + + const error = new Error('wat'); + const handler: Handler = async (_event, _context, _callback) => { + throw error; + }; + const wrappedHandler = wrapHandler(handler); + + try { + await wrappedHandler(fakeEvent, fakeContext, fakeCallback); + } catch (e) { + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); + // @ts-ignore see "Why @ts-ignore" note + const scopeFunction = SentryNode.captureException.mock.calls[0][1]; + const event: Event = { exception: { values: [{}] } }; + let evtProcessor: ((e: Event) => Event) | undefined = undefined; + scopeFunction({ addEventProcessor: jest.fn().mockImplementation(proc => (evtProcessor = proc)) }); + + expect(evtProcessor).toBeInstanceOf(Function); + // @ts-ignore just mocking around... + expect(evtProcessor(event).exception.values[0].mechanism).toEqual({ + handled: false, + type: 'generic', + }); + } + }); + describe('init()', () => { test('calls Sentry.init with correct sdk info metadata', () => { Sentry.AWSLambda.init({}); diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 74939f1f574a..812447106ad5 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -1,4 +1,5 @@ import * as SentryNode from '@sentry/node'; +import type { Event } from '@sentry/types'; import * as domain from 'domain'; import * as Sentry from '../src'; @@ -12,7 +13,6 @@ import type { Request, Response, } from '../src/gcpfunction/general'; - /** * Why @ts-ignore some Sentry.X calls * @@ -198,7 +198,7 @@ describe('GCPFunction', () => { expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - expect(SentryNode.captureException).toBeCalledWith(error); + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); @@ -317,7 +317,7 @@ describe('GCPFunction', () => { expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - expect(SentryNode.captureException).toBeCalledWith(error); + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); @@ -382,7 +382,7 @@ describe('GCPFunction', () => { expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - expect(SentryNode.captureException).toBeCalledWith(error); + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); @@ -440,7 +440,7 @@ describe('GCPFunction', () => { expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - expect(SentryNode.captureException).toBeCalledWith(error); + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); @@ -469,7 +469,33 @@ describe('GCPFunction', () => { expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - expect(SentryNode.captureException).toBeCalledWith(error); + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); + }); + }); + + test('marks the captured error as unhandled', async () => { + expect.assertions(4); + + const error = new Error('wat'); + const handler: EventFunctionWithCallback = (_data, _context, _cb) => { + throw error; + }; + const wrappedHandler = wrapEventFunction(handler); + await expect(handleEvent(wrappedHandler)).rejects.toThrowError(error); + + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); + + // @ts-ignore just mocking around... + const scopeFunction = SentryNode.captureException.mock.calls[0][1]; + const event: Event = { exception: { values: [{}] } }; + let evtProcessor: ((e: Event) => Event) | undefined = undefined; + scopeFunction({ addEventProcessor: jest.fn().mockImplementation(proc => (evtProcessor = proc)) }); + + expect(evtProcessor).toBeInstanceOf(Function); + // @ts-ignore just mocking around... + expect(evtProcessor(event).exception.values[0].mechanism).toEqual({ + handled: false, + type: 'generic', }); }); @@ -537,7 +563,7 @@ describe('GCPFunction', () => { expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - expect(SentryNode.captureException).toBeCalledWith(error); + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); @@ -595,7 +621,7 @@ describe('GCPFunction', () => { expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - expect(SentryNode.captureException).toBeCalledWith(error); + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); @@ -625,7 +651,7 @@ describe('GCPFunction', () => { // @ts-ignore see "Why @ts-ignore" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - expect(SentryNode.captureException).toBeCalledWith(error); + expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); }); });