it('should add a stacktrace when missing', done => { const c = new Client(VALID_NOTIFIER) c.delivery(client => ({ sendReport: (report, cb) => { expect(report.events[0].errorMessage).toBe('ENOENT: no such file or directory, open \'does not exist\'') expect(report.events[0].stacktrace[0].file).toBe(`${__filename}`) cb(null) }, sendSession: () => {} })) c.setOptions({ apiKey: 'api_key', onUncaughtException: () => { done() } }) c.configure({ ...schema, onUncaughtException: { validate: val => typeof val === 'function', message: 'should be a function', defaultValue: () => {} } }) c.use(plugin) const contextualize = c.getPlugin('contextualize') contextualize(() => { fs.createReadStream('does not exist') }) })
it('should tolerate delivery errors', done => { const c = new Client(VALID_NOTIFIER) c.delivery(client => ({ sendReport: (...args) => args[args.length - 1](new Error('failed')), sendSession: (...args) => args[args.length - 1]() })) c.setOptions({ apiKey: 'api_key', onUncaughtException: (err, report) => { expect(err.message).toBe('never gonna catch me') expect(report.errorMessage).toBe('never gonna catch me') expect(report._handledState.unhandled).toBe(true) expect(report._handledState.severity).toBe('error') expect(report._handledState.severityReason).toEqual({ type: 'unhandledException' }) plugin.destroy() done() } }) c.configure({ ...schema, onUncaughtException: { validate: val => typeof val === 'function', message: 'should be a function', defaultValue: () => {} } }) c.use(plugin) process.listeners('uncaughtException')[1](new Error('never gonna catch me')) })
it('should tolerate a failed report', done => { const c = new Client(VALID_NOTIFIER) c.delivery(client => ({ sendReport: (report, cb) => { cb(new Error('sending failed')) }, sendSession: () => {} })) c.setOptions({ apiKey: 'api_key', onUncaughtException: (err) => { expect(err.message).toBe('no item available') done() } }) c.configure({ ...schema, onUncaughtException: { validate: val => typeof val === 'function', message: 'should be a function', defaultValue: () => {} } }) c.use(plugin) const contextualize = c.getPlugin('contextualize') contextualize(() => { load(8, (err) => { if (err) throw err }) }) })
it('tracks handled/unhandled error counts and sends them in error payloads', (done) => { const c = new Client(VALID_NOTIFIER) c.setOptions({ apiKey: 'API_KEY' }) c.configure() let i = 0 c.use(plugin) c.delivery(client => ({ sendSession: () => {}, sendReport: (report, cb) => { if (++i < 10) return const r = JSON.parse(JSON.stringify(report.events[0])) expect(r.session).toBeDefined() expect(r.session.events.handled).toBe(6) expect(r.session.events.unhandled).toBe(4) done() } })) const sessionClient = c.startSession() sessionClient.notify(new Error('broke')) sessionClient.notify(new c.BugsnagReport('err', 'bad', [], { unhandled: true, severity: 'error', severityReason: { type: 'unhandledException' } })) sessionClient.notify(new Error('broke')) sessionClient.notify(new Error('broke')) sessionClient.notify(new c.BugsnagReport('err', 'bad', [], { unhandled: true, severity: 'error', severityReason: { type: 'unhandledException' } })) sessionClient.notify(new Error('broke')) sessionClient.notify(new Error('broke')) sessionClient.notify(new Error('broke')) sessionClient.notify(new c.BugsnagReport('err', 'bad', [], { unhandled: true, severity: 'error', severityReason: { type: 'unhandledException' } })) sessionClient.notify(new c.BugsnagReport('err', 'bad', [], { unhandled: true, severity: 'error', severityReason: { type: 'unhandledException' } })) })
it('doesn’t send when releaseStage is not in notifyReleaseStages', (done) => { const c = new Client(VALID_NOTIFIER) c.setOptions({ apiKey: 'API_KEY', releaseStage: 'foo', notifyReleaseStages: [ 'baz' ] }) c.configure() c.use(plugin) c.delivery(client => ({ sendSession: (session, cb) => { expect(true).toBe(false) } })) c.startSession() setTimeout(done, 150) })
it('sets report.request to window.location.href', () => { const client = new Client(VALID_NOTIFIER) const payloads = [] client.setOptions({ apiKey: 'API_KEY_YEAH' }) client.configure() client.use(plugin, window) client.delivery(client => ({ sendReport: (payload) => payloads.push(payload) })) client.notify(new Error('noooo')) expect(payloads.length).toEqual(1) expect(payloads[0].events[0].request).toEqual({ url: window.location.href }) })
it('correctly infers releaseStage', (done) => { const c = new Client(VALID_NOTIFIER) c.setOptions({ apiKey: 'API_KEY', releaseStage: 'foo' }) c.configure() c.use(plugin) c.delivery(client => ({ sendSession: (session, cb) => { expect(typeof session).toBe('object') expect(session.app.releaseStage).toBe('foo') done() } })) c.startSession() })
it('redacts user IP if none is provided', () => { const client = new Client(VALID_NOTIFIER) const payloads = [] client.setOptions({ apiKey: 'API_KEY_YEAH', collectUserIp: false }) client.configure() client.use(plugin) client.delivery(client => ({ sendReport: (payload) => payloads.push(payload) })) client.notify(new Error('noooo')) expect(payloads.length).toEqual(1) expect(payloads[0].events[0].user).toEqual({ id: '[NOT COLLECTED]' }) expect(payloads[0].events[0].request).toEqual({ clientIp: '[NOT COLLECTED]' }) })
it('does nothing when collectUserIp=true', () => { const client = new Client(VALID_NOTIFIER) const payloads = [] client.setOptions({ apiKey: 'API_KEY_YEAH' }) client.configure() client.use(plugin) client.delivery(client => ({ sendReport: (payload) => payloads.push(payload) })) client.notify(new Error('noooo'), { beforeSend: report => { report.request = { 'some': 'detail' } } }) expect(payloads.length).toEqual(1) expect(payloads[0].events[0].request).toEqual({ 'some': 'detail' }) })
it('sets doesn’t overwrite an existing request', () => { const client = new Client(VALID_NOTIFIER) const payloads = [] client.setOptions({ apiKey: 'API_KEY_YEAH' }) client.configure() client.use(plugin, window) client.request = { url: 'foobar' } client.delivery(client => ({ sendReport: (payload) => payloads.push(payload) })) client.notify(new Error('noooo')) expect(payloads.length).toEqual(1) expect(payloads[0].events[0].request).toEqual({ url: 'foobar' }) })
it('overwrites a user id if it is explicitly `undefined`', () => { const client = new Client(VALID_NOTIFIER) const payloads = [] client.setOptions({ apiKey: 'API_KEY_YEAH', collectUserIp: false }) client.configure() client.use(plugin) client.user = { id: undefined } client.delivery(client => ({ sendReport: (payload) => payloads.push(payload) })) client.notify(new Error('noooo')) expect(payloads.length).toEqual(1) expect(payloads[0].events[0].user).toEqual({ id: '[NOT COLLECTED]' }) expect(payloads[0].events[0].request).toEqual({ clientIp: '[NOT COLLECTED]' }) })
it('should call the onUnhandledException callback when an error is captured', done => { const c = new Client(VALID_NOTIFIER) c.delivery(client => ({ sendReport: (report, cb) => { expect(report.events[0].errorMessage).toBe('no item available') expect(report.events[0].severity).toBe('warning') expect(report.events[0].user).toEqual({ id: '1a2c3cd4', name: 'Ben Gourley', email: '*****@*****.**' }) cb(null) }, sendSession: () => {} })) c.setOptions({ apiKey: 'api_key', onUncaughtException: (err) => { expect(err.message).toBe('no item available') done() } }) c.configure({ ...schema, onUncaughtException: { validate: val => typeof val === 'function', message: 'should be a function', defaultValue: () => {} } }) c.use(plugin) const contextualize = c.getPlugin('contextualize') contextualize(() => { load(8, (err) => { if (err) throw err }) }, { user: { id: '1a2c3cd4', name: 'Ben Gourley', email: '*****@*****.**' }, severity: 'warning' }) })
it('notifies the session endpoint', (done) => { const c = new Client(VALID_NOTIFIER) c.setOptions({ apiKey: 'API_KEY' }) c.configure() c.use(plugin) c.delivery(client => ({ sendSession: (session, cb) => { expect(typeof session).toBe('object') expect(session.notifier).toEqual(VALID_NOTIFIER) expect(session.sessions.length).toBe(1) expect(session.sessions[0].id).toBeTruthy() expect(session.sessions[0].id.length).toBeGreaterThan(10) expect(session.sessions[0].startedAt).toBeTruthy() done() } })) c.startSession() })
module.exports = (opts) => { // handle very simple use case where user supplies just the api key as a string if (typeof opts === 'string') opts = { apiKey: opts } // ensure opts is actually an object (at this point it // could be null, undefined, a number, a boolean etc.) opts = { ...opts } // attempt to fetch apiKey from app.json if we didn't get one explicitly passed if (!opts.apiKey && Constants.manifest && Constants.manifest.extra && Constants.manifest.extra.bugsnag && Constants.manifest.extra.bugsnag.apiKey) { opts.apiKey = Constants.manifest.extra.bugsnag.apiKey } const bugsnag = new Client({ name, version, url }) bugsnag.delivery(delivery) bugsnag.setOptions(opts) bugsnag.configure(schema) plugins.forEach(pl => { switch (pl.name) { case 'networkBreadcrumbs': bugsnag.use(pl, () => [ bugsnag.config.endpoints.notify, bugsnag.config.endpoints.sessions, Constants.manifest.logUrl ]) break default: bugsnag.use(pl) } }) bugsnag.use(bugsnagReact, React) bugsnag._logger.debug(`Loaded!`) return bugsnag.config.autoCaptureSessions ? bugsnag.startSession() : bugsnag }
it('should not remove a matching substring if it is not at the start', done => { const client = new Client(VALID_NOTIFIER) client.delivery(client => ({ sendReport: (report) => { const evt = report.events[0] expect(evt.stacktrace[0].file).toBe(join('/var', 'lib', '01.js')) expect(evt.stacktrace[1].file).toBe(join('/foo', 'lib', '02.js')) expect(evt.stacktrace[2].file).toBe(join('/tmp', 'lib', '03.js')) done() }, sendSession: () => {} })) client.setOptions({ apiKey: 'api_key', projectRoot: '/app' }) client.configure({ ...schema, projectRoot: { validate: () => true, defaultValue: () => '', message: '' } }) client.use(plugin) client.notify(new Report('Error', 'strip project root test', [ { lineNumber: 22, columnNumber: 18, fileName: join('/var', 'lib', '01.js') }, { lineNumber: 31, columnNumber: 7, fileName: join('/foo', 'lib', '02.js') }, { lineNumber: 118, columnNumber: 28, fileName: join('/tmp', 'lib', '03.js') } ])) })
it('should tolerate stackframe.file not being a string', done => { const client = new Client(VALID_NOTIFIER) client.delivery(client => ({ sendReport: (report) => { const evt = report.events[0] expect(evt.stacktrace[0].file).toBe('global code') expect(evt.stacktrace[1].file).toBe('global code') expect(evt.stacktrace[2].file).toEqual({}) done() }, sendSession: () => {} })) client.setOptions({ apiKey: 'api_key', projectRoot: '/app' }) client.configure({ ...schema, projectRoot: { validate: () => true, defaultValue: () => '', message: '' } }) client.use(plugin) client.notify(new Report('Error', 'strip project root test', [ { lineNumber: 22, columnNumber: 18, fileName: undefined }, { lineNumber: 31, columnNumber: 7, fileName: null }, { lineNumber: 31, columnNumber: 7, fileName: {} } ])) })
it('logs a warning when no session endpoint is set', (done) => { const c = new Client(VALID_NOTIFIER) c.setOptions({ apiKey: 'API_KEY', releaseStage: 'foo', endpoints: { notify: '/foo' }, autoCaptureSessions: false }) c.configure() c.use(plugin) c.logger({ warn: msg => { expect(msg).toMatch(/session not sent/i) done() } }) c.delivery(client => ({ sendSession: (session, cb) => { expect(true).toBe(false) } })) c.startSession() })
it('should work with node_modules and node internals', done => { const client = new Client(VALID_NOTIFIER) client.delivery(client => ({ sendReport: (report) => { const evt = report.events[0] expect(evt.stacktrace[0].file).toBe('_module.js') expect(evt.stacktrace[1].file).toBe(join('node_modules', 'bugsnag-example', 'index.js')) done() }, sendSession: () => {} })) client.setOptions({ apiKey: 'api_key', projectRoot: '/app' }) client.configure({ ...schema, projectRoot: { validate: () => true, defaultValue: () => '', message: '' } }) client.use(plugin) client.notify(new Report('Error', 'strip project root test', [ { lineNumber: 22, columnNumber: 18, fileName: '_module.js' }, { lineNumber: 31, columnNumber: 7, fileName: join('/app', 'node_modules', 'bugsnag-example', 'index.js') } ])) })