diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bf8f6ed43..ee1612d096 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ - Rename `filters` option to `redactedKeys` [#704](https://github.com/bugsnag/bugsnag-js/pull/704) - Rename `device.modelName` to `device.model` [#726](https://github.com/bugsnag/bugsnag-js/pull/726) - Rename `client.refresh()` to `client.resetEventCount()` [#727](https://github.com/bugsnag/bugsnag-js/pull/727) +- `client.use(plugin)` has been removed and plugins must now be passed in to configuration [#759](https://github.com/bugsnag/bugsnag-js/pull/759) +- Invalid configuration (except for `apiKey`) now falls back to default values rather than throwing an error [#759](https://github.com/bugsnag/bugsnag-js/pull/759) ### Removed - Remove non-public methods from `Client` interface: `logger()`, `delivery()` and `sessionDelegate()` [#659](https://github.com/bugsnag/bugsnag-js/pull/659) diff --git a/UPGRADING.md b/UPGRADING.md index e71e974dcb..c10cd22d1c 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -129,6 +129,9 @@ Many options have been renamed, reworked or replaced. + onBreadcrumb: (breadcrumb) => { + // a callback to run each time a breadcrumb is created + } + + // plugins must now be supplied in config rather than via client.use() ++ plugins: [] } ``` @@ -364,6 +367,45 @@ Previously it was valid to supply a `notify` endpoint without supplying a `sessi } ``` +## Plugins + +Plugins must now be supplied in configuration, and the `client.use()` method has been removed. For users of the following plugins, some changes are required: + +### `@bugsnag/plugin-react` + +```diff +- import bugsnagReact from '@bugsnag/plugin-react' ++ import BugsnagPluginReact from '@bugsnag/plugin-react' + +- const bugsnagClient = bugsnag('YOUR_API_KEY') ++ Bugsnag.start({ apiKey: 'YOUR_API_KEY', plugins: [new BugsnagPluginReact(React)] }) + +- bugsnagClient.use(bugsnagReact, React) +``` + +### `@bugsnag/plugin-vue` + +```diff +- import bugsnagVue from '@bugsnag/plugin-vue' ++ import BugsnagPluginVue from '@bugsnag/plugin-vue' + +- const bugsnagClient = bugsnag('YOUR_API_KEY') ++ Bugsnag.start({ apiKey: 'YOUR_API_KEY', plugins: [new BugsnagPluginVue(Vue)] }) + +- bugsnagClient.use(bugsnagVue, Vue) +``` + +### `@bugsnag/plugin-{restify|koa|express}` + +Since these plugins don't need any input, their usage is simpler and follows this pattern + +```diff +- bugsnagClient.use(plugin) ++ Bugsnag.start({ ++ plugins: [plugin] ++ }) +``` + --- See the [full documentation](https://docs.bugsnag.com/platforms/javascript) for more information. diff --git a/packages/browser/src/config.js b/packages/browser/src/config.js index c7dd123d37..e4a6173246 100644 --- a/packages/browser/src/config.js +++ b/packages/browser/src/config.js @@ -1,17 +1,14 @@ const { schema } = require('@bugsnag/core/config') const map = require('@bugsnag/core/lib/es-utils/map') const assign = require('@bugsnag/core/lib/es-utils/assign') -const stringWithLength = require('@bugsnag/core/lib/validators/string-with-length') module.exports = { - releaseStage: { + releaseStage: assign({}, schema.releaseStage, { defaultValue: () => { if (/^localhost(:\d+)?$/.test(window.location.host)) return 'development' return 'production' - }, - message: 'should be set', - validate: stringWithLength - }, + } + }), logger: assign({}, schema.logger, { defaultValue: () => // set logger based on browser capability diff --git a/packages/browser/src/notifier.js b/packages/browser/src/notifier.js index 456b0d7bca..36c4b560c9 100644 --- a/packages/browser/src/notifier.js +++ b/packages/browser/src/notifier.js @@ -40,30 +40,32 @@ const Bugsnag = { if (typeof opts === 'string') opts = { apiKey: opts } if (!opts) opts = {} + const internalPlugins = [ + // add browser-specific plugins + pluginDevice(), + pluginContext(), + pluginRequest(), + pluginThrottle, + pluginSession, + pluginIp, + pluginStripQueryString, + pluginWindowOnerror(), + pluginUnhandledRejection(), + pluginNavigationBreadcrumbs(), + pluginInteractionBreadcrumbs(), + pluginNetworkBreadcrumbs(), + pluginConsoleBreadcrumbs, + + // this one added last to avoid wrapping functionality before bugsnag uses it + pluginInlineScriptContent() + ] + // configure a client with user supplied options - const bugsnag = new Client(opts, schema, { name, version, url }) + const bugsnag = new Client(opts, schema, internalPlugins, { name, version, url }) // set delivery based on browser capability (IE 8+9 have an XDomainRequest object) bugsnag._setDelivery(window.XDomainRequest ? dXDomainRequest : dXMLHttpRequest) - // add browser-specific plugins - bugsnag.use(pluginDevice) - bugsnag.use(pluginContext) - bugsnag.use(pluginRequest) - bugsnag.use(pluginThrottle) - bugsnag.use(pluginSession) - bugsnag.use(pluginIp) - bugsnag.use(pluginStripQueryString) - bugsnag.use(pluginWindowOnerror) - bugsnag.use(pluginUnhandledRejection) - bugsnag.use(pluginNavigationBreadcrumbs) - bugsnag.use(pluginInteractionBreadcrumbs) - bugsnag.use(pluginNetworkBreadcrumbs) - bugsnag.use(pluginConsoleBreadcrumbs) - - // this one added last to avoid wrapping functionality before bugsnag uses it - bugsnag.use(pluginInlineScriptContent) - bugsnag._logger.debug('Loaded!') return bugsnag._config.autoTrackSessions diff --git a/packages/browser/types/test/fixtures/plugins.ts b/packages/browser/types/test/fixtures/plugins.ts index 11d7ec6303..58b35cc114 100644 --- a/packages/browser/types/test/fixtures/plugins.ts +++ b/packages/browser/types/test/fixtures/plugins.ts @@ -1,7 +1,9 @@ import Bugsnag from "../../.."; -Bugsnag.start('api_key'); -Bugsnag.use({ - name: 'foobar', - init: client => 10 -}) +Bugsnag.start({ + apiKey:'api_key', + plugins: [{ + name: 'foobar', + load: client => 10 + }] +}); console.log(Bugsnag.getPlugin('foo') === 10) diff --git a/packages/core/client.d.ts b/packages/core/client.d.ts index 64dc0a3f44..9f50ffcf88 100644 --- a/packages/core/client.d.ts +++ b/packages/core/client.d.ts @@ -1,4 +1,4 @@ -import { Client, OnErrorCallback, Config, Breadcrumb, Session, OnSessionCallback, OnBreadcrumbCallback } from './types' +import { Client, OnErrorCallback, Config, Breadcrumb, Session, OnSessionCallback, OnBreadcrumbCallback, Plugin } from './types' interface LoggerConfig { debug: (msg: any) => void @@ -14,7 +14,7 @@ interface LoggerConfig { * module itself once it is converted to TypeScript. */ export default class ClientWithInternals extends Client { - public constructor(opts: Config) + public constructor(opts: Config, schema?: {[key: string]: any}, internalPlugins?: Plugin[]) _config: { [key: string]: {} } _logger: LoggerConfig _breadcrumbs: Breadcrumb[]; diff --git a/packages/core/client.js b/packages/core/client.js index 5909655702..0c299a012a 100644 --- a/packages/core/client.js +++ b/packages/core/client.js @@ -5,6 +5,8 @@ const Session = require('./session') const map = require('./lib/es-utils/map') const includes = require('./lib/es-utils/includes') const filter = require('./lib/es-utils/filter') +const reduce = require('./lib/es-utils/reduce') +const keys = require('./lib/es-utils/keys') const assign = require('./lib/es-utils/assign') const runCallbacks = require('./lib/callback-runner') const metadataDelegate = require('./lib/metadata-delegate') @@ -13,12 +15,11 @@ const runSyncCallbacks = require('./lib/sync-callback-runner') const noop = () => {} class Client { - constructor (configuration, schema = config.schema, notifier) { + constructor (configuration, schema = config.schema, internalPlugins = [], notifier) { // notifier id this._notifier = notifier // intialise opts and config - this._opts = configuration this._config = {} this._schema = schema @@ -56,7 +57,10 @@ class Client { this.Breadcrumb = Breadcrumb this.Session = Session - this._extractConfiguration() + this._config = this._configure(configuration, internalPlugins) + map(internalPlugins.concat(this._config.plugins), pl => { + if (pl) this._loadPlugin(pl) + }) // when notify() is called we need to know how many frames are from our own source // this inital value is 1 not 0 because we wrap notify() to ensure it is always @@ -90,25 +94,53 @@ class Client { this._context = c } - _extractConfiguration (partialSchema = this._schema) { - const conf = config.mergeDefaults(this._opts, partialSchema) - const validity = config.validate(conf, partialSchema) + _configure (opts, internalPlugins) { + const schema = reduce(internalPlugins, (schema, plugin) => { + if (plugin && plugin.configSchema) return assign({}, schema, plugin.configSchema) + return schema + }, this._schema) + + // accumulate configuration and error messages + const { errors, config } = reduce(keys(schema), (accum, key) => { + const defaultValue = schema[key].defaultValue(opts[key]) + + if (opts[key] !== undefined) { + const valid = schema[key].validate(opts[key]) + if (!valid) { + accum.errors[key] = schema[key].message + accum.config[key] = defaultValue + } else { + accum.config[key] = opts[key] + } + } else { + accum.config[key] = defaultValue + } - if (!validity.valid === true) throw new Error(generateConfigErrorMessage(validity.errors)) + return accum + }, { errors: {}, config: {} }) - // update and elevate some special options if they were passed in at this point - if (conf.metadata) this._metadata = conf.metadata - if (conf.user) this._user = conf.user - if (conf.context) this._context = conf.context - if (conf.logger) this._logger = conf.logger + // missing api key is the only fatal error + if (schema.apiKey && !config.apiKey) { + throw new Error('No Bugsnag API Key set') + } + + // update and elevate some options + this._metadata = config.metadata + this._user = config.user + this._context = config.context + if (config.logger) this._logger = config.logger // add callbacks - if (conf.onError && conf.onError.length) this._cbs.e = this._cbs.e.concat(conf.onError) - if (conf.onBreadcrumb && conf.onBreadcrumb.length) this._cbs.b = this._cbs.b.concat(conf.onBreadcrumb) - if (conf.onSession && conf.onSession.length) this._cbs.s = this._cbs.s.concat(conf.onSession) + if (config.onError && config.onError.length) this._cbs.e = this._cbs.e.concat(config.onError) + if (config.onBreadcrumb && config.onBreadcrumb.length) this._cbs.b = this._cbs.b.concat(config.onBreadcrumb) + if (config.onSession && config.onSession.length) this._cbs.s = this._cbs.s.concat(config.onSession) - // merge with existing config - this._config = assign({}, this._config, conf) + // finally warn about any invalid config where we fell back to the default + if (keys(errors).length) { + this._logger.warn(generateConfigErrorMessage(errors, opts)) + } + + return config } getUser () { @@ -119,9 +151,8 @@ class Client { this._user = { id, email, name } } - use (plugin) { - if (plugin.configSchema) this._extractConfiguration(plugin.configSchema) - const result = plugin.init.apply(null, [this].concat([].slice.call(arguments, 1))) + _loadPlugin (plugin) { + const result = plugin.load(this) // JS objects are not the safest way to store arbitrarily keyed values, // so bookend the key with some characters that prevent tampering with // stuff like __proto__ etc. (only store the result if the plugin had a @@ -286,9 +317,20 @@ class Client { } } -const generateConfigErrorMessage = errors => - `Bugsnag configuration error\n${map(errors, (err) => `"${err.key}" ${err.message} \n got ${stringify(err.value)}`).join('\n\n')}` +const generateConfigErrorMessage = (errors, rawInput) => { + const er = new Error( + `Invalid configuration\n${map(keys(errors), key => ` - ${key} ${errors[key]}, got ${stringify(rawInput[key])}`).join('\n\n')}`) + return er +} -const stringify = val => typeof val === 'object' ? JSON.stringify(val) : String(val) +const stringify = val => { + switch (typeof val) { + case 'string': + case 'number': + case 'object': + return JSON.stringify(val) + default: return String(val) + } +} module.exports = Client diff --git a/packages/core/config.js b/packages/core/config.js index 127a141a31..1cfe7b6ff8 100644 --- a/packages/core/config.js +++ b/packages/core/config.js @@ -137,22 +137,13 @@ module.exports.schema = { isArray(value) && value.length === filter(value, s => (typeof s === 'string' || (s && typeof s.test === 'function')) ).length + }, + plugins: { + defaultValue: () => ([]), + message: 'should be an array of plugin objects', + validate: value => + isArray(value) && value.length === filter(value, p => + (p && typeof p === 'object' && typeof p.load === 'function') + ).length } } - -module.exports.mergeDefaults = (opts, schema) => { - if (!opts || !schema) throw new Error('opts and schema objects are required') - return reduce(keys(schema), (accum, key) => { - accum[key] = opts[key] !== undefined ? opts[key] : schema[key].defaultValue(opts[key], opts) - return accum - }, {}) -} - -module.exports.validate = (opts, schema) => { - if (!opts || !schema) throw new Error('opts and schema objects are required') - const errors = reduce(keys(schema), (accum, key) => { - if (schema[key].validate(opts[key], opts)) return accum - return accum.concat({ key, message: schema[key].message, value: opts[key] }) - }, []) - return { valid: !errors.length, errors } -} diff --git a/packages/core/lib/clone-client.js b/packages/core/lib/clone-client.js index c54a365569..5da2bef4df 100644 --- a/packages/core/lib/clone-client.js +++ b/packages/core/lib/clone-client.js @@ -1,7 +1,7 @@ const assign = require('./es-utils/assign') module.exports = (client) => { - const clone = new client.Client({}, {}, client._notifier) + const clone = new client.Client({}, {}, [], client._notifier) clone._config = client._config diff --git a/packages/core/test/client.test.ts b/packages/core/test/client.test.ts index ab2461520c..6ac3a6a0a8 100644 --- a/packages/core/test/client.test.ts +++ b/packages/core/test/client.test.ts @@ -29,16 +29,18 @@ describe('@bugsnag/core/client', () => { describe('use()', () => { it('supports plugins', done => { - const client = new Client({ apiKey: '123' }) - client.use({ - name: 'test plugin', - // @ts-ignore - description: 'nothing much to see here', - init: (c) => { - expect(c).toEqual(client) - done() - } + let pluginClient + const client = new Client({ + apiKey: '123', + plugins: [{ + name: 'test plugin', + load: (c) => { + pluginClient = c + done() + } + }] }) + expect(pluginClient).toEqual(client) }) }) @@ -442,7 +444,7 @@ describe('@bugsnag/core/client', () => { }) describe('startSession()', () => { - it('calls the provided the session delegate and return delegate’s return value', () => { + it('calls the provided session delegate and return delegate’s return value', () => { const client = new Client({ apiKey: 'API_KEY' }) let ret client._sessionDelegate = { diff --git a/packages/core/test/config.test.ts b/packages/core/test/config.test.ts index bafa3c8dab..58d46af946 100644 --- a/packages/core/test/config.test.ts +++ b/packages/core/test/config.test.ts @@ -1,53 +1,6 @@ import config from '../config' describe('@bugsnag/core/config', () => { - describe('validate()', () => { - it('needs opts/schema', () => { - expect(() => config.validate()).toThrow() - expect(() => config.validate({})).toThrow() - }) - - it('passes without errors', () => { - const validity = config.validate({}, { str: { validate: () => true, message: 'never valid' } }) - expect(validity.valid).toBe(true) - expect(validity.errors[0]).toBe(undefined) - }) - - it('fails with errors', () => { - const validity = config.validate({}, { str: { validate: () => false, message: 'never valid' } }) - expect(validity.valid).toBe(false) - expect(validity.errors[0]).toEqual({ key: 'str', message: 'never valid', value: undefined }) - }) - }) - - describe('mergeDefaults()', () => { - it('needs opts/schema', () => { - expect(() => config.mergeDefaults()).toThrow() - expect(() => config.mergeDefaults({})).toThrow() - }) - - it('merges correctly', () => { - const str = 'bugs bugs bugs' - const a = config.mergeDefaults({}, { str: { defaultValue: () => str } }) - expect(a.str).toBe(str) - - const b = config.mergeDefaults({ str }, { str: { defaultValue: () => 'not bugs' } }) - expect(b.str).toBe(str) - - const c = config.mergeDefaults({ str: '', bool: false }, { - str: { defaultValue: () => str }, - bool: { defaultValue: () => true } - }) - expect(c).toEqual({ str: '', bool: false }) - - const d = config.mergeDefaults({ str: undefined, bool: undefined }, { - str: { defaultValue: () => str }, - bool: { defaultValue: () => true } - }) - expect(d).toEqual({ str: str, bool: true }) - }) - }) - describe('schema', () => { it('has the required properties { validate(), defaultValue(), message }', () => { Object.keys(config.schema).forEach(k => { diff --git a/packages/core/test/types.test.ts b/packages/core/test/types.test.ts index d2cad459d1..5c1ab46bed 100644 --- a/packages/core/test/types.test.ts +++ b/packages/core/test/types.test.ts @@ -2,7 +2,7 @@ import Bugsnag, { Client, Config } from '..' // the client's constructor isn't public in TS so this drops down to JS to create one for the tests function createClient (opts: Config): Client { - const c = new (Bugsnag.Client as any)(opts, undefined, { name: 'Type Tests', version: 'nope', url: 'https://github.com/bugsnag/bugsnag-js' }) + const c = new (Bugsnag.Client as any)(opts, undefined, [], { name: 'Type Tests', version: 'nope', url: 'https://github.com/bugsnag/bugsnag-js' }) c._setDelivery(() => ({ sendSession: (p: any, cb: () => void): void => { cb() }, sendEvent: (p: any, cb: () => void): void => { cb() } diff --git a/packages/core/types/client.d.ts b/packages/core/types/client.d.ts index 2bdbf6eef1..6f846d8c9e 100644 --- a/packages/core/types/client.d.ts +++ b/packages/core/types/client.d.ts @@ -2,7 +2,6 @@ import Breadcrumb from './breadcrumb' import { NotifiableError, BreadcrumbType, - Plugin, OnErrorCallback, OnSessionCallback, OnBreadcrumbCallback, @@ -64,7 +63,6 @@ declare class Client { public removeOnBreadcrumb(fn: OnBreadcrumbCallback): void; // plugins - public use(plugin: Plugin, ...args: any[]): Client; public getPlugin(name: string): any; // implemented on the browser notifier only diff --git a/packages/core/types/common.d.ts b/packages/core/types/common.d.ts index 3cf0354628..e3586edfec 100644 --- a/packages/core/types/common.d.ts +++ b/packages/core/types/common.d.ts @@ -14,8 +14,8 @@ export interface Config { } autoTrackSessions?: boolean context?: string - enabledBreadcrumbTypes?: BreadcrumbType[] - enabledReleaseStages?: string[] + enabledBreadcrumbTypes?: BreadcrumbType[] | null + enabledReleaseStages?: string[] | null endpoints?: { notify: string, sessions: string } redactedKeys?: Array onBreadcrumb?: OnBreadcrumbCallback | OnBreadcrumbCallback[] @@ -26,6 +26,7 @@ export interface Config { metadata?: { [key: string]: any } releaseStage?: string user?: {} | null + plugins?: Plugin[] } export type OnErrorCallback = (event: Event, cb?: (err: null | Error) => void) => void | Promise | boolean; @@ -34,21 +35,10 @@ export type OnBreadcrumbCallback = (breadcrumb: Breadcrumb) => void | boolean; export interface Plugin { name?: string - init: (client: Client) => any - configSchema?: ConfigSchema + load: (client: Client) => any destroy?(): void } -export interface ConfigSchemaEntry { - message: string - validate: (val: any) => boolean - defaultValue: () => any -} - -export interface ConfigSchema { - [key: string]: ConfigSchemaEntry -} - export interface Logger { debug: (...args: any[]) => void info: (...args: any[]) => void diff --git a/packages/expo/src/notifier.js b/packages/expo/src/notifier.js index 31ebf483b5..2f76c204b5 100644 --- a/packages/expo/src/notifier.js +++ b/packages/expo/src/notifier.js @@ -13,27 +13,27 @@ const Breadcrumb = require('@bugsnag/core/breadcrumb') const delivery = require('@bugsnag/delivery-expo') const schema = { ...require('@bugsnag/core/config').schema, ...require('./config') } +const BugsnagPluginReact = require('@bugsnag/plugin-react') -const plugins = [ - require('@bugsnag/plugin-react-native-global-error-handler'), +// The NetInfo module makes requests to this URL to detect if the device is connected +// to the internet. We don't want these requests to be recorded as breadcrumbs. +// see https://github.com/react-native-community/react-native-netinfo/blob/d39b18c61e220d518d8403b6f4f4ab5bcc8c973c/src/index.ts#L16 +const NET_INFO_REACHABILITY_URL = 'https://clients3.google.com/generate_204' + +const internalPlugins = [ + require('@bugsnag/plugin-react-native-global-error-handler')(), require('@bugsnag/plugin-react-native-unhandled-rejection'), require('@bugsnag/plugin-expo-device'), require('@bugsnag/plugin-expo-app'), require('@bugsnag/plugin-console-breadcrumbs'), - require('@bugsnag/plugin-network-breadcrumbs'), + require('@bugsnag/plugin-network-breadcrumbs')([NET_INFO_REACHABILITY_URL, Constants.manifest.logUrl]), require('@bugsnag/plugin-react-native-app-state-breadcrumbs'), require('@bugsnag/plugin-react-native-connectivity-breadcrumbs'), require('@bugsnag/plugin-react-native-orientation-breadcrumbs'), - require('@bugsnag/plugin-browser-session') + require('@bugsnag/plugin-browser-session'), + new BugsnagPluginReact(React) ] -const bugsnagReact = require('@bugsnag/plugin-react') - -// The NetInfo module makes requests to this URL to detect if the device is connected -// to the internet. We don't want these requests to be recorded as breadcrumbs. -// see https://github.com/react-native-community/react-native-netinfo/blob/d39b18c61e220d518d8403b6f4f4ab5bcc8c973c/src/index.ts#L16 -const NET_INFO_REACHABILITY_URL = 'https://clients3.google.com/generate_204' - const Bugsnag = { _client: null, createClient: (opts) => { @@ -54,26 +54,10 @@ const Bugsnag = { opts.appVersion = Constants.manifest.version } - const bugsnag = new Client(opts, schema, { name, version, url }) + const bugsnag = new Client(opts, schema, internalPlugins, { name, version, url }) bugsnag._setDelivery(delivery) - plugins.forEach(pl => { - switch (pl.name) { - case 'networkBreadcrumbs': - bugsnag.use(pl, () => [ - bugsnag._config.endpoints.notify, - bugsnag._config.endpoints.sessions, - Constants.manifest.logUrl, - NET_INFO_REACHABILITY_URL - ]) - break - default: - bugsnag.use(pl) - } - }) - bugsnag.use(bugsnagReact, React) - bugsnag._logger.debug('Loaded!') return bugsnag._config.autoTrackSessions diff --git a/packages/expo/types/test/fixtures/plugins.ts b/packages/expo/types/test/fixtures/plugins.ts index 11d7ec6303..58b35cc114 100644 --- a/packages/expo/types/test/fixtures/plugins.ts +++ b/packages/expo/types/test/fixtures/plugins.ts @@ -1,7 +1,9 @@ import Bugsnag from "../../.."; -Bugsnag.start('api_key'); -Bugsnag.use({ - name: 'foobar', - init: client => 10 -}) +Bugsnag.start({ + apiKey:'api_key', + plugins: [{ + name: 'foobar', + load: client => 10 + }] +}); console.log(Bugsnag.getPlugin('foo') === 10) diff --git a/packages/node/src/notifier.js b/packages/node/src/notifier.js index 7048cd753f..a0392a7aba 100644 --- a/packages/node/src/notifier.js +++ b/packages/node/src/notifier.js @@ -25,7 +25,7 @@ const pluginNodeUnhandledRejection = require('@bugsnag/plugin-node-unhandled-rej const pluginIntercept = require('@bugsnag/plugin-intercept') const pluginContextualize = require('@bugsnag/plugin-contextualize') -const plugins = [ +const internalPlugins = [ pluginSurroundingCode, pluginInProject, pluginStripProjectRoot, @@ -44,12 +44,10 @@ const Bugsnag = { if (typeof opts === 'string') opts = { apiKey: opts } if (!opts) opts = {} - const bugsnag = new Client(opts, schema, { name, version, url }) + const bugsnag = new Client(opts, schema, internalPlugins, { name, version, url }) bugsnag._setDelivery(delivery) - plugins.forEach(pl => bugsnag.use(pl)) - bugsnag._logger.debug('Loaded!') bugsnag.leaveBreadcrumb = function () { diff --git a/packages/plugin-angular/src/index.ts b/packages/plugin-angular/src/index.ts index 720eb8c277..d2dd855834 100644 --- a/packages/plugin-angular/src/index.ts +++ b/packages/plugin-angular/src/index.ts @@ -41,7 +41,7 @@ export class BugsnagErrorHandler extends ErrorHandler { } const plugin: Plugin = { - init: (client: Client): ErrorHandler => { + load: (client: Client): ErrorHandler => { return new BugsnagErrorHandler(client) }, name: 'Angular' diff --git a/packages/plugin-browser-context/context.js b/packages/plugin-browser-context/context.js index aac35dadcd..42dd661b5e 100644 --- a/packages/plugin-browser-context/context.js +++ b/packages/plugin-browser-context/context.js @@ -1,11 +1,11 @@ /* * Sets the default context to be the current URL */ -module.exports = { - init: (client, win = window) => { +module.exports = (win = window) => ({ + load: (client) => { client.addOnError(event => { if (event.context !== undefined) return event.context = win.location.pathname }, true) } -} +}) diff --git a/packages/plugin-browser-context/test/context.test.js b/packages/plugin-browser-context/test/context.test.js index 78ec6216ef..e8b595572c 100644 --- a/packages/plugin-browser-context/test/context.test.js +++ b/packages/plugin-browser-context/test/context.test.js @@ -12,9 +12,8 @@ const window = { describe('plugin: context', () => { it('sets client.context (and event.context) to window.location.pathname', done => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(window)]) const payloads = [] - client.use(plugin, window) client._setDelivery(client => ({ sendEvent: (payload, cb) => { @@ -33,9 +32,8 @@ describe('plugin: context', () => { }) it('sets doesn’t overwrite an existing context', done => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(window)]) const payloads = [] - client.use(plugin, window) client.setContext('something else') diff --git a/packages/plugin-browser-device/device.js b/packages/plugin-browser-device/device.js index b4a3bf083c..7032899450 100644 --- a/packages/plugin-browser-device/device.js +++ b/packages/plugin-browser-device/device.js @@ -3,8 +3,8 @@ const assign = require('@bugsnag/core/lib/es-utils/assign') /* * Automatically detects browser device details */ -module.exports = { - init: (client, nav = navigator) => { +module.exports = (nav = navigator) => ({ + load: (client) => { const device = { locale: nav.browserLanguage || nav.systemLanguage || nav.userLanguage || nav.language, userAgent: nav.userAgent @@ -23,4 +23,4 @@ module.exports = { ) }, true) } -} +}) diff --git a/packages/plugin-browser-device/test/device.test.js b/packages/plugin-browser-device/test/device.test.js index 09b4c8ca1d..22bdcd7e32 100644 --- a/packages/plugin-browser-device/test/device.test.js +++ b/packages/plugin-browser-device/test/device.test.js @@ -8,9 +8,8 @@ const navigator = { locale: 'en_GB', userAgent: 'testing browser 1.2.3' } describe('plugin: device', () => { it('should add an onError callback which captures device information', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(navigator)] }) const payloads = [] - client.use(plugin, navigator) expect(client._cbs.e.length).toBe(1) @@ -25,14 +24,13 @@ describe('plugin: device', () => { }) it('should add an onSession callback which captures device information', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(navigator)] }) const payloads = [] client._sessionDelegate = { startSession: (client, session) => { client._delivery.sendSession(session) } } - client.use(plugin, navigator) expect(client._cbs.s.length).toBe(1) diff --git a/packages/plugin-browser-request/request.js b/packages/plugin-browser-request/request.js index 77a8eaa6a5..83e18d771b 100644 --- a/packages/plugin-browser-request/request.js +++ b/packages/plugin-browser-request/request.js @@ -3,11 +3,11 @@ const assign = require('@bugsnag/core/lib/es-utils/assign') /* * Sets the event request: { url } to be the current href */ -module.exports = { - init: (client, win = window) => { +module.exports = (win = window) => ({ + load: (client) => { client.addOnError(event => { if (event.request && event.request.url) return event.request = assign({}, event.request, { url: win.location.href }) }, true) } -} +}) diff --git a/packages/plugin-browser-request/test/request.test.js b/packages/plugin-browser-request/test/request.test.js index 1e41397ce5..444eecbc71 100644 --- a/packages/plugin-browser-request/test/request.test.js +++ b/packages/plugin-browser-request/test/request.test.js @@ -8,9 +8,8 @@ const window = { location: { href: 'http://xyz.abc/foo/bar.html' } } describe('plugin: request', () => { it('sets event.request to window.location.href', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(window)] }) const payloads = [] - client.use(plugin, window) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) client.notify(new Error('noooo')) @@ -20,9 +19,8 @@ describe('plugin: request', () => { }) it('sets doesn’t overwrite an existing request', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(window)] }) const payloads = [] - client.use(plugin, window) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) client.notify(new Error('noooo'), event => { diff --git a/packages/plugin-browser-session/session.js b/packages/plugin-browser-session/session.js index fcbe5ec703..0313debe43 100644 --- a/packages/plugin-browser-session/session.js +++ b/packages/plugin-browser-session/session.js @@ -1,7 +1,7 @@ const includes = require('@bugsnag/core/lib/es-utils/includes') module.exports = { - init: client => { client._sessionDelegate = sessionDelegate } + load: client => { client._sessionDelegate = sessionDelegate } } const sessionDelegate = { diff --git a/packages/plugin-browser-session/test/session.test.js b/packages/plugin-browser-session/test/session.test.js index 07695beaac..7e62adef5a 100644 --- a/packages/plugin-browser-session/test/session.test.js +++ b/packages/plugin-browser-session/test/session.test.js @@ -1,4 +1,4 @@ -const { describe, it, expect } = global +const { describe, it, expect, spyOn } = global const plugin = require('../') @@ -7,8 +7,7 @@ const VALID_NOTIFIER = { name: 't', version: '0', url: 'http://' } describe('plugin: sessions', () => { it('notifies the session endpoint', (done) => { - const c = new Client({ apiKey: 'API_KEY' }, undefined, VALID_NOTIFIER) - c.use(plugin) + const c = new Client({ apiKey: 'API_KEY' }, undefined, [plugin], VALID_NOTIFIER) c._setDelivery(client => ({ sendSession: (session, cb) => { expect(typeof session).toBe('object') @@ -24,9 +23,8 @@ describe('plugin: sessions', () => { }) it('tracks handled/unhandled error counts and sends them in error payloads', (done) => { - const c = new Client({ apiKey: 'API_KEY' }) + const c = new Client({ apiKey: 'API_KEY' }, undefined, [plugin], VALID_NOTIFIER) let i = 0 - c.use(plugin) c._setDelivery(client => ({ sendSession: () => {}, sendEvent: (payload, cb) => { @@ -52,8 +50,8 @@ describe('plugin: sessions', () => { }) it('correctly infers releaseStage', (done) => { - const c = new Client({ apiKey: 'API_KEY', releaseStage: 'foo' }) - c.use(plugin) + const c = new Client({ apiKey: 'API_KEY', releaseStage: 'foo' }, undefined, [plugin], VALID_NOTIFIER) + c._setDelivery(client => ({ sendSession: (session, cb) => { expect(typeof session).toBe('object') @@ -65,8 +63,7 @@ describe('plugin: sessions', () => { }) it('doesn’t send when releaseStage is not in enabledReleaseStages', (done) => { - const c = new Client({ apiKey: 'API_KEY', releaseStage: 'foo', enabledReleaseStages: ['baz'] }) - c.use(plugin) + const c = new Client({ apiKey: 'API_KEY', releaseStage: 'foo', enabledReleaseStages: ['baz'] }, undefined, [plugin], VALID_NOTIFIER) c._setDelivery(client => ({ sendSession: (session, cb) => { expect(true).toBe(false) @@ -76,26 +73,25 @@ describe('plugin: sessions', () => { setTimeout(done, 150) }) - it('rejects config when session endpoint is not set', () => { - expect(() => { - const c = new Client({ - apiKey: 'API_KEY', - releaseStage: 'foo', - endpoints: { notify: '/foo' }, - autoTrackSessions: false - }) - expect(c).toBe(c) - }).toThrowError( - /"endpoints" should be an object containing endpoint URLs { notify, sessions }/ - ) + it('uses default endpoints when session endpoint is not set', () => { + const logger = { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} } + const warnSpy = spyOn(logger, 'warn') + const c = new Client({ + apiKey: 'API_KEY', + releaseStage: 'foo', + endpoints: { notify: '/foo' }, + autoTrackSessions: false, + logger + }, undefined, [plugin], VALID_NOTIFIER) + expect(c._config.endpoints.sessions).toBe('https://sessions.bugsnag.com') + expect(warnSpy.calls.first().args[0].message).toBe('Invalid configuration\n - endpoints should be an object containing endpoint URLs { notify, sessions }, got {"notify":"/foo"}') }) it('supports pausing and resuming sessions', (done) => { const payloads = [] const c = new Client({ apiKey: 'API_KEY' - }) - c.use(plugin) + }, undefined, [plugin], VALID_NOTIFIER) c._setDelivery(client => ({ sendEvent: (p, cb = () => {}) => { payloads.push(p) diff --git a/packages/plugin-client-ip/client-ip.js b/packages/plugin-client-ip/client-ip.js index 8ad1cff853..0e4b7e9de1 100644 --- a/packages/plugin-client-ip/client-ip.js +++ b/packages/plugin-client-ip/client-ip.js @@ -4,7 +4,7 @@ const assign = require('@bugsnag/core/lib/es-utils/assign') * Prevent collection of user IPs */ module.exports = { - init: (client) => { + load: (client) => { if (client._config.collectUserIp) return client.addOnError(event => { diff --git a/packages/plugin-client-ip/test/client-ip.test.js b/packages/plugin-client-ip/test/client-ip.test.js index 4b99f5d925..e7eb4835a3 100644 --- a/packages/plugin-client-ip/test/client-ip.test.js +++ b/packages/plugin-client-ip/test/client-ip.test.js @@ -6,9 +6,8 @@ const Client = require('@bugsnag/core/client') describe('plugin: ip', () => { it('does nothing when collectUserIp=true', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin]) const payloads = [] - client.use(plugin) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) client.notify(new Error('noooo'), event => { event.request = { some: 'detail' } }) @@ -18,9 +17,8 @@ describe('plugin: ip', () => { }) it('doesn’t overwrite an existing user id', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH', collectUserIp: false }) + const client = new Client({ apiKey: 'API_KEY_YEAH', collectUserIp: false }, undefined, [plugin]) const payloads = [] - client.use(plugin) client._user = { id: 'foobar' } @@ -33,9 +31,8 @@ describe('plugin: ip', () => { }) it('overwrites a user id if it is explicitly `undefined`', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH', collectUserIp: false }) + const client = new Client({ apiKey: 'API_KEY_YEAH', collectUserIp: false }, undefined, [plugin]) const payloads = [] - client.use(plugin) client._user = { id: undefined } @@ -48,9 +45,8 @@ describe('plugin: ip', () => { }) it('redacts user IP if none is provided', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH', collectUserIp: false }) + const client = new Client({ apiKey: 'API_KEY_YEAH', collectUserIp: false }, undefined, [plugin]) const payloads = [] - client.use(plugin) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) client.notify(new Error('noooo')) diff --git a/packages/plugin-console-breadcrumbs/console-breadcrumbs.js b/packages/plugin-console-breadcrumbs/console-breadcrumbs.js index 4c7084b803..cf4aeeccc4 100644 --- a/packages/plugin-console-breadcrumbs/console-breadcrumbs.js +++ b/packages/plugin-console-breadcrumbs/console-breadcrumbs.js @@ -6,7 +6,7 @@ const includes = require('@bugsnag/core/lib/es-utils/includes') /* * Leaves breadcrumbs when console log methods are called */ -exports.init = (client) => { +exports.load = (client) => { const isDev = /^dev(elopment)?$/.test(client._config.releaseStage) if (!client._config.enabledBreadcrumbTypes || !includes(client._config.enabledBreadcrumbTypes, 'log') || isDev) return diff --git a/packages/plugin-console-breadcrumbs/test/console-breadcrumbs.test.js b/packages/plugin-console-breadcrumbs/test/console-breadcrumbs.test.js index 53b795ce39..5997c12f0b 100644 --- a/packages/plugin-console-breadcrumbs/test/console-breadcrumbs.test.js +++ b/packages/plugin-console-breadcrumbs/test/console-breadcrumbs.test.js @@ -6,8 +6,7 @@ const Client = require('@bugsnag/core/client') describe('plugin: console breadcrumbs', () => { it('should leave a breadcrumb when console.log() is called', () => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - c.use(plugin) + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [plugin] }) console.log('check 1, 2') // make sure it's null-safe console.log(null) @@ -30,8 +29,7 @@ describe('plugin: console breadcrumbs', () => { }) it('should not throw when an object without toString is logged', () => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - c.use(plugin) + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [plugin] }) expect(() => console.log(Object.create(null))).not.toThrow() expect(c._breadcrumbs.length).toBe(1) expect(c._breadcrumbs[0].message).toBe('Console output') @@ -40,24 +38,21 @@ describe('plugin: console breadcrumbs', () => { }) it('should not be enabled when enabledBreadcrumbTypes=[]', () => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [] }) - c.use(plugin) + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [], plugins: [plugin] }) console.log(123) expect(c._breadcrumbs.length).toBe(0) plugin.destroy() }) it('should be enabled when enabledBreadcrumbTypes=["log"]', () => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['log'] }) - c.use(plugin) + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['log'], plugins: [plugin] }) console.log(123) expect(c._breadcrumbs.length).toBe(1) plugin.destroy() }) it('should be not enabled by default when releaseStage=development', () => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', releaseStage: 'development' }) - c.use(plugin) + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', releaseStage: 'development', plugins: [plugin] }) console.log(123) expect(c._breadcrumbs.length).toBe(0) plugin.destroy() diff --git a/packages/plugin-contextualize/contextualize.js b/packages/plugin-contextualize/contextualize.js index 1eb236c853..65008e9dd0 100644 --- a/packages/plugin-contextualize/contextualize.js +++ b/packages/plugin-contextualize/contextualize.js @@ -4,7 +4,7 @@ const { getStack, maybeUseFallbackStack } = require('@bugsnag/core/lib/node-fall module.exports = { name: 'contextualize', - init: client => { + load: client => { const contextualize = (fn, onError) => { // capture a stacktrace in case a resulting error has nothing const fallbackStack = getStack() diff --git a/packages/plugin-contextualize/test/contextualize.test.js b/packages/plugin-contextualize/test/contextualize.test.js index 37a0556b74..fc79c7aefb 100644 --- a/packages/plugin-contextualize/test/contextualize.test.js +++ b/packages/plugin-contextualize/test/contextualize.test.js @@ -25,7 +25,8 @@ describe('plugin: contextualize', () => { onUncaughtException: (err) => { expect(err.message).toBe('no item available') done() - } + }, + plugins: [plugin] }, { ...schema, onUncaughtException: { @@ -47,7 +48,6 @@ describe('plugin: contextualize', () => { }, sendSession: () => {} })) - c.use(plugin) const contextualize = c.getPlugin('contextualize') contextualize(() => { load(8, (err) => { @@ -64,7 +64,8 @@ describe('plugin: contextualize', () => { apiKey: 'api_key', onUncaughtException: () => { done() - } + }, + plugins: [plugin] }, { ...schema, onUncaughtException: { @@ -81,7 +82,6 @@ describe('plugin: contextualize', () => { }, sendSession: () => {} })) - c.use(plugin) const contextualize = c.getPlugin('contextualize') contextualize(() => { fs.createReadStream('does not exist') @@ -94,7 +94,8 @@ describe('plugin: contextualize', () => { onUncaughtException: (err) => { expect(err.message).toBe('no item available') done() - } + }, + plugins: [plugin] }, { ...schema, onUncaughtException: { @@ -109,7 +110,6 @@ describe('plugin: contextualize', () => { }, sendSession: () => {} })) - c.use(plugin) const contextualize = c.getPlugin('contextualize') contextualize(() => { load(8, (err) => { diff --git a/packages/plugin-expo-app/app.js b/packages/plugin-expo-app/app.js index 90f39ef5af..67afc36246 100644 --- a/packages/plugin-expo-app/app.js +++ b/packages/plugin-expo-app/app.js @@ -4,7 +4,7 @@ const { AppState } = require('react-native') const appStart = new Date() module.exports = { - init: client => { + load: client => { let lastEnteredForeground = appStart let lastState = AppState.currentState diff --git a/packages/plugin-expo-app/test/app.test.js b/packages/plugin-expo-app/test/app.test.js index 6af76bf157..0099d179c0 100644 --- a/packages/plugin-expo-app/test/app.test.js +++ b/packages/plugin-expo-app/test/app.test.js @@ -22,9 +22,8 @@ describe('plugin: expo app', () => { } } }) - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) - c.use(plugin) c._setDelivery(client => ({ sendEvent: (payload) => { const r = JSON.parse(JSON.stringify(payload)) @@ -55,9 +54,8 @@ describe('plugin: expo app', () => { } } }) - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) - c.use(plugin) c._setDelivery(client => ({ sendEvent: (payload) => { const r = JSON.parse(JSON.stringify(payload)) @@ -88,9 +86,8 @@ describe('plugin: expo app', () => { } } }) - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) - c.use(plugin) c._setDelivery(client => ({ sendEvent: (payload) => { const r = JSON.parse(JSON.stringify(payload)) @@ -119,9 +116,8 @@ describe('plugin: expo app', () => { }, 'react-native': { AppState } }) - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) - c.use(plugin) expect(typeof listener).toBe('function') const events = [] c._setDelivery(client => ({ diff --git a/packages/plugin-expo-device/device.js b/packages/plugin-expo-device/device.js index 0a024dacd1..d55e3ab65e 100644 --- a/packages/plugin-expo-device/device.js +++ b/packages/plugin-expo-device/device.js @@ -3,7 +3,7 @@ const { Dimensions, Platform } = require('react-native') const rnVersion = require('react-native/package.json').version module.exports = { - init: client => { + load: client => { let orientation const updateOrientation = () => { const { height, width } = Dimensions.get('screen') diff --git a/packages/plugin-expo-device/test/device.test.js b/packages/plugin-expo-device/test/device.test.js index 15b01e1ed4..b248eae4c2 100644 --- a/packages/plugin-expo-device/test/device.test.js +++ b/packages/plugin-expo-device/test/device.test.js @@ -30,7 +30,7 @@ describe('plugin: expo device', () => { }, 'react-native/package.json': { version: REACT_NATIVE_VERSION } }) - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) const before = (new Date()).toISOString() c._setDelivery(client => ({ sendEvent: (payload) => { @@ -51,7 +51,6 @@ describe('plugin: expo device', () => { done() } })) - c.use(plugin) c.notify(new Error('device testing')) }) @@ -81,7 +80,7 @@ describe('plugin: expo device', () => { }, 'react-native/package.json': { version: REACT_NATIVE_VERSION } }) - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) const before = (new Date()).toISOString() c._setDelivery(client => ({ sendEvent: (payload) => { @@ -103,7 +102,6 @@ describe('plugin: expo device', () => { done() } })) - c.use(plugin) c.notify(new Error('device testing')) }) @@ -154,7 +152,7 @@ describe('plugin: expo device', () => { }, 'react-native/package.json': { version: REACT_NATIVE_VERSION } }) - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) const events = [] c._setDelivery(client => ({ sendEvent: (payload) => { @@ -169,7 +167,6 @@ describe('plugin: expo device', () => { } } })) - c.use(plugin) expect(d._listeners.change.length).toBe(1) c.notify(new Error('device testing')) d._set(1024, 768) diff --git a/packages/plugin-express/src/express.js b/packages/plugin-express/src/express.js index caefbbe5bf..a83e2f3ed2 100644 --- a/packages/plugin-express/src/express.js +++ b/packages/plugin-express/src/express.js @@ -13,7 +13,7 @@ const handledState = { module.exports = { name: 'express', - init: client => { + load: client => { const requestHandler = (req, res, next) => { const dom = domain.create() diff --git a/packages/plugin-express/test/express.test.js b/packages/plugin-express/test/express.test.js index 83ba8e5dd3..c786ed1d15 100644 --- a/packages/plugin-express/test/express.test.js +++ b/packages/plugin-express/test/express.test.js @@ -6,8 +6,7 @@ const plugin = require('../') describe('plugin: express', () => { it('exports two middleware functions', () => { - const c = new Client({ apiKey: 'api_key' }) - c.use(plugin) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) const middleware = c.getPlugin('express') expect(typeof middleware.requestHandler).toBe('function') expect(middleware.requestHandler.length).toBe(3) diff --git a/packages/plugin-inline-script-content/inline-script-content.js b/packages/plugin-inline-script-content/inline-script-content.js index f1fb76d5f5..e60952d6ea 100644 --- a/packages/plugin-inline-script-content/inline-script-content.js +++ b/packages/plugin-inline-script-content/inline-script-content.js @@ -5,8 +5,8 @@ const filter = require('@bugsnag/core/lib/es-utils/filter') const MAX_LINE_LENGTH = 200 const MAX_SCRIPT_LENGTH = 500000 -module.exports = { - init: (client, doc = document, win = window) => { +module.exports = (doc = document, win = window) => ({ + load: (client) => { if (!client._config.trackInlineScripts) return const originalLocation = win.location.href @@ -167,7 +167,7 @@ module.exports = { message: 'should be true|false' } } -} +}) function __proxy (host, name, replacer) { const original = host[name] diff --git a/packages/plugin-inline-script-content/test/inline-script-content.test.js b/packages/plugin-inline-script-content/test/inline-script-content.test.js index 386e5573bc..2f3f8b8216 100644 --- a/packages/plugin-inline-script-content/test/inline-script-content.test.js +++ b/packages/plugin-inline-script-content/test/inline-script-content.test.js @@ -28,9 +28,8 @@ Lorem ipsum dolor sit amet. } const window = { location: { href: 'https://app.bugsnag.com/errors' } } - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) const payloads = [] - client.use(plugin, document, window) expect(client._cbs.e.length).toBe(1) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) @@ -47,12 +46,12 @@ Lorem ipsum dolor sit amet. const prevHandler = () => { done() } const document = { documentElement: { outerHTML: '' }, onreadystatechange: prevHandler } const window = { location: { href: 'https://app.bugsnag.com/errors' }, document } - const client = new Client({ apiKey: 'API_KEY_YEAH' }) - client.use(plugin, document, window) + const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) // check it installed a new onreadystatechange handler expect(document.onreadystatechange === prevHandler).toBe(false) // now check it calls the previous one document.onreadystatechange() + expect(client).toBe(client) }) it('does no wrapping of global functions when disabled', () => { @@ -62,10 +61,10 @@ Lorem ipsum dolor sit amet. function EventTarget () {} EventTarget.prototype.addEventListener = addEventListener window.EventTarget = EventTarget - const client = new Client({ apiKey: 'API_KEY_YEAH', trackInlineScripts: false }) - client.use(plugin, document, window) + const client = new Client({ apiKey: 'API_KEY_YEAH', trackInlineScripts: false }, undefined, [plugin(document, window)]) // check the addEventListener function was not wrapped expect(window.EventTarget.prototype.addEventListener).toBe(addEventListener) + expect(client).toBe(client) }) it('truncates script content to a reasonable length', () => { @@ -92,9 +91,8 @@ Lorem ipsum dolor sit amet. } const window = { location: { href: 'https://app.bugsnag.com/errors' } } - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) const payloads = [] - client.use(plugin, document, window) expect(client._cbs.e.length).toBe(1) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) @@ -129,9 +127,8 @@ Lorem ipsum dolor sit amet. } const window = { location: { href: 'https://app.bugsnag.com/errors' } } - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) const payloads = [] - client.use(plugin, document, window) expect(client._cbs.e.length).toBe(1) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) @@ -165,9 +162,8 @@ Lorem ipsum dolor sit amet. } const window = { location: { href: 'https://app.bugsnag.com/errors' } } - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) const payloads = [] - client.use(plugin, document, window) expect(client._cbs.e.length).toBe(1) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) @@ -210,11 +206,11 @@ Lorem ipsum dolor sit amet. window.addEventListener('click', myfun) const spy = spyOn(Window.prototype, 'removeEventListener') - const client = new Client({ apiKey: 'API_KEY_YEAH' }) - client.use(plugin, document, window) + const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) window.removeEventListener('click', myfun) expect(spy).toHaveBeenCalledTimes(2) + expect(client).toBe(client) }) it('gets the correct line numbers for errors at the start of the document', () => { @@ -228,9 +224,8 @@ Lorem ipsum dolor sit amet. } const window = { location: { href: 'https://app.bugsnag.com/errors' } } - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) const payloads = [] - client.use(plugin, document, window) expect(client._cbs.e.length).toBe(1) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) diff --git a/packages/plugin-interaction-breadcrumbs/interaction-breadcrumbs.js b/packages/plugin-interaction-breadcrumbs/interaction-breadcrumbs.js index 109786d4d7..7195892e16 100644 --- a/packages/plugin-interaction-breadcrumbs/interaction-breadcrumbs.js +++ b/packages/plugin-interaction-breadcrumbs/interaction-breadcrumbs.js @@ -3,8 +3,8 @@ const includes = require('@bugsnag/core/lib/es-utils/includes') /* * Leaves breadcrumbs when the user interacts with the DOM */ -module.exports = { - init: (client, win = window) => { +module.exports = (win = window) => ({ + load: (client) => { if (!('addEventListener' in win)) return if (!client._config.enabledBreadcrumbTypes || !includes(client._config.enabledBreadcrumbTypes, 'user')) return @@ -22,7 +22,7 @@ module.exports = { client.leaveBreadcrumb('UI click', { targetText, targetSelector }, 'user') }, true) } -} +}) // extract text content from a element const getNodeText = el => { diff --git a/packages/plugin-interaction-breadcrumbs/test/interaction-breadcrumbs.test.js b/packages/plugin-interaction-breadcrumbs/test/interaction-breadcrumbs.test.js index 201c7dfc3c..7b6ee3000e 100644 --- a/packages/plugin-interaction-breadcrumbs/test/interaction-breadcrumbs.test.js +++ b/packages/plugin-interaction-breadcrumbs/test/interaction-breadcrumbs.test.js @@ -6,25 +6,22 @@ const Client = require('@bugsnag/core/client') describe('plugin: interaction breadcrumbs', () => { it('should drop a breadcrumb when an element is clicked', () => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) const { window, winHandlers, els } = getMockWindow() - c.use(plugin, window) + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [plugin(window)] }) winHandlers.click.forEach(fn => fn.call(window, { target: els[0] })) expect(c._breadcrumbs.length).toBe(1) }) it('should not be enabled when enabledBreadcrumbTypes=[]', () => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [] }) const { window, winHandlers, els } = getMockWindow() - c.use(plugin, window) + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [], plugins: [plugin(window)] }) winHandlers.click.forEach(fn => fn.call(window, { target: els[0] })) expect(c._breadcrumbs.length).toBe(0) }) it('should be enabled when enabledBreadcrumbTypes=["user"]', () => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['user'] }) const { window, winHandlers, els } = getMockWindow() - c.use(plugin, window) + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['user'], plugins: [plugin(window)] }) winHandlers.click.forEach(fn => fn.call(window, { target: els[0] })) expect(c._breadcrumbs.length).toBe(1) }) diff --git a/packages/plugin-intercept/intercept.js b/packages/plugin-intercept/intercept.js index 8cd78df5fa..1cd921981e 100644 --- a/packages/plugin-intercept/intercept.js +++ b/packages/plugin-intercept/intercept.js @@ -2,7 +2,7 @@ const { getStack, maybeUseFallbackStack } = require('@bugsnag/core/lib/node-fall module.exports = { name: 'intercept', - init: client => { + load: client => { const intercept = (onError = () => {}, cb) => { if (typeof cb !== 'function') { cb = onError diff --git a/packages/plugin-intercept/test/intercept.test.js b/packages/plugin-intercept/test/intercept.test.js index c1d62a210d..a0352ee274 100644 --- a/packages/plugin-intercept/test/intercept.test.js +++ b/packages/plugin-intercept/test/intercept.test.js @@ -30,12 +30,11 @@ function pload (index, cb) { describe('plugin: intercept', () => { it('does nothing with a happy-case callback', done => { - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) c._setDelivery(client => ({ sendEvent: () => expect(true).toBe(false), sendSession: () => {} })) - c.use(plugin) const intercept = c.getPlugin('intercept') load(1, intercept((item) => { expect(item).toBe('b') @@ -44,7 +43,7 @@ describe('plugin: intercept', () => { }) it('sends an event when the callback recieves an error', done => { - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) c._setDelivery(client => ({ sendEvent: (payload) => { expect(payload.events[0].errors[0].errorMessage).toBe('no item available') @@ -52,7 +51,6 @@ describe('plugin: intercept', () => { }, sendSession: () => {} })) - c.use(plugin) const intercept = c.getPlugin('intercept') load(4, intercept((item) => { expect(true).toBe(false) @@ -61,12 +59,11 @@ describe('plugin: intercept', () => { }) it('works with resolved promises', done => { - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) c._setDelivery(client => ({ sendEvent: () => expect(true).toBe(false), sendSession: () => {} })) - c.use(plugin) const intercept = c.getPlugin('intercept') pload(0).then(item => { expect(item).toBe('a') @@ -75,7 +72,7 @@ describe('plugin: intercept', () => { }) it('works with rejected promises', done => { - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) c._setDelivery(client => ({ sendEvent: (payload) => { expect(payload.events[0].errors[0].errorMessage).toBe('no item available') @@ -83,7 +80,6 @@ describe('plugin: intercept', () => { }, sendSession: () => {} })) - c.use(plugin) const intercept = c.getPlugin('intercept') pload(7).then(item => { expect(true).toBe(false) @@ -92,7 +88,7 @@ describe('plugin: intercept', () => { }) it('should add a stacktrace when missing', done => { - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) c._setDelivery(client => ({ sendEvent: (payload, cb) => { expect(payload.events[0].errors[0].errorMessage).toBe('ENOENT: no such file or directory, open \'does not exist\'') @@ -102,7 +98,6 @@ describe('plugin: intercept', () => { }, sendSession: () => {} })) - c.use(plugin) const intercept = c.getPlugin('intercept') fs.readFile('does not exist', intercept(data => { expect(true).toBe(false) diff --git a/packages/plugin-koa/src/koa.js b/packages/plugin-koa/src/koa.js index c630ea5698..4c9daf4f82 100644 --- a/packages/plugin-koa/src/koa.js +++ b/packages/plugin-koa/src/koa.js @@ -12,7 +12,7 @@ const handledState = { module.exports = { name: 'koa', - init: client => { + 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. diff --git a/packages/plugin-koa/test/koa.test.js b/packages/plugin-koa/test/koa.test.js index 61c07429a3..f56435a79c 100644 --- a/packages/plugin-koa/test/koa.test.js +++ b/packages/plugin-koa/test/koa.test.js @@ -6,13 +6,12 @@ const plugin = require('../') describe('plugin: koa', () => { it('exports two middleware functions', () => { - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) c._sessionDelegate = { startSession: () => c, pauseSession: () => {}, resumeSession: () => {} } - c.use(plugin) const middleware = c.getPlugin('koa') expect(typeof middleware.requestHandler).toBe('function') expect(middleware.requestHandler.length).toBe(2) @@ -22,13 +21,13 @@ describe('plugin: koa', () => { describe('requestHandler', () => { it('should call through to app.onerror to ensure the error is logged out', (done) => { - const c = new Client({ apiKey: 'api_key' }) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) c._sessionDelegate = { startSession: () => c, pauseSession: () => {}, resumeSession: () => {} } - c.use(plugin) + const middleware = c.getPlugin('koa') const mockCtx = { req: { connection: { address: () => ({ port: 1234 }) }, headers: {} }, diff --git a/packages/plugin-navigation-breadcrumbs/navigation-breadcrumbs.js b/packages/plugin-navigation-breadcrumbs/navigation-breadcrumbs.js index 1a7780811a..4456dfd6f6 100644 --- a/packages/plugin-navigation-breadcrumbs/navigation-breadcrumbs.js +++ b/packages/plugin-navigation-breadcrumbs/navigation-breadcrumbs.js @@ -1,38 +1,51 @@ const includes = require('@bugsnag/core/lib/es-utils/includes') /* - * Leaves breadcrumbs when navigation methods are called or events are emitted - */ -exports.init = (client, win = window) => { - if (!('addEventListener' in win)) return +* Leaves breadcrumbs when navigation methods are called or events are emitted +*/ +module.exports = (win = window) => { + const plugin = { + load: (client) => { + if (!('addEventListener' in win)) return - if (!client._config.enabledBreadcrumbTypes || !includes(client._config.enabledBreadcrumbTypes, 'navigation')) return + if (!client._config.enabledBreadcrumbTypes || !includes(client._config.enabledBreadcrumbTypes, 'navigation')) return - // returns a function that will drop a breadcrumb with a given name - const drop = name => () => client.leaveBreadcrumb(name, {}, 'navigation') + // returns a function that will drop a breadcrumb with a given name + const drop = name => () => client.leaveBreadcrumb(name, {}, 'navigation') - // simple drops – just names, no meta - win.addEventListener('pagehide', drop('Page hidden'), true) - win.addEventListener('pageshow', drop('Page shown'), true) - win.addEventListener('load', drop('Page loaded'), true) - win.document.addEventListener('DOMContentLoaded', drop('DOMContentLoaded'), true) - // some browsers like to emit popstate when the page loads, so only add the popstate listener after that - win.addEventListener('load', () => win.addEventListener('popstate', drop('Navigated back'), true)) + // simple drops – just names, no meta + win.addEventListener('pagehide', drop('Page hidden'), true) + win.addEventListener('pageshow', drop('Page shown'), true) + win.addEventListener('load', drop('Page loaded'), true) + win.document.addEventListener('DOMContentLoaded', drop('DOMContentLoaded'), true) + // some browsers like to emit popstate when the page loads, so only add the popstate listener after that + win.addEventListener('load', () => win.addEventListener('popstate', drop('Navigated back'), true)) - // hashchange has some metadata that we care about - win.addEventListener('hashchange', event => { - const metadata = event.oldURL - ? { from: relativeLocation(event.oldURL, win), to: relativeLocation(event.newURL, win), state: getCurrentState(win) } - : { to: relativeLocation(win.location.href, win) } - client.leaveBreadcrumb('Hash changed', metadata, 'navigation') - }, true) + // hashchange has some metadata that we care about + win.addEventListener('hashchange', event => { + const metadata = event.oldURL + ? { from: relativeLocation(event.oldURL, win), to: relativeLocation(event.newURL, win), state: getCurrentState(win) } + : { to: relativeLocation(win.location.href, win) } + client.leaveBreadcrumb('Hash changed', metadata, 'navigation') + }, true) - // the only way to know about replaceState/pushState is to wrap them… >_< + // the only way to know about replaceState/pushState is to wrap them… >_< - if (win.history.replaceState) wrapHistoryFn(client, win.history, 'replaceState', win) - if (win.history.pushState) wrapHistoryFn(client, win.history, 'pushState', win) + if (win.history.replaceState) wrapHistoryFn(client, win.history, 'replaceState', win) + if (win.history.pushState) wrapHistoryFn(client, win.history, 'pushState', win) - client.leaveBreadcrumb('Bugsnag loaded', {}, 'navigation') + client.leaveBreadcrumb('Bugsnag loaded', {}, 'navigation') + } + } + + if (process.env.NODE_ENV !== 'production') { + plugin.destroy = (win = window) => { + win.history.replaceState._restore() + win.history.pushState._restore() + } + } + + return plugin } if (process.env.NODE_ENV !== 'production') { diff --git a/packages/plugin-navigation-breadcrumbs/test/navigation-breadcrumbs.test.js b/packages/plugin-navigation-breadcrumbs/test/navigation-breadcrumbs.test.js index 53ac4bbeb0..8e6d0a835e 100644 --- a/packages/plugin-navigation-breadcrumbs/test/navigation-breadcrumbs.test.js +++ b/packages/plugin-navigation-breadcrumbs/test/navigation-breadcrumbs.test.js @@ -6,15 +6,14 @@ const Client = require('@bugsnag/core/client') describe('plugin: navigation breadcrumbs', () => { it('should drop breadcrumb for navigational activity', done => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) + const { winHandlers, docHandlers, window } = getMockWindow() + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [plugin(window)] }) c._sessionDelegate = { startSession: () => {}, pauseSession: () => {}, resumeSession: () => {} } - const { winHandlers, docHandlers, window } = getMockWindow() - c.use(plugin, window) winHandlers.load.forEach((h) => h.call(window)) docHandlers.DOMContentLoaded.forEach((h) => h.call(window.document)) @@ -37,14 +36,13 @@ describe('plugin: navigation breadcrumbs', () => { }) it('should not be enabled when enabledBreadcrumbTypes=[]', () => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [] }) + const { winHandlers, docHandlers, window } = getMockWindow() + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [], plugins: [plugin(window)] }) c._sessionDelegate = { startSession: () => {}, pauseSession: () => {}, resumeSession: () => {} } - const { winHandlers, docHandlers, window } = getMockWindow() - c.use(plugin, window) winHandlers.load.forEach((h) => h.call(window)) docHandlers.DOMContentLoaded.forEach((h) => h.call(window.document)) window.history.replaceState({}, 'bar', 'network-breadcrumb-test.html') @@ -53,29 +51,27 @@ describe('plugin: navigation breadcrumbs', () => { }) it('should start a new session if autoTrackSessions=true', (done) => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) + const { winHandlers, docHandlers, window } = getMockWindow() + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [plugin(window)] }) c._sessionDelegate = { startSession: client => { done() } } - const { winHandlers, docHandlers, window } = getMockWindow() - c.use(plugin, window) winHandlers.load.forEach((h) => h.call(window)) docHandlers.DOMContentLoaded.forEach((h) => h.call(window.document)) window.history.replaceState({}, 'bar', 'network-breadcrumb-test.html') }) it('should not a new session if autoTrackSessions=false', (done) => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', autoTrackSessions: false }) + const { winHandlers, docHandlers, window } = getMockWindow() + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', autoTrackSessions: false, plugins: [plugin(window)] }) c._sessionDelegate = { startSession: client => { expect('shouldn’t get here').toBe(false) done() } } - const { winHandlers, docHandlers, window } = getMockWindow() - c.use(plugin, window) winHandlers.load.forEach((h) => h.call(window)) docHandlers.DOMContentLoaded.forEach((h) => h.call(window.document)) window.history.replaceState({}, 'bar', 'network-breadcrumb-test.html') @@ -83,14 +79,13 @@ describe('plugin: navigation breadcrumbs', () => { }) it('should be enabled when enabledReleaseStages=["navigation"]', () => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledReleaseStages: ['navigation'] }) + const { winHandlers, docHandlers, window } = getMockWindow() + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledReleaseStages: ['navigation'], plugins: [plugin(window)] }) c._sessionDelegate = { startSession: () => {}, pauseSession: () => {}, resumeSession: () => {} } - const { winHandlers, docHandlers, window } = getMockWindow() - c.use(plugin, window) winHandlers.load.forEach((h) => h.call(window)) docHandlers.DOMContentLoaded.forEach((h) => h.call(window.document)) window.history.replaceState({}, 'bar', 'network-breadcrumb-test.html') diff --git a/packages/plugin-network-breadcrumbs/network-breadcrumbs.js b/packages/plugin-network-breadcrumbs/network-breadcrumbs.js index 073ba8a197..444c07ff59 100644 --- a/packages/plugin-network-breadcrumbs/network-breadcrumbs.js +++ b/packages/plugin-network-breadcrumbs/network-breadcrumbs.js @@ -7,167 +7,165 @@ const REQUEST_METHOD_KEY = 'BS~~M' const includes = require('@bugsnag/core/lib/es-utils/includes') -let restoreFunctions = [] -let client -let win -let getIgnoredUrls - -const defaultIgnoredUrls = () => [ - client._config.endpoints.notify, - client._config.endpoints.sessions -] - /* * Leaves breadcrumbs when network requests occur */ -exports.name = 'networkBreadcrumbs' -exports.init = (_client, _getIgnoredUrls = defaultIgnoredUrls, _win = window) => { - if (!_client._config.enabledBreadcrumbTypes || !includes(_client._config.enabledBreadcrumbTypes, 'request')) return - - client = _client - win = _win - getIgnoredUrls = _getIgnoredUrls - monkeyPatchXMLHttpRequest() - monkeyPatchFetch() -} - -if (process.env.NODE_ENV !== 'production') { - exports.destroy = () => { - restoreFunctions.forEach(fn => fn()) - restoreFunctions = [] - } -} - -// XMLHttpRequest monkey patch -const monkeyPatchXMLHttpRequest = () => { - if (!('addEventListener' in win.XMLHttpRequest.prototype)) return - const nativeOpen = win.XMLHttpRequest.prototype.open - - // override native open() - win.XMLHttpRequest.prototype.open = function open (method, url) { - // store url and HTTP method for later - this[REQUEST_URL_KEY] = url - this[REQUEST_METHOD_KEY] = method - - // if we have already setup listeners, it means open() was called twice, we need to remove - // the listeners and recreate them - if (this[REQUEST_SETUP_KEY]) { - this.removeEventListener('load', handleXHRLoad) - this.removeEventListener('error', handleXHRError) - } - - // attach load event listener - this.addEventListener('load', handleXHRLoad) - // attach error event listener - this.addEventListener('error', handleXHRError) - - this[REQUEST_SETUP_KEY] = true - - nativeOpen.apply(this, arguments) - } - - if (process.env.NODE_ENV !== 'production') { - restoreFunctions.push(() => { - win.XMLHttpRequest.prototype.open = nativeOpen - }) - } -} +module.exports = (_ignoredUrls = [], win = window) => { + let restoreFunctions = [] + const plugin = { + load: client => { + if (!client._config.enabledBreadcrumbTypes || !includes(client._config.enabledBreadcrumbTypes, 'request')) return + + const ignoredUrls = [ + client._config.endpoints.notify, + client._config.endpoints.sessions + ].concat(_ignoredUrls) + + monkeyPatchXMLHttpRequest() + monkeyPatchFetch() + + // XMLHttpRequest monkey patch + function monkeyPatchXMLHttpRequest () { + if (!('addEventListener' in win.XMLHttpRequest.prototype)) return + const nativeOpen = win.XMLHttpRequest.prototype.open + + // override native open() + win.XMLHttpRequest.prototype.open = function open (method, url) { + // store url and HTTP method for later + this[REQUEST_URL_KEY] = url + this[REQUEST_METHOD_KEY] = method + + // if we have already setup listeners, it means open() was called twice, we need to remove + // the listeners and recreate them + if (this[REQUEST_SETUP_KEY]) { + this.removeEventListener('load', handleXHRLoad) + this.removeEventListener('error', handleXHRError) + } + + // attach load event listener + this.addEventListener('load', handleXHRLoad) + // attach error event listener + this.addEventListener('error', handleXHRError) + + this[REQUEST_SETUP_KEY] = true + + nativeOpen.apply(this, arguments) + } + + if (process.env.NODE_ENV !== 'production') { + restoreFunctions.push(() => { + win.XMLHttpRequest.prototype.open = nativeOpen + }) + } + } -function handleXHRLoad () { - if (includes(getIgnoredUrls(), this[REQUEST_URL_KEY])) { - // don't leave a network breadcrumb from bugsnag notify calls - return - } - const metadata = { - status: this.status, - request: `${this[REQUEST_METHOD_KEY]} ${this[REQUEST_URL_KEY]}` - } - if (this.status >= 400) { - // contacted server but got an error response - client.leaveBreadcrumb('XMLHttpRequest failed', metadata, BREADCRUMB_TYPE) - } else { - client.leaveBreadcrumb('XMLHttpRequest succeeded', metadata, BREADCRUMB_TYPE) - } -} + function handleXHRLoad () { + if (includes(ignoredUrls, this[REQUEST_URL_KEY])) { + // don't leave a network breadcrumb from bugsnag notify calls + return + } + const metadata = { + status: this.status, + request: `${this[REQUEST_METHOD_KEY]} ${this[REQUEST_URL_KEY]}` + } + if (this.status >= 400) { + // contacted server but got an error response + client.leaveBreadcrumb('XMLHttpRequest failed', metadata, BREADCRUMB_TYPE) + } else { + client.leaveBreadcrumb('XMLHttpRequest succeeded', metadata, BREADCRUMB_TYPE) + } + } -function handleXHRError () { - if (includes(getIgnoredUrls, this[REQUEST_URL_KEY])) { - // don't leave a network breadcrumb from bugsnag notify calls - return - } - // failed to contact server - client.leaveBreadcrumb('XMLHttpRequest error', { - request: `${this[REQUEST_METHOD_KEY]} ${this[REQUEST_URL_KEY]}` - }, BREADCRUMB_TYPE) -} + function handleXHRError () { + if (includes(ignoredUrls, this[REQUEST_URL_KEY])) { + // don't leave a network breadcrumb from bugsnag notify calls + return + } + // failed to contact server + client.leaveBreadcrumb('XMLHttpRequest error', { + request: `${this[REQUEST_METHOD_KEY]} ${this[REQUEST_URL_KEY]}` + }, BREADCRUMB_TYPE) + } -// window.fetch monkey patch -const monkeyPatchFetch = () => { - // only patch it if it exists and if it is not a polyfill (patching a polyfilled - // fetch() results in duplicate breadcrumbs for the same request because the - // implementation uses XMLHttpRequest which is also patched) - if (!('fetch' in win) || win.fetch.polyfill) return - - const oldFetch = win.fetch - win.fetch = function fetch () { - const urlOrRequest = arguments[0] - const options = arguments[1] - - let method - let url = null - - if (urlOrRequest && typeof urlOrRequest === 'object') { - url = urlOrRequest.url - if (options && 'method' in options) { - method = options.method - } else if (urlOrRequest && 'method' in urlOrRequest) { - method = urlOrRequest.method + // window.fetch monkey patch + function monkeyPatchFetch () { + // only patch it if it exists and if it is not a polyfill (patching a polyfilled + // fetch() results in duplicate breadcrumbs for the same request because the + // implementation uses XMLHttpRequest which is also patched) + if (!('fetch' in win) || win.fetch.polyfill) return + + const oldFetch = win.fetch + win.fetch = function fetch () { + const urlOrRequest = arguments[0] + const options = arguments[1] + + let method + let url = null + + if (urlOrRequest && typeof urlOrRequest === 'object') { + url = urlOrRequest.url + if (options && 'method' in options) { + method = options.method + } else if (urlOrRequest && 'method' in urlOrRequest) { + method = urlOrRequest.method + } + } else { + url = urlOrRequest + if (options && 'method' in options) { + method = options.method + } + } + + if (method === undefined) { + method = 'GET' + } + + return new Promise((resolve, reject) => { + // pass through to native fetch + oldFetch(...arguments) + .then(response => { + handleFetchSuccess(response, method, url) + resolve(response) + }) + .catch(error => { + handleFetchError(method, url) + reject(error) + }) + }) + } + + if (process.env.NODE_ENV !== 'production') { + restoreFunctions.push(() => { + win.fetch = oldFetch + }) + } } - } else { - url = urlOrRequest - if (options && 'method' in options) { - method = options.method + + const handleFetchSuccess = (response, method, url) => { + const metadata = { + status: response.status, + request: `${method} ${url}` + } + if (response.status >= 400) { + // when the request comes back with a 4xx or 5xx status it does not reject the fetch promise, + client.leaveBreadcrumb('fetch() failed', metadata, BREADCRUMB_TYPE) + } else { + client.leaveBreadcrumb('fetch() succeeded', metadata, BREADCRUMB_TYPE) + } } - } - if (method === undefined) { - method = 'GET' + const handleFetchError = (method, url) => { + client.leaveBreadcrumb('fetch() error', { request: `${method} ${url}` }, BREADCRUMB_TYPE) + } } - - return new Promise((resolve, reject) => { - // pass through to native fetch - oldFetch(...arguments) - .then(response => { - handleFetchSuccess(response, method, url) - resolve(response) - }) - .catch(error => { - handleFetchError(method, url) - reject(error) - }) - }) } if (process.env.NODE_ENV !== 'production') { - restoreFunctions.push(() => { - win.fetch = oldFetch - }) - } -} - -const handleFetchSuccess = (response, method, url) => { - const metadata = { - status: response.status, - request: `${method} ${url}` - } - if (response.status >= 400) { - // when the request comes back with a 4xx or 5xx status it does not reject the fetch promise, - client.leaveBreadcrumb('fetch() failed', metadata, BREADCRUMB_TYPE) - } else { - client.leaveBreadcrumb('fetch() succeeded', metadata, BREADCRUMB_TYPE) + plugin.destroy = () => { + restoreFunctions.forEach(fn => fn()) + restoreFunctions = [] + } } -} -const handleFetchError = (method, url) => { - client.leaveBreadcrumb('fetch() error', { request: `${method} ${url}` }, BREADCRUMB_TYPE) + return plugin } diff --git a/packages/plugin-network-breadcrumbs/test/network-breadcrumbs.test.js b/packages/plugin-network-breadcrumbs/test/network-breadcrumbs.test.js index a08c20dd6f..d2d461d1f4 100644 --- a/packages/plugin-network-breadcrumbs/test/network-breadcrumbs.test.js +++ b/packages/plugin-network-breadcrumbs/test/network-breadcrumbs.test.js @@ -1,6 +1,7 @@ const { describe, it, expect, jasmine, afterEach } = global const plugin = require('../') +let p const Client = require('@bugsnag/core/client') @@ -46,14 +47,14 @@ function Request (url, opts) { describe('plugin: network breadcrumbs', () => { afterEach(() => { // undo the global side effects - plugin.destroy() + if (p) p.destroy() }) it('should leave a breadcrumb when an XMLHTTPRequest resolves', () => { const window = { XMLHttpRequest } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) const request = new window.XMLHttpRequest() request.open('GET', '/') @@ -74,8 +75,8 @@ describe('plugin: network breadcrumbs', () => { it('should not leave duplicate breadcrumbs if open() is called twice', () => { const window = { XMLHttpRequest } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, undefined, window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) const request = new window.XMLHttpRequest() request.open('GET', '/') @@ -87,8 +88,8 @@ describe('plugin: network breadcrumbs', () => { it('should leave a breadcrumb when an XMLHTTPRequest has a failed response', () => { const window = { XMLHttpRequest } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) const request = new window.XMLHttpRequest() request.open('GET', '/this-does-not-exist') @@ -108,8 +109,8 @@ describe('plugin: network breadcrumbs', () => { it('should leave a breadcrumb when an XMLHTTPRequest has a network error', () => { const window = { XMLHttpRequest } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) const request = new window.XMLHttpRequest() @@ -129,8 +130,8 @@ describe('plugin: network breadcrumbs', () => { it('should not leave a breadcrumb for request to bugsnag notify endpoint', () => { const window = { XMLHttpRequest } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, undefined, window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) const request = new window.XMLHttpRequest() request.open('GET', client._config.endpoints.notify) @@ -142,8 +143,8 @@ describe('plugin: network breadcrumbs', () => { it('should not leave a breadcrumb for session tracking requests', () => { const window = { XMLHttpRequest } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, undefined, window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) const request = new window.XMLHttpRequest() request.open('GET', client._config.endpoints.sessions) @@ -154,8 +155,8 @@ describe('plugin: network breadcrumbs', () => { it('should leave a breadcrumb when a fetch() resolves', (done) => { const window = { XMLHttpRequest, fetch } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) window.fetch('/', {}, false, 200).then(() => { expect(client._breadcrumbs.length).toBe(1) @@ -174,8 +175,8 @@ describe('plugin: network breadcrumbs', () => { it('should handle a fetch(url, { method: null })', (done) => { const window = { XMLHttpRequest, fetch } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) window.fetch('/', { method: null }, false, 405).then(() => { expect(client._breadcrumbs.length).toBe(1) @@ -194,8 +195,8 @@ describe('plugin: network breadcrumbs', () => { it('should handle a fetch() request supplied with a Request object', (done) => { const window = { XMLHttpRequest, fetch } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) const request = new Request('/') @@ -216,8 +217,8 @@ describe('plugin: network breadcrumbs', () => { it('should handle a fetch() request supplied with a Request object that has a method specified', (done) => { const window = { XMLHttpRequest, fetch } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) const request = new Request('/', { method: 'PUT' }) @@ -238,8 +239,8 @@ describe('plugin: network breadcrumbs', () => { it('should handle fetch(null)', (done) => { const window = { XMLHttpRequest, fetch } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) window.fetch(null, {}, false, 404).then(() => { expect(client._breadcrumbs.length).toBe(1) @@ -258,8 +259,8 @@ describe('plugin: network breadcrumbs', () => { it('should handle fetch(url, null)', (done) => { const window = { XMLHttpRequest, fetch } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) window.fetch('/', null, false, 200).then(() => { expect(client._breadcrumbs.length).toBe(1) @@ -278,8 +279,8 @@ describe('plugin: network breadcrumbs', () => { it('should handle fetch(undefined)', (done) => { const window = { XMLHttpRequest, fetch } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) window.fetch(undefined, {}, false, 404).then(() => { expect(client._breadcrumbs.length).toBe(1) @@ -298,8 +299,8 @@ describe('plugin: network breadcrumbs', () => { it('should handle a fetch(request, { method })', (done) => { const window = { XMLHttpRequest, fetch } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) window.fetch(new Request('/foo', { method: 'GET' }), { method: 'PUT' }, false, 200).then(() => { expect(client._breadcrumbs.length).toBe(1) @@ -318,8 +319,8 @@ describe('plugin: network breadcrumbs', () => { it('should handle a fetch(request, { method: null })', (done) => { const window = { XMLHttpRequest, fetch } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) window.fetch(new Request('/foo'), { method: null }, false, 405).then(() => { expect(client._breadcrumbs.length).toBe(1) @@ -338,8 +339,8 @@ describe('plugin: network breadcrumbs', () => { it('should handle a fetch(request, { method: undefined })', (done) => { const window = { XMLHttpRequest, fetch } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) window.fetch(new Request('/foo'), { method: undefined }, false, 200).then(() => { expect(client._breadcrumbs.length).toBe(1) @@ -358,8 +359,8 @@ describe('plugin: network breadcrumbs', () => { it('should leave a breadcrumb when a fetch() has a failed response', (done) => { const window = { XMLHttpRequest, fetch } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) window.fetch('/does-not-exist', {}, false, 404).then(() => { expect(client._breadcrumbs.length).toBe(1) @@ -378,8 +379,8 @@ describe('plugin: network breadcrumbs', () => { it('should leave a breadcrumb when a fetch() has a network error', (done) => { const window = { XMLHttpRequest, fetch } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [p] }) window.fetch('https://another-domain.xyz/foo/bar', {}, true).catch(() => { expect(client._breadcrumbs.length).toBe(1) @@ -397,8 +398,8 @@ describe('plugin: network breadcrumbs', () => { it('should not be enabled when enabledBreadcrumbTypes=[]', () => { const window = { XMLHttpRequest } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [] }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [], plugins: [p] }) const request = new window.XMLHttpRequest() request.open('GET', '/') @@ -410,8 +411,8 @@ describe('plugin: network breadcrumbs', () => { it('should be enabled when enabledBreadcrumbTypes=["request"]', () => { const window = { XMLHttpRequest } - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['request'] }) - client.use(plugin, () => [], window) + p = plugin([], window) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['request'], plugins: [p] }) const request = new window.XMLHttpRequest() request.open('GET', '/') diff --git a/packages/plugin-node-device/device.js b/packages/plugin-node-device/device.js index fa0e4ae2b5..2eeec4d464 100644 --- a/packages/plugin-node-device/device.js +++ b/packages/plugin-node-device/device.js @@ -2,7 +2,7 @@ * Automatically detects browser device details */ module.exports = { - init: (client) => { + load: (client) => { const device = { hostname: client._config.hostname, runtimeVersions: { node: process.versions.node } diff --git a/packages/plugin-node-device/test/device.test.js b/packages/plugin-node-device/test/device.test.js index 5b2f3e620c..48aae7896d 100644 --- a/packages/plugin-node-device/test/device.test.js +++ b/packages/plugin-node-device/test/device.test.js @@ -14,8 +14,7 @@ const schema = { describe('plugin: node device', () => { it('should set device = { hostname, runtimeVersions } add an onError callback which adds device time', done => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }, schema) - client.use(plugin) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin] }, schema) expect(client._cbs.sp.length).toBe(1) expect(client._cbs.e.length).toBe(1) diff --git a/packages/plugin-node-in-project/in-project.js b/packages/plugin-node-in-project/in-project.js index 0c4aa32823..d9012bde7a 100644 --- a/packages/plugin-node-in-project/in-project.js +++ b/packages/plugin-node-in-project/in-project.js @@ -1,7 +1,7 @@ const normalizePath = require('@bugsnag/core/lib/path-normalizer') module.exports = { - init: client => client.addOnError(event => { + load: client => client.addOnError(event => { if (!client._config.projectRoot) return const projectRoot = normalizePath(client._config.projectRoot) const allFrames = event.errors.reduce((accum, er) => accum.concat(er.stacktrace), []) diff --git a/packages/plugin-node-in-project/test/in-project.test.js b/packages/plugin-node-in-project/test/in-project.test.js index 8f7f154acd..dd19e0a2f6 100644 --- a/packages/plugin-node-in-project/test/in-project.test.js +++ b/packages/plugin-node-in-project/test/in-project.test.js @@ -8,7 +8,7 @@ const { schema } = require('@bugsnag/core/config') describe('plugin: node in project', () => { it('should mark stackframes as "inProject" if it is a descendent of the "projectRoot"', done => { - const client = new Client({ apiKey: 'api_key', projectRoot: '/app' }, { + const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, projectRoot: { validate: () => true, @@ -28,8 +28,6 @@ describe('plugin: node in project', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'in project test', [ { lineNumber: 22, @@ -48,7 +46,7 @@ describe('plugin: node in project', () => { }) it('should mark stackframes as "out of project" if it is not a descendent of "projectRoot"', done => { - const client = new Client({ apiKey: 'api_key', projectRoot: '/app' }, { + const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, projectRoot: { validate: () => true, @@ -68,8 +66,6 @@ describe('plugin: node in project', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'in project test', [ { lineNumber: 22, @@ -88,7 +84,7 @@ describe('plugin: node in project', () => { }) it('should work with node_modules and node internals', done => { - const client = new Client({ apiKey: 'api_key', projectRoot: '/app' }, { + const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, projectRoot: { validate: () => true, @@ -107,8 +103,6 @@ describe('plugin: node in project', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'in project test', [ { lineNumber: 22, @@ -123,7 +117,7 @@ describe('plugin: node in project', () => { }) it('should tolerate stackframe.file not being a string', done => { - const client = new Client({ apiKey: 'api_key', projectRoot: '/app' }, { + const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, projectRoot: { validate: () => true, @@ -143,8 +137,6 @@ describe('plugin: node in project', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'in project test', [ { lineNumber: 22, diff --git a/packages/plugin-node-surrounding-code/surrounding-code.js b/packages/plugin-node-surrounding-code/surrounding-code.js index 56d473b124..d3cceb7d2c 100644 --- a/packages/plugin-node-surrounding-code/surrounding-code.js +++ b/packages/plugin-node-surrounding-code/surrounding-code.js @@ -7,7 +7,7 @@ const pump = require('pump') const byline = require('byline') module.exports = { - init: client => { + load: client => { if (!client._config.sendCode) return const loadSurroundingCode = (stackframe, cache) => new Promise((resolve, reject) => { diff --git a/packages/plugin-node-surrounding-code/test/fixtures/04.js b/packages/plugin-node-surrounding-code/test/fixtures/04.js index cf64fc532b..941cea7301 100644 --- a/packages/plugin-node-surrounding-code/test/fixtures/04.js +++ b/packages/plugin-node-surrounding-code/test/fixtures/04.js @@ -1,2 +1,2 @@ -!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).bugsnag=e()}}(function(){var t=function(e,t,n){for(var r=n,i=0,o=e.length;i"].indexOf(n[0])?undefined:n[0];return new s({functionName:r,fileName:i,lineNumber:n[1],columnNumber:n[2],source:e})},this)},parseFFOrSafari:function(e){return e.stack.split("\n").filter(function(e){return!e.match(r)},this).map(function(e){if(-1 eval")&&(e=e.replace(/ line (\d+)(?: > eval line \d+)* > eval\:\d+\:\d+/g,":$1")),-1===e.indexOf("@")&&-1===e.indexOf(":"))return new s({functionName:e});var t=/((.*".+"[^@]*)?[^@]*)(?:@)/,n=e.match(t),r=n&&n[1]?n[1]:undefined,i=this.extractLocation(e.replace(t,""));return new s({functionName:r,fileName:i[0],lineNumber:i[1],columnNumber:i[2],source:e})},this)},parseOpera:function(e){return!e.stacktrace||-1e.stacktrace.split("\n").length?this.parseOpera9(e):e.stack?this.parseOpera11(e):this.parseOpera10(e)},parseOpera9:function(e){for(var t=/Line (\d+).*script (?:in )?(\S+)/i,n=e.message.split("\n"),r=[],i=2,o=n.length;i/,"$2").replace(/\([^\)]*\)/g,"")||undefined;i.match(/\(([^\)]*)\)/)&&(t=i.replace(/^[^\(]+\(([^\)]*)\)$/,"$1"));var a=t===undefined||"[arguments not available]"===t?undefined:t.split(",");return new s({functionName:o,args:a,fileName:r[0],lineNumber:r[1],columnNumber:r[2],source:e})},this)}}});var D=L,x=function(e){return!(!e||!e.stack&&!e.stacktrace&&!e["opera#sourceloc"]||"string"!=typeof(e.stack||e.stacktrace||e["opera#sourceloc"])||e.stack===e.name+": "+e.message)},q={};function T(){return(T=Object.assign||function(e){for(var t=1;tthis.config.maxBreadcrumbs&&(this.breadcrumbs=this.breadcrumbs.slice(this.breadcrumbs.length-this.config.maxBreadcrumbs)),this}},e.notify=function(e,t,n){var r=this;if(void 0===t&&(t={}),void 0===n&&(n=function(){}),!this._configured)throw new Error("client not configured");var i=N(this),o=fe(e,t,this._logger),a=o.err,s=o.errorFramesToSkip,u=o._opts;u&&(t=u),"object"==typeof t&&null!==t||(t={});var c=U.ensureReport(a,s,2);if(c.app=oe({},{releaseStage:i},c.app,this.app),c.context=c.context||t.context||this.context||undefined,c.device=oe({},c.device,this.device,t.device),c.request=oe({},c.request,this.request,t.request),c.user=oe({},c.user,this.user,t.user),c.metaData=oe({},c.metaData,this.metaData,t.metaData),c.breadcrumbs=this.breadcrumbs.slice(0),this._session&&(this._session.trackError(c),c.session=this._session),t.severity!==undefined&&(c.severity=t.severity,c._handledState.severityReason={type:"userSpecifiedSeverity"}),ue(this.config.notifyReleaseStages)&&!se(this.config.notifyReleaseStages,i))return this._logger.warn("Report not sent due to releaseStage/notifyReleaseStages configuration"),n(null,c);var f,l,d,g,p,h,v,m,y=c.severity,b=[].concat(t.onError).concat(this.config.onError),w=function(e){r._logger.error("Error occurred in onError callback, continuing anyway…"),r._logger.error(e)};v=c,m=w,l=function(e,n){if("function"!=typeof e)return n(null,!1);try{if(2!==e.length){var t=e(v);return t&&"function"==typeof t.then?t.then(function(e){return setTimeout(function(){return n(null,B(v,e))},0)},function(e){setTimeout(function(){return m(e),n(null,!1)})}):n(null,B(v,t))}e(v,function(e,t){if(e)return m(e),n(null,!1);n(null,B(v,t))})}catch(r){m(r),n(null,!1)}},d=function(e,t){if(e&&w(e),t)return r._logger.debug("Report not sent due to onError callback"),n(null,c);r.config.autoBreadcrumbs&&r.leaveBreadcrumb(c.errorClass,{errorClass:c.errorClass,errorMessage:c.errorMessage,severity:c.severity},"error"),y!==c.severity&&(c._handledState.severityReason={type:"userCallbackSetSeverity"}),r._delivery.sendReport({apiKey:c.apiKey||r.config.apiKey,notifier:r.notifier,events:[c]},function(e){return n(e,c)})},g=(f=b).length,p=0,(h=function(){if(g<=p)return d(null,!1);l(f[p],function(e,t){return e?d(e,!1):!0===t?d(null,!0):(p++,void h())})})()},r}(),fe=function(e,t,n){var r,i,o=function(e){var t=ge(e);return n.warn("Usage error. "+t),new Error("Bugsnag usage error. "+t)},a=0;switch(typeof e){case"string":"string"==typeof t?(r=o("string/string"),i={metaData:{notifier:{notifyArgs:[e,t]}}}):(r=new Error(String(e)),a=3);break;case"number":case"boolean":r=new Error(String(e));break;case"function":r=o("function");break;case"object":null!==e&&(k(e)||e.__isBugsnagReport)?r=e:null!==e&&le(e)?((r=new Error(e.message||e.errorMessage)).name=e.name||e.errorClass,a=3):r=o(null===e?"null":"unsupported object");break;default:r=o("nothing")}return{err:r,errorFramesToSkip:a,_opts:i}},le=function(e){return!("string"!=typeof e.name&&"string"!=typeof e.errorClass||"string"!=typeof e.message&&"string"!=typeof e.errorMessage)},de=function(e){return"Bugsnag configuration error\n"+ae(e,function(e){return'"'+e.key+'" '+e.message+" \n got "+pe(e.value)}).join("\n\n")},ge=function(e){return"notify() expected error/opts parameters, got "+e},pe=function(e){return"object"==typeof e?JSON.stringify(e):String(e)},he=ce,ve=function(e,t,n,r){var i=r&&r.filterKeys?r.filterKeys:[],o=r&&r.filterPaths?r.filterPaths:[];return JSON.stringify(function a(e,h,v){var m=[],y=0;return function b(e,t){function n(){return t.length>be&&yeme)return we;if(n())return we;if(null===e||"object"!=typeof e)return e;if(Oe(m,e))return"[Circular]";m.push(e);if("function"==typeof e.toJSON)try{y--;var r=b(e.toJSON(),t);return m.pop(),r}catch(g){return Se(g)}var i=(o=e,o instanceof Error||/^\[object (Error|(Dom)?Exception)\]$/.test(Object.prototype.toString.call(o)));var o;if(i){y--;var a=b({name:e.name,message:e.message},t);return m.pop(),a}if(f=e,"[object Array]"===Object.prototype.toString.call(f)){for(var s=[],u=0,c=e.length;u "+n.join("");return n.join("")}(e.target,a)}catch(r){n=t="[hidden]",i._logger.error("Cross domain error when tracking click event. See docs: https://tinyurl.com/y94fq5zm")}i.leaveBreadcrumb("UI click",{targetText:t,targetSelector:n},"user")},!0)}},configSchema:{interactionBreadcrumbsEnabled:{defaultValue:function(){return undefined},validate:function(e){return!0===e||!1===e||e===undefined},message:"should be true|false"}}},st=function(e){var t=e.textContent||e.innerText||"";return t||"submit"!==e.type&&"button"!==e.type||(t=e.value),function n(e,t){return e&&e.length<=t?e:e.slice(0,t-"(...)".length)+"(...)"}(t=t.replace(/^\s+|\s+$/g,""),140)};var ut={init:function(n,r){if(void 0===r&&(r=window),"addEventListener"in r){var e=!1===n.config.navigationBreadcrumbsEnabled,t=!1===n.config.autoBreadcrumbs&&!0!==n.config.navigationBreadcrumbsEnabled;if(!e&&!t){var i=function(e){return function(){return n.leaveBreadcrumb(e,{},"navigation")}};r.addEventListener("pagehide",i("Page hidden"),!0),r.addEventListener("pageshow",i("Page shown"),!0),r.addEventListener("load",i("Page loaded"),!0),r.document.addEventListener("DOMContentLoaded",i("DOMContentLoaded"),!0),r.addEventListener("load",function(){return r.addEventListener("popstate",i("Navigated back"),!0)}),r.addEventListener("hashchange",function(e){var t=e.oldURL?{from:dt(e.oldURL,r),to:dt(e.newURL,r),state:pt(r)}:{to:dt(r.location.href,r)};n.leaveBreadcrumb("Hash changed",t,"navigation")},!0),r.history.replaceState&>(n,r.history,"replaceState",r),r.history.pushState&>(n,r.history,"pushState",r),n.leaveBreadcrumb("Bugsnag loaded",{},"navigation")}}}};ut.configSchema={navigationBreadcrumbsEnabled:{defaultValue:function(){return undefined},validate:function(e){return!0===e||!1===e||e===undefined},message:"should be true|false"}};var ct,ft,lt,dt=function(e,t){var n=t.document.createElement("A");return n.href=e,""+n.pathname+n.search+n.hash},gt=function(u,c,f,l){var d=c[f];c[f]=function(e,t,n){var r,i,o,a,s;u.leaveBreadcrumb("History "+f,(i=e,o=t,a=n,s=dt((r=l).location.href,r),{title:o,state:i,prevState:pt(r),to:a||s,from:s}),"navigation"),"function"==typeof u.refresh&&u.refresh(),u.config.autoCaptureSessions&&u.startSession(),d.apply(c,[e,t].concat(n!==undefined?n:[]))},c[f]._restore=function(){c[f]=d}},pt=function(e){try{return e.history.state}catch(t){}},ht={},vt="request",mt="BS~~U",yt="BS~~M",bt=s,wt=function(){return[ct.config.endpoints.notify,ct.config.endpoints.sessions]};ht.name="networkBreadcrumbs",ht.init=function(e,t,n){void 0===t&&(t=wt),void 0===n&&(n=window);var r=!1===e.config.networkBreadcrumbsEnabled,i=!1===e.config.autoBreadcrumbs&&!0!==e.config.networkBreadcrumbsEnabled;r||i||(ct=e,ft=n,lt=t,St(),Et())},ht.configSchema={networkBreadcrumbsEnabled:{defaultValue:function(){return undefined},validate:function(e){return!0===e||!1===e||e===undefined},message:"should be true|false"}};var St=function(){if("addEventListener"in ft.XMLHttpRequest.prototype){var n=ft.XMLHttpRequest.prototype.open;ft.XMLHttpRequest.prototype.open=function(e,t){this[mt]=t,this[yt]=e,this["BS~~S"]&&(this.removeEventListener("load",Ot),this.removeEventListener("error",_t)),this.addEventListener("load",Ot),this.addEventListener("error",_t),this["BS~~S"]=!0,n.apply(this,arguments)}}};function Ot(){if(!bt(lt(),this[mt])){var e={status:this.status,request:this[yt]+" "+this[mt]};400<=this.status?ct.leaveBreadcrumb("XMLHttpRequest failed",e,vt):ct.leaveBreadcrumb("XMLHttpRequest succeeded",e,vt)}}function _t(){bt(lt,this[mt])||ct.leaveBreadcrumb("XMLHttpRequest error",{request:this[yt]+" "+this[mt]},vt)}var Et=function(){if("fetch"in ft&&!ft.fetch.polyfill){var a=ft.fetch;ft.fetch=function(){for(var e=arguments.length,r=new Array(e),t=0;t=t.config.maxEvents)return e.ignore();n++}),t.refresh=function(){n=0}},configSchema:{maxEvents:{defaultValue:function(){return 10},message:"should be a positive integer ≤100",validate:function(e){return kt(1,100)(e)}}}},Rt={};function Lt(){return(Lt=Object.assign||function(e){for(var t=1;t"].indexOf(n[0])?undefined:n[0];return new s({functionName:r,fileName:i,lineNumber:n[1],columnNumber:n[2],source:e})},this)},parseFFOrSafari:function(e){return e.stack.split("\n").filter(function(e){return!e.match(r)},this).map(function(e){if(-1 eval")&&(e=e.replace(/ line (\d+)(?: > eval line \d+)* > eval\:\d+\:\d+/g,":$1")),-1===e.indexOf("@")&&-1===e.indexOf(":"))return new s({functionName:e});var t=/((.*".+"[^@]*)?[^@]*)(?:@)/,n=e.match(t),r=n&&n[1]?n[1]:undefined,i=this.extractLocation(e.replace(t,""));return new s({functionName:r,fileName:i[0],lineNumber:i[1],columnNumber:i[2],source:e})},this)},parseOpera:function(e){return!e.stacktrace||-1e.stacktrace.split("\n").length?this.parseOpera9(e):e.stack?this.parseOpera11(e):this.parseOpera10(e)},parseOpera9:function(e){for(var t=/Line (\d+).*script (?:in )?(\S+)/i,n=e.message.split("\n"),r=[],i=2,o=n.length;i/,"$2").replace(/\([^\)]*\)/g,"")||undefined;i.match(/\(([^\)]*)\)/)&&(t=i.replace(/^[^\(]+\(([^\)]*)\)$/,"$1"));var a=t===undefined||"[arguments not available]"===t?undefined:t.split(",");return new s({functionName:o,args:a,fileName:r[0],lineNumber:r[1],columnNumber:r[2],source:e})},this)}}});var D=L,x=function(e){return!(!e||!e.stack&&!e.stacktrace&&!e["opera#sourceloc"]||"string"!=typeof(e.stack||e.stacktrace||e["opera#sourceloc"])||e.stack===e.name+": "+e.message)},q={};function T(){return(T=Object.assign||function(e){for(var t=1;tthis.config.maxBreadcrumbs&&(this.breadcrumbs=this.breadcrumbs.slice(this.breadcrumbs.length-this.config.maxBreadcrumbs)),this}},e.notify=function(e,t,n){var r=this;if(void 0===t&&(t={}),void 0===n&&(n=function(){}),!this._configured)throw new Error("client not configured");var i=N(this),o=fe(e,t,this._logger),a=o.err,s=o.errorFramesToSkip,u=o._opts;u&&(t=u),"object"==typeof t&&null!==t||(t={});var c=U.ensureReport(a,s,2);if(c.app=oe({},{releaseStage:i},c.app,this.app),c.context=c.context||t.context||this.context||undefined,c.device=oe({},c.device,this.device,t.device),c.request=oe({},c.request,this.request,t.request),c.user=oe({},c.user,this.user,t.user),c.metaData=oe({},c.metaData,this.metaData,t.metaData),c.breadcrumbs=this.breadcrumbs.slice(0),this._session&&(this._session.trackError(c),c.session=this._session),t.severity!==undefined&&(c.severity=t.severity,c._handledState.severityReason={type:"userSpecifiedSeverity"}),ue(this.config.notifyReleaseStages)&&!se(this.config.notifyReleaseStages,i))return this._logger.warn("Report not sent due to releaseStage/notifyReleaseStages configuration"),n(null,c);var f,l,d,g,p,h,v,m,y=c.severity,b=[].concat(t.onError).concat(this.config.onError),w=function(e){r._logger.error("Error occurred in onError callback, continuing anyway…"),r._logger.error(e)};v=c,m=w,l=function(e,n){if("function"!=typeof e)return n(null,!1);try{if(2!==e.length){var t=e(v);return t&&"function"==typeof t.then?t.then(function(e){return setTimeout(function(){return n(null,B(v,e))},0)},function(e){setTimeout(function(){return m(e),n(null,!1)})}):n(null,B(v,t))}e(v,function(e,t){if(e)return m(e),n(null,!1);n(null,B(v,t))})}catch(r){m(r),n(null,!1)}},d=function(e,t){if(e&&w(e),t)return r._logger.debug("Report not sent due to onError callback"),n(null,c);r.config.autoBreadcrumbs&&r.leaveBreadcrumb(c.errorClass,{errorClass:c.errorClass,errorMessage:c.errorMessage,severity:c.severity},"error"),y!==c.severity&&(c._handledState.severityReason={type:"userCallbackSetSeverity"}),r._delivery.sendReport({apiKey:c.apiKey||r.config.apiKey,notifier:r.notifier,events:[c]},function(e){return n(e,c)})},g=(f=b).length,p=0,(h=function(){if(g<=p)return d(null,!1);l(f[p],function(e,t){return e?d(e,!1):!0===t?d(null,!0):(p++,void h())})})()},r}(),fe=function(e,t,n){var r,i,o=function(e){var t=ge(e);return n.warn("Usage error. "+t),new Error("Bugsnag usage error. "+t)},a=0;switch(typeof e){case"string":"string"==typeof t?(r=o("string/string"),i={metaData:{notifier:{notifyArgs:[e,t]}}}):(r=new Error(String(e)),a=3);break;case"number":case"boolean":r=new Error(String(e));break;case"function":r=o("function");break;case"object":null!==e&&(k(e)||e.__isBugsnagReport)?r=e:null!==e&&le(e)?((r=new Error(e.message||e.errorMessage)).name=e.name||e.errorClass,a=3):r=o(null===e?"null":"unsupported object");break;default:r=o("nothing")}return{err:r,errorFramesToSkip:a,_opts:i}},le=function(e){return!("string"!=typeof e.name&&"string"!=typeof e.errorClass||"string"!=typeof e.message&&"string"!=typeof e.errorMessage)},de=function(e){return"Bugsnag configuration error\n"+ae(e,function(e){return'"'+e.key+'" '+e.message+" \n got "+pe(e.value)}).join("\n\n")},ge=function(e){return"notify() expected error/opts parameters, got "+e},pe=function(e){return"object"==typeof e?JSON.stringify(e):String(e)},he=ce,ve=function(e,t,n,r){var i=r&&r.filterKeys?r.filterKeys:[],o=r&&r.filterPaths?r.filterPaths:[];return JSON.stringify(function a(e,h,v){var m=[],y=0;return function b(e,t){function n(){return t.length>be&&yeme)return we;if(n())return we;if(null===e||"object"!=typeof e)return e;if(Oe(m,e))return"[Circular]";m.push(e);if("function"==typeof e.toJSON)try{y--;var r=b(e.toJSON(),t);return m.pop(),r}catch(g){return Se(g)}var i=(o=e,o instanceof Error||/^\[object (Error|(Dom)?Exception)\]$/.test(Object.prototype.toString.call(o)));var o;if(i){y--;var a=b({name:e.name,message:e.message},t);return m.pop(),a}if(f=e,"[object Array]"===Object.prototype.toString.call(f)){for(var s=[],u=0,c=e.length;u "+n.join("");return n.join("")}(e.target,a)}catch(r){n=t="[hidden]",i._logger.error("Cross domain error when tracking click event. See docs: https://tinyurl.com/y94fq5zm")}i.leaveBreadcrumb("UI click",{targetText:t,targetSelector:n},"user")},!0)}},configSchema:{interactionBreadcrumbsEnabled:{defaultValue:function(){return undefined},validate:function(e){return!0===e||!1===e||e===undefined},message:"should be true|false"}}},st=function(e){var t=e.textContent||e.innerText||"";return t||"submit"!==e.type&&"button"!==e.type||(t=e.value),function n(e,t){return e&&e.length<=t?e:e.slice(0,t-"(...)".length)+"(...)"}(t=t.replace(/^\s+|\s+$/g,""),140)};var ut={loadfunction(n,r){if(void 0===r&&(r=window),"addEventListener"in r){var e=!1===n.config.navigationBreadcrumbsEnabled,t=!1===n.config.autoBreadcrumbs&&!0!==n.config.navigationBreadcrumbsEnabled;if(!e&&!t){var i=function(e){return function(){return n.leaveBreadcrumb(e,{},"navigation")}};r.addEventListener("pagehide",i("Page hidden"),!0),r.addEventListener("pageshow",i("Page shown"),!0),r.addEventListener("load",i("Page loaded"),!0),r.document.addEventListener("DOMContentLoaded",i("DOMContentLoaded"),!0),r.addEventListener("load",function(){return r.addEventListener("popstate",i("Navigated back"),!0)}),r.addEventListener("hashchange",function(e){var t=e.oldURL?{from:dt(e.oldURL,r),to:dt(e.newURL,r),state:pt(r)}:{to:dt(r.location.href,r)};n.leaveBreadcrumb("Hash changed",t,"navigation")},!0),r.history.replaceState&>(n,r.history,"replaceState",r),r.history.pushState&>(n,r.history,"pushState",r),n.leaveBreadcrumb("Bugsnag loaded",{},"navigation")}}}};ut.configSchema={navigationBreadcrumbsEnabled:{defaultValue:function(){return undefined},validate:function(e){return!0===e||!1===e||e===undefined},message:"should be true|false"}};var ct,ft,lt,dt=function(e,t){var n=t.document.createElement("A");return n.href=e,""+n.pathname+n.search+n.hash},gt=function(u,c,f,l){var d=c[f];c[f]=function(e,t,n){var r,i,o,a,s;u.leaveBreadcrumb("History "+f,(i=e,o=t,a=n,s=dt((r=l).location.href,r),{title:o,state:i,prevState:pt(r),to:a||s,from:s}),"navigation"),"function"==typeof u.refresh&&u.refresh(),u.config.autoCaptureSessions&&u.startSession(),d.apply(c,[e,t].concat(n!==undefined?n:[]))},c[f]._restore=function(){c[f]=d}},pt=function(e){try{return e.history.state}catch(t){}},ht={},vt="request",mt="BS~~U",yt="BS~~M",bt=s,wt=function(){return[ct.config.endpoints.notify,ct.config.endpoints.sessions]};ht.name="networkBreadcrumbs",ht.init=function(e,t,n){void 0===t&&(t=wt),void 0===n&&(n=window);var r=!1===e.config.networkBreadcrumbsEnabled,i=!1===e.config.autoBreadcrumbs&&!0!==e.config.networkBreadcrumbsEnabled;r||i||(ct=e,ft=n,lt=t,St(),Et())},ht.configSchema={networkBreadcrumbsEnabled:{defaultValue:function(){return undefined},validate:function(e){return!0===e||!1===e||e===undefined},message:"should be true|false"}};var St=function(){if("addEventListener"in ft.XMLHttpRequest.prototype){var n=ft.XMLHttpRequest.prototype.open;ft.XMLHttpRequest.prototype.open=function(e,t){this[mt]=t,this[yt]=e,this["BS~~S"]&&(this.removeEventListener("load",Ot),this.removeEventListener("error",_t)),this.addEventListener("load",Ot),this.addEventListener("error",_t),this["BS~~S"]=!0,n.apply(this,arguments)}}};function Ot(){if(!bt(lt(),this[mt])){var e={status:this.status,request:this[yt]+" "+this[mt]};400<=this.status?ct.leaveBreadcrumb("XMLHttpRequest failed",e,vt):ct.leaveBreadcrumb("XMLHttpRequest succeeded",e,vt)}}function _t(){bt(lt,this[mt])||ct.leaveBreadcrumb("XMLHttpRequest error",{request:this[yt]+" "+this[mt]},vt)}var Et=function(){if("fetch"in ft&&!ft.fetch.polyfill){var a=ft.fetch;ft.fetch=function(){for(var e=arguments.length,r=new Array(e),t=0;t=t.config.maxEvents)return e.ignore();n++}),t.refresh=function(){n=0}},configSchema:{maxEvents:{defaultValue:function(){return 10},message:"should be a positive integer ≤100",validate:function(e){return kt(1,100)(e)}}}},Rt={};function Lt(){return(Lt=Object.assign||function(e){for(var t=1;t { it('should load code successfully for stackframes whose files exist', done => { - const client = new Client({ apiKey: 'api_key' }) + const client = new Client({ apiKey: 'api_key' }, undefined, [plugin]) client._setDelivery(client => ({ sendEvent: (payload) => { @@ -40,8 +40,6 @@ describe('plugin: node surrounding code', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'surrounding code loading test', [ { lineNumber: 22, @@ -60,7 +58,7 @@ describe('plugin: node surrounding code', () => { }) it('should tolerate missing files for some stackframes', done => { - const client = new Client({ apiKey: 'api_key' }) + const client = new Client({ apiKey: 'api_key' }, undefined, [plugin]) client._setDelivery(client => ({ sendEvent: (payload) => { @@ -73,8 +71,6 @@ describe('plugin: node surrounding code', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'surrounding code loading test', [ { lineNumber: 22, @@ -93,7 +89,7 @@ describe('plugin: node surrounding code', () => { }) it('behaves sensibly for code at the beginning and end of a file', done => { - const client = new Client({ apiKey: 'api_key' }) + const client = new Client({ apiKey: 'api_key' }, undefined, [plugin]) client._setDelivery(client => ({ sendEvent: (payload) => { @@ -115,8 +111,6 @@ describe('plugin: node surrounding code', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'surrounding code loading test', [ { lineNumber: 1, @@ -132,7 +126,7 @@ describe('plugin: node surrounding code', () => { }) it('only loads code once for the same file/line/column', done => { - const client = new Client({ apiKey: 'api_key' }) + const client = new Client({ apiKey: 'api_key' }, undefined, [plugin]) const startCount = createReadStreamCount @@ -153,8 +147,6 @@ describe('plugin: node surrounding code', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'surrounding code loading test', [ { lineNumber: 1, @@ -200,7 +192,7 @@ describe('plugin: node surrounding code', () => { }) it('truncates lines to a sensible number of characters', done => { - const client = new Client({ apiKey: 'api_key' }) + const client = new Client({ apiKey: 'api_key' }, undefined, [plugin]) client._setDelivery(client => ({ sendEvent: (payload) => { @@ -214,8 +206,6 @@ describe('plugin: node surrounding code', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'surrounding code loading test', [ { lineNumber: 1, diff --git a/packages/plugin-node-uncaught-exception/test/uncaught-exception.test.js b/packages/plugin-node-uncaught-exception/test/uncaught-exception.test.js index 9bde3ddf29..facc28cbdf 100644 --- a/packages/plugin-node-uncaught-exception/test/uncaught-exception.test.js +++ b/packages/plugin-node-uncaught-exception/test/uncaught-exception.test.js @@ -7,30 +7,31 @@ const plugin = require('../') describe('plugin: node uncaught exception handler', () => { it('should listen to the process#uncaughtException event', () => { const before = process.listeners('uncaughtException').length - const c = new Client({ apiKey: 'api_key' }) - c.use(plugin) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) const after = process.listeners('uncaughtException').length expect(after - before).toBe(1) + expect(c).toBe(c) plugin.destroy() }) it('does not add a process#uncaughtException listener when autoDetectErrors=false', () => { const before = process.listeners('uncaughtException').length - const c = new Client({ apiKey: 'api_key', autoDetectErrors: false }) - c.use(plugin) + const c = new Client({ apiKey: 'api_key', autoDetectErrors: false, plugins: [plugin] }) const after = process.listeners('uncaughtException').length expect(after).toBe(before) + expect(c).toBe(c) }) it('does not add a process#uncaughtException listener when enabledErrorTypes.unhandledExceptions=false', () => { const before = process.listeners('uncaughtException').length const c = new Client({ apiKey: 'api_key', - enabledErrorTypes: { unhandledExceptions: false, unhandledRejections: true } + enabledErrorTypes: { unhandledExceptions: false, unhandledRejections: true }, + plugins: [plugin] }) - c.use(plugin) const after = process.listeners('uncaughtException').length expect(after).toBe(before) + expect(c).toBe(c) }) it('should call the configured onUncaughtException callback', done => { @@ -44,7 +45,8 @@ describe('plugin: node uncaught exception handler', () => { expect(event._handledState.severityReason).toEqual({ type: 'unhandledException' }) plugin.destroy() done() - } + }, + plugins: [plugin] }, { ...schema, onUncaughtException: { @@ -57,7 +59,6 @@ describe('plugin: node uncaught exception handler', () => { sendEvent: (...args) => args[args.length - 1](), sendSession: (...args) => args[args.length - 1]() })) - c.use(plugin) process.listeners('uncaughtException')[1](new Error('never gonna catch me')) }) @@ -72,7 +73,8 @@ describe('plugin: node uncaught exception handler', () => { expect(event._handledState.severityReason).toEqual({ type: 'unhandledException' }) plugin.destroy() done() - } + }, + plugins: [plugin] }, { ...schema, onUncaughtException: { @@ -85,7 +87,6 @@ describe('plugin: node uncaught exception handler', () => { sendEvent: (...args) => args[args.length - 1](new Error('failed')), sendSession: (...args) => args[args.length - 1]() })) - c.use(plugin) process.listeners('uncaughtException')[1](new Error('never gonna catch me')) }) }) diff --git a/packages/plugin-node-uncaught-exception/uncaught-exception.js b/packages/plugin-node-uncaught-exception/uncaught-exception.js index 43b44633c2..cf703a34bf 100644 --- a/packages/plugin-node-uncaught-exception/uncaught-exception.js +++ b/packages/plugin-node-uncaught-exception/uncaught-exception.js @@ -1,6 +1,6 @@ let _handler module.exports = { - init: client => { + load: client => { if (!client._config.autoDetectErrors) return if (!client._config.enabledErrorTypes.unhandledExceptions) return _handler = err => { diff --git a/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.js b/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.js index 87d39cc9c4..bfc409e7fd 100644 --- a/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.js +++ b/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.js @@ -7,18 +7,18 @@ const plugin = require('../') describe('plugin: node unhandled rejection handler', () => { it('should listen to the process#unhandledRejection event', () => { const before = process.listeners('unhandledRejection').length - const c = new Client({ apiKey: 'api_key' }) - c.use(plugin) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) const after = process.listeners('unhandledRejection').length expect(before < after).toBe(true) + expect(c).toBe(c) plugin.destroy() }) it('does not add a process#unhandledRejection listener if autoDetectErrors=false', () => { const before = process.listeners('unhandledRejection').length - const c = new Client({ apiKey: 'api_key', autoDetectErrors: false }) - c.use(plugin) + const c = new Client({ apiKey: 'api_key', autoDetectErrors: false, plugins: [plugin] }) const after = process.listeners('unhandledRejection').length + expect(c).toBe(c) expect(after).toBe(before) }) @@ -26,10 +26,11 @@ describe('plugin: node unhandled rejection handler', () => { const before = process.listeners('unhandledRejection').length const c = new Client({ apiKey: 'api_key', - enabledErrorTypes: { unhandledExceptions: false, unhandledRejections: false } + enabledErrorTypes: { unhandledExceptions: false, unhandledRejections: false }, + plugins: [plugin] }) - c.use(plugin) const after = process.listeners('unhandledRejection').length + expect(c).toBe(c) expect(after).toBe(before) }) @@ -44,7 +45,8 @@ describe('plugin: node unhandled rejection handler', () => { expect(event._handledState.severityReason).toEqual({ type: 'unhandledPromiseRejection' }) plugin.destroy() done() - } + }, + plugins: [plugin] }, { ...schema, onUnhandledRejection: { @@ -57,7 +59,6 @@ describe('plugin: node unhandled rejection handler', () => { sendEvent: (...args) => args[args.length - 1](), sendSession: (...args) => args[args.length - 1]() })) - c.use(plugin) process.listeners('unhandledRejection')[1](new Error('never gonna catch me')) }) @@ -72,7 +73,8 @@ describe('plugin: node unhandled rejection handler', () => { expect(event._handledState.severityReason).toEqual({ type: 'unhandledPromiseRejection' }) plugin.destroy() done() - } + }, + plugins: [plugin] }, { ...schema, onUnhandledRejection: { @@ -85,7 +87,6 @@ describe('plugin: node unhandled rejection handler', () => { sendEvent: (...args) => args[args.length - 1](new Error('floop')), sendSession: (...args) => args[args.length - 1]() })) - c.use(plugin) process.listeners('unhandledRejection')[1](new Error('never gonna catch me')) }) }) diff --git a/packages/plugin-node-unhandled-rejection/unhandled-rejection.js b/packages/plugin-node-unhandled-rejection/unhandled-rejection.js index 927965c192..d67aa837ad 100644 --- a/packages/plugin-node-unhandled-rejection/unhandled-rejection.js +++ b/packages/plugin-node-unhandled-rejection/unhandled-rejection.js @@ -1,6 +1,6 @@ let _handler module.exports = { - init: client => { + load: client => { if (!client._config.autoDetectErrors || !client._config.enabledErrorTypes.unhandledRejections) return _handler = err => { const event = client.Event.create(err, false, { diff --git a/packages/plugin-react-native-app-state-breadcrumbs/app-state.js b/packages/plugin-react-native-app-state-breadcrumbs/app-state.js index d657391e33..bbdae9ded3 100644 --- a/packages/plugin-react-native-app-state-breadcrumbs/app-state.js +++ b/packages/plugin-react-native-app-state-breadcrumbs/app-state.js @@ -1,7 +1,7 @@ const { AppState } = require('react-native') module.exports = { - init: client => { + load: client => { if (!client._config.enabledBreadcrumbTypes || !client._config.enabledBreadcrumbTypes.includes('state')) return AppState.addEventListener('change', state => { diff --git a/packages/plugin-react-native-app-state-breadcrumbs/test/app-state.test.js b/packages/plugin-react-native-app-state-breadcrumbs/test/app-state.test.js index 271123b86a..fd9d3d5179 100644 --- a/packages/plugin-react-native-app-state-breadcrumbs/test/app-state.test.js +++ b/packages/plugin-react-native-app-state-breadcrumbs/test/app-state.test.js @@ -16,8 +16,8 @@ describe('plugin: react native app state breadcrumbs', () => { 'react-native': { AppState } }) - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [plugin] }) + expect(client).toBe(client) expect(typeof _cb).toBe('function') expect(client._breadcrumbs.length).toBe(0) @@ -46,8 +46,8 @@ describe('plugin: react native app state breadcrumbs', () => { 'react-native': { AppState } }) - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: null }) - client.use(plugin) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: null, plugins: [plugin] }) + expect(client).toBe(client) expect(_cb).toBe(undefined) }) @@ -63,8 +63,8 @@ describe('plugin: react native app state breadcrumbs', () => { 'react-native': { AppState } }) - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [] }) - client.use(plugin) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [], plugins: [plugin] }) + expect(client).toBe(client) expect(_cb).toBe(undefined) }) @@ -80,8 +80,8 @@ describe('plugin: react native app state breadcrumbs', () => { 'react-native': { AppState } }) - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['state'] }) - client.use(plugin) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['state'], plugins: [plugin] }) + expect(client).toBe(client) expect(typeof _cb).toBe('function') }) diff --git a/packages/plugin-react-native-connectivity-breadcrumbs/connectivity.js b/packages/plugin-react-native-connectivity-breadcrumbs/connectivity.js index 4c2d8cb575..6a91e165ef 100644 --- a/packages/plugin-react-native-connectivity-breadcrumbs/connectivity.js +++ b/packages/plugin-react-native-connectivity-breadcrumbs/connectivity.js @@ -1,7 +1,7 @@ const NetInfo = require('@react-native-community/netinfo') module.exports = { - init: client => { + load: client => { if (!client._config.enabledBreadcrumbTypes || !client._config.enabledBreadcrumbTypes.includes('state')) return NetInfo.addEventListener(({ isConnected, isInternetReachable, type }) => { diff --git a/packages/plugin-react-native-connectivity-breadcrumbs/test/connectivity.test.js b/packages/plugin-react-native-connectivity-breadcrumbs/test/connectivity.test.js index 89f4d1fbce..c41382fadf 100644 --- a/packages/plugin-react-native-connectivity-breadcrumbs/test/connectivity.test.js +++ b/packages/plugin-react-native-connectivity-breadcrumbs/test/connectivity.test.js @@ -16,8 +16,8 @@ describe('plugin: react native connectivity breadcrumbs', () => { '@react-native-community/netinfo': NetInfo }) - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - client.use(plugin) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [plugin] }) + expect(client).toBe(client) expect(typeof _cb).toBe('function') expect(client._breadcrumbs.length).toBe(0) @@ -46,8 +46,8 @@ describe('plugin: react native connectivity breadcrumbs', () => { '@react-native-community/netinfo': NetInfo }) - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: null }) - client.use(plugin) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: null, plugins: [plugin] }) + expect(client).toBe(client) expect(_cb).toBe(undefined) }) @@ -63,8 +63,8 @@ describe('plugin: react native connectivity breadcrumbs', () => { '@react-native-community/netinfo': NetInfo }) - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [] }) - client.use(plugin) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [], plugins: [plugin] }) + expect(client).toBe(client) expect(_cb).toBe(undefined) }) @@ -80,8 +80,8 @@ describe('plugin: react native connectivity breadcrumbs', () => { '@react-native-community/netinfo': NetInfo }) - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['state'] }) - client.use(plugin) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['state'], plugins: [plugin] }) + expect(client).toBe(client) expect(typeof _cb).toBe('function') }) diff --git a/packages/plugin-react-native-global-error-handler/error-handler.js b/packages/plugin-react-native-global-error-handler/error-handler.js index 0c2ce2f1b2..dbcbf69f22 100644 --- a/packages/plugin-react-native-global-error-handler/error-handler.js +++ b/packages/plugin-react-native-global-error-handler/error-handler.js @@ -2,8 +2,8 @@ * Automatically notifies Bugsnag when React Native's global error handler is called */ -module.exports = { - init: (client, ErrorUtils = global.ErrorUtils) => { +module.exports = (ErrorUtils = global.ErrorUtils) => ({ + load: (client) => { if (!client._config.autoDetectErrors) return if (!client._config.enabledErrorTypes.unhandledExceptions) return if (!ErrorUtils) { @@ -24,4 +24,4 @@ module.exports = { }) }) } -} +}) diff --git a/packages/plugin-react-native-global-error-handler/test/error-handler.test.js b/packages/plugin-react-native-global-error-handler/test/error-handler.test.js index 6f7b6ccee1..7ba660df96 100644 --- a/packages/plugin-react-native-global-error-handler/test/error-handler.test.js +++ b/packages/plugin-react-native-global-error-handler/test/error-handler.test.js @@ -20,10 +20,10 @@ class MockErrorUtils { describe('plugin: react native global error handler', () => { it('should set a global error handler', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) const eu = new MockErrorUtils() - client.use(plugin, eu) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(eu)] }) expect(typeof eu.getGlobalHandler()).toBe('function') + expect(client).toBe(client) }) it('should warn if ErrorUtils is not defined', done => { @@ -37,50 +37,53 @@ describe('plugin: react native global error handler', () => { done() }, error: () => {} - } + }, + plugins: [plugin()] }) - client.use(plugin) + expect(client).toBe(client) }) it('should not set a global error handler when autoDetectErrors=false', () => { + const eu = new MockErrorUtils() const client = new Client({ apiKey: 'API_KEY_YEAH', - autoDetectErrors: false + autoDetectErrors: false, + plugins: [plugin(eu)] }) - const eu = new MockErrorUtils() - client.use(plugin, eu) expect(eu.getGlobalHandler()).toBe(null) + expect(client).toBe(client) }) it('should not set a global error handler when enabledErrorTypes.unhandledExceptions=false', () => { + const eu = new MockErrorUtils() const client = new Client({ apiKey: 'API_KEY_YEAH', - enabledErrorTypes: { unhandledExceptions: false, unhandledRejections: false } + enabledErrorTypes: { unhandledExceptions: false, unhandledRejections: false }, + plugins: [plugin(eu)] }) - const eu = new MockErrorUtils() - client.use(plugin, eu) expect(eu.getGlobalHandler()).toBe(null) + expect(client).toBe(client) }) it('should call through to an existing handler', done => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const eu = new MockErrorUtils() + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(eu)] }) client._setDelivery(client => ({ sendSession: () => {}, sendEvent: (...args) => args[args.length - 1](null) })) - const eu = new MockErrorUtils() const error = new Error('floop') eu.setGlobalHandler(function (err, isFatal) { expect(err).toBe(error) expect(isFatal).toBe(true) done() }) - client.use(plugin, eu) eu.getGlobalHandler()(error, true) }) it('should have the correct handled state', done => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const eu = new MockErrorUtils() + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(eu)] }) client._setDelivery(client => ({ sendSession: () => {}, sendEvent: (payload, cb) => { @@ -91,8 +94,6 @@ describe('plugin: react native global error handler', () => { done() } })) - const eu = new MockErrorUtils() - client.use(plugin, eu) eu._globalHandler(new Error('argh')) }) }) diff --git a/packages/plugin-react-native-orientation-breadcrumbs/orientation.js b/packages/plugin-react-native-orientation-breadcrumbs/orientation.js index aadca86cbe..af149f721f 100644 --- a/packages/plugin-react-native-orientation-breadcrumbs/orientation.js +++ b/packages/plugin-react-native-orientation-breadcrumbs/orientation.js @@ -1,7 +1,7 @@ const { Dimensions } = require('react-native') module.exports = { - init: client => { + load: client => { if (!client._config.enabledBreadcrumbTypes || !client._config.enabledBreadcrumbTypes.includes('state')) return let lastOrientation diff --git a/packages/plugin-react-native-orientation-breadcrumbs/test/orientation.test.js b/packages/plugin-react-native-orientation-breadcrumbs/test/orientation.test.js index 4e61113cff..7260d7242c 100644 --- a/packages/plugin-react-native-orientation-breadcrumbs/test/orientation.test.js +++ b/packages/plugin-react-native-orientation-breadcrumbs/test/orientation.test.js @@ -18,11 +18,8 @@ describe('plugin: react native orientation breadcrumbs', () => { 'react-native': { Dimensions } }) - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - currentDimensions = { height: 100, width: 200 } - - client.use(plugin) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', plugins: [plugin] }) expect(typeof _cb).toBe('function') expect(client._breadcrumbs.length).toBe(0) @@ -55,10 +52,10 @@ describe('plugin: react native orientation breadcrumbs', () => { 'react-native': { Dimensions } }) - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: null }) - client.use(plugin) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: null, plugins: [plugin] }) expect(_cb).toBe(undefined) + expect(client).toBe(client) }) it('should not be enabled when enabledBreadcrumbTypes=[]', () => { @@ -72,10 +69,10 @@ describe('plugin: react native orientation breadcrumbs', () => { 'react-native': { Dimensions } }) - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [] }) - client.use(plugin) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [], plugins: [plugin] }) expect(_cb).toBe(undefined) + expect(client).toBe(client) }) it('should be enabled when enabledBreadcrumbTypes=["state"]', () => { @@ -90,9 +87,9 @@ describe('plugin: react native orientation breadcrumbs', () => { 'react-native': { Dimensions } }) - const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['state'] }) - client.use(plugin) + const client = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['state'], plugins: [plugin] }) expect(typeof _cb).toBe('function') + expect(client).toBe(client) }) }) diff --git a/packages/plugin-react-native-unhandled-rejection/rejection-handler.js b/packages/plugin-react-native-unhandled-rejection/rejection-handler.js index 0e2941b2a8..9fe605d995 100644 --- a/packages/plugin-react-native-unhandled-rejection/rejection-handler.js +++ b/packages/plugin-react-native-unhandled-rejection/rejection-handler.js @@ -5,7 +5,7 @@ const rnPromise = require('promise/setimmediate/rejection-tracking') module.exports = { - init: (client) => { + load: (client) => { if (!client._config.autoDetectErrors || !client._config.enabledErrorTypes.unhandledRejections) return () => {} rnPromise.enable({ allRejections: true, diff --git a/packages/plugin-react-native-unhandled-rejection/test/rejection-handler.test.ts b/packages/plugin-react-native-unhandled-rejection/test/rejection-handler.test.ts index 49fd9ed998..cf7e8f5e46 100644 --- a/packages/plugin-react-native-unhandled-rejection/test/rejection-handler.test.ts +++ b/packages/plugin-react-native-unhandled-rejection/test/rejection-handler.test.ts @@ -21,7 +21,7 @@ describe('plugin: react native rejection handler', () => { done() } })) - const stop = plugin.init(c) + const stop = plugin.load(c) // in the interests of keeping the tests quick, TypeErrors get rejected quicker // see: https://github.com/then/promise/blob/d980ed01b7a383bfec416c96095e2f40fd18ab34/src/rejection-tracking.js#L48-L54 try { @@ -42,7 +42,7 @@ describe('plugin: react native rejection handler', () => { done(new Error('event should not be sent when autoDetectErrors=false')) } })) - const stop = plugin.init(c) + const stop = plugin.load(c) try { // @ts-ignore String.floop() @@ -65,7 +65,7 @@ describe('plugin: react native rejection handler', () => { done(new Error('event should not be sent when enabledErrorTypes.unhandledRejections=false')) } })) - const stop = plugin.init(c) + const stop = plugin.load(c) try { // @ts-ignore String.floop() diff --git a/packages/plugin-react/src/index.js b/packages/plugin-react/src/index.js index 2644eb837e..e05c50152c 100644 --- a/packages/plugin-react/src/index.js +++ b/packages/plugin-react/src/index.js @@ -1,7 +1,12 @@ -module.exports = { - name: 'react', - init: (client, React = window.React) => { +module.exports = class BugsnagReactPlugin { + constructor (React = window.React) { if (!React) throw new Error('cannot find React') + this.React = React + this.name = 'react' + } + + load (client) { + const React = this.React class ErrorBoundary extends React.Component { constructor (props) { diff --git a/packages/plugin-react/src/test/index.test.js b/packages/plugin-react/src/test/index.test.js index d40685e82c..33fee79858 100644 --- a/packages/plugin-react/src/test/index.test.js +++ b/packages/plugin-react/src/test/index.test.js @@ -2,7 +2,7 @@ import React from 'react' import renderer from 'react-test-renderer' -import plugin from '../' +import BugsnagPluginReact from '../' class Event { addMetadata () { @@ -19,7 +19,8 @@ bugsnag.Event.create = jest.fn(function () { return new Event() }) -const ErrorBoundary = plugin.init(bugsnag, React) +const plugin = new BugsnagPluginReact(React) +const ErrorBoundary = plugin.load(bugsnag) beforeEach(() => { bugsnag._notify.mockReset() @@ -29,7 +30,7 @@ test('formatComponentStack(str)', () => { const str = ` in BadButton in ErrorBoundary` - expect(plugin.formatComponentStack(str)) + expect(BugsnagPluginReact.formatComponentStack(str)) .toBe('in BadButton\nin ErrorBoundary') }) diff --git a/packages/plugin-react/types/bugsnag-react.d.ts b/packages/plugin-react/types/bugsnag-react.d.ts index a67fe0e319..9e80061cdc 100644 --- a/packages/plugin-react/types/bugsnag-react.d.ts +++ b/packages/plugin-react/types/bugsnag-react.d.ts @@ -1,3 +1,8 @@ -import { Bugsnag } from '@bugsnag/browser' -declare const bugsnagPluginReact: Bugsnag.Plugin -export default bugsnagPluginReact +import { Plugin } from '@bugsnag/browser' +import React from 'react' + +declare class BugsnagPluginReact extends Plugin { + constructor(React?: React) +} + +export default BugsnagPluginReact diff --git a/packages/plugin-restify/src/restify.js b/packages/plugin-restify/src/restify.js index 2073911006..58f61c3087 100644 --- a/packages/plugin-restify/src/restify.js +++ b/packages/plugin-restify/src/restify.js @@ -12,7 +12,7 @@ const handledState = { module.exports = { name: 'restify', - init: client => { + load: client => { const requestHandler = (req, res, next) => { const dom = domain.create() diff --git a/packages/plugin-restify/test/restify.test.js b/packages/plugin-restify/test/restify.test.js index eea1bf71ee..41cbe413aa 100644 --- a/packages/plugin-restify/test/restify.test.js +++ b/packages/plugin-restify/test/restify.test.js @@ -5,8 +5,7 @@ const plugin = require('../') describe('plugin: restify', () => { it('exports two middleware functions', () => { - const c = new Client({ apiKey: 'api_key' }) - c.use(plugin) + const c = new Client({ apiKey: 'api_key', plugins: [plugin] }) const middleware = c.getPlugin('restify') expect(typeof middleware.requestHandler).toBe('function') expect(middleware.requestHandler.length).toBe(3) diff --git a/packages/plugin-server-session/session.js b/packages/plugin-server-session/session.js index 4456b33094..491548a117 100644 --- a/packages/plugin-server-session/session.js +++ b/packages/plugin-server-session/session.js @@ -5,7 +5,7 @@ const Backoff = require('backo') const runSyncCallbacks = require('@bugsnag/core/lib/sync-callback-runner') module.exports = { - init: (client) => { + load: (client) => { const sessionTracker = new SessionTracker(client._config.sessionSummaryInterval) sessionTracker.on('summary', sendSessionSummary(client)) sessionTracker.start() diff --git a/packages/plugin-server-session/test/session.test.ts b/packages/plugin-server-session/test/session.test.ts index 1623b0fab1..91f31da90d 100644 --- a/packages/plugin-server-session/test/session.test.ts +++ b/packages/plugin-server-session/test/session.test.ts @@ -12,9 +12,9 @@ describe('plugin: server sessions', () => { beforeEach(() => { class TrackerMock extends EventEmitter { start () { - this.emit('summary', [ + setTimeout(() => this.emit('summary', [ { startedAt: '2017-12-12T13:54:00.000Z', sessionsStarted: 123 } - ]) + ])) } stop () {} @@ -23,8 +23,9 @@ describe('plugin: server sessions', () => { Tracker.mockImplementation(() => new TrackerMock() as any) }) + it('should send the session', done => { - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }, undefined, [plugin]) c._setDelivery(client => ({ sendEvent: () => {}, sendSession: (session: any, cb = () => {}) => { @@ -34,7 +35,6 @@ describe('plugin: server sessions', () => { } })) - c.use(plugin) c.startSession() }) @@ -54,7 +54,7 @@ describe('plugin: server sessions', () => { endpoints: { notify: 'bloo', sessions: 'blah' }, releaseStage: 'qa', enabledReleaseStages: ['production'] - }) + }, undefined, [plugin]) c._setDelivery(client => ({ sendEvent: () => {}, sendSession: (session: any, cb = () => {}) => { @@ -62,7 +62,6 @@ describe('plugin: server sessions', () => { } })) - c.use(plugin) c.startSession() }) @@ -75,7 +74,7 @@ describe('plugin: server sessions', () => { releaseStage: 'qa', appType: 'server', appVersion: '1.2.3' - }) + }, undefined, [plugin]) // this is normally set by a plugin c._addOnSessionPayload(sp => { @@ -94,7 +93,6 @@ describe('plugin: server sessions', () => { } })) - c.use(plugin) c.startSession() }) @@ -106,8 +104,7 @@ describe('plugin: server sessions', () => { } Tracker.mockImplementation(() => new TrackerMock() as any) - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - c.use(plugin) + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }, undefined, [plugin]) c.leaveBreadcrumb('tick') c._metadata = { datetime: { tz: 'GMT+1' } } @@ -131,8 +128,7 @@ describe('plugin: server sessions', () => { } Tracker.mockImplementation(() => new TrackerMock() as any) - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - c.use(plugin) + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }, undefined, [plugin]) // start a session and get its id const sessionClient = c.startSession() diff --git a/packages/plugin-simple-throttle/test/throttle.test.js b/packages/plugin-simple-throttle/test/throttle.test.js index a43fcafcfa..0aa22f6f8c 100644 --- a/packages/plugin-simple-throttle/test/throttle.test.js +++ b/packages/plugin-simple-throttle/test/throttle.test.js @@ -7,8 +7,7 @@ const Client = require('@bugsnag/core/client') describe('plugin: throttle', () => { it('prevents more than maxEvents being sent', () => { const payloads = [] - const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) - c.use(plugin) + const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }, undefined, [plugin]) c._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) for (let i = 0; i < 100; i++) c.notify(new Error('This is fail')) expect(payloads.length).toBe(10) diff --git a/packages/plugin-simple-throttle/throttle.js b/packages/plugin-simple-throttle/throttle.js index 94db93ff79..d1333b8b28 100644 --- a/packages/plugin-simple-throttle/throttle.js +++ b/packages/plugin-simple-throttle/throttle.js @@ -5,7 +5,7 @@ const intRange = require('@bugsnag/core/lib/validators/int-range') */ module.exports = { - init: (client) => { + load: (client) => { // track sent events for each init of the plugin let n = 0 diff --git a/packages/plugin-strip-project-root/strip-project-root.js b/packages/plugin-strip-project-root/strip-project-root.js index 1d47c1b005..b9515b8975 100644 --- a/packages/plugin-strip-project-root/strip-project-root.js +++ b/packages/plugin-strip-project-root/strip-project-root.js @@ -1,7 +1,7 @@ const normalizePath = require('@bugsnag/core/lib/path-normalizer') module.exports = { - init: client => client.addOnError(event => { + load: client => client.addOnError(event => { if (!client._config.projectRoot) return const projectRoot = normalizePath(client._config.projectRoot) const allFrames = event.errors.reduce((accum, er) => accum.concat(er.stacktrace), []) diff --git a/packages/plugin-strip-project-root/test/strip-project-root.test.js b/packages/plugin-strip-project-root/test/strip-project-root.test.js index 6a33c9072b..203d7561dc 100644 --- a/packages/plugin-strip-project-root/test/strip-project-root.test.js +++ b/packages/plugin-strip-project-root/test/strip-project-root.test.js @@ -8,7 +8,7 @@ const { schema } = require('@bugsnag/core/config') describe('plugin: strip project root', () => { it('should remove the project root if it matches the start of the stackframe’s file', done => { - const client = new Client({ apiKey: 'api_key', projectRoot: '/app' }, { + const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, projectRoot: { validate: () => true, @@ -28,8 +28,6 @@ describe('plugin: strip project root', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'strip project root test', [ { lineNumber: 22, @@ -48,7 +46,7 @@ describe('plugin: strip project root', () => { }) it('should not remove a matching substring if it is not at the start', done => { - const client = new Client({ apiKey: 'api_key', projectRoot: '/app' }, { + const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, projectRoot: { validate: () => true, @@ -68,8 +66,6 @@ describe('plugin: strip project root', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'strip project root test', [ { lineNumber: 22, @@ -88,7 +84,7 @@ describe('plugin: strip project root', () => { }) it('should work with node_modules and node internals', done => { - const client = new Client({ apiKey: 'api_key', projectRoot: '/app' }, { + const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, projectRoot: { validate: () => true, @@ -107,8 +103,6 @@ describe('plugin: strip project root', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'strip project root test', [ { lineNumber: 22, @@ -123,7 +117,7 @@ describe('plugin: strip project root', () => { }) it('should tolerate stackframe.file not being a string', done => { - const client = new Client({ apiKey: 'api_key', projectRoot: '/app' }, { + const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, projectRoot: { validate: () => true, @@ -143,8 +137,6 @@ describe('plugin: strip project root', () => { sendSession: () => {} })) - client.use(plugin) - client._notify(new Event('Error', 'strip project root test', [ { lineNumber: 22, diff --git a/packages/plugin-strip-query-string/strip-query-string.js b/packages/plugin-strip-query-string/strip-query-string.js index 359a9c4b1b..c5c15b6836 100644 --- a/packages/plugin-strip-query-string/strip-query-string.js +++ b/packages/plugin-strip-query-string/strip-query-string.js @@ -5,7 +5,7 @@ const map = require('@bugsnag/core/lib/es-utils/map') const reduce = require('@bugsnag/core/lib/es-utils/reduce') module.exports = { - init: (client) => { + load: (client) => { client.addOnError(event => { const allFrames = reduce(event.errors, (accum, er) => accum.concat(er.stacktrace), []) map(allFrames, frame => { diff --git a/packages/plugin-strip-query-string/test/strip-query-string.test.js b/packages/plugin-strip-query-string/test/strip-query-string.test.js index 82d98f727b..cf8812b949 100644 --- a/packages/plugin-strip-query-string/test/strip-query-string.test.js +++ b/packages/plugin-strip-query-string/test/strip-query-string.test.js @@ -35,11 +35,11 @@ describe('plugin: strip query string', () => { apiKey: 'API_KEY_YEAH', onError: event => { originalStacktrace = event.errors[0].stacktrace.map(f => ({ ...f })) - } + }, + plugins: [plugin] }) const payloads = [] let originalStacktrace - client.use(plugin) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) const err = new Error('noooo') diff --git a/packages/plugin-vue/src/index.js b/packages/plugin-vue/src/index.js index 3665a1c4df..aa1b379b38 100644 --- a/packages/plugin-vue/src/index.js +++ b/packages/plugin-vue/src/index.js @@ -1,7 +1,12 @@ -module.exports = { - name: 'vue', - init: (client, Vue = window.Vue) => { +module.exports = class BugsnagVuePlugin { + constructor (Vue = window.Vue) { if (!Vue) throw new Error('cannot find Vue') + this.Vue = Vue + this.name = 'vue' + } + + load (client) { + const Vue = this.Vue const prev = Vue.config.errorHandler const handler = (err, vm, info) => { diff --git a/packages/plugin-vue/test/index.test.js b/packages/plugin-vue/test/index.test.js index d9b6643a1c..f3c5376ad3 100644 --- a/packages/plugin-vue/test/index.test.js +++ b/packages/plugin-vue/test/index.test.js @@ -1,16 +1,17 @@ const { describe, it, expect } = global -const plugin = require('../src') +const BugsnagVuePlugin = require('../src') const Client = require('@bugsnag/core/client') describe('bugsnag vue', () => { it('throws when missing Vue', () => { expect(() => { - plugin.init(new Client({ apiKey: 'API_KEYYY' })) + new BugsnagVuePlugin().load(new Client({ apiKey: 'API_KEYYY' })) }).toThrow() }) it('installs Vue.config.errorHandler', done => { - const client = new Client({ apiKey: 'API_KEYYY' }) + const Vue = { config: {} } + const client = new Client({ apiKey: 'API_KEYYY', plugins: [new BugsnagVuePlugin(Vue)] }) client._setDelivery(client => ({ sendEvent: (payload) => { expect(payload.events[0].errors[0].errorClass).toBe('Error') @@ -19,14 +20,12 @@ describe('bugsnag vue', () => { done() } })) - const Vue = { config: {} } - client.use(plugin, Vue) expect(typeof Vue.config.errorHandler).toBe('function') Vue.config.errorHandler(new Error('oops'), { $root: true, $options: {} }, 'callback for watcher "fooBarBaz"') }) it('bugsnag vue: classify(str)', () => { - expect(plugin.classify('foo_bar')).toBe('FooBar') - expect(plugin.classify('foo-bar')).toBe('FooBar') + expect(BugsnagVuePlugin.classify('foo_bar')).toBe('FooBar') + expect(BugsnagVuePlugin.classify('foo-bar')).toBe('FooBar') }) }) diff --git a/packages/plugin-vue/types/bugsnag-vue.d.ts b/packages/plugin-vue/types/bugsnag-vue.d.ts index 7da1bbcdb3..048af28dd3 100644 --- a/packages/plugin-vue/types/bugsnag-vue.d.ts +++ b/packages/plugin-vue/types/bugsnag-vue.d.ts @@ -1,3 +1,8 @@ -import { Bugsnag } from '@bugsnag/browser' -declare const bugsnagPluginVue: Bugsnag.Plugin +import { Plugin } from '@bugsnag/browser' +import Vue from 'vue' + +declare class BugsnagPluginVue extends Plugin { + constructor(Vue?: Vue) +} + export default bugsnagPluginVue diff --git a/packages/plugin-window-onerror/onerror.js b/packages/plugin-window-onerror/onerror.js index d25de6ec09..4ae1c1781a 100644 --- a/packages/plugin-window-onerror/onerror.js +++ b/packages/plugin-window-onerror/onerror.js @@ -2,8 +2,8 @@ * Automatically notifies Bugsnag when window.onerror is called */ -module.exports = { - init: (client, win = window) => { +module.exports = (win = window) => ({ + load: (client) => { if (!client._config.autoDetectErrors) return if (!client._config.enabledErrorTypes.unhandledExceptions) return function onerror (messageOrEvent, url, lineNo, charNo, error) { @@ -66,7 +66,7 @@ module.exports = { const prevOnError = win.onerror win.onerror = onerror } -} +}) // Sometimes the stacktrace has less information than was passed to window.onerror. // This function will augment the first stackframe with any useful info that was diff --git a/packages/plugin-window-onerror/test/onerror.test.js b/packages/plugin-window-onerror/test/onerror.test.js index dcf4634e0c..3a28aca8e3 100644 --- a/packages/plugin-window-onerror/test/onerror.test.js +++ b/packages/plugin-window-onerror/test/onerror.test.js @@ -10,15 +10,15 @@ describe('plugin: window onerror', () => { beforeEach(() => { window = {} }) it('should set a window.onerror event handler', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) - client.use(plugin, window) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(window)] }) expect(typeof window.onerror).toBe('function') + expect(client).toBe(client) }) it('should not add a window.onerror event handler when autoDetectErrors=false', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH', autoDetectErrors: false }) - client.use(plugin, window) + const client = new Client({ apiKey: 'API_KEY_YEAH', autoDetectErrors: false, plugins: [plugin(window)] }) expect(window.onerror).toBe(undefined) + expect(client).toBe(client) }) it('should not add a window.onerror event handler when enabledErrorTypes.unhandledExceptions=false', () => { @@ -27,17 +27,17 @@ describe('plugin: window onerror', () => { enabledErrorTypes: { unhandledExceptions: false, unhandledRejections: false - } + }, + plugins: [plugin(window)] }) - client.use(plugin, window) expect(window.onerror).toBe(undefined) + expect(client).toBe(client) }) describe('window.onerror function', () => { it('captures uncaught errors in timer callbacks', done => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(window)] }) const payloads = [] - client.use(plugin, window) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) window.onerror('Uncaught Error: Bad things', 'foo.js', 10, 20, new Error('Bad things')) @@ -84,18 +84,16 @@ describe('plugin: window onerror', () => { it('calls any previously registered window.onerror callback', done => { window.onerror = () => done() - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(window)] }) const payloads = [] - client.use(plugin, window) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) window.onerror('Uncaught Error: Bad things', 'foo.js', 10, 20, new Error('Bad things')) }) it('handles single argument usage of window.onerror', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(window)] }) const payloads = [] - client.use(plugin, window) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) const evt = { type: 'error', detail: 'something bad happened' } @@ -111,9 +109,8 @@ describe('plugin: window onerror', () => { }) it('handles single argument usage of window.onerror with extra parameter', () => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(window)] }) const payloads = [] - client.use(plugin, window) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) // this situation is caused by the following kind of jQuery call: @@ -190,9 +187,8 @@ describe('plugin: window onerror', () => { // } it('extracts meaning from non-error values as error messages', function (done) { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(window)] }) const payloads = [] - client.use(plugin, window) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) // call onerror as it would be when `throw 'hello' is run` @@ -225,9 +221,8 @@ describe('plugin: window onerror', () => { expect(payloads.length).toBe(1) done() } - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(window)] }) const payloads = [] - client.use(plugin, window) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) // call onerror as it would be when `throw 'hello' is run` @@ -245,9 +240,8 @@ describe('plugin: window onerror', () => { expect(payloads.length).toBe(0) done() } - const client = new Client({ apiKey: 'API_KEY_YEAH' }) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [plugin(window)] }) const payloads = [] - client.use(plugin, window) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload) })) // call onerror as it would be when `throw 'hello' is run` diff --git a/packages/plugin-window-unhandled-rejection/test/unhandled-rejection.test.js b/packages/plugin-window-unhandled-rejection/test/unhandled-rejection.test.js index 4cdfc1047e..df0b63a774 100644 --- a/packages/plugin-window-unhandled-rejection/test/unhandled-rejection.test.js +++ b/packages/plugin-window-unhandled-rejection/test/unhandled-rejection.test.js @@ -17,15 +17,15 @@ const window = { describe('plugin: unhandled rejection', () => { describe('window.onunhandledrejection function', () => { it('captures unhandled promise rejections', done => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) - client.use(plugin, window) + const p = plugin(window) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [p] }) client._setDelivery(client => ({ sendEvent: (payload) => { const event = payload.events[0].toJSON() expect(event.severity).toBe('error') expect(event.unhandled).toBe(true) expect(event.severityReason).toEqual({ type: 'unhandledPromiseRejection' }) - plugin.destroy(window) + p.destroy(window) done() } })) @@ -35,8 +35,8 @@ describe('plugin: unhandled rejection', () => { }) it('handles bad user input', done => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) - client.use(plugin, window) + const p = plugin(window) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [p] }) client._setDelivery(client => ({ sendEvent: (payload) => { const event = payload.events[0].toJSON() @@ -46,7 +46,7 @@ describe('plugin: unhandled rejection', () => { expect(event.exceptions[0].message).toBe('unhandledrejection handler received a non-error. See "unhandledrejection handler" tab for more detail.') expect(event.severityReason).toEqual({ type: 'unhandledPromiseRejection' }) expect(event.metaData['unhandledrejection handler']['non-error parameter']).toEqual('null') - plugin.destroy(window) + p.destroy(window) done() } })) @@ -98,8 +98,8 @@ describe('plugin: unhandled rejection', () => { // }) it('handles errors with non-string stacks', done => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) - client.use(plugin, window) + const p = plugin(window) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [p] }) client._setDelivery(client => ({ sendEvent: (payload) => { const event = payload.events[0].toJSON() @@ -108,7 +108,7 @@ describe('plugin: unhandled rejection', () => { expect(event.exceptions[0].errorClass).toBe('Error') expect(event.exceptions[0].message).toBe('blah') expect(event.severityReason).toEqual({ type: 'unhandledPromiseRejection' }) - plugin.destroy(window) + p.destroy(window) done() } })) @@ -119,8 +119,8 @@ describe('plugin: unhandled rejection', () => { }) it('tolerates event.detail propties which throw', done => { - const client = new Client({ apiKey: 'API_KEY_YEAH' }) - client.use(plugin, window) + const p = plugin(window) + const client = new Client({ apiKey: 'API_KEY_YEAH', plugins: [p] }) client._setDelivery(client => ({ sendEvent: (payload) => { const event = payload.events[0].toJSON() @@ -129,7 +129,7 @@ describe('plugin: unhandled rejection', () => { expect(event.exceptions[0].errorClass).toBe('Error') expect(event.exceptions[0].message).toBe('blah') expect(event.severityReason).toEqual({ type: 'unhandledPromiseRejection' }) - plugin.destroy(window) + p.destroy(window) done() } })) @@ -147,9 +147,9 @@ describe('plugin: unhandled rejection', () => { addEventListener: () => {} } const addEventListenerSpy = spyOn(window, 'addEventListener') - const client = new Client({ apiKey: 'API_KEY_YEAH', autoDetectErrors: false }) - client.use(plugin, window) + const client = new Client({ apiKey: 'API_KEY_YEAH', autoDetectErrors: false, plugins: [plugin(window)] }) expect(addEventListenerSpy).toHaveBeenCalledTimes(0) + expect(client).toBe(client) }) it('is disabled when enabledErrorTypes.unhandledRejections=false', () => { @@ -159,10 +159,11 @@ describe('plugin: unhandled rejection', () => { const addEventListenerSpy = spyOn(window, 'addEventListener') const client = new Client({ apiKey: 'API_KEY_YEAH', - enabledErrorTypes: { unhandledExceptions: false, unhandledRejections: false } + enabledErrorTypes: { unhandledExceptions: false, unhandledRejections: false }, + plugins: [plugin(window)] }) - client.use(plugin, window) expect(addEventListenerSpy).toHaveBeenCalledTimes(0) + expect(client).toBe(client) }) }) }) diff --git a/packages/plugin-window-unhandled-rejection/unhandled-rejection.js b/packages/plugin-window-unhandled-rejection/unhandled-rejection.js index 4d2f1d21ad..f766ab2707 100644 --- a/packages/plugin-window-unhandled-rejection/unhandled-rejection.js +++ b/packages/plugin-window-unhandled-rejection/unhandled-rejection.js @@ -1,67 +1,73 @@ const map = require('@bugsnag/core/lib/es-utils/map') const isError = require('@bugsnag/core/lib/iserror') +let _listener /* * Automatically notifies Bugsnag when window.onunhandledrejection is called */ -let _listener -exports.init = (client, win = window) => { - if (!client._config.autoDetectErrors || !client._config.enabledErrorTypes.unhandledRejections) return - const listener = evt => { - let error = evt.reason - let isBluebird = false +module.exports = (win = window) => { + const plugin = { + load: (client) => { + if (!client._config.autoDetectErrors || !client._config.enabledErrorTypes.unhandledRejections) return + const listener = evt => { + let error = evt.reason + let isBluebird = false - // accessing properties on evt.detail can throw errors (see #394) - try { - if (evt.detail && evt.detail.reason) { - error = evt.detail.reason - isBluebird = true - } - } catch (e) {} + // accessing properties on evt.detail can throw errors (see #394) + try { + if (evt.detail && evt.detail.reason) { + error = evt.detail.reason + isBluebird = true + } + } catch (e) {} - const event = client.Event.create(error, false, { - severity: 'error', - unhandled: true, - severityReason: { type: 'unhandledPromiseRejection' } - }, 'unhandledrejection handler', 1, client._logger) + const event = client.Event.create(error, false, { + severity: 'error', + unhandled: true, + severityReason: { type: 'unhandledPromiseRejection' } + }, 'unhandledrejection handler', 1, client._logger) - if (isBluebird) { - map(event.errors[0].stacktrace, fixBluebirdStacktrace(error)) - } + if (isBluebird) { + map(event.errors[0].stacktrace, fixBluebirdStacktrace(error)) + } - client._notify(event, (event) => { - if (isError(event.originalError) && !event.originalError.stack) { - event.addMetadata('unhandledRejection handler', { - [Object.prototype.toString.call(event.originalError)]: { - name: event.originalError.name, - message: event.originalError.message, - code: event.originalError.code + client._notify(event, (event) => { + if (isError(event.originalError) && !event.originalError.stack) { + event.addMetadata('unhandledRejection handler', { + [Object.prototype.toString.call(event.originalError)]: { + name: event.originalError.name, + message: event.originalError.message, + code: event.originalError.code + } + }) } }) } - }) - } - if ('addEventListener' in win) { - win.addEventListener('unhandledrejection', listener) - } else { - win.onunhandledrejection = (reason, promise) => { - listener({ detail: { reason, promise } }) + if ('addEventListener' in win) { + win.addEventListener('unhandledrejection', listener) + } else { + win.onunhandledrejection = (reason, promise) => { + listener({ detail: { reason, promise } }) + } + } + _listener = listener } } - _listener = listener -} -if (process.env.NODE_ENV !== 'production') { - exports.destroy = (win = window) => { - if (_listener) { - if ('addEventListener' in win) { - win.removeEventListener('unhandledrejection', _listener) - } else { - win.onunhandledrejection = null + if (process.env.NODE_ENV !== 'production') { + plugin.destroy = (win = window) => { + if (_listener) { + if ('addEventListener' in win) { + win.removeEventListener('unhandledrejection', _listener) + } else { + win.onunhandledrejection = null + } } + _listener = null } - _listener = null } + + return plugin } // The stack parser on bluebird stacks in FF get a suprious first frame: diff --git a/test/browser/features/fixtures/plugin_react/webpack4/src/app.js b/test/browser/features/fixtures/plugin_react/webpack4/src/app.js index bb67d39682..0664f64968 100644 --- a/test/browser/features/fixtures/plugin_react/webpack4/src/app.js +++ b/test/browser/features/fixtures/plugin_react/webpack4/src/app.js @@ -1,11 +1,9 @@ var Bugsnag = require('@bugsnag/browser') -var bugsnagReact = require('@bugsnag/plugin-react') -var React = require('react') var ReactDOM = require('react-dom') +var React = require('react') var config = require('./lib/config') Bugsnag.start(config) -Bugsnag.use(bugsnagReact, React) var ErrorBoundary = Bugsnag.getPlugin('react') diff --git a/test/browser/features/fixtures/plugin_react/webpack4/src/lib/config.js b/test/browser/features/fixtures/plugin_react/webpack4/src/lib/config.js index 2ffdefdd51..f28950b68f 100644 --- a/test/browser/features/fixtures/plugin_react/webpack4/src/lib/config.js +++ b/test/browser/features/fixtures/plugin_react/webpack4/src/lib/config.js @@ -1,5 +1,9 @@ +var BugsnagReactPlugin = require('@bugsnag/plugin-react') +var React = require('react') + var ENDPOINT = decodeURIComponent(window.location.search.match(/ENDPOINT=([^&]+)/)[1]) var API_KEY = decodeURIComponent(window.location.search.match(/API_KEY=([^&]+)/)[1]) exports.apiKey = API_KEY -exports.endpoints = { notify: ENDPOINT, sessions: '/noop' } \ No newline at end of file +exports.endpoints = { notify: ENDPOINT, sessions: '/noop' } +exports.plugins = [new BugsnagReactPlugin(React)] diff --git a/test/browser/features/fixtures/plugin_vue/webpack4/src/app.js b/test/browser/features/fixtures/plugin_vue/webpack4/src/app.js index b8449f3260..b63b59bbf1 100644 --- a/test/browser/features/fixtures/plugin_vue/webpack4/src/app.js +++ b/test/browser/features/fixtures/plugin_vue/webpack4/src/app.js @@ -1,10 +1,8 @@ var Bugsnag = require('@bugsnag/browser') -var bugsnagVue = require('@bugsnag/plugin-vue') -var Vue = require('vue/dist/vue.common.js') var config = require('./lib/config') +var Vue = require('vue/dist/vue.common.js') Bugsnag.start(config) -Bugsnag.use(bugsnagVue, Vue) var app = new Vue({ el: '#app', diff --git a/test/browser/features/fixtures/plugin_vue/webpack4/src/lib/config.js b/test/browser/features/fixtures/plugin_vue/webpack4/src/lib/config.js index 505caa3d53..77c7308607 100644 --- a/test/browser/features/fixtures/plugin_vue/webpack4/src/lib/config.js +++ b/test/browser/features/fixtures/plugin_vue/webpack4/src/lib/config.js @@ -1,6 +1,9 @@ +var Vue = require('vue/dist/vue.common.js') +var BugsnagVuePlugin = require('@bugsnag/plugin-vue') var ENDPOINT = decodeURIComponent(window.location.search.match(/ENDPOINT=([^&]+)/)[1]) var API_KEY = decodeURIComponent(window.location.search.match(/API_KEY=([^&]+)/)[1]) exports.apiKey = API_KEY exports.endpoints = { notify: ENDPOINT, sessions: '/noop' } +exports.plugins = [new BugsnagVuePlugin(Vue)] diff --git a/test/node/features/fixtures/connect/scenarios/app.js b/test/node/features/fixtures/connect/scenarios/app.js index 8cc17ce6fd..f51562ef08 100644 --- a/test/node/features/fixtures/connect/scenarios/app.js +++ b/test/node/features/fixtures/connect/scenarios/app.js @@ -7,9 +7,9 @@ Bugsnag.start({ endpoints: { notify: process.env.BUGSNAG_NOTIFY_ENDPOINT, sessions: process.env.BUGSNAG_SESSIONS_ENDPOINT - } + }, + plugins: [bugsnagExpress] }) -Bugsnag.use(bugsnagExpress) var middleware = Bugsnag.getPlugin('express') diff --git a/test/node/features/fixtures/express/scenarios/app.js b/test/node/features/fixtures/express/scenarios/app.js index e473e0ad12..b230458ff6 100644 --- a/test/node/features/fixtures/express/scenarios/app.js +++ b/test/node/features/fixtures/express/scenarios/app.js @@ -7,10 +7,10 @@ Bugsnag.start({ endpoints: { notify: process.env.BUGSNAG_NOTIFY_ENDPOINT, sessions: process.env.BUGSNAG_SESSIONS_ENDPOINT - } + }, + plugins: [bugsnagExpress] }) -Bugsnag.use(bugsnagExpress) var middleware = Bugsnag.getPlugin('express') diff --git a/test/node/features/fixtures/koa-1x/scenarios/app.js b/test/node/features/fixtures/koa-1x/scenarios/app.js index 1d1b793229..2ecc9a8e05 100644 --- a/test/node/features/fixtures/koa-1x/scenarios/app.js +++ b/test/node/features/fixtures/koa-1x/scenarios/app.js @@ -7,10 +7,10 @@ Bugsnag.start({ endpoints: { notify: process.env.BUGSNAG_NOTIFY_ENDPOINT, sessions: process.env.BUGSNAG_SESSIONS_ENDPOINT - } + }, + plugins: [bugsnagKoa] }) -Bugsnag.use(bugsnagKoa) const middleware = Bugsnag.getPlugin('koa') diff --git a/test/node/features/fixtures/koa/scenarios/app.js b/test/node/features/fixtures/koa/scenarios/app.js index a82c9f6b6f..2aae3c6bf2 100644 --- a/test/node/features/fixtures/koa/scenarios/app.js +++ b/test/node/features/fixtures/koa/scenarios/app.js @@ -7,10 +7,10 @@ Bugsnag.start({ endpoints: { notify: process.env.BUGSNAG_NOTIFY_ENDPOINT, sessions: process.env.BUGSNAG_SESSIONS_ENDPOINT - } + }, + plugins: [bugsnagKoa] }) -Bugsnag.use(bugsnagKoa) const middleware = Bugsnag.getPlugin('koa') diff --git a/test/node/features/fixtures/restify/scenarios/app.js b/test/node/features/fixtures/restify/scenarios/app.js index 90da6044fe..9b7decf5b5 100644 --- a/test/node/features/fixtures/restify/scenarios/app.js +++ b/test/node/features/fixtures/restify/scenarios/app.js @@ -1,5 +1,5 @@ var Bugsnag = require('@bugsnag/node') -var bugsnagExpress = require('@bugsnag/plugin-restify') +var bugsnagRestify = require('@bugsnag/plugin-restify') var restify = require('restify') var errors = require('restify-errors') @@ -8,11 +8,10 @@ Bugsnag.start({ endpoints: { notify: process.env.BUGSNAG_NOTIFY_ENDPOINT, sessions: process.env.BUGSNAG_SESSIONS_ENDPOINT - } + }, + plugins: [bugsnagRestify] }) -Bugsnag.use(bugsnagExpress) - var middleware = Bugsnag.getPlugin('restify') var server = restify.createServer()