From a831b092d9ae3e2fdd972a8ac66710bd3d6681bd Mon Sep 17 00:00:00 2001 From: Ben Gourley Date: Wed, 6 May 2020 14:41:07 +0100 Subject: [PATCH 01/10] feat(plugin-react): Defer obtaining React reference --- packages/plugin-react/src/index.js | 95 +++++++++++-------- packages/plugin-react/src/test/index.test.tsx | 2 +- 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/packages/plugin-react/src/index.js b/packages/plugin-react/src/index.js index e05c50152c..000165008f 100644 --- a/packages/plugin-react/src/index.js +++ b/packages/plugin-react/src/index.js @@ -1,48 +1,32 @@ module.exports = class BugsnagReactPlugin { - constructor (React = window.React) { - if (!React) throw new Error('cannot find React') - this.React = React + constructor (...args) { this.name = 'react' + this.lazy = args.length === 0 && !window.React + if (!this.lazy) { + this.React = args[0] || window.React + if (!this.React) throw new Error('@bugsnag/plugin-react reference to `React` was undefined') + } } load (client) { - const React = this.React - - class ErrorBoundary extends React.Component { - constructor (props) { - super(props) - this.state = { - error: null, - info: null - } - } + if (!this.lazy) return createClass(this.React, client) + const BugsnagPluginReactLazyInitializer = function () { + throw new Error(`@bugsnag/plugin-react was used incorrectly. Valid usage is as follows: +Pass React to the plugin constructor + \`Bugsnag.start({ plugins: [new BugsnagPluginReact(React)] })\` +and then call \`const ErrorBoundary = Bugsnag.getPlugin('react')\` - componentDidCatch (error, info) { - const { onError } = this.props - const handledState = { severity: 'error', unhandled: true, severityReason: { type: 'unhandledException' } } - const event = client.Event.create( - error, - true, - handledState, - 1 - ) - if (info && info.componentStack) info.componentStack = formatComponentStack(info.componentStack) - event.addMetadata('react', info) - client._notify(event, onError) - this.setState({ error, info }) - } - - render () { - const { error } = this.state - if (error) { - const { FallbackComponent } = this.props - if (FallbackComponent) return React.createElement(FallbackComponent, this.state) - return null - } - return this.props.children - } +Or if React is not available until after Bugsnag has started, +construct the plugin with no arguments + \`Bugsnag.start({ plugins: [new BugsnagPluginReact()] })\`, +then pass in React when available to construct your error boundary + \`Bugsnag.getPlugin('react').createErrorBoundary(React)\``) + } + BugsnagPluginReactLazyInitializer.createErrorBoundary = (React) => { + if (!React) throw new Error('@bugsnag/plugin-react reference to `React` was undefined') + createClass(React, client) } - return ErrorBoundary + return BugsnagPluginReactLazyInitializer } } @@ -55,5 +39,40 @@ const formatComponentStack = str => { return ret } +const createClass = (React, client) => class ErrorBoundary extends React.Component { + constructor (props) { + super(props) + this.state = { + error: null, + info: null + } + } + + componentDidCatch (error, info) { + const { onError } = this.props + const handledState = { severity: 'error', unhandled: true, severityReason: { type: 'unhandledException' } } + const event = client.Event.create( + error, + true, + handledState, + 1 + ) + if (info && info.componentStack) info.componentStack = formatComponentStack(info.componentStack) + event.addMetadata('react', info) + client._notify(event, onError) + this.setState({ error, info }) + } + + render () { + const { error } = this.state + if (error) { + const { FallbackComponent } = this.props + if (FallbackComponent) return React.createElement(FallbackComponent, this.state) + return null + } + return this.props.children + } +} + module.exports.formatComponentStack = formatComponentStack module.exports.default = module.exports diff --git a/packages/plugin-react/src/test/index.test.tsx b/packages/plugin-react/src/test/index.test.tsx index ef14aad973..b41a3cab14 100644 --- a/packages/plugin-react/src/test/index.test.tsx +++ b/packages/plugin-react/src/test/index.test.tsx @@ -18,7 +18,7 @@ const bugsnag = { } const plugin = new BugsnagPluginReact(React) -const ErrorBoundary = plugin.load(bugsnag) +const ErrorBoundary = plugin.load(bugsnag) as unknown as typeof React.Component beforeEach(() => { bugsnag._notify.mockReset() From 6f4da4543b1c134a9935232581a173c7b2197294 Mon Sep 17 00:00:00 2001 From: Ben Gourley Date: Wed, 6 May 2020 17:21:40 +0100 Subject: [PATCH 02/10] style(plugin-react): Tweak error message formatting and consistency --- packages/plugin-react/src/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/plugin-react/src/index.js b/packages/plugin-react/src/index.js index 000165008f..d9b01194f9 100644 --- a/packages/plugin-react/src/index.js +++ b/packages/plugin-react/src/index.js @@ -13,6 +13,7 @@ module.exports = class BugsnagReactPlugin { const BugsnagPluginReactLazyInitializer = function () { throw new Error(`@bugsnag/plugin-react was used incorrectly. Valid usage is as follows: Pass React to the plugin constructor + \`Bugsnag.start({ plugins: [new BugsnagPluginReact(React)] })\` and then call \`const ErrorBoundary = Bugsnag.getPlugin('react')\` @@ -20,7 +21,7 @@ Or if React is not available until after Bugsnag has started, construct the plugin with no arguments \`Bugsnag.start({ plugins: [new BugsnagPluginReact()] })\`, then pass in React when available to construct your error boundary - \`Bugsnag.getPlugin('react').createErrorBoundary(React)\``) + \`const ErrorBoundary = Bugsnag.getPlugin('react').createErrorBoundary(React)\``) } BugsnagPluginReactLazyInitializer.createErrorBoundary = (React) => { if (!React) throw new Error('@bugsnag/plugin-react reference to `React` was undefined') From 4b4d98f51e52e0216cebe2dbe474ef7867f7e672 Mon Sep 17 00:00:00 2001 From: Ben Gourley Date: Wed, 6 May 2020 17:23:15 +0100 Subject: [PATCH 03/10] fix(plugin-react): Add missing return statement --- packages/plugin-react/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-react/src/index.js b/packages/plugin-react/src/index.js index d9b01194f9..4f7c50f485 100644 --- a/packages/plugin-react/src/index.js +++ b/packages/plugin-react/src/index.js @@ -25,7 +25,7 @@ then pass in React when available to construct your error boundary } BugsnagPluginReactLazyInitializer.createErrorBoundary = (React) => { if (!React) throw new Error('@bugsnag/plugin-react reference to `React` was undefined') - createClass(React, client) + return createClass(React, client) } return BugsnagPluginReactLazyInitializer } From e135ed89b575b427760c7f816c47b69f3df633ad Mon Sep 17 00:00:00 2001 From: Ben Gourley Date: Tue, 12 May 2020 09:43:17 +0100 Subject: [PATCH 04/10] refactor(plugin-react): Make plugin return-type consistent --- packages/plugin-react/src/index.js | 7 ++++++- .../plugin-react/types/bugsnag-plugin-react.d.ts | 13 ++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/plugin-react/src/index.js b/packages/plugin-react/src/index.js index 4f7c50f485..bbd300cfad 100644 --- a/packages/plugin-react/src/index.js +++ b/packages/plugin-react/src/index.js @@ -9,7 +9,12 @@ module.exports = class BugsnagReactPlugin { } load (client) { - if (!this.lazy) return createClass(this.React, client) + if (!this.lazy) { + const ErrorBoundary = createClass(this.React, client) + ErrorBoundary.createErrorBoundary = () => ErrorBoundary + return ErrorBoundary + } + const BugsnagPluginReactLazyInitializer = function () { throw new Error(`@bugsnag/plugin-react was used incorrectly. Valid usage is as follows: Pass React to the plugin constructor diff --git a/packages/plugin-react/types/bugsnag-plugin-react.d.ts b/packages/plugin-react/types/bugsnag-plugin-react.d.ts index 8d5a41ea8e..916131912f 100644 --- a/packages/plugin-react/types/bugsnag-plugin-react.d.ts +++ b/packages/plugin-react/types/bugsnag-plugin-react.d.ts @@ -1,4 +1,4 @@ -import { Plugin } from '@bugsnag/core' +import { Plugin, Client } from '@bugsnag/core' import React from 'react' // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -8,4 +8,15 @@ declare class BugsnagPluginReact { constructor(react?: typeof React) } +interface BugsnagPluginReactResult { + createErrorBoundary(react?: typeof React): React.Component +} + +type ReactPluginId = 'react' +declare module '@bugsnag/core' { + interface Client { + getPlugin(id: ReactPluginId): BugsnagPluginReactResult | undefined + } +} + export default BugsnagPluginReact From c61c04e6dece1c6ddacf368fceb2671feba53ddc Mon Sep 17 00:00:00 2001 From: Ben Gourley Date: Tue, 12 May 2020 10:52:04 +0100 Subject: [PATCH 05/10] refactor: Stop vendoring @bugsnag/core types --- bin/bundle-types | 21 --------------------- packages/browser/package.json | 13 ++++++++----- packages/expo/package.json | 10 +++------- packages/node/package.json | 10 +++++----- packages/plugin-express/package.json | 2 +- packages/plugin-koa/package.json | 2 +- packages/plugin-react/package.json | 10 +++++----- packages/plugin-restify/package.json | 2 +- packages/plugin-vue/package.json | 12 +++++++----- 9 files changed, 31 insertions(+), 51 deletions(-) delete mode 100755 bin/bundle-types diff --git a/bin/bundle-types b/bin/bundle-types deleted file mode 100755 index cfb7f8a9cb..0000000000 --- a/bin/bundle-types +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -# exit on all errors -set -e - -if [ -z "$1" ]; then - echo "ERROR: bin/bundle-types {name} :: {name} argument is required" - exit 1 -fi - -# ensure directory exists -mkdir -p dist/types/bugsnag-core - -# copy all .d.ts files from @bugsnag/core -cp node_modules/@bugsnag/core/types/*.d.ts dist/types/bugsnag-core - -# copy all .d.ts files from this module -cp types/*.d.ts dist/types - -# replace any references to @bugsnag/core with the new, bundled, local path -cat types/$1.d.ts | sed 's/@bugsnag\/core/\.\/bugsnag-core/' > dist/types/$1.d.ts diff --git a/packages/browser/package.json b/packages/browser/package.json index 38f6606cbb..84beadbdf5 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -2,7 +2,7 @@ "name": "@bugsnag/browser", "version": "7.0.1", "main": "dist/bugsnag.js", - "types": "dist/types/bugsnag.d.ts", + "types": "types/bugsnag.d.ts", "description": "Bugsnag error reporter for browser JavaScript", "homepage": "https://www.bugsnag.com/", "repository": { @@ -10,19 +10,19 @@ "url": "git@github.com:bugsnag/bugsnag-js.git" }, "browser": { - "dist/types/bugsnag": "./dist/bugsnag.js" + "types/bugsnag": "./dist/bugsnag.js" }, "publishConfig": { "access": "public" }, "files": [ - "dist" + "dist", + "types" ], "scripts": { "size": "./bin/size", "clean": "rm -fr dist && mkdir dist", - "bundle-types": "../../bin/bundle-types bugsnag", - "build": "npm run clean && npm run build:dist && npm run build:dist:min && npm run bundle-types", + "build": "npm run clean && npm run build:dist && npm run build:dist:min", "build:dist": "NODE_ENV=production IS_BROWSER=yes ../../bin/bundle src/notifier.js --standalone=Bugsnag | ../../bin/extract-source-map dist/bugsnag.js", "build:dist:min": "NODE_ENV=production IS_BROWSER=yes ../../bin/bundle src/notifier.js --standalone=Bugsnag | ../../bin/minify dist/bugsnag.min.js", "test:types": "jasmine 'types/**/*.test.js'", @@ -56,5 +56,8 @@ "nyc": "^12.0.2", "semver": "^5.5.1", "typescript": "^3.7.5" + }, + "dependencies": { + "@bugsnag/core": "^7.0.1" } } diff --git a/packages/expo/package.json b/packages/expo/package.json index 5871b344ac..a9ba484ea8 100644 --- a/packages/expo/package.json +++ b/packages/expo/package.json @@ -2,7 +2,7 @@ "name": "@bugsnag/expo", "version": "7.0.1", "main": "src/notifier.js", - "types": "dist/types/bugsnag.d.ts", + "types": "types/bugsnag.d.ts", "description": "Bugsnag error reporter for Expo applications", "keywords": [ "bugsnag", @@ -27,14 +27,10 @@ "files": [ "src", "hooks", - "dist" + "types" ], "scripts": { - "clean": "rm -fr dist && mkdir dist", - "bundle-types": "../../bin/bundle-types bugsnag", - "build": "npm run clean && npm run bundle-types", - "test:types": "jasmine 'types/**/*.test.js'", - "postversion": "npm run build" + "test:types": "jasmine 'types/**/*.test.js'" }, "author": "Bugsnag", "license": "MIT", diff --git a/packages/node/package.json b/packages/node/package.json index f944ce9f48..7a49e250e0 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -2,7 +2,7 @@ "name": "@bugsnag/node", "version": "7.0.1", "main": "dist/bugsnag.js", - "types": "dist/types/bugsnag.d.ts", + "types": "types/bugsnag.d.ts", "description": "Bugsnag error reporter for Node.js", "homepage": "https://www.bugsnag.com/", "repository": { @@ -13,19 +13,18 @@ "access": "public" }, "files": [ - "dist" + "dist", + "types" ], "scripts": { "clean": "rm -fr dist && mkdir dist", - "bundle-types": "../../bin/bundle-types bugsnag", - "build": "npm run clean && npm run build:dist && npm run bundle-types", + "build": "npm run clean && npm run build:dist", "build:dist": "../../bin/bundle src/notifier.js --node --exclude=iserror,stack-generator,error-stack-parser,pump,byline --standalone=bugsnag | ../../bin/extract-source-map dist/bugsnag.js", "postversion": "npm run build" }, "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^7.0.1", "@bugsnag/delivery-node": "^7.0.1", "@bugsnag/plugin-contextualize": "^7.0.1", "@bugsnag/plugin-intercept": "^7.0.1", @@ -40,6 +39,7 @@ "nyc": "^12.0.2" }, "dependencies": { + "@bugsnag/core": "^7.0.1", "byline": "^5.0.0", "error-stack-parser": "^2.0.2", "iserror": "^0.0.2", diff --git a/packages/plugin-express/package.json b/packages/plugin-express/package.json index db6e44d477..eacb88332a 100644 --- a/packages/plugin-express/package.json +++ b/packages/plugin-express/package.json @@ -29,11 +29,11 @@ "@bugsnag/js": "*" }, "devDependencies": { - "@bugsnag/core": "^7.0.1", "jasmine": "^3.1.0", "nyc": "^12.0.2" }, "dependencies": { + "@bugsnag/core": "^7.0.1", "iserror": "^0.0.2" } } diff --git a/packages/plugin-koa/package.json b/packages/plugin-koa/package.json index 88cac8b170..b97c3af2bf 100644 --- a/packages/plugin-koa/package.json +++ b/packages/plugin-koa/package.json @@ -29,11 +29,11 @@ "@bugsnag/js": "*" }, "devDependencies": { - "@bugsnag/core": "^7.0.1", "jasmine": "^3.1.0", "nyc": "^12.0.2" }, "dependencies": { + "@bugsnag/core": "^7.0.1", "iserror": "^0.0.2" } } diff --git a/packages/plugin-react/package.json b/packages/plugin-react/package.json index 3fecd93504..3541229715 100644 --- a/packages/plugin-react/package.json +++ b/packages/plugin-react/package.json @@ -4,7 +4,7 @@ "main": "dist/bugsnag-react.js", "description": "React integration for @bugsnag/js", "browser": "dist/bugsnag-react.js", - "types": "dist/types/bugsnag-plugin-react.d.ts", + "types": "types/bugsnag-plugin-react.d.ts", "homepage": "https://www.bugsnag.com/", "repository": { "type": "git", @@ -14,17 +14,17 @@ "access": "public" }, "files": [ - "dist" + "dist", + "types" ], "scripts": { "clean": "rm -fr dist && mkdir dist", - "bundle-types": "../../bin/bundle-types bugsnag-plugin-react", - "build": "npm run clean && ../../bin/bundle src/index.js --standalone=BugsnagPluginReact | ../../bin/extract-source-map dist/bugsnag-react.js && npm run bundle-types", + "build": "npm run clean && ../../bin/bundle src/index.js --standalone=BugsnagPluginReact | ../../bin/extract-source-map dist/bugsnag-react.js", "postversion": "npm run build" }, "author": "Bugsnag", "license": "MIT", - "devDependencies": { + "dependencies": { "@bugsnag/core": "^7.0.1" } } diff --git a/packages/plugin-restify/package.json b/packages/plugin-restify/package.json index b30c179f03..33002b379b 100644 --- a/packages/plugin-restify/package.json +++ b/packages/plugin-restify/package.json @@ -29,11 +29,11 @@ "@bugsnag/js": "*" }, "devDependencies": { - "@bugsnag/core": "^7.0.1", "jasmine": "^3.1.0", "nyc": "^12.0.2" }, "dependencies": { + "@bugsnag/core": "^7.0.1", "iserror": "^0.0.2" } } diff --git a/packages/plugin-vue/package.json b/packages/plugin-vue/package.json index e67a33ea5d..760f4dfd92 100644 --- a/packages/plugin-vue/package.json +++ b/packages/plugin-vue/package.json @@ -4,7 +4,7 @@ "description": "Vue.js integration for bugsnag-js", "main": "dist/bugsnag-vue.js", "browser": "dist/bugsnag-vue.js", - "types": "dist/types/bugsnag-plugin-vue.d.ts", + "types": "types/bugsnag-plugin-vue.d.ts", "homepage": "https://www.bugsnag.com/", "repository": { "type": "git", @@ -14,12 +14,12 @@ "access": "public" }, "files": [ - "dist" + "dist", + "types" ], "scripts": { "clean": "rm -fr dist && mkdir dist", - "bundle-types": "../../bin/bundle-types bugsnag-plugin-vue", - "build": "npm run clean && ../../bin/bundle src/index.js --standalone=BugsnagPluginVue | ../../bin/extract-source-map dist/bugsnag-vue.js && npm run bundle-types", + "build": "npm run clean && ../../bin/bundle src/index.js --standalone=BugsnagPluginVue | ../../bin/extract-source-map dist/bugsnag-vue.js", "postversion": "npm run build" }, "author": "Bugsnag", @@ -28,9 +28,11 @@ "@bugsnag/js": "*" }, "devDependencies": { - "@bugsnag/core": "^7.0.1", "jasmine": "^3.1.0", "nyc": "^12.0.2", "vue": "^2.5.8" + }, + "dependencies": { + "@bugsnag/core": "^7.0.1" } } From 5dcc22aae42373e52ae980b89ed137633d396a5b Mon Sep 17 00:00:00 2001 From: Ben Gourley Date: Tue, 12 May 2020 15:44:44 +0100 Subject: [PATCH 06/10] refactor(plugin-react): Consistency, correct typing, rework tests to use public types --- packages/plugin-react/src/index.js | 2 +- packages/plugin-react/src/test/index.test.tsx | 40 ++++++++----------- .../types/bugsnag-plugin-react.d.ts | 6 +-- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/packages/plugin-react/src/index.js b/packages/plugin-react/src/index.js index bbd300cfad..247b603dbb 100644 --- a/packages/plugin-react/src/index.js +++ b/packages/plugin-react/src/index.js @@ -1,4 +1,4 @@ -module.exports = class BugsnagReactPlugin { +module.exports = class BugsnagPluginReact { constructor (...args) { this.name = 'react' this.lazy = args.length === 0 && !window.React diff --git a/packages/plugin-react/src/test/index.test.tsx b/packages/plugin-react/src/test/index.test.tsx index b41a3cab14..6fbf523e75 100644 --- a/packages/plugin-react/src/test/index.test.tsx +++ b/packages/plugin-react/src/test/index.test.tsx @@ -1,28 +1,15 @@ import React from 'react' import renderer from 'react-test-renderer' import BugsnagPluginReact from '..' +import Client from '@bugsnag/core/client' -class Event { - static create () { - return new Event() - } +const client = new Client({ apiKey: '123', plugins: [new BugsnagPluginReact(React)] }, undefined) +client._notify = jest.fn() - addMetadata () { - return this - } -} - -const bugsnag = { - Event, - _notify: jest.fn() -} +// eslint-disable-next-line +const ErrorBoundary = client.getPlugin('react')!.createErrorBoundary() -const plugin = new BugsnagPluginReact(React) -const ErrorBoundary = plugin.load(bugsnag) as unknown as typeof React.Component - -beforeEach(() => { - bugsnag._notify.mockReset() -}) +beforeEach(() => (client._notify as jest.Mock).mockClear()) test('formatComponentStack(str)', () => { const str = ` @@ -57,7 +44,7 @@ it('calls notify on error', () => { renderer .create() .toJSON() - expect(bugsnag._notify).toHaveBeenCalledTimes(1) + expect(client._notify).toHaveBeenCalledTimes(1) }) it('does not render FallbackComponent when no error', () => { @@ -87,13 +74,20 @@ it('passes the props to the FallbackComponent', () => { }, {}) }) -it('it passes the onError function to the Bugsnag notify call', () => { +it('passes the onError function to the Bugsnag notify call', () => { const onError = () => {} renderer .create() .toJSON() - expect(bugsnag._notify).toBeCalledWith( - expect.any(Event), + expect(client._notify).toBeCalledWith( + expect.any(client.Event), onError ) }) + +it('supports passing reference to React when the error boundary is created', () => { + const client = new Client({ apiKey: '123', plugins: [new BugsnagPluginReact()] }, undefined) + // eslint-disable-next-line + const ErrorBoundary = client.getPlugin('react')!.createErrorBoundary(React) + expect(ErrorBoundary).toBeTruthy() +}) diff --git a/packages/plugin-react/types/bugsnag-plugin-react.d.ts b/packages/plugin-react/types/bugsnag-plugin-react.d.ts index 916131912f..0a9a086177 100644 --- a/packages/plugin-react/types/bugsnag-plugin-react.d.ts +++ b/packages/plugin-react/types/bugsnag-plugin-react.d.ts @@ -9,13 +9,13 @@ declare class BugsnagPluginReact { } interface BugsnagPluginReactResult { - createErrorBoundary(react?: typeof React): React.Component + createErrorBoundary(react?: typeof React): typeof React.Component } -type ReactPluginId = 'react' +// add a new call signature for the getPlugin() method that types the react plugin result declare module '@bugsnag/core' { interface Client { - getPlugin(id: ReactPluginId): BugsnagPluginReactResult | undefined + getPlugin(id: 'react'): BugsnagPluginReactResult | undefined } } From 112dc8a6dbdd6caad037988217a275035b755d57 Mon Sep 17 00:00:00 2001 From: Ben Gourley Date: Tue, 12 May 2020 16:15:05 +0100 Subject: [PATCH 07/10] refactor(plugin-vue): Support deferred passing of framework reference --- packages/plugin-vue/src/index.js | 56 ++++++++++++------- packages/plugin-vue/test/index.test.ts | 19 ++++++- .../plugin-vue/types/bugsnag-plugin-vue.d.ts | 13 ++++- 3 files changed, 66 insertions(+), 22 deletions(-) diff --git a/packages/plugin-vue/src/index.js b/packages/plugin-vue/src/index.js index aa1b379b38..74a9e4c517 100644 --- a/packages/plugin-vue/src/index.js +++ b/packages/plugin-vue/src/index.js @@ -1,33 +1,49 @@ -module.exports = class BugsnagVuePlugin { - constructor (Vue = window.Vue) { - if (!Vue) throw new Error('cannot find Vue') - this.Vue = Vue +module.exports = class BugsnagPluginVue { + constructor (...args) { this.name = 'vue' + this.lazy = args.length === 0 && !window.Vue + if (!this.lazy) { + this.Vue = args[0] || window.Vue + if (!this.Vue) throw new Error('@bugsnag/plugin-vue reference to `Vue` was undefined') + } } load (client) { - const Vue = this.Vue - const prev = Vue.config.errorHandler + if (this.Vue) { + install(this.Vue, client) + return { + installVueErrorHandler: () => client._logger.warn('installVueErrorHandler() was called unnecessarily') + } + } + return { + installVueErrorHandler: Vue => { + if (!Vue) client._logger.error(new Error('@bugsnag/plugin-vue reference to `Vue` was undefined')) + install(Vue, client) + } + } + } +} - const handler = (err, vm, info) => { - const handledState = { severity: 'error', unhandled: true, severityReason: { type: 'unhandledException' } } - const event = client.Event.create(err, true, handledState, 1) +const install = (Vue, client) => { + const prev = Vue.config.errorHandler - event.addMetadata('vue', { - errorInfo: info, - component: vm ? formatComponentName(vm, true) : undefined, - props: vm ? vm.$options.propsData : undefined - }) + const handler = (err, vm, info) => { + const handledState = { severity: 'error', unhandled: true, severityReason: { type: 'unhandledException' } } + const event = client.Event.create(err, true, handledState, 1) - client._notify(event) - if (typeof console !== 'undefined' && typeof console.error === 'function') console.error(err) + event.addMetadata('vue', { + errorInfo: info, + component: vm ? formatComponentName(vm, true) : undefined, + props: vm ? vm.$options.propsData : undefined + }) - if (typeof prev === 'function') prev.call(this, err, vm, info) - } + client._notify(event) + if (typeof console !== 'undefined' && typeof console.error === 'function') console.error(err) - Vue.config.errorHandler = handler - return null + if (typeof prev === 'function') prev.call(this, err, vm, info) } + + Vue.config.errorHandler = handler } // taken and reworked from Vue.js source diff --git a/packages/plugin-vue/test/index.test.ts b/packages/plugin-vue/test/index.test.ts index 604520410c..b972d05824 100644 --- a/packages/plugin-vue/test/index.test.ts +++ b/packages/plugin-vue/test/index.test.ts @@ -5,7 +5,7 @@ import Vue from 'vue' describe('bugsnag vue', () => { it('throws when missing Vue', () => { expect(() => { - new BugsnagVuePlugin().load(new Client({ apiKey: 'API_KEYYY' })) + new BugsnagVuePlugin(undefined).load(new Client({ apiKey: 'API_KEYYY' })) }).toThrow() }) @@ -24,6 +24,23 @@ describe('bugsnag vue', () => { Vue.config.errorHandler(new Error('oops'), { $root: true, $options: {} } as unknown as Vue, 'callback for watcher "fooBarBaz"') }) + it('supports Vue being passed later', done => { + const client = new Client({ apiKey: 'API_KEYYY', plugins: [new BugsnagVuePlugin()] }) + // eslint-disable-next-line + client.getPlugin('vue')!.installVueErrorHandler(Vue) + client._setDelivery(client => ({ + sendEvent: (payload) => { + expect(payload.events[0].errors[0].errorClass).toBe('Error') + expect(payload.events[0].errors[0].errorMessage).toBe('oops') + expect(payload.events[0]._metadata.vue).toBeDefined() + done() + }, + sendSession: () => {} + })) + expect(typeof Vue.config.errorHandler).toBe('function') + Vue.config.errorHandler(new Error('oops'), { $root: true, $options: {} } as unknown as Vue, 'callback for watcher "fooBarBaz"') + }) + it('bugsnag vue: classify(str)', () => { expect(BugsnagVuePlugin.classify('foo_bar')).toBe('FooBar') expect(BugsnagVuePlugin.classify('foo-bar')).toBe('FooBar') diff --git a/packages/plugin-vue/types/bugsnag-plugin-vue.d.ts b/packages/plugin-vue/types/bugsnag-plugin-vue.d.ts index 8982cbfc6b..b405b13fd4 100644 --- a/packages/plugin-vue/types/bugsnag-plugin-vue.d.ts +++ b/packages/plugin-vue/types/bugsnag-plugin-vue.d.ts @@ -1,4 +1,4 @@ -import { Plugin } from '@bugsnag/core' +import { Plugin, Client } from '@bugsnag/core' import { VueConstructor } from 'vue' // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -8,4 +8,15 @@ declare class BugsnagPluginVue { constructor(Vue?: VueConstructor) } +interface BugsnagPluginVueResult { + installVueErrorHandler(vue?: VueConstructor): void +} + +// add a new call signature for the getPlugin() method that types the vue plugin result +declare module '@bugsnag/core' { + interface Client { + getPlugin(id: 'vue'): BugsnagPluginVueResult | undefined + } +} + export default BugsnagPluginVue From 289918ff4856d20f14f38a41b9cda10454469aae Mon Sep 17 00:00:00 2001 From: Ben Gourley Date: Tue, 12 May 2020 17:17:50 +0100 Subject: [PATCH 08/10] test: Fix problem with test image picking up wrong files --- dockerfiles/Dockerfile.browser | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockerfiles/Dockerfile.browser b/dockerfiles/Dockerfile.browser index 33aefc836f..3a9426f860 100644 --- a/dockerfiles/Dockerfile.browser +++ b/dockerfiles/Dockerfile.browser @@ -37,7 +37,7 @@ RUN npm install --no-package-lock --no-save \ # install the dependencies and build each fixture WORKDIR /app/test/browser/features/fixtures -RUN find . -name package.json -type f -mindepth 2 -maxdepth 3 | \ +RUN find . -name package.json -type f -mindepth 2 -maxdepth 3 ! -path "./node_modules/*" | \ xargs -I % bash -c 'cd `dirname %` && npm install --no-package-lock && npm run build' # once the fixtures are built we no longer need node_modules and From 7d2351850846a4a85f5610997572672d62b4bf0b Mon Sep 17 00:00:00 2001 From: Ben Gourley Date: Wed, 13 May 2020 16:24:16 +0100 Subject: [PATCH 09/10] chore: Document plugin/type changes --- CHANGELOG.md | 13 ++++++ UPGRADING.md | 66 +++++++++++++++++++++++++++++- packages/plugin-react/src/index.js | 2 +- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90e7deeda5..fe3d44b062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 7.1.0 (TBD) + +This update contains some substantial changes to plugin type definitions. If you are using TypeScript alongside a framework, you may need to make changes to your app. Please refer to the [upgrade guide](./UPGRADING.md). + +### Changed + +- (plugin-react|plugin-vue): Support late passing of framework reference [#839](https://github.com/bugsnag/bugsnag-js/pull/839) + +### Added + +- (plugin-react): Add type definitions for `Bugsnag.getPlugin('react')` [#839](https://github.com/bugsnag/bugsnag-js/pull/839) +- (plugin-vue): Add type definitions for `Bugsnag.getPlugin('vue')` [#839](https://github.com/bugsnag/bugsnag-js/pull/839) + ## 7.0.2 (2020-05-12) ### Fixed diff --git a/UPGRADING.md b/UPGRADING.md index 2cc8ed4b30..2756aea6ee 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,70 @@ Upgrading ========= +## 7.0 to 7.1 + +This release contains an update to the way the React and Vue plugins work, allowing the reference to the framework to be supplied after Bugsnag has been initialized. + +### Types + +From a JS perspective, the update is backwards compatible. Despite being compatible at runtime, the change to type definitions will cause a compile error when TypeScript is used in conjunction with `@bugsnag/plugin-react`. The error is straightforward to resolve: + +```TypeScript +// WRONG: return type was 'any', this will now fail to compile +const ErrorBoundary = Bugsnag.getPlugin('react') + +// OK: to use exactly the same logic you will need to cast +const ErrorBoundary = Bugsnag.getPlugin('react') as unknown as React.Component + +// RECOMMENDED: or to make use of the provided type definitions, update to the new api +const ErrorBoundary = Bugsnag.getPlugin('react')!.createErrorBoundary() +``` + +_Note the use of the `!` operator._ The `getPlugin('react')` call will only return something if the react plugin was provided to `Bugsnag.start({ plugins: […] })`. + +### Plugins + +In order to work, The React and Vue plugin both require a reference to the respective framework to be passed in. This was required in the constructor, which meant there was no way to load Bugsnag _before_ the framework. To support this, we now support supplying the framework reference _after_ Bugsnag has started. + +Note that the existing usage is still supported. + +#### React + +```diff +import Bugsnag from '@bugsnag/js' +import BugsnagPluginReact from '@bugsnag/plugin-react' +import * as React from 'react' + +Bugsnag.start({ + apiKey: 'YOUR_API_KEY', + plugins: [ +- new BugsnagPluginReact(React) ++ new BugsnagPluginReact() + ] +}) + +- const ErrorBoundary = Bugsnag.getPlugin('react') ++ const ErrorBoundary = Bugsnag.getPlugin('react').createErrorBoundary(React) +``` + +#### Vue + +```diff +import Bugsnag from '@bugsnag/js' +import BugsnagPluginVue from '@bugsnag/plugin-vue' +import Vue from 'vue' + +Bugsnag.start({ + apiKey: 'YOUR_API_KEY', + plugins: [ +- new BugsnagPluginVue(Vue) ++ new BugsnagPluginVue() + ] +}) + ++ Bugsnag.getPlugin('vue').installVueErrorHandler() +``` + ## 6.x to 7.x __This version contains many breaking changes__. It is part of an effort to unify our notifier libraries across platforms, making the user interface more consistent, and implementations better on multi-layered environments where multiple Bugsnag libraries need to work together (such as React Native). @@ -290,7 +354,7 @@ Here are some examples: // adding metadata - bugsnagClient.notify(err, { - metaData: { -- component: { +- component: { - instanceId: component.instanceId - } - } diff --git a/packages/plugin-react/src/index.js b/packages/plugin-react/src/index.js index 247b603dbb..fec00b84a6 100644 --- a/packages/plugin-react/src/index.js +++ b/packages/plugin-react/src/index.js @@ -20,7 +20,7 @@ module.exports = class BugsnagPluginReact { Pass React to the plugin constructor \`Bugsnag.start({ plugins: [new BugsnagPluginReact(React)] })\` -and then call \`const ErrorBoundary = Bugsnag.getPlugin('react')\` +and then call \`const ErrorBoundary = Bugsnag.getPlugin('react').createErrorBoundary()\` Or if React is not available until after Bugsnag has started, construct the plugin with no arguments From 31f1f725d5252a82e1c44389e215fd8f09b9992c Mon Sep 17 00:00:00 2001 From: Ben Gourley Date: Wed, 13 May 2020 16:30:32 +0100 Subject: [PATCH 10/10] chore: Typos and tweaks --- UPGRADING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index 2756aea6ee..655b0625e0 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -16,7 +16,7 @@ const ErrorBoundary = Bugsnag.getPlugin('react') // OK: to use exactly the same logic you will need to cast const ErrorBoundary = Bugsnag.getPlugin('react') as unknown as React.Component -// RECOMMENDED: or to make use of the provided type definitions, update to the new api +// RECOMMENDED: to make use of the provided type definitions, update to the new api const ErrorBoundary = Bugsnag.getPlugin('react')!.createErrorBoundary() ``` @@ -62,7 +62,7 @@ Bugsnag.start({ ] }) -+ Bugsnag.getPlugin('vue').installVueErrorHandler() ++ Bugsnag.getPlugin('vue').installVueErrorHandler(Vue) ``` ## 6.x to 7.x