diff --git a/packages/plugin-aws-lambda/src/index.js b/packages/plugin-aws-lambda/src/index.js index 512cbef65d..b8819c5637 100644 --- a/packages/plugin-aws-lambda/src/index.js +++ b/packages/plugin-aws-lambda/src/index.js @@ -38,6 +38,20 @@ const BugsnagPluginAwsLambda = { }) } + // same for uncaught exceptions + if (client._config.autoDetectErrors && client._config.enabledErrorTypes.unhandledExceptions) { + const listeners = process.listeners('uncaughtException') + process.removeAllListeners('uncaughtException') + + // This relies on our unhandled rejection plugin adding its listener first + // using process.prependListener, so we can call it first instead of AWS' + process.on('uncaughtException', async (err, origin) => { + for (const listener of listeners) { + await listener.call(process, err, origin) + } + }) + } + return { createHandler ({ flushTimeoutMs = 2000, lambdaTimeoutNotifyMs = 1000 } = {}) { return wrapHandler.bind(null, client, flushTimeoutMs, lambdaTimeoutNotifyMs) diff --git a/packages/plugin-node-uncaught-exception/uncaught-exception.js b/packages/plugin-node-uncaught-exception/uncaught-exception.js index 8cd80d596d..8286f034b8 100644 --- a/packages/plugin-node-uncaught-exception/uncaught-exception.js +++ b/packages/plugin-node-uncaught-exception/uncaught-exception.js @@ -12,12 +12,15 @@ module.exports = { unhandled: true, severityReason: { type: 'unhandledException' } }, 'uncaughtException handler', 1) - c._notify(event, () => {}, (e, event) => { - if (e) c._logger.error('Failed to send event to Bugsnag') - c._config.onUncaughtException(err, event, c._logger) + return new Promise(resolve => { + c._notify(event, () => {}, (e, event) => { + if (e) c._logger.error('Failed to send event to Bugsnag') + c._config.onUncaughtException(err, event, c._logger) + resolve() + }) }) } - process.on('uncaughtException', _handler) + process.prependListener('uncaughtException', _handler) }, destroy: () => { process.removeListener('uncaughtException', _handler) diff --git a/test/aws-lambda/features/fixtures/simple-app/async/async-unhandled-exception.js b/test/aws-lambda/features/fixtures/simple-app/async/async-unhandled-exception.js new file mode 100644 index 0000000000..e1caa5666a --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/async/async-unhandled-exception.js @@ -0,0 +1,30 @@ +const Bugsnag = require('@bugsnag/js') +const BugsnagPluginAwsLambda = require('@bugsnag/plugin-aws-lambda') + +Bugsnag.start({ + apiKey: process.env.BUGSNAG_API_KEY, + endpoints: { + notify: process.env.BUGSNAG_NOTIFY_ENDPOINT, + sessions: process.env.BUGSNAG_SESSIONS_ENDPOINT + }, + plugins: [BugsnagPluginAwsLambda], + autoDetectErrors: process.env.BUGSNAG_AUTO_DETECT_ERRORS !== 'false', + autoTrackSessions: process.env.BUGSNAG_AUTO_TRACK_SESSIONS !== 'false' +}) + +const bugsnagHandler = Bugsnag.getPlugin('awsLambda').createHandler() + +const handler = async (event, context) => { + setTimeout(() => { + throw new Error('Oh no!') + }, 100) + + await new Promise(resolve => setTimeout(resolve, 1000)) + + return { + statusCode: 200, + body: JSON.stringify({ message: 'Did not crash immediately!' }) + } +} + +module.exports.lambdaHandler = bugsnagHandler(handler) diff --git a/test/aws-lambda/features/fixtures/simple-app/events/async/async-unhandled-exception.json b/test/aws-lambda/features/fixtures/simple-app/events/async/async-unhandled-exception.json new file mode 100644 index 0000000000..cb21bed8d6 --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/events/async/async-unhandled-exception.json @@ -0,0 +1,46 @@ +{ + "body": "", + "resource": "/{proxy+}", + "path": "/async/async/unhandled/exception", + "httpMethod": "GET", + "isBase64Encoded": false, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, + "pathParameters": {}, + "stageVariables": {}, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/async/async/unhandled/exception", + "resourcePath": "/{proxy+}", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/test/aws-lambda/features/fixtures/simple-app/template.yaml b/test/aws-lambda/features/fixtures/simple-app/template.yaml index c5772d3b47..ca2cf0898e 100644 --- a/test/aws-lambda/features/fixtures/simple-app/template.yaml +++ b/test/aws-lambda/features/fixtures/simple-app/template.yaml @@ -27,6 +27,19 @@ Resources: Path: /async/unhandled/exception Method: get + AsyncAsyncUnhandledExceptionFunctionNode14: + Type: AWS::Serverless::Function + Properties: + CodeUri: async/ + Handler: async-unhandled-exception.lambdaHandler + Runtime: nodejs14.x + Events: + AsyncAsyncUnhandledException: + Type: Api + Properties: + Path: /async/async/unhandled/exception + Method: get + AsyncHandledExceptionFunctionNode14: Type: AWS::Serverless::Function Properties: diff --git a/test/aws-lambda/features/unhandled.feature b/test/aws-lambda/features/unhandled.feature index a1fb4394a4..5c8d68225c 100644 --- a/test/aws-lambda/features/unhandled.feature +++ b/test/aws-lambda/features/unhandled.feature @@ -37,6 +37,32 @@ Scenario Outline: unhandled exceptions are reported | CallbackThrownUnhandledExceptionFunctionNode14 | callback | thrown-unhandled-exception.js | 14 | 7 | | CallbackThrownUnhandledExceptionFunctionNode12 | callback | thrown-unhandled-exception.js | 12 | 7 | +@simple-app +Scenario Outline: unhandled exceptions thrown async are reported + Given I setup the environment + When I invoke the "" lambda in "features/fixtures/simple-app" with the "events//async-unhandled-exception.json" event + And the SAM exit code equals 0 + When I wait to receive an error + Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier + And the event "unhandled" is true + And the event "severity" equals "error" + And the event "severityReason.type" equals "unhandledException" + And the exception "errorClass" equals "Error" + And the exception "message" equals "Oh no!" + And the exception "type" equals "nodejs" + And the "file" of stack frame 0 equals "" + And the event "metaData.AWS Lambda context.functionName" equals "" + And the event "metaData.AWS Lambda context.awsRequestId" is not null + And the event "device.runtimeVersions.node" matches "^\.\d+\.\d+$" + When I wait to receive a session + Then the session is valid for the session reporting API version "1" for the "Bugsnag Node" notifier + And the session "id" is not null + And the session "startedAt" is a timestamp + + Examples: + | lambda | type | file | node-version | trace-length | + | AsyncAsyncUnhandledExceptionFunctionNode14 | async | async-unhandled-exception.js | 14 | 4 | + @simple-app Scenario Outline: no error is reported when autoDetectErrors is false Given I setup the environment @@ -91,7 +117,6 @@ Scenario Outline: unhandled exceptions are reported when using serverless-expres Scenario: unhandled asynchronous exceptions are reported when using serverless-express Given I setup the environment When I invoke the "ExpressFunction" lambda in "features/fixtures/serverless-express-app" with the "events/unhandled-async.json" event - Then the lambda response is empty And the SAM exit code equals 0 When I wait to receive an error Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier