Skip to content

Commit

Permalink
Merge pull request #1900 from bugsnag/node-remove-domain-3
Browse files Browse the repository at this point in the history
capture uncaught exceptions in aws-lambda
  • Loading branch information
djskinner committed Mar 1, 2023
2 parents 4b02048 + eda4675 commit 054ea19
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 5 deletions.
14 changes: 14 additions & 0 deletions packages/plugin-aws-lambda/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 7 additions & 4 deletions packages/plugin-node-uncaught-exception/uncaught-exception.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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"
}
}
13 changes: 13 additions & 0 deletions test/aws-lambda/features/fixtures/simple-app/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
27 changes: 26 additions & 1 deletion test/aws-lambda/features/unhandled.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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>" lambda in "features/fixtures/simple-app" with the "events/<type>/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 "<file>"
And the event "metaData.AWS Lambda context.functionName" equals "<lambda>"
And the event "metaData.AWS Lambda context.awsRequestId" is not null
And the event "device.runtimeVersions.node" matches "^<node-version>\.\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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 054ea19

Please sign in to comment.