diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 78307a0b51..57ac1f43ea 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -85,6 +85,18 @@ steps: message: '${BUILDKITE_MESSAGE}' async: true + - label: ':aws-lambda: AWS Lambda tests' + timeout_in_minutes: 35 + agents: + queue: 'opensource-mac-aws-sam' + commands: + # force the NPM registry as the default on CI is artifactory, which can't + # currently install from our lockfile + - npm ci --registry https://registry.npmjs.org + - cd test/aws-lambda + - bundle install + - bundle exec maze-runner + # # Core tests and checks # diff --git a/CHANGELOG.md b/CHANGELOG.md index 99304923e3..4b04b64d37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## TBD +### Added + +- (plugin-aws-lambda): Add support for Node.js on AWS Lambda. See the [docs](https://docs.bugsnag.com/platforms/javascript/aws-lambda/) for usage. [#1334](https://github.com/bugsnag/bugsnag-js/pull/1334) + ### Changed - (plugin-koa): Ensure `ctx.request.body` is present on the event's request property by collecting it at the last possible moment [#1292](https://github.com/bugsnag/bugsnag-js/pull/1292) diff --git a/jest.config.js b/jest.config.js index 7611ae701d..57ff343ce4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -78,6 +78,8 @@ module.exports = { ]), project('node plugins', [ 'delivery-node', + 'in-flight', + 'plugin-aws-lambda', 'plugin-express', 'plugin-koa', 'plugin-restify', diff --git a/lerna.json b/lerna.json index 418141f3cb..a22f53601d 100644 --- a/lerna.json +++ b/lerna.json @@ -3,5 +3,5 @@ "packages": [ "packages/*" ], - "version": "7.8.2" + "version": "7.9.0-alpha.0" } diff --git a/packages/browser/package-lock.json b/packages/browser/package-lock.json index c2dab0f0ee..70ff18526e 100644 --- a/packages/browser/package-lock.json +++ b/packages/browser/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/browser", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/browser/package.json b/packages/browser/package.json index 40387cf842..998031e6ca 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/browser", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "dist/bugsnag.js", "types": "types/bugsnag.d.ts", "description": "Bugsnag error reporter for browser JavaScript", @@ -32,29 +32,29 @@ "license": "MIT", "devDependencies": { "@bugsnag/core": "^7.0.1", - "@bugsnag/delivery-x-domain-request": "^7.7.0", - "@bugsnag/delivery-xml-http-request": "^7.7.0", - "@bugsnag/plugin-app-duration": "^7.7.0", - "@bugsnag/plugin-browser-context": "^7.7.0", - "@bugsnag/plugin-browser-device": "^7.7.0", - "@bugsnag/plugin-browser-request": "^7.7.0", - "@bugsnag/plugin-browser-session": "^7.7.0", - "@bugsnag/plugin-client-ip": "^7.7.0", - "@bugsnag/plugin-console-breadcrumbs": "^7.7.0", - "@bugsnag/plugin-inline-script-content": "^7.7.0", - "@bugsnag/plugin-interaction-breadcrumbs": "^7.7.0", - "@bugsnag/plugin-navigation-breadcrumbs": "^7.7.0", - "@bugsnag/plugin-network-breadcrumbs": "^7.7.0", - "@bugsnag/plugin-simple-throttle": "^7.7.0", - "@bugsnag/plugin-strip-query-string": "^7.7.0", - "@bugsnag/plugin-window-onerror": "^7.7.0", - "@bugsnag/plugin-window-unhandled-rejection": "^7.7.0", + "@bugsnag/delivery-x-domain-request": "^7.9.0-alpha.0", + "@bugsnag/delivery-xml-http-request": "^7.9.0-alpha.0", + "@bugsnag/plugin-app-duration": "^7.9.0-alpha.0", + "@bugsnag/plugin-browser-context": "^7.9.0-alpha.0", + "@bugsnag/plugin-browser-device": "^7.9.0-alpha.0", + "@bugsnag/plugin-browser-request": "^7.9.0-alpha.0", + "@bugsnag/plugin-browser-session": "^7.9.0-alpha.0", + "@bugsnag/plugin-client-ip": "^7.9.0-alpha.0", + "@bugsnag/plugin-console-breadcrumbs": "^7.9.0-alpha.0", + "@bugsnag/plugin-inline-script-content": "^7.9.0-alpha.0", + "@bugsnag/plugin-interaction-breadcrumbs": "^7.9.0-alpha.0", + "@bugsnag/plugin-navigation-breadcrumbs": "^7.9.0-alpha.0", + "@bugsnag/plugin-network-breadcrumbs": "^7.9.0-alpha.0", + "@bugsnag/plugin-simple-throttle": "^7.9.0-alpha.0", + "@bugsnag/plugin-strip-query-string": "^7.9.0-alpha.0", + "@bugsnag/plugin-window-onerror": "^7.9.0-alpha.0", + "@bugsnag/plugin-window-unhandled-rejection": "^7.9.0-alpha.0", "cloudfront": "^0.4.1", "knox": "^0.9.2", "mime": "1.4.1", "semver": "^5.5.1" }, "dependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" } } diff --git a/packages/core/client.d.ts b/packages/core/client.d.ts index 8b251eb1c8..72f30ae42e 100644 --- a/packages/core/client.d.ts +++ b/packages/core/client.d.ts @@ -41,6 +41,7 @@ interface Delivery { export default class ClientWithInternals extends Client { public constructor(opts: T, schema?: {[key: string]: any}, internalPlugins?: Plugin[], notifier?: Notifier) _config: T + _depth: number _logger: LoggerConfig _breadcrumbs: Breadcrumb[]; _delivery: Delivery diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index ab146cc114..8b06f3543e 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/core", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/core/package.json b/packages/core/package.json index 15493de749..e9bcb5e395 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@bugsnag/core", "main": "index.js", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "types": "types/index.d.ts", "description": "Core classes and utilities for Bugsnag notifiers", "homepage": "https://www.bugsnag.com/", diff --git a/packages/delivery-expo/package-lock.json b/packages/delivery-expo/package-lock.json index e0ff54d5e3..57f13deadf 100644 --- a/packages/delivery-expo/package-lock.json +++ b/packages/delivery-expo/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/delivery-expo", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/delivery-expo/package.json b/packages/delivery-expo/package.json index ab7e50e94a..b4ac389875 100644 --- a/packages/delivery-expo/package.json +++ b/packages/delivery-expo/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/delivery-expo", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "delivery.js", "description": "@bugsnag/js delivery mechanism to send events and sessions from Expo, using the FileSystem API to cache and retry sending failed payloads", "homepage": "https://www.bugsnag.com/", @@ -22,7 +22,7 @@ "expo-file-system": "~9.3.0" }, "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/delivery-node/package-lock.json b/packages/delivery-node/package-lock.json index 4ff22c8c24..5990aa7add 100644 --- a/packages/delivery-node/package-lock.json +++ b/packages/delivery-node/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/delivery-node", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/delivery-node/package.json b/packages/delivery-node/package.json index 8fb41fe403..4e80ee3a5c 100644 --- a/packages/delivery-node/package.json +++ b/packages/delivery-node/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/delivery-node", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "delivery.js", "description": "@bugsnag/node delivery mechanism", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/delivery-react-native/package-lock.json b/packages/delivery-react-native/package-lock.json index 13ff3c3ea5..0acb089e52 100644 --- a/packages/delivery-react-native/package-lock.json +++ b/packages/delivery-react-native/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/delivery-react-native", - "version": "7.8.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/delivery-react-native/package.json b/packages/delivery-react-native/package.json index 4d6442472d..02a93f62bf 100644 --- a/packages/delivery-react-native/package.json +++ b/packages/delivery-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/delivery-react-native", - "version": "7.8.0", + "version": "7.9.0-alpha.0", "main": "delivery.js", "description": "@bugsnag/js delivery mechanism for React Native", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/delivery-x-domain-request/package-lock.json b/packages/delivery-x-domain-request/package-lock.json index 2d21c981fe..54a28c8715 100644 --- a/packages/delivery-x-domain-request/package-lock.json +++ b/packages/delivery-x-domain-request/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/delivery-x-domain-request", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/delivery-x-domain-request/package.json b/packages/delivery-x-domain-request/package.json index 7067798567..1f584b0b85 100644 --- a/packages/delivery-x-domain-request/package.json +++ b/packages/delivery-x-domain-request/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/delivery-x-domain-request", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "delivery.js", "description": "@bugsnag/js delivery mechanism for IE 8, 9 and 10", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/delivery-xml-http-request/package-lock.json b/packages/delivery-xml-http-request/package-lock.json index c585b81b04..886b8439a4 100644 --- a/packages/delivery-xml-http-request/package-lock.json +++ b/packages/delivery-xml-http-request/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/delivery-xml-http-request", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/delivery-xml-http-request/package.json b/packages/delivery-xml-http-request/package.json index 18b5797eaa..cbc66c2825 100644 --- a/packages/delivery-xml-http-request/package.json +++ b/packages/delivery-xml-http-request/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/delivery-xml-http-request", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "delivery.js", "description": "@bugsnag/js delivery mechanism for most browsers", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/expo/package-lock.json b/packages/expo/package-lock.json index c0399efe97..67caca971f 100644 --- a/packages/expo/package-lock.json +++ b/packages/expo/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/expo", - "version": "7.8.2", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/expo/package.json b/packages/expo/package.json index b6906fb105..de1f296e28 100644 --- a/packages/expo/package.json +++ b/packages/expo/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/expo", - "version": "7.8.2", + "version": "7.9.0-alpha.0", "main": "src/notifier.js", "types": "types/bugsnag.d.ts", "description": "Bugsnag error reporter for Expo applications", @@ -32,19 +32,19 @@ "author": "Bugsnag", "license": "MIT", "dependencies": { - "@bugsnag/core": "^7.7.0", - "@bugsnag/delivery-expo": "^7.7.0", - "@bugsnag/plugin-browser-session": "^7.7.0", - "@bugsnag/plugin-console-breadcrumbs": "^7.7.0", - "@bugsnag/plugin-expo-app": "^7.7.0", - "@bugsnag/plugin-expo-device": "^7.7.0", - "@bugsnag/plugin-network-breadcrumbs": "^7.7.0", - "@bugsnag/plugin-react": "^7.7.0", - "@bugsnag/plugin-react-native-app-state-breadcrumbs": "^7.7.0", - "@bugsnag/plugin-react-native-connectivity-breadcrumbs": "^7.8.2", - "@bugsnag/plugin-react-native-global-error-handler": "^7.7.0", - "@bugsnag/plugin-react-native-orientation-breadcrumbs": "^7.7.0", - "@bugsnag/plugin-react-native-unhandled-rejection": "^7.7.0", + "@bugsnag/core": "^7.9.0-alpha.0", + "@bugsnag/delivery-expo": "^7.9.0-alpha.0", + "@bugsnag/plugin-browser-session": "^7.9.0-alpha.0", + "@bugsnag/plugin-console-breadcrumbs": "^7.9.0-alpha.0", + "@bugsnag/plugin-expo-app": "^7.9.0-alpha.0", + "@bugsnag/plugin-expo-device": "^7.9.0-alpha.0", + "@bugsnag/plugin-network-breadcrumbs": "^7.9.0-alpha.0", + "@bugsnag/plugin-react": "^7.9.0-alpha.0", + "@bugsnag/plugin-react-native-app-state-breadcrumbs": "^7.9.0-alpha.0", + "@bugsnag/plugin-react-native-connectivity-breadcrumbs": "^7.9.0-alpha.0", + "@bugsnag/plugin-react-native-global-error-handler": "^7.9.0-alpha.0", + "@bugsnag/plugin-react-native-orientation-breadcrumbs": "^7.9.0-alpha.0", + "@bugsnag/plugin-react-native-unhandled-rejection": "^7.9.0-alpha.0", "@bugsnag/source-maps": "^1.0.1", "bugsnag-build-reporter": "^1.0.1", "expo-constants": "~9.3.3" diff --git a/packages/in-flight/LICENSE.txt b/packages/in-flight/LICENSE.txt new file mode 100644 index 0000000000..ddc0631e24 --- /dev/null +++ b/packages/in-flight/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) Bugsnag, https://www.bugsnag.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/in-flight/README.md b/packages/in-flight/README.md new file mode 100644 index 0000000000..388679e7a3 --- /dev/null +++ b/packages/in-flight/README.md @@ -0,0 +1,7 @@ +# @bugsnag/in-flight + +Internal [@bugsnag/js](https://github.com/bugsnag/bugsnag-js) package to keep track of in-flight requests + +## License + +This package is free software released under the MIT License. See [LICENSE.txt](./LICENSE.txt) for details. diff --git a/packages/in-flight/package-lock.json b/packages/in-flight/package-lock.json new file mode 100644 index 0000000000..97d26ee771 --- /dev/null +++ b/packages/in-flight/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "@bugsnag/in-flight", + "version": "7.9.0-alpha.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@bugsnag/cuid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@bugsnag/cuid/-/cuid-3.0.0.tgz", + "integrity": "sha512-LOt8aaBI+KvOQGneBtpuCz3YqzyEAehd1f3nC5yr9TIYW1+IzYKa2xWS4EiMz5pPOnRPHkyyS5t/wmSmN51Gjg==" + } + } +} diff --git a/packages/in-flight/package.json b/packages/in-flight/package.json new file mode 100644 index 0000000000..4e82979ece --- /dev/null +++ b/packages/in-flight/package.json @@ -0,0 +1,30 @@ +{ + "name": "@bugsnag/in-flight", + "version": "7.9.0-alpha.0", + "main": "src/in-flight.js", + "types": "types/bugsnag-in-flight.d.ts", + "description": "Internal package to keep track of in-flight requests to Bugsnag", + "homepage": "https://www.bugsnag.com/", + "repository": { + "type": "git", + "url": "git@github.com:bugsnag/bugsnag-js.git" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "src", + "types" + ], + "author": "Bugsnag", + "license": "MIT", + "dependencies": { + "@bugsnag/cuid": "^3.0.0" + }, + "devDependencies": { + "@bugsnag/core": "^7.9.0-alpha.0" + }, + "peerDependencies": { + "@bugsnag/core": "^7.0.0" + } +} diff --git a/packages/in-flight/src/in-flight.js b/packages/in-flight/src/in-flight.js new file mode 100644 index 0000000000..364d270228 --- /dev/null +++ b/packages/in-flight/src/in-flight.js @@ -0,0 +1,87 @@ +const cuid = require('@bugsnag/cuid') + +const FLUSH_POLL_INTERVAL_MS = 50 +const inFlightRequests = new Map() + +const noop = () => {} + +module.exports = { + trackInFlight (client) { + const originalNotify = client._notify + + client._notify = function (event, onError, callback = noop) { + const id = cuid() + inFlightRequests.set(id, true) + + const _callback = function () { + inFlightRequests.delete(id) + callback.apply(null, arguments) + } + + client._depth += 1 + + try { + originalNotify.call(client, event, onError, _callback) + } finally { + client._depth -= 1 + } + } + + const patchDelivery = (delivery) => { + const originalSendSession = delivery.sendSession + + delivery.sendSession = function (session, callback = noop) { + const id = cuid() + inFlightRequests.set(id, true) + + const _callback = function () { + inFlightRequests.delete(id) + callback.apply(null, arguments) + } + + originalSendSession.call(delivery, session, _callback) + } + } + + let delivery = client._delivery + patchDelivery(delivery) + + // ensure we also monkey-patch any new delivery that might be set + Object.defineProperty(client, '_delivery', { + get () { + return delivery + }, + set (newDeliviery) { + patchDelivery(newDeliviery) + delivery = newDeliviery + } + }) + }, + + flush (timeoutMs) { + return new Promise(function (resolve, reject) { + let resolveTimeout + const rejectTimeout = setTimeout( + () => { + if (resolveTimeout) clearTimeout(resolveTimeout) + + reject(new Error(`flush timed out after ${timeoutMs}ms`)) + }, + timeoutMs + ) + + const resolveIfNoRequests = function () { + if (inFlightRequests.size === 0) { + clearTimeout(rejectTimeout) + resolve() + + return + } + + resolveTimeout = setTimeout(resolveIfNoRequests, FLUSH_POLL_INTERVAL_MS) + } + + resolveIfNoRequests() + }) + } +} diff --git a/packages/in-flight/test/in-flight.test.ts b/packages/in-flight/test/in-flight.test.ts new file mode 100644 index 0000000000..b9c0098776 --- /dev/null +++ b/packages/in-flight/test/in-flight.test.ts @@ -0,0 +1,294 @@ +import Client, { EventDeliveryPayload, SessionDeliveryPayload } from '@bugsnag/core/client' + +// The in-flight package has module level state which can leak between tests +// We can avoid this using jest's 'isolateModules' but need to type the +// 'bugsnagInFlight' variable for this test to compile +import BugsnagInFlightJustForTypescript from '../types/bugsnag-in-flight' + +let bugsnagInFlight: BugsnagInFlightJustForTypescript +jest.isolateModules(() => { bugsnagInFlight = require('../src/in-flight') }) + +describe('@bugsnag/in-flight', () => { + it('tracks in-flight events', () => { + const client = new Client({ apiKey: 'AN_API_KEY' }) + const payloads: EventDeliveryPayload[] = [] + const sendSession = jest.fn() + + client._setDelivery(() => ({ + sendEvent: (payload, cb) => { + expect(client._depth).toBe(2) + payloads.push(payload) + cb() + }, + sendSession + })) + + bugsnagInFlight.trackInFlight(client) + + expect(payloads.length).toBe(0) + + const onError = jest.fn() + const callback = jest.fn() + + expect(client._depth).toBe(1) + + client.notify(new Error('xyz'), onError, callback) + + expect(client._depth).toBe(1) + expect(onError).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledTimes(1) + expect(payloads.length).toBe(1) + expect(sendSession).not.toHaveBeenCalled() + }) + + it('tracks in-flight sessions', () => { + const client = new Client({ apiKey: 'AN_API_KEY' }) + const payloads: SessionDeliveryPayload[] = [] + const sendEvent = jest.fn() + const callback = jest.fn() + + client._sessionDelegate = { + startSession: jest.fn(function (client, session) { + client._delivery.sendSession(session, callback) + }), + pauseSession: jest.fn(), + resumeSession: jest.fn() + } + + client._setDelivery(() => ({ + sendEvent, + sendSession: (payload, cb) => { + payloads.push(payload) + cb() + } + })) + + bugsnagInFlight.trackInFlight(client) + + expect(payloads.length).toBe(0) + expect(callback).not.toHaveBeenCalled() + expect(client._sessionDelegate.startSession).not.toHaveBeenCalled() + expect(client._sessionDelegate.pauseSession).not.toHaveBeenCalled() + expect(client._sessionDelegate.resumeSession).not.toHaveBeenCalled() + + client.startSession() + + expect(payloads.length).toBe(1) + expect(callback).toHaveBeenCalledTimes(1) + expect(client._sessionDelegate.startSession).toHaveBeenCalledTimes(1) + expect(client._sessionDelegate.pauseSession).not.toHaveBeenCalled() + expect(client._sessionDelegate.resumeSession).not.toHaveBeenCalled() + }) + + it('tracks all in-flight requests', () => { + const client = new Client({ apiKey: 'AN_API_KEY' }) + const eventPayloads: EventDeliveryPayload[] = [] + const sessionPayloads: SessionDeliveryPayload[] = [] + const sessionCallback = jest.fn() + + client._sessionDelegate = { + startSession: jest.fn(function (client, session) { + client._delivery.sendSession(session, sessionCallback) + }), + pauseSession: jest.fn(), + resumeSession: jest.fn() + } + + client._setDelivery(() => ({ + sendEvent: (payload, cb) => { + expect(client._depth).toBe(2) + eventPayloads.push(payload) + cb() + }, + sendSession: (payload, cb) => { + sessionPayloads.push(payload) + cb() + } + })) + + bugsnagInFlight.trackInFlight(client) + + expect(eventPayloads.length).toBe(0) + expect(sessionPayloads.length).toBe(0) + + const onError = jest.fn() + const notifyCallback = jest.fn() + + expect(client._depth).toBe(1) + + client.notify(new Error('xyz'), onError, notifyCallback) + client.startSession() + + expect(client._depth).toBe(1) + expect(onError).toHaveBeenCalledTimes(1) + expect(notifyCallback).toHaveBeenCalledTimes(1) + expect(sessionCallback).toHaveBeenCalledTimes(1) + expect(eventPayloads.length).toBe(1) + expect(sessionPayloads.length).toBe(1) + }) + + it('can flush successfully', async () => { + const client = new Client({ apiKey: 'AN_API_KEY' }) + const eventPayloads: EventDeliveryPayload[] = [] + const sessionPayloads: SessionDeliveryPayload[] = [] + + client._sessionDelegate = { + startSession (client, session) { + client._delivery.sendSession(session, () => {}) + }, + pauseSession: () => {}, + resumeSession: () => {} + } + + client._setDelivery(() => ({ + sendEvent (payload, cb) { + setTimeout(function () { + eventPayloads.push(payload) + cb() + }, 100) + }, + sendSession (payload, cb) { + setTimeout(function () { + sessionPayloads.push(payload) + cb() + }, 100) + } + })) + + bugsnagInFlight.trackInFlight(client) + + client.notify(new Error('xyz')) + client.startSession() + + expect(eventPayloads.length).toBe(0) + expect(sessionPayloads.length).toBe(0) + + await bugsnagInFlight.flush(1000) + + expect(eventPayloads.length).toBe(1) + expect(sessionPayloads.length).toBe(1) + }) + + it('will timeout if flush takes too long', async () => { + const client = new Client({ apiKey: 'AN_API_KEY' }) + const eventPayloads: EventDeliveryPayload[] = [] + const sessionPayloads: SessionDeliveryPayload[] = [] + + client._sessionDelegate = { + startSession: (client, session) => { + client._delivery.sendSession(session, () => {}) + }, + pauseSession: () => {}, + resumeSession: () => {} + } + + client._setDelivery(() => ({ + sendEvent (payload, cb) { + setTimeout(() => { + eventPayloads.push(payload) + cb() + }, 250) + }, + sendSession (payload, cb) { + setTimeout(() => { + sessionPayloads.push(payload) + cb() + }, 250) + } + })) + + bugsnagInFlight.trackInFlight(client) + + client.notify(new Error('xyz')) + client.startSession() + + expect(eventPayloads.length).toBe(0) + expect(sessionPayloads.length).toBe(0) + + const expected = new Error('flush timed out after 10ms') + await expect(() => bugsnagInFlight.flush(10)).rejects.toThrow(expected) + + expect(eventPayloads.length).toBe(0) + expect(sessionPayloads.length).toBe(0) + + await bugsnagInFlight.flush(1000) + + expect(eventPayloads.length).toBe(1) + expect(sessionPayloads.length).toBe(1) + }) + + it('can track requests when delivery is changed', async () => { + const client = new Client({ apiKey: 'AN_API_KEY' }) + const originalEventPayloads: EventDeliveryPayload[] = [] + const originalSessionPayloads: SessionDeliveryPayload[] = [] + + client._sessionDelegate = { + startSession (client, session) { + client._delivery.sendSession(session, () => {}) + }, + pauseSession: () => {}, + resumeSession: () => {} + } + + client._setDelivery(() => ({ + sendEvent (payload, cb) { + setTimeout(function () { + originalEventPayloads.push(payload) + cb() + }, 100) + }, + sendSession (payload, cb) { + setTimeout(function () { + originalSessionPayloads.push(payload) + cb() + }, 100) + } + })) + + bugsnagInFlight.trackInFlight(client) + + client.notify(new Error('xyz')) + client.startSession() + + expect(originalEventPayloads.length).toBe(0) + expect(originalSessionPayloads.length).toBe(0) + + await bugsnagInFlight.flush(1000) + + expect(originalEventPayloads.length).toBe(1) + expect(originalSessionPayloads.length).toBe(1) + + const newEventPayloads: EventDeliveryPayload[] = [] + const newSessionPayloads: SessionDeliveryPayload[] = [] + + client._setDelivery(() => ({ + sendEvent (payload, cb) { + setTimeout(function () { + newEventPayloads.push(payload) + cb() + }, 100) + }, + sendSession (payload, cb) { + setTimeout(function () { + newSessionPayloads.push(payload) + cb() + }, 100) + } + })) + + client.notify(new Error('xyz')) + client.startSession() + + expect(originalEventPayloads.length).toBe(1) + expect(originalSessionPayloads.length).toBe(1) + expect(newEventPayloads.length).toBe(0) + expect(newSessionPayloads.length).toBe(0) + + await bugsnagInFlight.flush(1000) + + expect(originalEventPayloads.length).toBe(1) + expect(originalSessionPayloads.length).toBe(1) + expect(newEventPayloads.length).toBe(1) + expect(newSessionPayloads.length).toBe(1) + }) +}) diff --git a/packages/in-flight/types/bugsnag-in-flight.d.ts b/packages/in-flight/types/bugsnag-in-flight.d.ts new file mode 100644 index 0000000000..c2dd1a86ea --- /dev/null +++ b/packages/in-flight/types/bugsnag-in-flight.d.ts @@ -0,0 +1,8 @@ +import { Client } from '@bugsnag/core' + +interface BugsnagInFlight { + trackInFlight (client: Client): void + flush (timeoutMs: number): Promise +} + +export default BugsnagInFlight diff --git a/packages/js/package-lock.json b/packages/js/package-lock.json index 5be2a0eedc..b084d221fd 100644 --- a/packages/js/package-lock.json +++ b/packages/js/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/js", - "version": "7.8.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/js/package.json b/packages/js/package.json index 6dfd2b8033..cc5e1b69f0 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/js", - "version": "7.8.0", + "version": "7.9.0-alpha.0", "main": "node/notifier.js", "browser": "browser/notifier.js", "types": "types.d.ts", @@ -33,8 +33,8 @@ "author": "Bugsnag", "license": "MIT", "dependencies": { - "@bugsnag/browser": "^7.7.0", - "@bugsnag/node": "^7.8.0" + "@bugsnag/browser": "^7.9.0-alpha.0", + "@bugsnag/node": "^7.9.0-alpha.0" }, "devDependencies": { "@babel/cli": "^7.0.0" diff --git a/packages/node/package-lock.json b/packages/node/package-lock.json index cd1c08a0fd..aa2a22e94a 100644 --- a/packages/node/package-lock.json +++ b/packages/node/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/node", - "version": "7.8.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/node/package.json b/packages/node/package.json index 47563decb5..19bda5eca9 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/node", - "version": "7.8.0", + "version": "7.9.0-alpha.0", "main": "dist/bugsnag.js", "types": "types/bugsnag.d.ts", "description": "Bugsnag error reporter for Node.js", @@ -25,20 +25,20 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/delivery-node": "^7.7.0", - "@bugsnag/plugin-app-duration": "^7.7.0", - "@bugsnag/plugin-contextualize": "^7.7.0", - "@bugsnag/plugin-intercept": "^7.7.0", - "@bugsnag/plugin-node-device": "^7.7.0", - "@bugsnag/plugin-node-in-project": "^7.7.0", - "@bugsnag/plugin-node-surrounding-code": "^7.7.0", - "@bugsnag/plugin-node-uncaught-exception": "^7.7.0", - "@bugsnag/plugin-node-unhandled-rejection": "^7.7.0", - "@bugsnag/plugin-server-session": "^7.7.0", - "@bugsnag/plugin-strip-project-root": "^7.7.0" + "@bugsnag/delivery-node": "^7.9.0-alpha.0", + "@bugsnag/plugin-app-duration": "^7.9.0-alpha.0", + "@bugsnag/plugin-contextualize": "^7.9.0-alpha.0", + "@bugsnag/plugin-intercept": "^7.9.0-alpha.0", + "@bugsnag/plugin-node-device": "^7.9.0-alpha.0", + "@bugsnag/plugin-node-in-project": "^7.9.0-alpha.0", + "@bugsnag/plugin-node-surrounding-code": "^7.9.0-alpha.0", + "@bugsnag/plugin-node-uncaught-exception": "^7.9.0-alpha.0", + "@bugsnag/plugin-node-unhandled-rejection": "^7.9.0-alpha.0", + "@bugsnag/plugin-server-session": "^7.9.0-alpha.0", + "@bugsnag/plugin-strip-project-root": "^7.9.0-alpha.0" }, "dependencies": { - "@bugsnag/core": "^7.7.0", + "@bugsnag/core": "^7.9.0-alpha.0", "byline": "^5.0.0", "error-stack-parser": "^2.0.2", "iserror": "^0.0.2", diff --git a/packages/plugin-angular/package-lock.json b/packages/plugin-angular/package-lock.json index d4f83e1f1c..84ec28b7e4 100644 --- a/packages/plugin-angular/package-lock.json +++ b/packages/plugin-angular/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-angular", - "version": "7.8.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-angular/package.json b/packages/plugin-angular/package.json index f73d18563e..22d81491a9 100644 --- a/packages/plugin-angular/package.json +++ b/packages/plugin-angular/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-angular", - "version": "7.8.0", + "version": "7.9.0-alpha.0", "description": "Angular integration for bugsnag-js", "main": "dist/esm5/index.js", "browser": "dist/esm5/index.js", @@ -35,7 +35,7 @@ "@angular/compiler": "^7.2.15", "@angular/compiler-cli": "^7.2.15", "@angular/core": "^7.2.15", - "@bugsnag/js": "^7.8.0", + "@bugsnag/js": "^7.9.0-alpha.0", "rxjs": "^5.5.8", "typescript": "^3.2.4", "zone.js": "^0.8.26" diff --git a/packages/plugin-app-duration/app.js b/packages/plugin-app-duration/app.js index f987caef79..7d3c370bf3 100644 --- a/packages/plugin-app-duration/app.js +++ b/packages/plugin-app-duration/app.js @@ -1,11 +1,15 @@ -const appStart = new Date() +let appStart = new Date() +const reset = () => { appStart = new Date() } module.exports = { + name: 'appDuration', load: client => { client.addOnError(event => { const now = new Date() event.app.duration = now - appStart }, true) + + return { reset } } } diff --git a/packages/plugin-app-duration/package.json b/packages/plugin-app-duration/package.json index 14c09896c8..47ad15912f 100644 --- a/packages/plugin-app-duration/package.json +++ b/packages/plugin-app-duration/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-app-duration", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "app.js", "description": "@bugsnag/js plugin to set app duration in browsers and node", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-app-duration/test/app.test.ts b/packages/plugin-app-duration/test/app.test.ts index 686eb2979a..7f6911d16c 100644 --- a/packages/plugin-app-duration/test/app.test.ts +++ b/packages/plugin-app-duration/test/app.test.ts @@ -1,5 +1,6 @@ import plugin from '../app' import Client from '@bugsnag/core/client' +import Event from '@bugsnag/core/event' describe('plugin-app-duration', () => { it('includes duration in event.app', done => { @@ -20,4 +21,44 @@ describe('plugin-app-duration', () => { client.notify(new Error('acbd')) }) + + it('has a name', () => { + expect(plugin.name).toBe('appDuration') + + const client = new Client({ apiKey: 'api_key', plugins: [plugin] }) + expect(client.getPlugin('appDuration')).toBeDefined() + }) + + it('can be restarted', async () => { + let appDurationCallback = (event: Event) => { throw new Error('Should never be called') } + + const event = { app: {} } as unknown as Event + const client = { + addOnError (callback: typeof appDurationCallback) { + appDurationCallback = callback + } + } + + const result = plugin.load(client) + + const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + + await sleep(50) + + appDurationCallback(event) + expect(event.app.duration).toBeGreaterThanOrEqual(45) + + await sleep(50) + + appDurationCallback(event) + expect(event.app.duration).toBeGreaterThanOrEqual(90) + + result.reset() + + await sleep(25) + + appDurationCallback(event) + expect(event.app.duration).toBeGreaterThanOrEqual(20) + expect(event.app.duration).toBeLessThanOrEqual(90) + }) }) diff --git a/packages/plugin-aws-lambda/LICENSE.txt b/packages/plugin-aws-lambda/LICENSE.txt new file mode 100644 index 0000000000..ddc0631e24 --- /dev/null +++ b/packages/plugin-aws-lambda/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) Bugsnag, https://www.bugsnag.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/plugin-aws-lambda/README.md b/packages/plugin-aws-lambda/README.md new file mode 100644 index 0000000000..cb4ca2585b --- /dev/null +++ b/packages/plugin-aws-lambda/README.md @@ -0,0 +1,7 @@ +# @bugsnag/plugin-aws-lambda + +A [@bugsnag/js](https://github.com/bugsnag/bugsnag-js) plugin for capturing errors in AWS Lambda functions. + +## License + +This package is free software released under the MIT License. See [LICENSE.txt](./LICENSE.txt) for details. diff --git a/packages/plugin-aws-lambda/package.json b/packages/plugin-aws-lambda/package.json new file mode 100644 index 0000000000..3d9db1a635 --- /dev/null +++ b/packages/plugin-aws-lambda/package.json @@ -0,0 +1,36 @@ +{ + "name": "@bugsnag/plugin-aws-lambda", + "version": "7.9.0-alpha.0", + "main": "dist/bugsnag-aws-lambda.js", + "types": "types/bugsnag-plugin-aws-lambda.d.ts", + "description": "AWS Lambda support for @bugsnag/node", + "homepage": "https://www.bugsnag.com/", + "repository": { + "type": "git", + "url": "git@github.com:bugsnag/bugsnag-js.git" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist", + "types" + ], + "scripts": { + "clean": "rm -fr dist && mkdir dist", + "build": "npm run clean && ../../bin/bundle src/index.js --node --standalone=BugsnagPluginAwsLambda | ../../bin/extract-source-map dist/bugsnag-aws-lambda.js", + "postversion": "npm run build" + }, + "author": "Bugsnag", + "license": "MIT", + "dependencies": { + "@bugsnag/in-flight": "^7.9.0-alpha.0", + "@bugsnag/plugin-browser-session": "^7.9.0-alpha.0" + }, + "devDependencies": { + "@bugsnag/core": "^7.9.0-alpha.0" + }, + "peerDependencies": { + "@bugsnag/core": "^7.0.0" + } +} diff --git a/packages/plugin-aws-lambda/src/index.js b/packages/plugin-aws-lambda/src/index.js new file mode 100644 index 0000000000..c79296e9dd --- /dev/null +++ b/packages/plugin-aws-lambda/src/index.js @@ -0,0 +1,155 @@ +const bugsnagInFlight = require('@bugsnag/in-flight') +const BugsnagPluginBrowserSession = require('@bugsnag/plugin-browser-session') +const LambdaTimeoutApproaching = require('./lambda-timeout-approaching') + +// JS timers use a signed 32 bit integer for the millisecond parameter. SAM's +// "local invoke" has a bug that means it exceeds this amount, resulting in +// warnings. See https://github.com/aws/aws-sam-cli/issues/2519 +const MAX_TIMER_VALUE = Math.pow(2, 31) - 1 + +const BugsnagPluginAwsLambda = { + name: 'awsLambda', + + load (client) { + bugsnagInFlight.trackInFlight(client) + client._loadPlugin(BugsnagPluginBrowserSession) + + // Reset the app duration between invocations, if the plugin is loaded + const appDurationPlugin = client.getPlugin('appDuration') + + if (appDurationPlugin) { + appDurationPlugin.reset() + } + + // AWS add a default unhandledRejection listener that forcefully exits the + // process. This breaks reporting of unhandled rejections, so we have to + // remove all existing listeners and call them after we handle the rejection + if (client._config.autoDetectErrors && client._config.enabledErrorTypes.unhandledRejections) { + const listeners = process.listeners('unhandledRejection') + process.removeAllListeners('unhandledRejection') + + // 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('unhandledRejection', async (reason, promise) => { + for (const listener of listeners) { + await listener.call(process, reason, promise) + } + }) + } + + return { + createHandler ({ flushTimeoutMs = 2000, lambdaTimeoutNotifyMs = 1000 } = {}) { + return wrapHandler.bind(null, client, flushTimeoutMs, lambdaTimeoutNotifyMs) + } + } + } +} + +function wrapHandler (client, flushTimeoutMs, lambdaTimeoutNotifyMs, handler) { + let _handler = handler + + if (handler.length > 2) { + // This is a handler expecting a 'callback' argument, so we convert + // it to return a Promise so '_handler' always has the same API + _handler = promisifyHandler(handler) + } + + return async function (event, context) { + let lambdaTimeout + + // Guard against the "getRemainingTimeInMillis" being missing. This should + // never happen but could when unit testing + if (typeof context.getRemainingTimeInMillis === 'function' && + lambdaTimeoutNotifyMs > 0 + ) { + const timeoutMs = context.getRemainingTimeInMillis() - lambdaTimeoutNotifyMs + + if (timeoutMs <= MAX_TIMER_VALUE) { + lambdaTimeout = setTimeout(function () { + const handledState = { + severity: 'warning', + unhandled: true, + severityReason: { type: 'log' } + } + + const event = client.Event.create( + new LambdaTimeoutApproaching(context.getRemainingTimeInMillis()), + true, + handledState, + 'aws lambda plugin', + 0 + ) + + event.context = context.functionName || 'Lambda timeout approaching' + + client._notify(event) + }, timeoutMs) + } + } + + client.addMetadata('AWS Lambda context', context) + + if (client._config.autoTrackSessions) { + client.startSession() + } + + try { + return await _handler(event, context) + } catch (err) { + if (client._config.autoDetectErrors && client._config.enabledErrorTypes.unhandledExceptions) { + const handledState = { + severity: 'error', + unhandled: true, + severityReason: { type: 'unhandledException' } + } + + const event = client.Event.create(err, true, handledState, 'aws lambda plugin', 1) + + client._notify(event) + } + + throw err + } finally { + if (lambdaTimeout) { + clearTimeout(lambdaTimeout) + } + + try { + await bugsnagInFlight.flush(flushTimeoutMs) + } catch (err) { + client._logger.error(`Delivery may be unsuccessful: ${err.message}`) + } + } + } +} + +// Convert a handler that uses callbacks to an async handler +function promisifyHandler (handler) { + return function (event, context) { + return new Promise(function (resolve, reject) { + const result = handler(event, context, function (err, response) { + if (err) { + reject(err) + return + } + + resolve(response) + }) + + // Handle an edge case where the passed handler has the callback parameter + // but actually returns a promise. In this case we need to resolve/reject + // based on the returned promise instead of in the callback + if (isPromise(result)) { + result.then(resolve).catch(reject) + } + }) + } +} + +function isPromise (value) { + return (typeof value === 'object' || typeof value === 'function') && + typeof value.then === 'function' && + typeof value.catch === 'function' +} + +module.exports = BugsnagPluginAwsLambda diff --git a/packages/plugin-aws-lambda/src/lambda-timeout-approaching.js b/packages/plugin-aws-lambda/src/lambda-timeout-approaching.js new file mode 100644 index 0000000000..ad48611bca --- /dev/null +++ b/packages/plugin-aws-lambda/src/lambda-timeout-approaching.js @@ -0,0 +1,10 @@ +module.exports = class LambdaTimeoutApproaching extends Error { + constructor (remainingMs) { + const message = `Lambda will timeout in ${remainingMs}ms` + + super(message) + + this.name = 'LambdaTimeoutApproaching' + this.stack = [] + } +} diff --git a/packages/plugin-aws-lambda/test/index.test.ts b/packages/plugin-aws-lambda/test/index.test.ts new file mode 100644 index 0000000000..0532d8ccaa --- /dev/null +++ b/packages/plugin-aws-lambda/test/index.test.ts @@ -0,0 +1,738 @@ +import util from 'util' +import BugsnagPluginAwsLambda from '../src/' +import Client, { EventDeliveryPayload, SessionDeliveryPayload } from '@bugsnag/core/client' + +const createClient = (events: EventDeliveryPayload[], sessions: SessionDeliveryPayload[], config = {}) => { + const client = new Client({ apiKey: 'AN_API_KEY', plugins: [BugsnagPluginAwsLambda], ...config }) + + // a flush failure won't throw as we don't want to crash apps if delivery takes + // too long. To avoid the unit tests passing when this happens, we make the logger + // throw on any 'error' log call + client._logger.error = (...args) => { throw new Error(util.format(args)) } + + client._delivery = { + sendEvent (payload, cb = () => {}) { + events.push(payload) + cb() + }, + sendSession (payload, cb = () => {}) { + sessions.push(payload) + cb() + } + } + + return client +} + +const DEFAULT_REMAINING_MS = 250 +let getRemainingTimeInMillis: jest.MockedFunction<() => number> + +beforeEach(() => { + getRemainingTimeInMillis = jest.fn() + .mockReturnValueOnce(DEFAULT_REMAINING_MS) + .mockReturnValueOnce(DEFAULT_REMAINING_MS / 2) + .mockImplementationOnce(() => { throw new Error('unexpected call to "getRemainingTimeInMillis"') }) +}) + +describe('plugin: aws lambda', () => { + it('has a name', () => { + expect(BugsnagPluginAwsLambda.name).toBe('awsLambda') + + const client = new Client({ apiKey: 'AN_API_KEY', plugins: [BugsnagPluginAwsLambda] }) + const plugin = client.getPlugin('awsLambda') + + expect(plugin).toBeTruthy() + }) + + it('exports a "createHandler" function', () => { + const client = new Client({ apiKey: 'AN_API_KEY', plugins: [BugsnagPluginAwsLambda] }) + const plugin = client.getPlugin('awsLambda') + + expect(plugin).toMatchObject({ createHandler: expect.any(Function) }) + }) + + it('adds the context as metadata', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const handler = (event: any, context: any) => 'abc' + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(await wrappedHandler(event, context)).toBe('abc') + + expect(client.getMetadata('AWS Lambda context')).toEqual(context) + }) + + it('logs an error if flush times out', async () => { + const client = new Client({ apiKey: 'AN_API_KEY', plugins: [BugsnagPluginAwsLambda] }) + client._logger.error = jest.fn() + + client._delivery = { + sendEvent (payload, cb = () => {}) { + setTimeout(cb, 250) + }, + sendSession (payload, cb = () => {}) { + setTimeout(cb, 250) + } + } + + const handler = () => { + client.notify('hello') + + return 'abc' + } + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const timeoutError = new Error('flush timed out after 20ms') + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler({ flushTimeoutMs: 20 }) + const wrappedHandler = bugsnagHandler(handler) + + expect(await wrappedHandler(event, context)).toBe('abc') + expect(client._logger.error).toHaveBeenCalledWith(`Delivery may be unsuccessful: ${timeoutError.message}`) + }) + + it('returns a wrapped handler that resolves to the original return value (async)', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const handler = () => 'abc' + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(await handler()).toBe('abc') + expect(await wrappedHandler(event, context)).toBe('abc') + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(1) + }) + + it('notifies when an error is thrown (async)', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const error = new Error('oh no') + const handler = (event: any, context: any) => { throw error } + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + await expect(() => wrappedHandler(event, context)).rejects.toThrow(error) + + expect(events).toHaveLength(1) + expect(events[0].events[0].errors[0].errorMessage).toBe(error.message) + + expect(sessions).toHaveLength(1) + }) + + it('does not notify when "autoDetectErrors" is false (async)', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions, { autoDetectErrors: false }) + + const error = new Error('oh no') + const handler = (event: any, context: any) => { throw error } + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + await expect(() => wrappedHandler(event, context)).rejects.toThrow(error) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(1) + }) + + it('does not notify when "unhandledExceptions" are disabled (async)', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions, { enabledErrorTypes: { unhandledExceptions: false } }) + + const error = new Error('oh no') + const handler = (event: any, context: any) => { throw error } + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + await expect(() => wrappedHandler(event, context)).rejects.toThrow(error) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(1) + }) + + it('returns a wrapped handler that resolves to the value passed to the callback (callback)', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const handler = (event: any, context: any, callback: any) => { callback(null, 'xyz') } + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + expect(await wrappedHandler(event, context)).toBe('xyz') + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(1) + }) + + it('notifies when an error is passed (callback)', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const error = new Error('uh oh') + const handler = (event: any, context: any, callback: any) => { callback(error, 'xyz') } + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + await expect(() => wrappedHandler(event, context)).rejects.toThrow(error) + + expect(events).toHaveLength(1) + expect(events[0].events[0].errors[0].errorMessage).toBe(error.message) + + expect(sessions).toHaveLength(1) + }) + + it('does not notify when "autoDetectErrors" is false (callback)', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions, { autoDetectErrors: false }) + + const error = new Error('uh oh') + const handler = (event: any, context: any, callback: any) => { callback(error, 'xyz') } + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + await expect(() => wrappedHandler(event, context)).rejects.toThrow(error) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(1) + }) + + it('does not notify when "unhandledExceptions" are disabled (callback)', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions, { enabledErrorTypes: { unhandledExceptions: false } }) + + const error = new Error('uh oh') + const handler = (event: any, context: any, callback: any) => { callback(error, 'xyz') } + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + await expect(() => wrappedHandler(event, context)).rejects.toThrow(error) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(1) + }) + + it('works when an async handler has the callback parameter', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const handler = async (event: any, context: any, callback: any) => 'abcxyz' + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + expect(await wrappedHandler(event, context)).toBe('abcxyz') + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(1) + }) + + it('works when an async handler has the callback parameter and calls it', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const handler = async (event: any, context: any, callback: any) => { callback(null, 'abcxyz') } + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + expect(await wrappedHandler(event, context)).toBe('abcxyz') + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(1) + }) + + it('works when an async handler has the callback parameter and throws', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const error = new Error('abcxyz') + const handler = async (event: any, context: any, callback: any) => { throw error } + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + await expect(() => wrappedHandler(event, context)).rejects.toThrow(error) + + expect(events).toHaveLength(1) + expect(events[0].events[0].errors[0].errorMessage).toBe(error.message) + + expect(sessions).toHaveLength(1) + }) + + it('works when an async handler has the callback parameter and calls it with an error', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const error = new Error('abcxyz') + const handler = async (event: any, context: any, callback: any) => { callback(error) } + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + await expect(() => wrappedHandler(event, context)).rejects.toThrow(error) + + expect(events).toHaveLength(1) + expect(events[0].events[0].errors[0].errorMessage).toBe(error.message) + + expect(sessions).toHaveLength(1) + }) + + it('will track sessions when "autoTrackSessions" is enabled', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + const client = createClient(events, sessions, { autoTrackSessions: true }) + + const handler = () => 'abc' + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(await wrappedHandler(event, context)).toBe('abc') + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(1) + }) + + it('will not track sessions when "autoTrackSessions" is disabled', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + const client = createClient(events, sessions, { autoTrackSessions: false }) + + const handler = () => 'abc' + + const event = { very: 'eventy' } + const context = { extremely: 'contextual' } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(await wrappedHandler(event, context)).toBe('abc') + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + }) + + it('notifies when it is close to timing out (async)', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const handler = async (event: any, context: any) => new Promise(resolve => { + setTimeout(() => resolve('xyz'), DEFAULT_REMAINING_MS + 100) + }) + + const event = { very: 'eventy' } + const context = { extremely: 'contextual', getRemainingTimeInMillis } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + expect(await wrappedHandler(event, context)).toBe('xyz') + + expect(events).toHaveLength(1) + expect(events[0].events).toHaveLength(1) + expect(events[0].events[0].errors).toHaveLength(1) + expect(events[0].events[0].context).toBe('Lambda timeout approaching') + + const expectedError = { + errorClass: 'LambdaTimeoutApproaching', + errorMessage: `Lambda will timeout in ${DEFAULT_REMAINING_MS / 2}ms`, + stacktrace: [], + type: 'nodejs' + } + + expect(events[0].events[0].errors[0]).toEqual(expectedError) + + expect(sessions).toHaveLength(1) + }) + + it('notifies when it is close to timing out (callback)', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const handler = (event: any, context: any, callback: any) => new Promise(resolve => { + setTimeout(() => callback(null, 'xyz'), DEFAULT_REMAINING_MS + 100) + }) + + const event = { very: 'eventy' } + const context = { extremely: 'contextual', getRemainingTimeInMillis } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + expect(await wrappedHandler(event, context)).toBe('xyz') + + expect(events).toHaveLength(1) + expect(events[0].events).toHaveLength(1) + expect(events[0].events[0].errors).toHaveLength(1) + expect(events[0].events[0].context).toBe('Lambda timeout approaching') + + const expectedError = { + errorClass: 'LambdaTimeoutApproaching', + errorMessage: `Lambda will timeout in ${DEFAULT_REMAINING_MS / 2}ms`, + stacktrace: [], + type: 'nodejs' + } + + expect(events[0].events[0].errors[0]).toEqual(expectedError) + + expect(sessions).toHaveLength(1) + }) + + it('uses the function name as the event context when present', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const handler = async (event: any, context: any) => new Promise(resolve => { + setTimeout(() => resolve('xyz'), DEFAULT_REMAINING_MS + 100) + }) + + const event = { very: 'eventy' } + const context = { functionName: 'MyCoolAndGoodLambdaFunction', getRemainingTimeInMillis } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler() + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + expect(await wrappedHandler(event, context)).toBe('xyz') + + expect(events).toHaveLength(1) + expect(events[0].events[0].errors[0].errorClass).toBe('LambdaTimeoutApproaching') + expect(events[0].events[0].errors[0].errorMessage).toBe(`Lambda will timeout in ${DEFAULT_REMAINING_MS / 2}ms`) + expect(events[0].events[0].errors[0].stacktrace).toHaveLength(0) + expect(events[0].events[0].context).toBe('MyCoolAndGoodLambdaFunction') + + expect(sessions).toHaveLength(1) + }) + + it('allows the "lambdaTimeoutNotifyMs" to be changed', async () => { + // With 6 seconds remaining and a resolve timeout of 500ms, the timeout + // warning will never be triggered unless the custom "lambdaTimeoutNotifyMs" + // takes effect + const superLongWaitMs = 6000 + const resolveTimeoutMs = 500 + const lambdaTimeoutNotifyMs = superLongWaitMs - (resolveTimeoutMs / 2) + + getRemainingTimeInMillis = jest.fn() + .mockReturnValueOnce(superLongWaitMs) + .mockReturnValueOnce(superLongWaitMs - lambdaTimeoutNotifyMs) + .mockImplementationOnce(() => { throw new Error('unexpected call to "getRemainingTimeInMillis"') }) + + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const handler = async (event: any, context: any) => new Promise(resolve => { + setTimeout(() => resolve('xyz'), resolveTimeoutMs) + }) + + const event = { very: 'eventy' } + const context = { extremely: 'contextual', getRemainingTimeInMillis } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler({ lambdaTimeoutNotifyMs }) + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + expect(await wrappedHandler(event, context)).toBe('xyz') + + expect(events).toHaveLength(1) + expect(events[0].events).toHaveLength(1) + expect(events[0].events[0].errors).toHaveLength(1) + expect(events[0].events[0].context).toBe('Lambda timeout approaching') + + const expectedError = { + errorClass: 'LambdaTimeoutApproaching', + errorMessage: `Lambda will timeout in ${resolveTimeoutMs / 2}ms`, + stacktrace: [], + type: 'nodejs' + } + + expect(events[0].events[0].errors[0]).toEqual(expectedError) + + expect(sessions).toHaveLength(1) + }) + + it('does not notify if "lambdaTimeoutNotifyMs" is 0', async () => { + const events: EventDeliveryPayload[] = [] + const sessions: SessionDeliveryPayload[] = [] + + const client = createClient(events, sessions) + + const handler = async (event: any, context: any) => new Promise(resolve => { + setTimeout(() => resolve('xyz'), 100) + }) + + const event = { very: 'eventy' } + const context = { extremely: 'contextual', getRemainingTimeInMillis } + + const plugin = client.getPlugin('awsLambda') + + if (!plugin) { + throw new Error('Plugin was not loaded!') + } + + const bugsnagHandler = plugin.createHandler({ lambdaTimeoutNotifyMs: 0 }) + const wrappedHandler = bugsnagHandler(handler) + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(0) + + expect(await wrappedHandler(event, context)).toBe('xyz') + + expect(events).toHaveLength(0) + expect(sessions).toHaveLength(1) + }) +}) diff --git a/packages/plugin-aws-lambda/types/bugsnag-plugin-aws-lambda.d.ts b/packages/plugin-aws-lambda/types/bugsnag-plugin-aws-lambda.d.ts new file mode 100644 index 0000000000..5b0b0e1a3f --- /dev/null +++ b/packages/plugin-aws-lambda/types/bugsnag-plugin-aws-lambda.d.ts @@ -0,0 +1,25 @@ +import { Plugin, Client } from '@bugsnag/core' + +declare const BugsnagPluginAwsLambda: Plugin +export default BugsnagPluginAwsLambda + +type AsyncHandler = (event: any, context: any) => Promise +type CallbackHandler = (event: any, context: any, callback: (err: Error|null, response: any) => void) => void + +export type BugsnagPluginAwsLambdaHandler = (handler: AsyncHandler|CallbackHandler) => AsyncHandler + +export interface BugsnagPluginAwsLambdaConfiguration { + flushTimeoutMs?: number + lambdaTimeoutNotifyMs?: number +} + +export interface BugsnagPluginAwsLambdaResult { + createHandler(configuration?: BugsnagPluginAwsLambdaConfiguration): BugsnagPluginAwsLambdaHandler +} + +// add a new call signature for the getPlugin() method that types the plugin result +declare module '@bugsnag/core' { + interface Client { + getPlugin(id: 'awsLambda'): BugsnagPluginAwsLambdaResult | undefined + } +} diff --git a/packages/plugin-browser-context/package-lock.json b/packages/plugin-browser-context/package-lock.json index 7539db5c8e..16849b23c2 100644 --- a/packages/plugin-browser-context/package-lock.json +++ b/packages/plugin-browser-context/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-browser-context", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-browser-context/package.json b/packages/plugin-browser-context/package.json index e202890404..aad6a72d9c 100644 --- a/packages/plugin-browser-context/package.json +++ b/packages/plugin-browser-context/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-browser-context", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "context.js", "description": "@bugsnag/js plugin to set event context in browsers", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-browser-device/package-lock.json b/packages/plugin-browser-device/package-lock.json index 0c088eace9..7ad37cda2b 100644 --- a/packages/plugin-browser-device/package-lock.json +++ b/packages/plugin-browser-device/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-browser-device", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-browser-device/package.json b/packages/plugin-browser-device/package.json index b9e72d057d..b8e1481dd4 100644 --- a/packages/plugin-browser-device/package.json +++ b/packages/plugin-browser-device/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-browser-device", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "device.js", "description": "@bugsnag/js plugin to set device info in browsers", "homepage": "https://www.bugsnag.com/", @@ -20,7 +20,7 @@ "@bugsnag/cuid": "^3.0.0" }, "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-browser-request/package-lock.json b/packages/plugin-browser-request/package-lock.json index 5cf4f8fdb0..ef1334a92f 100644 --- a/packages/plugin-browser-request/package-lock.json +++ b/packages/plugin-browser-request/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-browser-request", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-browser-request/package.json b/packages/plugin-browser-request/package.json index b1e2f847a4..e4bcd8425d 100644 --- a/packages/plugin-browser-request/package.json +++ b/packages/plugin-browser-request/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-browser-request", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "request.js", "description": "@bugsnag/js plugin to set request info in browsers", "homepage": "https://www.bugsnag.com/", @@ -18,7 +18,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-browser-session/package-lock.json b/packages/plugin-browser-session/package-lock.json index 996ab94c5e..dab8cf0036 100644 --- a/packages/plugin-browser-session/package-lock.json +++ b/packages/plugin-browser-session/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-browser-session", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-browser-session/package.json b/packages/plugin-browser-session/package.json index 314698debb..e1742a0786 100644 --- a/packages/plugin-browser-session/package.json +++ b/packages/plugin-browser-session/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-browser-session", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "session.js", "description": "@bugsnag/js plugin to enable session tracking in browsers", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-browser-session/session.js b/packages/plugin-browser-session/session.js index 0313debe43..fdee3c102d 100644 --- a/packages/plugin-browser-session/session.js +++ b/packages/plugin-browser-session/session.js @@ -31,13 +31,21 @@ const sessionDelegate = { return sessionClient }, resumeSession: (client) => { + // Do nothing if there's already an active session + if (client._session) { + return client + } + + // If we have a paused session then make it the active session if (client._pausedSession) { client._session = client._pausedSession client._pausedSession = null + return client - } else { - return client.startSession() } + + // Otherwise start a new session + return client.startSession() }, pauseSession: (client) => { client._pausedSession = client._session diff --git a/packages/plugin-client-ip/package-lock.json b/packages/plugin-client-ip/package-lock.json index 6d6d36cef3..e911387141 100644 --- a/packages/plugin-client-ip/package-lock.json +++ b/packages/plugin-client-ip/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-client-ip", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-client-ip/package.json b/packages/plugin-client-ip/package.json index 222f22699e..972eb80bf1 100644 --- a/packages/plugin-client-ip/package.json +++ b/packages/plugin-client-ip/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-client-ip", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "client-ip.js", "description": "@bugsnag/js plugin to disable client IP from error reports", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-console-breadcrumbs/package-lock.json b/packages/plugin-console-breadcrumbs/package-lock.json index ad6d2ade27..cf02dba3a4 100644 --- a/packages/plugin-console-breadcrumbs/package-lock.json +++ b/packages/plugin-console-breadcrumbs/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-console-breadcrumbs", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-console-breadcrumbs/package.json b/packages/plugin-console-breadcrumbs/package.json index b776e58a22..1a07d9dfaf 100644 --- a/packages/plugin-console-breadcrumbs/package.json +++ b/packages/plugin-console-breadcrumbs/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-console-breadcrumbs", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "console-breadcrumbs.js", "description": "@bugsnag/js plugin to record console log method calls as breadcrumbs", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-contextualize/package-lock.json b/packages/plugin-contextualize/package-lock.json index 4f42c352ec..5c1ed56093 100644 --- a/packages/plugin-contextualize/package-lock.json +++ b/packages/plugin-contextualize/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-contextualize", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-contextualize/package.json b/packages/plugin-contextualize/package.json index fc3b09146d..437d6173e2 100644 --- a/packages/plugin-contextualize/package.json +++ b/packages/plugin-contextualize/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-contextualize", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "contextualize.js", "description": "@bugsnag/js plugin to add context to unhandled events", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-expo-app/package-lock.json b/packages/plugin-expo-app/package-lock.json index 2da41aa54e..290de42033 100644 --- a/packages/plugin-expo-app/package-lock.json +++ b/packages/plugin-expo-app/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-expo-app", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-expo-app/package.json b/packages/plugin-expo-app/package.json index 25e0296d65..b961bfeebd 100644 --- a/packages/plugin-expo-app/package.json +++ b/packages/plugin-expo-app/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-expo-app", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "app.js", "description": "@bugsnag/js plugin to provide information about an Expo app", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "dependencies": { "expo-constants": "~9.3.3" diff --git a/packages/plugin-expo-device/package-lock.json b/packages/plugin-expo-device/package-lock.json index 612e592a34..47d275754e 100644 --- a/packages/plugin-expo-device/package-lock.json +++ b/packages/plugin-expo-device/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-expo-device", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-expo-device/package.json b/packages/plugin-expo-device/package.json index 03f2e2512d..78100b3a1e 100644 --- a/packages/plugin-expo-device/package.json +++ b/packages/plugin-expo-device/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-expo-device", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "device.js", "description": "@bugsnag/js plugin to attach Expo-specific device info to events", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "dependencies": { "expo-constants": "~9.3.3", diff --git a/packages/plugin-express/package-lock.json b/packages/plugin-express/package-lock.json index 298c87a621..75ea03e78e 100644 --- a/packages/plugin-express/package-lock.json +++ b/packages/plugin-express/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-express", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-express/package.json b/packages/plugin-express/package.json index 3d7a2cdb0c..0eba31ca36 100644 --- a/packages/plugin-express/package.json +++ b/packages/plugin-express/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-express", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "dist/bugsnag-express.js", "types": "types/bugsnag-express.d.ts", "description": "@bugsnag/js error handling middleware for Express (and Connect) web servers", @@ -28,7 +28,7 @@ "@bugsnag/core": "^7.0.0" }, "devDependencies": { - "@bugsnag/core": "^7.7.0", + "@bugsnag/core": "^7.9.0-alpha.0", "@types/express": "^4.17.6" }, "dependencies": { diff --git a/packages/plugin-express/src/express.js b/packages/plugin-express/src/express.js index 80c4864571..c2f72b7a10 100644 --- a/packages/plugin-express/src/express.js +++ b/packages/plugin-express/src/express.js @@ -18,8 +18,8 @@ module.exports = { const dom = domain.create() // Get a client to be scoped to this request. If sessions are enabled, use the - // startSession() call to get a session client, otherwise, clone the existing client. - const requestClient = client._config.autoTrackSessions ? client.startSession() : clone(client) + // resumeSession() call to get a session client, otherwise, clone the existing client. + const requestClient = client._config.autoTrackSessions ? client.resumeSession() : clone(client) // attach it to the request req.bugsnag = requestClient diff --git a/packages/plugin-inline-script-content/package-lock.json b/packages/plugin-inline-script-content/package-lock.json index af9fa54444..7b5bbf2344 100644 --- a/packages/plugin-inline-script-content/package-lock.json +++ b/packages/plugin-inline-script-content/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-inline-script-content", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-inline-script-content/package.json b/packages/plugin-inline-script-content/package.json index 30b0bb6ac7..8cb077ae17 100644 --- a/packages/plugin-inline-script-content/package.json +++ b/packages/plugin-inline-script-content/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-inline-script-content", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "inline-script-content.js", "description": "@bugsnag/js plugin to attach inline script content to error events", "homepage": "https://www.bugsnag.com/", @@ -18,7 +18,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-interaction-breadcrumbs/package-lock.json b/packages/plugin-interaction-breadcrumbs/package-lock.json index 36a79c59f3..599ac7cc3d 100644 --- a/packages/plugin-interaction-breadcrumbs/package-lock.json +++ b/packages/plugin-interaction-breadcrumbs/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-interaction-breadcrumbs", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-interaction-breadcrumbs/package.json b/packages/plugin-interaction-breadcrumbs/package.json index e9ad63b8e1..8dc9f881d9 100644 --- a/packages/plugin-interaction-breadcrumbs/package.json +++ b/packages/plugin-interaction-breadcrumbs/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-interaction-breadcrumbs", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "interaction-breadcrumbs.js", "description": "@bugsnag/js plugin to record UI click events as breadcrumbs", "homepage": "https://www.bugsnag.com/", @@ -18,7 +18,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-intercept/package-lock.json b/packages/plugin-intercept/package-lock.json index a0f0218d3d..1d32d2078b 100644 --- a/packages/plugin-intercept/package-lock.json +++ b/packages/plugin-intercept/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-intercept", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-intercept/package.json b/packages/plugin-intercept/package.json index f5f6269c1c..dd43c2958b 100644 --- a/packages/plugin-intercept/package.json +++ b/packages/plugin-intercept/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-intercept", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "intercept.js", "description": "@bugsnag/js plugin providing convenience functions for intercepting asynchronous errors", "homepage": "https://www.bugsnag.com/", @@ -18,7 +18,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-koa/package-lock.json b/packages/plugin-koa/package-lock.json index 2b9f7cc247..288e5460ec 100644 --- a/packages/plugin-koa/package-lock.json +++ b/packages/plugin-koa/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-koa", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-koa/package.json b/packages/plugin-koa/package.json index ce51fa6150..9ac786de04 100644 --- a/packages/plugin-koa/package.json +++ b/packages/plugin-koa/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-koa", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "dist/bugsnag-koa.js", "types": "types/bugsnag-koa.d.ts", "description": "@bugsnag/js error handling middleware for Koa web servers", @@ -28,7 +28,7 @@ "@bugsnag/core": "^7.0.0" }, "devDependencies": { - "@bugsnag/core": "^7.7.0", + "@bugsnag/core": "^7.9.0-alpha.0", "@types/koa": "^2.11.3" }, "dependencies": { diff --git a/packages/plugin-koa/src/koa.js b/packages/plugin-koa/src/koa.js index 0419b7a7d1..ed7e148137 100644 --- a/packages/plugin-koa/src/koa.js +++ b/packages/plugin-koa/src/koa.js @@ -15,8 +15,8 @@ module.exports = { load: client => { const requestHandler = async (ctx, next) => { // Get a client to be scoped to this request. If sessions are enabled, use the - // startSession() call to get a session client, otherwise, clone the existing client. - const requestClient = client._config.autoTrackSessions ? client.startSession() : clone(client) + // resumeSession() call to get a session client, otherwise, clone the existing client. + const requestClient = client._config.autoTrackSessions ? client.resumeSession() : clone(client) ctx.bugsnag = requestClient @@ -47,8 +47,8 @@ module.exports = { requestHandler.v1 = function * (next) { // Get a client to be scoped to this request. If sessions are enabled, use the - // startSession() call to get a session client, otherwise, clone the existing client. - const requestClient = client._config.autoTrackSessions ? client.startSession() : clone(client) + // resumeSession() call to get a session client, otherwise, clone the existing client. + const requestClient = client._config.autoTrackSessions ? client.resumeSession() : clone(client) this.bugsnag = requestClient diff --git a/packages/plugin-koa/test/koa.test.ts b/packages/plugin-koa/test/koa.test.ts index 8320e3d169..ad46abc040 100644 --- a/packages/plugin-koa/test/koa.test.ts +++ b/packages/plugin-koa/test/koa.test.ts @@ -7,7 +7,7 @@ describe('plugin: koa', () => { c._sessionDelegate = { startSession: () => c, pauseSession: () => {}, - resumeSession: () => {} + resumeSession: () => c } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const middleware = c.getPlugin('koa')! @@ -23,7 +23,7 @@ describe('plugin: koa', () => { c._sessionDelegate = { startSession: () => c, pauseSession: () => {}, - resumeSession: () => {} + resumeSession: () => c } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/packages/plugin-navigation-breadcrumbs/package-lock.json b/packages/plugin-navigation-breadcrumbs/package-lock.json index c76c11e326..4e3c41fe11 100644 --- a/packages/plugin-navigation-breadcrumbs/package-lock.json +++ b/packages/plugin-navigation-breadcrumbs/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-navigation-breadcrumbs", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-navigation-breadcrumbs/package.json b/packages/plugin-navigation-breadcrumbs/package.json index 6cc3a821c2..29b88804d9 100644 --- a/packages/plugin-navigation-breadcrumbs/package.json +++ b/packages/plugin-navigation-breadcrumbs/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-navigation-breadcrumbs", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "navigation-breadcrumbs.js", "description": "@bugsnag/js plugin to record browser navigation as breadcrumbs", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-network-breadcrumbs/package-lock.json b/packages/plugin-network-breadcrumbs/package-lock.json index 07dec45778..94c211264d 100644 --- a/packages/plugin-network-breadcrumbs/package-lock.json +++ b/packages/plugin-network-breadcrumbs/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-network-breadcrumbs", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-network-breadcrumbs/package.json b/packages/plugin-network-breadcrumbs/package.json index ebacd59701..faf63af9ed 100644 --- a/packages/plugin-network-breadcrumbs/package.json +++ b/packages/plugin-network-breadcrumbs/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-network-breadcrumbs", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "network-breadcrumbs.js", "description": "@bugsnag/js plugin to record browser requests as breadcrumbs", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-node-device/package-lock.json b/packages/plugin-node-device/package-lock.json index 2723bcf120..cf3b77806d 100644 --- a/packages/plugin-node-device/package-lock.json +++ b/packages/plugin-node-device/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-node-device", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-node-device/package.json b/packages/plugin-node-device/package.json index 2fe437b5f0..3cf1f798ea 100644 --- a/packages/plugin-node-device/package.json +++ b/packages/plugin-node-device/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-node-device", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "device.js", "description": "@bugsnag/js plugin to set device info in node", "homepage": "https://www.bugsnag.com/", @@ -18,7 +18,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-node-in-project/package-lock.json b/packages/plugin-node-in-project/package-lock.json index ee4b722aad..7b7fce0924 100644 --- a/packages/plugin-node-in-project/package-lock.json +++ b/packages/plugin-node-in-project/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-node-in-project", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-node-in-project/package.json b/packages/plugin-node-in-project/package.json index 71ab00b5a6..e99ecdbd1a 100644 --- a/packages/plugin-node-in-project/package.json +++ b/packages/plugin-node-in-project/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-node-in-project", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "in-project.js", "description": "@bugsnag/js plugin to mark wether stackframes are 'in-project'", "homepage": "https://www.bugsnag.com/", @@ -18,7 +18,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-node-surrounding-code/package-lock.json b/packages/plugin-node-surrounding-code/package-lock.json index d48bba4987..21f969b6d6 100644 --- a/packages/plugin-node-surrounding-code/package-lock.json +++ b/packages/plugin-node-surrounding-code/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-node-surrounding-code", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-node-surrounding-code/package.json b/packages/plugin-node-surrounding-code/package.json index 6b1c3cce4b..7d7a482bab 100644 --- a/packages/plugin-node-surrounding-code/package.json +++ b/packages/plugin-node-surrounding-code/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-node-surrounding-code", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "surrounding-code.js", "description": "@bugsnag/js plugin to load surrounding code in Node stacktraces", "homepage": "https://www.bugsnag.com/", @@ -22,7 +22,7 @@ "pump": "^3.0.0" }, "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-node-uncaught-exception/package-lock.json b/packages/plugin-node-uncaught-exception/package-lock.json index c089247d33..9f77c3fd2c 100644 --- a/packages/plugin-node-uncaught-exception/package-lock.json +++ b/packages/plugin-node-uncaught-exception/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-node-uncaught-exception", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-node-uncaught-exception/package.json b/packages/plugin-node-uncaught-exception/package.json index 7dd6905061..3acbc3fbbf 100644 --- a/packages/plugin-node-uncaught-exception/package.json +++ b/packages/plugin-node-uncaught-exception/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-node-uncaught-exception", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "uncaught-exception.js", "description": "@bugsnag/js plugin to capture and report uncaught exceptions", "homepage": "https://www.bugsnag.com/", @@ -18,7 +18,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-node-unhandled-rejection/package-lock.json b/packages/plugin-node-unhandled-rejection/package-lock.json index 0f60f73e2d..91380e55b1 100644 --- a/packages/plugin-node-unhandled-rejection/package-lock.json +++ b/packages/plugin-node-unhandled-rejection/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-node-unhandled-rejection", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-node-unhandled-rejection/package.json b/packages/plugin-node-unhandled-rejection/package.json index 51ad2d25d4..ac8c5a85fd 100644 --- a/packages/plugin-node-unhandled-rejection/package.json +++ b/packages/plugin-node-unhandled-rejection/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-node-unhandled-rejection", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "unhandled-rejection.js", "description": "@bugsnag/js plugin to capture and report unhandled rejections", "homepage": "https://www.bugsnag.com/", @@ -18,7 +18,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.ts b/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.ts index cedf313d59..be8465a0f3 100644 --- a/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.ts +++ b/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.ts @@ -88,4 +88,89 @@ describe('plugin: node unhandled rejection handler', () => { })) process.listeners('unhandledRejection')[0](new Error('never gonna catch me'), Promise.resolve()) }) + + it('should return a promise that resolves after the onUnhandledRejection callback is called', async () => { + try { + const options = { + apiKey: 'api_key', + onUnhandledRejection: jest.fn(), + plugins: [plugin] + } + + const pluginSchema = { + ...schema, + onUnhandledRejection: { + validate: (val: unknown) => typeof val === 'function', + message: 'should be a function', + defaultValue: () => {} + } + } + + const client = new Client(options, pluginSchema) + + client._setDelivery(client => ({ + sendEvent: (payload, cb) => cb(), + sendSession: (payload, cb) => cb() + })) + + const listener = process.listeners('unhandledRejection')[0] + + expect(options.onUnhandledRejection).not.toHaveBeenCalled() + + await listener(new Error('never gonna catch me'), Promise.resolve()) + + expect(options.onUnhandledRejection).toHaveBeenCalledTimes(1) + } finally { + plugin.destroy() + } + }) + + it('should prepend its listener (Node 6+)', async () => { + // Skip this test on Node 4/5 as prependListener doesn't exist + if (process.version.startsWith('v4.') || process.version.startsWith('v5.')) { + return + } + + const listener = () => {} + + try { + process.on('unhandledRejection', listener) + + const listenersBefore = process.listeners('unhandledRejection') + + expect(listenersBefore).toHaveLength(1) + expect(listenersBefore[0]).toBe(listener) + + const options = { + apiKey: 'api_key', + onUnhandledRejection: jest.fn(), + plugins: [plugin] + } + + const pluginSchema = { + ...schema, + onUnhandledRejection: { + validate: (val: unknown) => typeof val === 'function', + message: 'should be a function', + defaultValue: () => {} + } + } + + const client = new Client(options, pluginSchema) + + client._setDelivery(client => ({ + sendEvent: (payload, cb) => cb(), + sendSession: (payload, cb) => cb() + })) + + const listenersAfter = process.listeners('unhandledRejection') + + expect(listenersAfter).toHaveLength(2) + expect(listenersAfter[0]).not.toBe(listener) + expect(listenersAfter[1]).toBe(listener) + } finally { + process.removeListener('unhandledRejection', listener) + plugin.destroy() + } + }) }) diff --git a/packages/plugin-node-unhandled-rejection/unhandled-rejection.js b/packages/plugin-node-unhandled-rejection/unhandled-rejection.js index d67aa837ad..20ac5530d6 100644 --- a/packages/plugin-node-unhandled-rejection/unhandled-rejection.js +++ b/packages/plugin-node-unhandled-rejection/unhandled-rejection.js @@ -8,12 +8,22 @@ module.exports = { unhandled: true, severityReason: { type: 'unhandledPromiseRejection' } }, 'unhandledRejection handler', 1) - client._notify(event, () => {}, (e, event) => { - if (e) client._logger.error('Failed to send event to Bugsnag') - client._config.onUnhandledRejection(err, event, client._logger) + + return new Promise(resolve => { + client._notify(event, () => {}, (e, event) => { + if (e) client._logger.error('Failed to send event to Bugsnag') + client._config.onUnhandledRejection(err, event, client._logger) + resolve() + }) }) } - process.on('unhandledRejection', _handler) + + // Prepend the listener if we can (Node 6+) + if (process.prependListener) { + process.prependListener('unhandledRejection', _handler) + } else { + process.on('unhandledRejection', _handler) + } }, destroy: () => { process.removeListener('unhandledRejection', _handler) diff --git a/packages/plugin-react-native-app-state-breadcrumbs/package-lock.json b/packages/plugin-react-native-app-state-breadcrumbs/package-lock.json index bf0cbf79f5..e26e4ed354 100644 --- a/packages/plugin-react-native-app-state-breadcrumbs/package-lock.json +++ b/packages/plugin-react-native-app-state-breadcrumbs/package-lock.json @@ -2133,5 +2133,5 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" } }, - "version": "7.7.0" + "version": "7.9.0-alpha.0" } diff --git a/packages/plugin-react-native-app-state-breadcrumbs/package.json b/packages/plugin-react-native-app-state-breadcrumbs/package.json index 427896f4bc..3f29f0fed8 100644 --- a/packages/plugin-react-native-app-state-breadcrumbs/package.json +++ b/packages/plugin-react-native-app-state-breadcrumbs/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-app-state-breadcrumbs", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "app-state.js", "description": "@bugsnag/js plugin to create breadcrumbs when a React Native app enters the foreground/background", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-react-native-client-sync/package-lock.json b/packages/plugin-react-native-client-sync/package-lock.json index 3b96684349..1b473e0056 100644 --- a/packages/plugin-react-native-client-sync/package-lock.json +++ b/packages/plugin-react-native-client-sync/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-client-sync", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-react-native-client-sync/package.json b/packages/plugin-react-native-client-sync/package.json index 7a549c5996..d164116e30 100644 --- a/packages/plugin-react-native-client-sync/package.json +++ b/packages/plugin-react-native-client-sync/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-client-sync", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "client-sync.js", "description": "@bugsnag/react-native plugin to sync information between JS and native layer", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-react-native-connectivity-breadcrumbs/package-lock.json b/packages/plugin-react-native-connectivity-breadcrumbs/package-lock.json index 1b76e85a68..27830ed190 100644 --- a/packages/plugin-react-native-connectivity-breadcrumbs/package-lock.json +++ b/packages/plugin-react-native-connectivity-breadcrumbs/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-connectivity-breadcrumbs", - "version": "7.8.2", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-react-native-connectivity-breadcrumbs/package.json b/packages/plugin-react-native-connectivity-breadcrumbs/package.json index 84d072e4c5..1a2995dd34 100644 --- a/packages/plugin-react-native-connectivity-breadcrumbs/package.json +++ b/packages/plugin-react-native-connectivity-breadcrumbs/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-connectivity-breadcrumbs", - "version": "7.8.2", + "version": "7.9.0-alpha.0", "main": "connectivity.js", "description": "@bugsnag/js plugin to create breadcrumbs when the network status changes in a React Native app", "homepage": "https://www.bugsnag.com/", @@ -20,7 +20,7 @@ "@react-native-community/netinfo": "5.9.7" }, "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-react-native-event-sync/package-lock.json b/packages/plugin-react-native-event-sync/package-lock.json index a6ba4cc2c3..f0349e8491 100644 --- a/packages/plugin-react-native-event-sync/package-lock.json +++ b/packages/plugin-react-native-event-sync/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-event-sync", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-react-native-event-sync/package.json b/packages/plugin-react-native-event-sync/package.json index ebf9f48d3b..c5fffd0164 100644 --- a/packages/plugin-react-native-event-sync/package.json +++ b/packages/plugin-react-native-event-sync/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-event-sync", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "event-sync.js", "description": "@bugsnag/react-native plugin to sync native event information in an onError callbacks", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-react-native-global-error-handler/package-lock.json b/packages/plugin-react-native-global-error-handler/package-lock.json index 3cf7bafd3c..164d8ae505 100644 --- a/packages/plugin-react-native-global-error-handler/package-lock.json +++ b/packages/plugin-react-native-global-error-handler/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-global-error-handler", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-react-native-global-error-handler/package.json b/packages/plugin-react-native-global-error-handler/package.json index c6d0edb16c..4f205aa1c4 100644 --- a/packages/plugin-react-native-global-error-handler/package.json +++ b/packages/plugin-react-native-global-error-handler/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-global-error-handler", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "error-handler.js", "description": "@bugsnag/js plugin to report unhandled exceptions in React Native", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-react-native-hermes/package.json b/packages/plugin-react-native-hermes/package.json index 82e1c04123..3255f49871 100644 --- a/packages/plugin-react-native-hermes/package.json +++ b/packages/plugin-react-native-hermes/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-hermes", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "hermes.js", "description": "@bugsnag/react-native plugin to support Hermes", "homepage": "https://www.bugsnag.com/", @@ -18,7 +18,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-react-native-navigation/package-lock.json b/packages/plugin-react-native-navigation/package-lock.json index b77352aa60..294edbaaa2 100644 --- a/packages/plugin-react-native-navigation/package-lock.json +++ b/packages/plugin-react-native-navigation/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-navigation", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-react-native-navigation/package.json b/packages/plugin-react-native-navigation/package.json index fa241942c6..087b6b2854 100644 --- a/packages/plugin-react-native-navigation/package.json +++ b/packages/plugin-react-native-navigation/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-navigation", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "react-native-navigation.js", "types": "types/react-native-navigation.d.ts", "description": "@bugsnag/react-native plugin for integration with react-native-navigation", @@ -19,7 +19,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0", + "@bugsnag/core": "^7.9.0-alpha.0", "@types/react-native": "^0.63.20", "react-native-navigation": "^7.0.0" }, diff --git a/packages/plugin-react-native-orientation-breadcrumbs/package-lock.json b/packages/plugin-react-native-orientation-breadcrumbs/package-lock.json index bbf9e8b3c9..2b59fb808b 100644 --- a/packages/plugin-react-native-orientation-breadcrumbs/package-lock.json +++ b/packages/plugin-react-native-orientation-breadcrumbs/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-orientation-breadcrumbs", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-react-native-orientation-breadcrumbs/package.json b/packages/plugin-react-native-orientation-breadcrumbs/package.json index cc29b56601..3e55928adb 100644 --- a/packages/plugin-react-native-orientation-breadcrumbs/package.json +++ b/packages/plugin-react-native-orientation-breadcrumbs/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-orientation-breadcrumbs", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "orientation.js", "description": "@bugsnag/js plugin to create breadcrumbs when the device orientation changes in a React Native app", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-react-native-session/package-lock.json b/packages/plugin-react-native-session/package-lock.json index 33bd9582fc..6d349fc68c 100644 --- a/packages/plugin-react-native-session/package-lock.json +++ b/packages/plugin-react-native-session/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-session", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-react-native-session/package.json b/packages/plugin-react-native-session/package.json index 88ce16d138..26f3120e68 100644 --- a/packages/plugin-react-native-session/package.json +++ b/packages/plugin-react-native-session/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-session", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "session.js", "description": "@bugsnag/react-native session implementation (which delegates all functionality to the native client)", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-react-native-unhandled-rejection/package-lock.json b/packages/plugin-react-native-unhandled-rejection/package-lock.json index 09d34e8c04..29c43d32c0 100644 --- a/packages/plugin-react-native-unhandled-rejection/package-lock.json +++ b/packages/plugin-react-native-unhandled-rejection/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-unhandled-rejection", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-react-native-unhandled-rejection/package.json b/packages/plugin-react-native-unhandled-rejection/package.json index a1d41510b4..e0d9c38c32 100644 --- a/packages/plugin-react-native-unhandled-rejection/package.json +++ b/packages/plugin-react-native-unhandled-rejection/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-native-unhandled-rejection", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "rejection-handler.js", "description": "@bugsnag/js plugin to report unhandled promise rejections in React Native", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0", + "@bugsnag/core": "^7.9.0-alpha.0", "promise": "^8.0.2" }, "peerDependencies": { diff --git a/packages/plugin-react-navigation/package-lock.json b/packages/plugin-react-navigation/package-lock.json index 0905d1b356..fa1c2712b6 100644 --- a/packages/plugin-react-navigation/package-lock.json +++ b/packages/plugin-react-navigation/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-navigation", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-react-navigation/package.json b/packages/plugin-react-navigation/package.json index 5566934755..f7137fffd3 100644 --- a/packages/plugin-react-navigation/package.json +++ b/packages/plugin-react-navigation/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react-navigation", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "react-navigation.js", "description": "@bugsnag/react-native plugin to update context and leave breadcrumb when the screen changes", "homepage": "https://www.bugsnag.com/", @@ -22,7 +22,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0", + "@bugsnag/core": "^7.9.0-alpha.0", "@react-navigation/native": "^5.7.3", "@types/react": "^16.9.49", "@types/react-native": "^0.63.20", diff --git a/packages/plugin-react/package-lock.json b/packages/plugin-react/package-lock.json index 5d5c1e9226..ec9c8f7ddd 100644 --- a/packages/plugin-react/package-lock.json +++ b/packages/plugin-react/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-react/package.json b/packages/plugin-react/package.json index 1ee92c86ac..e41e377ec4 100644 --- a/packages/plugin-react/package.json +++ b/packages/plugin-react/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-react", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "dist/bugsnag-react.js", "description": "React integration for @bugsnag/js", "browser": "dist/bugsnag-react.js", @@ -25,7 +25,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-restify/package-lock.json b/packages/plugin-restify/package-lock.json index 0c9c858577..4f6b7c5a28 100644 --- a/packages/plugin-restify/package-lock.json +++ b/packages/plugin-restify/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-restify", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-restify/package.json b/packages/plugin-restify/package.json index c81669b90f..3fb2f3eee0 100644 --- a/packages/plugin-restify/package.json +++ b/packages/plugin-restify/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-restify", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "dist/bugsnag-restify.js", "types": "types/bugsnag-restify.d.ts", "description": "@bugsnag/js error handling middleware for Restify web servers", @@ -28,7 +28,7 @@ "@bugsnag/core": "^7.0.0" }, "devDependencies": { - "@bugsnag/core": "^7.7.0", + "@bugsnag/core": "^7.9.0-alpha.0", "@types/restify": "^8.4.2" }, "dependencies": { diff --git a/packages/plugin-restify/src/restify.js b/packages/plugin-restify/src/restify.js index 58f61c3087..65bd4461b9 100644 --- a/packages/plugin-restify/src/restify.js +++ b/packages/plugin-restify/src/restify.js @@ -17,8 +17,8 @@ module.exports = { const dom = domain.create() // Get a client to be scoped to this request. If sessions are enabled, use the - // startSession() call to get a session client, otherwise, clone the existing client. - const requestClient = client._config.autoTrackSessions ? client.startSession() : clone(client) + // resumeSession() call to get a session client, otherwise, clone the existing client. + const requestClient = client._config.autoTrackSessions ? client.resumeSession() : clone(client) // attach it to the request req.bugsnag = requestClient diff --git a/packages/plugin-server-session/package-lock.json b/packages/plugin-server-session/package-lock.json index 629152a49a..afeb9bd779 100644 --- a/packages/plugin-server-session/package-lock.json +++ b/packages/plugin-server-session/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-server-session", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-server-session/package.json b/packages/plugin-server-session/package.json index ed5bcbbd23..35316fefa4 100644 --- a/packages/plugin-server-session/package.json +++ b/packages/plugin-server-session/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-server-session", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "session.js", "description": "@bugsnag/js plugin to enable session tracking in server applications", "homepage": "https://www.bugsnag.com/", @@ -20,7 +20,7 @@ "backo": "^1.1.0" }, "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-server-session/session.js b/packages/plugin-server-session/session.js index 491548a117..75234833e0 100644 --- a/packages/plugin-server-session/session.js +++ b/packages/plugin-server-session/session.js @@ -22,13 +22,21 @@ module.exports = { client._session = null }, resumeSession: (client) => { + // Do nothing if there's already an active session + if (client._session) { + return client + } + + // If we have a paused session then make it the active session if (client._pausedSession) { client._session = client._pausedSession client._pausedSession = null + return client - } else { - return client.startSession() } + + // Otherwise start a new session + return client.startSession() } } }, diff --git a/packages/plugin-simple-throttle/package-lock.json b/packages/plugin-simple-throttle/package-lock.json index b40b742776..2f391bfebe 100644 --- a/packages/plugin-simple-throttle/package-lock.json +++ b/packages/plugin-simple-throttle/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-simple-throttle", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-simple-throttle/package.json b/packages/plugin-simple-throttle/package.json index 6d7db5a8d8..227150e1d8 100644 --- a/packages/plugin-simple-throttle/package.json +++ b/packages/plugin-simple-throttle/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-simple-throttle", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "throttle.js", "description": "@bugsnag/js plugin to prevent too many events from being sent", "homepage": "https://www.bugsnag.com/", @@ -18,7 +18,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-strip-project-root/package-lock.json b/packages/plugin-strip-project-root/package-lock.json index 54838fd76f..9046bb4421 100644 --- a/packages/plugin-strip-project-root/package-lock.json +++ b/packages/plugin-strip-project-root/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-strip-project-root", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-strip-project-root/package.json b/packages/plugin-strip-project-root/package.json index 1e1a802d56..17a0fcc995 100644 --- a/packages/plugin-strip-project-root/package.json +++ b/packages/plugin-strip-project-root/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-strip-project-root", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "strip-project-root.js", "description": "@bugsnag/js plugin to remove common project root paths from stacktraces", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-strip-query-string/package-lock.json b/packages/plugin-strip-query-string/package-lock.json index 67e9de51ed..b7ddf98ad4 100644 --- a/packages/plugin-strip-query-string/package-lock.json +++ b/packages/plugin-strip-query-string/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-strip-query-string", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-strip-query-string/package.json b/packages/plugin-strip-query-string/package.json index 9195b8f1ba..b97edefdfe 100644 --- a/packages/plugin-strip-query-string/package.json +++ b/packages/plugin-strip-query-string/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-strip-query-string", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "strip-query-string.js", "description": "@bugsnag/js plugin to strip query string and document fragment from stackframe filenames", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-vue/package-lock.json b/packages/plugin-vue/package-lock.json index 762792690c..517f284521 100644 --- a/packages/plugin-vue/package-lock.json +++ b/packages/plugin-vue/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-vue", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-vue/package.json b/packages/plugin-vue/package.json index 2bd9381f6f..5310df5b5d 100644 --- a/packages/plugin-vue/package.json +++ b/packages/plugin-vue/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-vue", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "description": "Vue.js integration for bugsnag-js", "main": "dist/bugsnag-vue.js", "browser": "dist/bugsnag-vue.js", @@ -25,7 +25,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-window-onerror/package-lock.json b/packages/plugin-window-onerror/package-lock.json index c2375e37f0..588b3a02b3 100644 --- a/packages/plugin-window-onerror/package-lock.json +++ b/packages/plugin-window-onerror/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-window-onerror", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-window-onerror/package.json b/packages/plugin-window-onerror/package.json index 526b06fad1..345d56eceb 100644 --- a/packages/plugin-window-onerror/package.json +++ b/packages/plugin-window-onerror/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-window-onerror", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "onerror.js", "description": "@bugsnag/js plugin to report unhandled exceptions in browsers", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/plugin-window-unhandled-rejection/package-lock.json b/packages/plugin-window-unhandled-rejection/package-lock.json index c350046131..2e965440b6 100644 --- a/packages/plugin-window-unhandled-rejection/package-lock.json +++ b/packages/plugin-window-unhandled-rejection/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-window-unhandled-rejection", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/plugin-window-unhandled-rejection/package.json b/packages/plugin-window-unhandled-rejection/package.json index 9e5c499db6..082f8a69a3 100644 --- a/packages/plugin-window-unhandled-rejection/package.json +++ b/packages/plugin-window-unhandled-rejection/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/plugin-window-unhandled-rejection", - "version": "7.7.0", + "version": "7.9.0-alpha.0", "main": "unhandled-rejection.js", "description": "@bugsnag/js plugin to report unhandled promise rejections in browsers", "homepage": "https://www.bugsnag.com/", @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.7.0" + "@bugsnag/core": "^7.9.0-alpha.0" }, "peerDependencies": { "@bugsnag/core": "^7.0.0" diff --git a/packages/react-native-cli/package-lock.json b/packages/react-native-cli/package-lock.json index ae477670b1..5fd7149b03 100644 --- a/packages/react-native-cli/package-lock.json +++ b/packages/react-native-cli/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/react-native-cli", - "version": "7.8.0", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/react-native-cli/package.json b/packages/react-native-cli/package.json index 91eedb996f..04621fe2ea 100644 --- a/packages/react-native-cli/package.json +++ b/packages/react-native-cli/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/react-native-cli", - "version": "7.8.0", + "version": "7.9.0-alpha.0", "description": "A tool to help integrate Bugsnag with a React Native app", "bin": { "bugsnag-react-native-cli": "bin/cli" diff --git a/packages/react-native/package-lock.json b/packages/react-native/package-lock.json index 4a2646f0f8..5929ee713b 100644 --- a/packages/react-native/package-lock.json +++ b/packages/react-native/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/react-native", - "version": "7.8.2", + "version": "7.9.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/react-native/package.json b/packages/react-native/package.json index a442f5aebf..dbf13287a5 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/react-native", - "version": "7.8.2", + "version": "7.9.0-alpha.0", "main": "src/notifier.js", "types": "types/bugsnag.d.ts", "description": "Bugsnag error reporter for React Native applications", @@ -49,17 +49,17 @@ "typescript": "^3.3.3" }, "dependencies": { - "@bugsnag/core": "^7.7.0", - "@bugsnag/delivery-react-native": "^7.8.0", - "@bugsnag/plugin-console-breadcrumbs": "^7.7.0", - "@bugsnag/plugin-network-breadcrumbs": "^7.7.0", - "@bugsnag/plugin-react": "^7.7.0", - "@bugsnag/plugin-react-native-client-sync": "^7.7.0", - "@bugsnag/plugin-react-native-event-sync": "^7.7.0", - "@bugsnag/plugin-react-native-global-error-handler": "^7.7.0", - "@bugsnag/plugin-react-native-hermes": "^7.7.0", - "@bugsnag/plugin-react-native-session": "^7.7.0", - "@bugsnag/plugin-react-native-unhandled-rejection": "^7.7.0", + "@bugsnag/core": "^7.9.0-alpha.0", + "@bugsnag/delivery-react-native": "^7.9.0-alpha.0", + "@bugsnag/plugin-console-breadcrumbs": "^7.9.0-alpha.0", + "@bugsnag/plugin-network-breadcrumbs": "^7.9.0-alpha.0", + "@bugsnag/plugin-react": "^7.9.0-alpha.0", + "@bugsnag/plugin-react-native-client-sync": "^7.9.0-alpha.0", + "@bugsnag/plugin-react-native-event-sync": "^7.9.0-alpha.0", + "@bugsnag/plugin-react-native-global-error-handler": "^7.9.0-alpha.0", + "@bugsnag/plugin-react-native-hermes": "^7.9.0-alpha.0", + "@bugsnag/plugin-react-native-session": "^7.9.0-alpha.0", + "@bugsnag/plugin-react-native-unhandled-rejection": "^7.9.0-alpha.0", "iserror": "^0.0.2" } } diff --git a/test/aws-lambda/.gitignore b/test/aws-lambda/.gitignore new file mode 100644 index 0000000000..74390a5d87 --- /dev/null +++ b/test/aws-lambda/.gitignore @@ -0,0 +1,8 @@ +.aws-sam +node_modules/ +.tgz + +# Ignore the gem lockfile due to Bundler version differences on CI +# We don't really need this anyway because we have one dependency (MazeRunner) +# and we pin it to a specific tag, so it's effectively locked +/Gemfile.lock diff --git a/test/aws-lambda/Gemfile b/test/aws-lambda/Gemfile new file mode 100644 index 0000000000..77dcf3d466 --- /dev/null +++ b/test/aws-lambda/Gemfile @@ -0,0 +1,6 @@ +source 'https://rubygems.org' + +gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v4.12.1' + +# Locally, you can run against Maze Runner branches and uncommitted changes: +#gem 'bugsnag-maze-runner', path: '../../../maze-runner' diff --git a/test/aws-lambda/README.md b/test/aws-lambda/README.md new file mode 100644 index 0000000000..893710b167 --- /dev/null +++ b/test/aws-lambda/README.md @@ -0,0 +1,27 @@ +## Setup + +1. [Install the AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) + +1. Install Maze Runner: + + ```sh + $ bundle install + ``` + +## Running the tests + +Run Maze Runner with the `--bind-address` option: + +```sh +$ bundle exec maze-runner --bind-address=0.0.0.0 +``` + +This will build all of the fixtures before running the tests + +### Running tests for a specific fixture + +All tests are tagged with the name of the fixture they run against. For example, to run only the tests that use the "[serverless-express-app](./features/fixtures/serverless-express-app)" fixture: + +```sh +$ bundle exec maze-runner --bind-address=0.0.0.0 --tags @serverless-express-app +``` diff --git a/test/aws-lambda/features/fixtures/serverless-express-app/app/app.js b/test/aws-lambda/features/fixtures/serverless-express-app/app/app.js new file mode 100644 index 0000000000..939dc28bbd --- /dev/null +++ b/test/aws-lambda/features/fixtures/serverless-express-app/app/app.js @@ -0,0 +1,52 @@ +const Bugsnag = require('@bugsnag/js') +const express = require('express') + +const app = express() + +const middleware = Bugsnag.getPlugin('express') +app.use(middleware.requestHandler) + +app.get('/', (request, response) => { + response.json({ message: 'hello world!' }) +}) + +app.get('/handled', (request, response) => { + Bugsnag.notify('no crashing here') + + response.json({ message: 'did not crash :)' }) +}) + +app.get('/unhandled', (request, response) => { + throw new Error('broken :(') +}) + +app.get('/unhandled-async', (request, response) => { + setTimeout(function () { + throw new Error('busted') + }, 100) +}) + +app.get('/unhandled-next', (request, response, next) => { + next(new Error('borked')) +}) + +app.get('/promise-rejection', (request, response) => { + Promise.reject(new Error('abc')) + + response.json({ message: 'xyz' }) +}) + +app.use(middleware.errorHandler) + +// Replace the built-in error handler with one that returns JSON. This allows us +// to assert against it much more easily than the default HTML response +app.use((error, request, response, next) => { + response.status(500) + response.json({ + message: error.message, + type: error.constructor.name, + stacktrace: error.stack.split('\n') + }) +}) + +module.exports = app diff --git a/test/aws-lambda/features/fixtures/serverless-express-app/app/index.js b/test/aws-lambda/features/fixtures/serverless-express-app/app/index.js new file mode 100644 index 0000000000..20f9c39481 --- /dev/null +++ b/test/aws-lambda/features/fixtures/serverless-express-app/app/index.js @@ -0,0 +1,22 @@ +const Bugsnag = require('@bugsnag/js') +const BugsnagPluginAwsLambda = require('@bugsnag/plugin-aws-lambda') +const BugsnagPluginExpress = require('@bugsnag/plugin-express') +const serverlessExpress = require('@vendia/serverless-express') + +Bugsnag.start({ + apiKey: process.env.BUGSNAG_API_KEY, + plugins: [BugsnagPluginAwsLambda, BugsnagPluginExpress], + endpoints: { + notify: process.env.BUGSNAG_NOTIFY_ENDPOINT, + sessions: process.env.BUGSNAG_SESSIONS_ENDPOINT + }, + autoDetectErrors: process.env.BUGSNAG_AUTO_DETECT_ERRORS !== 'false', + autoTrackSessions: process.env.BUGSNAG_AUTO_TRACK_SESSIONS !== 'false' +}) + +const app = require('./app') + +const bugsnagHandler = Bugsnag.getPlugin('awsLambda').createHandler() +const serverlessExpressHandler = serverlessExpress({ app }) + +exports.lambdaHandler = bugsnagHandler(serverlessExpressHandler) diff --git a/test/aws-lambda/features/fixtures/serverless-express-app/app/package.json b/test/aws-lambda/features/fixtures/serverless-express-app/app/package.json new file mode 100644 index 0000000000..77b6671142 --- /dev/null +++ b/test/aws-lambda/features/fixtures/serverless-express-app/app/package.json @@ -0,0 +1,26 @@ +{ + "name": "serverless-express-app", + "version": "1.2.3", + "dependencies": { + "@vendia/serverless-express": "^4.3.3", + "express": "^4.17.1", + + "@bugsnag/core": "bugsnag-core.tgz", + "@bugsnag/delivery-node": "bugsnag-delivery-node.tgz", + "@bugsnag/in-flight": "bugsnag-in-flight.tgz", + "@bugsnag/js": "bugsnag-js.tgz", + "@bugsnag/node": "bugsnag-node.tgz", + "@bugsnag/plugin-app-duration": "bugsnag-plugin-app-duration.tgz", + "@bugsnag/plugin-aws-lambda": "bugsnag-plugin-aws-lambda.tgz", + "@bugsnag/plugin-contextualize": "bugsnag-plugin-contextualize.tgz", + "@bugsnag/plugin-express": "bugsnag-plugin-express.tgz", + "@bugsnag/plugin-intercept": "bugsnag-plugin-intercept.tgz", + "@bugsnag/plugin-node-device": "bugsnag-plugin-node-device.tgz", + "@bugsnag/plugin-node-in-project": "bugsnag-plugin-node-in-project.tgz", + "@bugsnag/plugin-node-surrounding-code": "bugsnag-plugin-node-surrounding-code.tgz", + "@bugsnag/plugin-node-uncaught-exception": "bugsnag-plugin-node-uncaught-exception.tgz", + "@bugsnag/plugin-node-unhandled-rejection": "bugsnag-plugin-node-unhandled-rejection.tgz", + "@bugsnag/plugin-server-session": "bugsnag-plugin-server-session.tgz", + "@bugsnag/plugin-strip-project-root": "bugsnag-plugin-strip-project-root.tgz" + } +} diff --git a/test/aws-lambda/features/fixtures/serverless-express-app/events/handled.json b/test/aws-lambda/features/fixtures/serverless-express-app/events/handled.json new file mode 100644 index 0000000000..8dd1a0b237 --- /dev/null +++ b/test/aws-lambda/features/fixtures/serverless-express-app/events/handled.json @@ -0,0 +1,51 @@ +{ + "resource": "/handled", + "path": "/handled", + "httpMethod": "GET", + "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", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "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/handled", + "resourcePath": "/handled", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/test/aws-lambda/features/fixtures/serverless-express-app/events/index.json b/test/aws-lambda/features/fixtures/serverless-express-app/events/index.json new file mode 100644 index 0000000000..c5c1ce9031 --- /dev/null +++ b/test/aws-lambda/features/fixtures/serverless-express-app/events/index.json @@ -0,0 +1,51 @@ +{ + "resource": "/", + "path": "/", + "httpMethod": "GET", + "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", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "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/", + "resourcePath": "/", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/test/aws-lambda/features/fixtures/serverless-express-app/events/promise-rejection.json b/test/aws-lambda/features/fixtures/serverless-express-app/events/promise-rejection.json new file mode 100644 index 0000000000..9ed94bcc09 --- /dev/null +++ b/test/aws-lambda/features/fixtures/serverless-express-app/events/promise-rejection.json @@ -0,0 +1,51 @@ +{ + "resource": "/promise-rejection", + "path": "/promise-rejection", + "httpMethod": "GET", + "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", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "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/promise-rejection", + "resourcePath": "/promise-rejection", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/test/aws-lambda/features/fixtures/serverless-express-app/events/unhandled-async.json b/test/aws-lambda/features/fixtures/serverless-express-app/events/unhandled-async.json new file mode 100644 index 0000000000..9486bd80b9 --- /dev/null +++ b/test/aws-lambda/features/fixtures/serverless-express-app/events/unhandled-async.json @@ -0,0 +1,51 @@ +{ + "resource": "/unhandled-async", + "path": "/unhandled-async", + "httpMethod": "GET", + "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", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "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/unhandled-async", + "resourcePath": "/unhandled-async", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/test/aws-lambda/features/fixtures/serverless-express-app/events/unhandled-next.json b/test/aws-lambda/features/fixtures/serverless-express-app/events/unhandled-next.json new file mode 100644 index 0000000000..7a31bae60e --- /dev/null +++ b/test/aws-lambda/features/fixtures/serverless-express-app/events/unhandled-next.json @@ -0,0 +1,51 @@ +{ + "resource": "/unhandled-next", + "path": "/unhandled-next", + "httpMethod": "GET", + "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", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "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/unhandled-next", + "resourcePath": "/unhandled-next", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/test/aws-lambda/features/fixtures/serverless-express-app/events/unhandled.json b/test/aws-lambda/features/fixtures/serverless-express-app/events/unhandled.json new file mode 100644 index 0000000000..f0701e88dc --- /dev/null +++ b/test/aws-lambda/features/fixtures/serverless-express-app/events/unhandled.json @@ -0,0 +1,51 @@ +{ + "resource": "/unhandled", + "path": "/unhandled", + "httpMethod": "GET", + "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", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "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/unhandled", + "resourcePath": "/unhandled", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/test/aws-lambda/features/fixtures/serverless-express-app/template.yaml b/test/aws-lambda/features/fixtures/serverless-express-app/template.yaml new file mode 100644 index 0000000000..f868413d7f --- /dev/null +++ b/test/aws-lambda/features/fixtures/serverless-express-app/template.yaml @@ -0,0 +1,41 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: serverless-express-app + +Globals: + Function: + Timeout: 30 + Environment: + Variables: + BUGSNAG_API_KEY: + BUGSNAG_NOTIFY_ENDPOINT: + BUGSNAG_SESSIONS_ENDPOINT: + BUGSNAG_AUTO_TRACK_SESSIONS: true + BUGSNAG_AUTO_DETECT_ERRORS: true + +Resources: + ExpressApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + BinaryMediaTypes: ['*/*'] + + ExpressFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: app/ + Handler: index.lambdaHandler + Runtime: nodejs14.x + Events: + ProxyApiRoot: + Type: Api + Properties: + RestApiId: !Ref ExpressApi + Path: / + Method: ANY + ProxyApiGreedy: + Type: Api + Properties: + RestApiId: !Ref ExpressApi + Path: /{proxy+} + Method: ANY diff --git a/test/aws-lambda/features/fixtures/simple-app/README.md b/test/aws-lambda/features/fixtures/simple-app/README.md new file mode 100644 index 0000000000..3c50358cc0 --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/README.md @@ -0,0 +1,17 @@ +1. Run the build script (requires Ruby): + ```sh + $ ../../scripts/build-fixtures simple-app + ``` + + Or run the build script all fixtures: + + ```sh + $ ../../scripts/build-fixtures + ``` + +1. Run a function: + ```sh + $ BUGSNAG_API_KEY=123 sam local invoke AsyncUnhandledExceptionFunction --event events/async/unhandled-exception.json + ``` + + Check `template.yaml` for all available functions diff --git a/test/aws-lambda/features/fixtures/simple-app/async/handled-exception.js b/test/aws-lambda/features/fixtures/simple-app/async/handled-exception.js new file mode 100644 index 0000000000..92494cdb14 --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/async/handled-exception.js @@ -0,0 +1,26 @@ +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) => { + Bugsnag.notify(new Error('Hello!')) + + return { + statusCode: 200, + body: JSON.stringify({ message: 'Did not crash!' }) + } +} + +module.exports.lambdaHandler = bugsnagHandler(handler) diff --git a/test/aws-lambda/features/fixtures/simple-app/async/package.json b/test/aws-lambda/features/fixtures/simple-app/async/package.json new file mode 100644 index 0000000000..a5c32db16e --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/async/package.json @@ -0,0 +1,23 @@ +{ + "name": "simple-app", + "version": "1.0.0", + "description": "simple lambda app for testing", + "dependencies": { + "@bugsnag/core": "bugsnag-core.tgz", + "@bugsnag/delivery-node": "bugsnag-delivery-node.tgz", + "@bugsnag/in-flight": "bugsnag-in-flight.tgz", + "@bugsnag/js": "bugsnag-js.tgz", + "@bugsnag/node": "bugsnag-node.tgz", + "@bugsnag/plugin-app-duration": "bugsnag-plugin-app-duration.tgz", + "@bugsnag/plugin-aws-lambda": "bugsnag-plugin-aws-lambda.tgz", + "@bugsnag/plugin-contextualize": "bugsnag-plugin-contextualize.tgz", + "@bugsnag/plugin-intercept": "bugsnag-plugin-intercept.tgz", + "@bugsnag/plugin-node-device": "bugsnag-plugin-node-device.tgz", + "@bugsnag/plugin-node-in-project": "bugsnag-plugin-node-in-project.tgz", + "@bugsnag/plugin-node-surrounding-code": "bugsnag-plugin-node-surrounding-code.tgz", + "@bugsnag/plugin-node-uncaught-exception": "bugsnag-plugin-node-uncaught-exception.tgz", + "@bugsnag/plugin-node-unhandled-rejection": "bugsnag-plugin-node-unhandled-rejection.tgz", + "@bugsnag/plugin-server-session": "bugsnag-plugin-server-session.tgz", + "@bugsnag/plugin-strip-project-root": "bugsnag-plugin-strip-project-root.tgz" + } +} diff --git a/test/aws-lambda/features/fixtures/simple-app/async/promise-rejection.js b/test/aws-lambda/features/fixtures/simple-app/async/promise-rejection.js new file mode 100644 index 0000000000..b37603d9f9 --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/async/promise-rejection.js @@ -0,0 +1,26 @@ +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) => { + Promise.reject(new Error('yikes')) + + return { + statusCode: 200, + body: JSON.stringify({ message: 'Did not crash!' }) + } +} + +module.exports.lambdaHandler = bugsnagHandler(handler) diff --git a/test/aws-lambda/features/fixtures/simple-app/async/unhandled-exception.js b/test/aws-lambda/features/fixtures/simple-app/async/unhandled-exception.js new file mode 100644 index 0000000000..0357e500c9 --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/async/unhandled-exception.js @@ -0,0 +1,21 @@ +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) => { + throw new Error('Oh no!') +} + +module.exports.lambdaHandler = bugsnagHandler(handler) diff --git a/test/aws-lambda/features/fixtures/simple-app/callback/handled-exception.js b/test/aws-lambda/features/fixtures/simple-app/callback/handled-exception.js new file mode 100644 index 0000000000..5d7fdd1b2e --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/callback/handled-exception.js @@ -0,0 +1,26 @@ +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 = (event, context, callback) => { + Bugsnag.notify(new Error('Hello!')) + + callback(null, { + statusCode: 200, + body: JSON.stringify({ message: 'Did not crash!' }) + }) +} + +module.exports.lambdaHandler = bugsnagHandler(handler) diff --git a/test/aws-lambda/features/fixtures/simple-app/callback/package.json b/test/aws-lambda/features/fixtures/simple-app/callback/package.json new file mode 100644 index 0000000000..a5c32db16e --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/callback/package.json @@ -0,0 +1,23 @@ +{ + "name": "simple-app", + "version": "1.0.0", + "description": "simple lambda app for testing", + "dependencies": { + "@bugsnag/core": "bugsnag-core.tgz", + "@bugsnag/delivery-node": "bugsnag-delivery-node.tgz", + "@bugsnag/in-flight": "bugsnag-in-flight.tgz", + "@bugsnag/js": "bugsnag-js.tgz", + "@bugsnag/node": "bugsnag-node.tgz", + "@bugsnag/plugin-app-duration": "bugsnag-plugin-app-duration.tgz", + "@bugsnag/plugin-aws-lambda": "bugsnag-plugin-aws-lambda.tgz", + "@bugsnag/plugin-contextualize": "bugsnag-plugin-contextualize.tgz", + "@bugsnag/plugin-intercept": "bugsnag-plugin-intercept.tgz", + "@bugsnag/plugin-node-device": "bugsnag-plugin-node-device.tgz", + "@bugsnag/plugin-node-in-project": "bugsnag-plugin-node-in-project.tgz", + "@bugsnag/plugin-node-surrounding-code": "bugsnag-plugin-node-surrounding-code.tgz", + "@bugsnag/plugin-node-uncaught-exception": "bugsnag-plugin-node-uncaught-exception.tgz", + "@bugsnag/plugin-node-unhandled-rejection": "bugsnag-plugin-node-unhandled-rejection.tgz", + "@bugsnag/plugin-server-session": "bugsnag-plugin-server-session.tgz", + "@bugsnag/plugin-strip-project-root": "bugsnag-plugin-strip-project-root.tgz" + } +} diff --git a/test/aws-lambda/features/fixtures/simple-app/callback/promise-rejection.js b/test/aws-lambda/features/fixtures/simple-app/callback/promise-rejection.js new file mode 100644 index 0000000000..5001333507 --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/callback/promise-rejection.js @@ -0,0 +1,26 @@ +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 = (event, context, callback) => { + Promise.reject(new Error('yikes')) + + callback(null, { + statusCode: 200, + body: JSON.stringify({ message: 'Did not crash!' }) + }) +} + +module.exports.lambdaHandler = bugsnagHandler(handler) diff --git a/test/aws-lambda/features/fixtures/simple-app/callback/thrown-unhandled-exception.js b/test/aws-lambda/features/fixtures/simple-app/callback/thrown-unhandled-exception.js new file mode 100644 index 0000000000..55c5f5124a --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/callback/thrown-unhandled-exception.js @@ -0,0 +1,21 @@ +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 = (event, context, callback) => { + throw new Error('Oh no!') +} + +module.exports.lambdaHandler = bugsnagHandler(handler) diff --git a/test/aws-lambda/features/fixtures/simple-app/callback/unhandled-exception.js b/test/aws-lambda/features/fixtures/simple-app/callback/unhandled-exception.js new file mode 100644 index 0000000000..9585d82008 --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/callback/unhandled-exception.js @@ -0,0 +1,21 @@ +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 = (event, context, callback) => { + callback(new Error('Oh no!')) +} + +module.exports.lambdaHandler = bugsnagHandler(handler) diff --git a/test/aws-lambda/features/fixtures/simple-app/events/async/handled-exception.json b/test/aws-lambda/features/fixtures/simple-app/events/async/handled-exception.json new file mode 100644 index 0000000000..52ba9c07a3 --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/events/async/handled-exception.json @@ -0,0 +1,46 @@ +{ + "body": "", + "resource": "/{proxy+}", + "path": "/async/handled/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/handled/exception", + "resourcePath": "/{proxy+}", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/test/aws-lambda/features/fixtures/simple-app/events/async/promise-rejection.json b/test/aws-lambda/features/fixtures/simple-app/events/async/promise-rejection.json new file mode 100644 index 0000000000..882eba811f --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/events/async/promise-rejection.json @@ -0,0 +1,46 @@ +{ + "body": "", + "resource": "/{proxy+}", + "path": "/async/promise/rejection", + "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/promise/rejection", + "resourcePath": "/{proxy+}", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/test/aws-lambda/features/fixtures/simple-app/events/async/unhandled-exception.json b/test/aws-lambda/features/fixtures/simple-app/events/async/unhandled-exception.json new file mode 100644 index 0000000000..362dafef60 --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/events/async/unhandled-exception.json @@ -0,0 +1,46 @@ +{ + "body": "", + "resource": "/{proxy+}", + "path": "/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/unhandled/exception", + "resourcePath": "/{proxy+}", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/test/aws-lambda/features/fixtures/simple-app/events/callback/handled-exception.json b/test/aws-lambda/features/fixtures/simple-app/events/callback/handled-exception.json new file mode 100644 index 0000000000..98e20b22cd --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/events/callback/handled-exception.json @@ -0,0 +1,46 @@ +{ + "body": "", + "resource": "/{proxy+}", + "path": "/callback/handled/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/callback/handled/exception", + "resourcePath": "/{proxy+}", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/test/aws-lambda/features/fixtures/simple-app/events/callback/promise-rejection.json b/test/aws-lambda/features/fixtures/simple-app/events/callback/promise-rejection.json new file mode 100644 index 0000000000..7efbcb1706 --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/events/callback/promise-rejection.json @@ -0,0 +1,46 @@ +{ + "body": "", + "resource": "/{proxy+}", + "path": "/callback/promise/rejection", + "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/callback/promise/rejection", + "resourcePath": "/{proxy+}", + "httpMethod": "GET", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/test/aws-lambda/features/fixtures/simple-app/events/callback/unhandled-exception.json b/test/aws-lambda/features/fixtures/simple-app/events/callback/unhandled-exception.json new file mode 100644 index 0000000000..8055062cc3 --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/events/callback/unhandled-exception.json @@ -0,0 +1,46 @@ +{ + "body": "", + "resource": "/{proxy+}", + "path": "/callback/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/callback/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 new file mode 100644 index 0000000000..c5772d3b47 --- /dev/null +++ b/test/aws-lambda/features/fixtures/simple-app/template.yaml @@ -0,0 +1,197 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: simple-app + +Globals: + Function: + Timeout: 30 + Environment: + Variables: + BUGSNAG_API_KEY: + BUGSNAG_NOTIFY_ENDPOINT: + BUGSNAG_SESSIONS_ENDPOINT: + BUGSNAG_AUTO_TRACK_SESSIONS: true + BUGSNAG_AUTO_DETECT_ERRORS: true + +Resources: + AsyncUnhandledExceptionFunctionNode14: + Type: AWS::Serverless::Function + Properties: + CodeUri: async/ + Handler: unhandled-exception.lambdaHandler + Runtime: nodejs14.x + Events: + AsyncUnhandledException: + Type: Api + Properties: + Path: /async/unhandled/exception + Method: get + + AsyncHandledExceptionFunctionNode14: + Type: AWS::Serverless::Function + Properties: + CodeUri: async/ + Handler: handled-exception.lambdaHandler + Runtime: nodejs14.x + Events: + AsyncHandledException: + Type: Api + Properties: + Path: /async/handled/exception + Method: get + + AsyncPromiseRejectionFunctionNode14: + Type: AWS::Serverless::Function + Properties: + CodeUri: async/ + Handler: promise-rejection.lambdaHandler + Runtime: nodejs14.x + Events: + AsyncPromiseRejection: + Type: Api + Properties: + Path: /async/promise/rejection + Method: get + + CallbackUnhandledExceptionFunctionNode14: + Type: AWS::Serverless::Function + Properties: + CodeUri: callback/ + Handler: unhandled-exception.lambdaHandler + Runtime: nodejs14.x + Events: + CallbackUnhandledException: + Type: Api + Properties: + Path: /callback/unhandled/exception + Method: get + + CallbackThrownUnhandledExceptionFunctionNode14: + Type: AWS::Serverless::Function + Properties: + CodeUri: callback/ + Handler: thrown-unhandled-exception.lambdaHandler + Runtime: nodejs14.x + Events: + CallbackThrownUnhandledException: + Type: Api + Properties: + Path: /callback/thrown/unhandled/exception + Method: get + + CallbackHandledExceptionFunctionNode14: + Type: AWS::Serverless::Function + Properties: + CodeUri: callback/ + Handler: handled-exception.lambdaHandler + Runtime: nodejs14.x + Events: + CallbackHandledException: + Type: Api + Properties: + Path: /callback/handled/exception + Method: get + + CallbackPromiseRejectionFunctionNode14: + Type: AWS::Serverless::Function + Properties: + CodeUri: callback/ + Handler: promise-rejection.lambdaHandler + Runtime: nodejs14.x + Events: + CallbackPromiseRejection: + Type: Api + Properties: + Path: /callback/promise/rejection + Method: get + + AsyncUnhandledExceptionFunctionNode12: + Type: AWS::Serverless::Function + Properties: + CodeUri: async/ + Handler: unhandled-exception.lambdaHandler + Runtime: nodejs12.x + Events: + AsyncUnhandledException: + Type: Api + Properties: + Path: /async/unhandled/exception + Method: get + + AsyncHandledExceptionFunctionNode12: + Type: AWS::Serverless::Function + Properties: + CodeUri: async/ + Handler: handled-exception.lambdaHandler + Runtime: nodejs12.x + Events: + AsyncHandledException: + Type: Api + Properties: + Path: /async/handled/exception + Method: get + + AsyncPromiseRejectionFunctionNode12: + Type: AWS::Serverless::Function + Properties: + CodeUri: async/ + Handler: promise-rejection.lambdaHandler + Runtime: nodejs12.x + Events: + AsyncPromiseRejection: + Type: Api + Properties: + Path: /async/promise/rejection + Method: get + + CallbackUnhandledExceptionFunctionNode12: + Type: AWS::Serverless::Function + Properties: + CodeUri: callback/ + Handler: unhandled-exception.lambdaHandler + Runtime: nodejs12.x + Events: + CallbackUnhandledException: + Type: Api + Properties: + Path: /callback/unhandled/exception + Method: get + + CallbackThrownUnhandledExceptionFunctionNode12: + Type: AWS::Serverless::Function + Properties: + CodeUri: callback/ + Handler: thrown-unhandled-exception.lambdaHandler + Runtime: nodejs12.x + Events: + CallbackThrownUnhandledException: + Type: Api + Properties: + Path: /callback/thrown/unhandled/exception + Method: get + + CallbackHandledExceptionFunctionNode12: + Type: AWS::Serverless::Function + Properties: + CodeUri: callback/ + Handler: handled-exception.lambdaHandler + Runtime: nodejs12.x + Events: + CallbackHandledException: + Type: Api + Properties: + Path: /callback/handled/exception + Method: get + + CallbackPromiseRejectionFunctionNode12: + Type: AWS::Serverless::Function + Properties: + CodeUri: callback/ + Handler: promise-rejection.lambdaHandler + Runtime: nodejs12.x + Events: + CallbackPromiseRejection: + Type: Api + Properties: + Path: /callback/promise/rejection + Method: get diff --git a/test/aws-lambda/features/handled.feature b/test/aws-lambda/features/handled.feature new file mode 100644 index 0000000000..4850ee4c7f --- /dev/null +++ b/test/aws-lambda/features/handled.feature @@ -0,0 +1,90 @@ +Feature: Handled exceptions are reported correctly in lambda functions + +@simple-app +Scenario Outline: handled exceptions are reported + Given I setup the environment + When I invoke the "" lambda in "features/fixtures/simple-app" with the "events//handled-exception.json" event + Then the lambda response "body.message" equals "Did not crash!" + And the lambda response "statusCode" equals 200 + 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 false + And the event "severity" equals "warning" + And the event "severityReason.type" equals "handledException" + And the exception "errorClass" equals "Error" + And the exception "message" equals "Hello!" + And the exception "type" equals "nodejs" + And the "file" of stack frame 0 equals "handled-exception.js" + 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 | node-version | + | AsyncHandledExceptionFunctionNode14 | async | 14 | + | AsyncHandledExceptionFunctionNode12 | async | 12 | + | CallbackHandledExceptionFunctionNode14 | callback | 14 | + | CallbackHandledExceptionFunctionNode12 | callback | 12 | + +@simple-app +Scenario Outline: handled exceptions are still reported when autoDetectErrors is false + Given I setup the environment + And I set environment variable "BUGSNAG_AUTO_DETECT_ERRORS" to "false" + When I invoke the "" lambda in "features/fixtures/simple-app" with the "events//handled-exception.json" event + Then the lambda response "body.message" equals "Did not crash!" + And the lambda response "statusCode" equals 200 + 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 false + And the event "severity" equals "warning" + And the event "severityReason.type" equals "handledException" + And the exception "errorClass" equals "Error" + And the exception "message" equals "Hello!" + And the exception "type" equals "nodejs" + And the "file" of stack frame 0 equals "handled-exception.js" + 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 | node-version | + | AsyncHandledExceptionFunctionNode14 | async | 14 | + | AsyncHandledExceptionFunctionNode12 | async | 12 | + | CallbackHandledExceptionFunctionNode14 | callback | 14 | + | CallbackHandledExceptionFunctionNode12 | callback | 12 | + +@serverless-express-app +Scenario: handled 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/handled.json" event + Then the lambda response "body.message" equals "did not crash :)" + And the lambda response "statusCode" equals 200 + 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 false + And the event "severity" equals "warning" + And the event "severityReason.type" equals "handledException" + And the exception "errorClass" equals "Error" + And the exception "message" equals "no crashing here" + And the exception "type" equals "nodejs" + And the "file" of stack frame 0 equals "app.js" + And the event "metaData.AWS Lambda context.functionName" equals "ExpressFunction" + And the event "metaData.AWS Lambda context.awsRequestId" is not null + And the event "device.runtimeVersions.node" matches "^14\.\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 + And the event "session.events.handled" equals 1 + And the event "session.events.unhandled" equals 0 diff --git a/test/aws-lambda/features/promise-rejection.feature b/test/aws-lambda/features/promise-rejection.feature new file mode 100644 index 0000000000..858a494be2 --- /dev/null +++ b/test/aws-lambda/features/promise-rejection.feature @@ -0,0 +1,87 @@ +Feature: Unhandled promise rejections are reported correctly in lambda functions + +@simple-app +Scenario Outline: unhandled promise rejections are reported + Given I setup the environment + When I invoke the "" lambda in "features/fixtures/simple-app" with the "events//promise-rejection.json" event + Then the lambda response "errorMessage" equals "Error: yikes" + And the lambda response "errorType" equals "Runtime.UnhandledPromiseRejection" + And the lambda response "trace" is an array with 4 elements + And the lambda response "trace.0" equals "Runtime.UnhandledPromiseRejection: Error: yikes" + And the lambda response "body" is null + And the lambda response "statusCode" is null + 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 "unhandledPromiseRejection" + And the exception "errorClass" equals "Error" + And the exception "message" equals "yikes" + And the exception "type" equals "nodejs" + And the "file" of stack frame 0 equals "promise-rejection.js" + 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 | node-version | + | AsyncPromiseRejectionFunctionNode14 | async | 14 | + | AsyncPromiseRejectionFunctionNode12 | async | 12 | + | CallbackPromiseRejectionFunctionNode14 | callback | 14 | + | CallbackPromiseRejectionFunctionNode12 | callback | 12 | + +@simple-app +Scenario Outline: unhandled promise rejections are not reported when autoDetectErrors is false + Given I setup the environment + And I set environment variable "BUGSNAG_AUTO_DETECT_ERRORS" to "false" + When I invoke the "" lambda in "features/fixtures/simple-app" with the "events//promise-rejection.json" event + Then the lambda response "errorMessage" equals "Error: yikes" + And the lambda response "errorType" equals "Runtime.UnhandledPromiseRejection" + And the lambda response "trace" is an array with 6 elements + And the lambda response "trace.0" equals "Runtime.UnhandledPromiseRejection: Error: yikes" + And the lambda response "body" is null + And the lambda response "statusCode" is null + And the SAM exit code equals 0 + And I should receive no errors + + Examples: + | lambda | type | + | AsyncPromiseRejectionFunctionNode14 | async | + | AsyncPromiseRejectionFunctionNode12 | async | + | CallbackPromiseRejectionFunctionNode14 | callback | + | CallbackPromiseRejectionFunctionNode12 | callback | + +@serverless-express-app +Scenario: promise rejections 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/promise-rejection.json" event + Then the lambda response "errorMessage" equals "Error: abc" + And the lambda response "errorType" equals "Runtime.UnhandledPromiseRejection" + And the lambda response "trace" is an array with 4 elements + And the lambda response "trace.0" equals "Runtime.UnhandledPromiseRejection: Error: abc" + And the lambda response "body" is null + And the lambda response "statusCode" is null + 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 "unhandledPromiseRejection" + And the exception "errorClass" equals "Error" + And the exception "message" equals "abc" + And the exception "type" equals "nodejs" + And the "file" of stack frame 0 equals "app.js" + And the event "metaData.AWS Lambda context.functionName" equals "ExpressFunction" + And the event "metaData.AWS Lambda context.awsRequestId" is not null + And the event "device.runtimeVersions.node" matches "^14\.\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 + And the event "session.events.handled" equals 0 + And the event "session.events.unhandled" equals 1 diff --git a/test/aws-lambda/features/scripts/build-fixtures b/test/aws-lambda/features/scripts/build-fixtures new file mode 100755 index 0000000000..f048f62e8a --- /dev/null +++ b/test/aws-lambda/features/scripts/build-fixtures @@ -0,0 +1,149 @@ +#!/usr/bin/env ruby + +require "yaml" +require "fileutils" + +# Allow specifying a specic fixture to build by passing it's name as an argument +# e.g. ./build-fixtures simple-app +SPECIFIC_FIXTURE = ARGV.first + +# Allow debugging by logging more verbosely and not tidying up packed packages +DEBUG = ["1", "true", true].include?(ENV.fetch("DEBUG", false)) + +# The root of bugsnag-js +ROOT = File.realpath("#{__dir__}/../../../../") + +# Bugsnag packages that we need to pack & install into the fixtures +BUGSNAG_PACKAGES = [ + "core", + "delivery-node", + "in-flight", + "js", + "node", + "plugin-app-duration", + "plugin-aws-lambda", + "plugin-contextualize", + "plugin-express", + "plugin-intercept", + "plugin-node-device", + "plugin-node-in-project", + "plugin-node-surrounding-code", + "plugin-node-uncaught-exception", + "plugin-node-unhandled-rejection", + "plugin-server-session", + "plugin-strip-project-root", +] + +def heading(message) + <<~HEADING + ====#{"=" * message.length}==== + = #{message} = + ====#{"=" * message.length}==== + HEADING +end + +# Bootstrap and build Bugsnag packages so they're ready to install +def bootstrap + Dir.chdir(ROOT) do + system("npm run bootstrap") + system("npm run build") + end +end + +# Run "npm pack" on the required packages and strip the version suffix so they +# can be depended on in a package.json file +def pack + puts heading("Packing Bugsnag packages") + + BUGSNAG_PACKAGES.each do |package| + path = "#{ROOT}/packages/#{package}" + flags = DEBUG ? "--verbose" : "--quiet" + + success = system("npm pack #{flags} #{path}/") + + raise "Failed to pack #{package}" unless success + end + + # Strip the version suffix from the packages + Dir["#{FileUtils.pwd}/bugsnag-*.tgz"].each do |package| + package_with_no_version = package.gsub(/-\d+\.\d+\.\d+.*(?=\.tgz)/, '') + File.rename(package, package_with_no_version) + end +end + +# Install the packed packages in each fixture and build the fixture +def install_and_build + Dir["#{__dir__}/../fixtures/*"].each do |fixture| + fixture = File.realpath(fixture) + fixture_name = File.basename(fixture) + + if SPECIFIC_FIXTURE && SPECIFIC_FIXTURE != fixture_name + puts heading("Not building #{fixture_name} because it's not '#{SPECIFIC_FIXTURE}'") + next + end + + puts heading("Installing Bugsnag packages in #{fixture_name}") + + # We have to parse the template file to find out where to put the packages as + # any file outside of the function's directory is unavailable when building + template = "#{fixture}/template.yaml" + + raise "template.yaml does not exist!" unless File.exist?(template) + + parsed_template = YAML.safe_load(File.read(template)) + + # Find all of the directories we need to install the packages in using the + # CodeUri of the "AWS::Serverless::Function" resources + destinations = parsed_template.fetch("Resources").values + .filter { |resource| resource["Type"] == "AWS::Serverless::Function" } + .map { |resource| "#{fixture}/#{resource.fetch("Properties").fetch("CodeUri")}" } + .uniq + + begin + # Install the packages into each of the destination directories + destinations.each do |destination| + puts "Installing packages to #{fixture_name}/#{File.basename(destination)}" + + FileUtils.copy(Dir["#{FileUtils.pwd}/bugsnag-*.tgz"], destination) + end + + puts heading("Building #{fixture_name}") + + # SAM builds projects in the PWD by default. This would mean we build all + # of the fixtures into the same directory so we change directories to + # build in the right place + Dir.chdir(fixture) do + success = system("sam build") + + raise "Failed to build #{fixture_name}" unless success + end + ensure + next if DEBUG + + # Remove the packages from the destination directories as they aren't + # needed anymore. A built lambda has them in its node_modules directory + destinations.each do |destination| + FileUtils.remove(Dir["#{destination}/bugsnag-*.tgz"]) + end + end + end +end + +# Tidy up PWD by removing all the packed packages created by "pack" +def tidy_up + return if DEBUG + + puts heading("Tidying up PWD") + + FileUtils.remove(Dir["#{FileUtils.pwd}/bugsnag-*.tgz"]) +end + +begin + bootstrap + pack + install_and_build +ensure + tidy_up + + puts "Done!" +end diff --git a/test/aws-lambda/features/sessions.feature b/test/aws-lambda/features/sessions.feature new file mode 100644 index 0000000000..49c69e77de --- /dev/null +++ b/test/aws-lambda/features/sessions.feature @@ -0,0 +1,47 @@ +Feature: Sessions are reported correctly in lambda functions + +@simple-app +Scenario Outline: sessions are reported + Given I setup the environment + When I invoke the "" lambda in "features/fixtures/simple-app" + And 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 + 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 "session.events.handled" equals + And the event "session.events.unhandled" equals + + Examples: + | lambda | handled-count | unhandled-count | + | AsyncUnhandledExceptionFunctionNode14 | 0 | 1 | + | AsyncUnhandledExceptionFunctionNode12 | 0 | 1 | + | CallbackUnhandledExceptionFunctionNode14 | 0 | 1 | + | CallbackUnhandledExceptionFunctionNode12 | 0 | 1 | + | CallbackThrownUnhandledExceptionFunctionNode14 | 0 | 1 | + | CallbackThrownUnhandledExceptionFunctionNode12 | 0 | 1 | + | AsyncHandledExceptionFunctionNode14 | 1 | 0 | + | AsyncHandledExceptionFunctionNode12 | 1 | 0 | + | CallbackHandledExceptionFunctionNode14 | 1 | 0 | + | CallbackHandledExceptionFunctionNode12 | 1 | 0 | + +@simple-app +Scenario Outline: no session is sent when autoTrackSessions is false + Given I setup the environment + And I set environment variable "BUGSNAG_AUTO_TRACK_SESSIONS" to "false" + When I invoke the "" lambda in "features/fixtures/simple-app" + Then I should receive no sessions + When I wait to receive an error + Then the error is valid for the error reporting API version "4" for the "Bugsnag Node" notifier + + Examples: + | lambda | + | AsyncUnhandledExceptionFunctionNode14 | + | AsyncUnhandledExceptionFunctionNode12 | + | CallbackUnhandledExceptionFunctionNode14 | + | CallbackUnhandledExceptionFunctionNode12 | + | AsyncHandledExceptionFunctionNode14 | + | AsyncHandledExceptionFunctionNode12 | + | CallbackHandledExceptionFunctionNode14 | + | CallbackHandledExceptionFunctionNode12 | diff --git a/test/aws-lambda/features/steps/aws-lambda-steps.rb b/test/aws-lambda/features/steps/aws-lambda-steps.rb new file mode 100644 index 0000000000..dbc8e28d13 --- /dev/null +++ b/test/aws-lambda/features/steps/aws-lambda-steps.rb @@ -0,0 +1,7 @@ +Given('I setup the environment') do + steps %Q{ + Given I store the api key in the environment variable "BUGSNAG_API_KEY" + And I set environment variable "BUGSNAG_NOTIFY_ENDPOINT" to "http://host.docker.internal:9339/notify" + And I set environment variable "BUGSNAG_SESSIONS_ENDPOINT" to "http://host.docker.internal:9339/sessions" + } +end diff --git a/test/aws-lambda/features/support/env.rb b/test/aws-lambda/features/support/env.rb new file mode 100644 index 0000000000..0bf428b198 --- /dev/null +++ b/test/aws-lambda/features/support/env.rb @@ -0,0 +1,26 @@ +`command -v sam` + +if $? != 0 + puts <<~ERROR + The AWS SAM CLI must be installed before running these tests! + + See the installation instructions: + https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html + + If you have already installed the SAM CLI, check that it's in your PATH: + $ command -v sam + ERROR + + exit 127 +end + +success = system(File.realpath("#{__dir__}/../scripts/build-fixtures")) + +unless success + puts "Unable to build fixtures!" + exit 1 +end + +Maze.config.enforce_bugsnag_integrity = false +Maze.config.receive_no_requests_wait = 10 +Maze.config.receive_requests_wait = 10 diff --git a/test/aws-lambda/features/unhandled.feature b/test/aws-lambda/features/unhandled.feature new file mode 100644 index 0000000000..a1fb4394a4 --- /dev/null +++ b/test/aws-lambda/features/unhandled.feature @@ -0,0 +1,113 @@ +Feature: Unhandled exceptions are reported correctly in lambda functions + +@simple-app +Scenario Outline: unhandled exceptions are reported + Given I setup the environment + When I invoke the "" lambda in "features/fixtures/simple-app" with the "events//unhandled-exception.json" event + Then the lambda response "errorMessage" equals "Oh no!" + And the lambda response "errorType" equals "Error" + And the lambda response "trace" is an array with elements + And the lambda response "trace.0" equals "Error: Oh no!" + And the lambda response "body" is null + And the lambda response "statusCode" is null + 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 | + | AsyncUnhandledExceptionFunctionNode14 | async | unhandled-exception.js | 14 | 4 | + | AsyncUnhandledExceptionFunctionNode12 | async | unhandled-exception.js | 12 | 4 | + | CallbackUnhandledExceptionFunctionNode14 | callback | unhandled-exception.js | 14 | 7 | + | CallbackUnhandledExceptionFunctionNode12 | callback | unhandled-exception.js | 12 | 7 | + | CallbackThrownUnhandledExceptionFunctionNode14 | callback | thrown-unhandled-exception.js | 14 | 7 | + | CallbackThrownUnhandledExceptionFunctionNode12 | callback | thrown-unhandled-exception.js | 12 | 7 | + +@simple-app +Scenario Outline: no error is reported when autoDetectErrors is false + Given I setup the environment + And I set environment variable "BUGSNAG_AUTO_DETECT_ERRORS" to "false" + When I invoke the "" lambda in "features/fixtures/simple-app" with the "events//unhandled-exception.json" event + Then I should receive no errors + + Examples: + | lambda | type | + | AsyncUnhandledExceptionFunctionNode14 | async | + | AsyncUnhandledExceptionFunctionNode12 | async | + | CallbackUnhandledExceptionFunctionNode14 | callback | + | CallbackUnhandledExceptionFunctionNode12 | callback | + | CallbackThrownUnhandledExceptionFunctionNode14 | callback | + | CallbackThrownUnhandledExceptionFunctionNode12 | callback | + +@serverless-express-app +Scenario Outline: unhandled 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/.json" event + Then the lambda response "body.message" equals "" + And the lambda response "body.type" equals "Error" + And the lambda response "body.stacktrace" is an array with 11 elements + And the lambda response "body.stacktrace.0" equals "Error: " + And the lambda response "statusCode" equals 500 + 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 "unhandledErrorMiddleware" + And the exception "errorClass" equals "Error" + And the exception "message" equals "" + And the exception "type" equals "nodejs" + And the "file" of stack frame 0 equals "app.js" + And the event "metaData.AWS Lambda context.functionName" equals "ExpressFunction" + And the event "metaData.AWS Lambda context.awsRequestId" is not null + And the event "device.runtimeVersions.node" matches "^14\.\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 + And the event "session.events.handled" equals 0 + And the event "session.events.unhandled" equals 1 + + Examples: + | event-name | message | + | unhandled | broken :( | + | unhandled-next | borked | + +@serverless-express-app +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 + And the event "unhandled" is true + And the event "severity" equals "error" + And the event "severityReason.type" equals "unhandledErrorMiddleware" + And the exception "errorClass" equals "Error" + And the exception "message" equals "busted" + And the exception "type" equals "nodejs" + And the "file" of stack frame 0 equals "app.js" + And the event "metaData.AWS Lambda context.functionName" equals "ExpressFunction" + And the event "metaData.AWS Lambda context.awsRequestId" is not null + And the event "device.runtimeVersions.node" matches "^14\.\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 + And the event "session.events.handled" equals 0 + And the event "session.events.unhandled" equals 1 diff --git a/tsconfig.json b/tsconfig.json index c5eb792567..8c5176a8df 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -70,7 +70,9 @@ "packages/delivery-react-native", "packages/delivery-x-domain-request", "packages/delivery-xml-http-request", + "packages/in-flight", "packages/plugin-app-duration", + "packages/plugin-aws-lambda", "packages/plugin-browser-context", "packages/plugin-browser-device", "packages/plugin-contextualize",