diff --git a/CHANGELOG.md b/CHANGELOG.md index 3df8a6ea11..9d7e88c3d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## TBD +- (plugin-network-breadcrumbs): Fixes the `window.fetch` monkey-patch to also accept `Request`. [#662](https://github.com/bugsnag/bugsnag-js/pull/662) + ## 6.4.3 (2019-10-21) ### Fixed diff --git a/packages/plugin-network-breadcrumbs/network-breadcrumbs.js b/packages/plugin-network-breadcrumbs/network-breadcrumbs.js index 1915b05ec2..e20836074a 100644 --- a/packages/plugin-network-breadcrumbs/network-breadcrumbs.js +++ b/packages/plugin-network-breadcrumbs/network-breadcrumbs.js @@ -119,15 +119,34 @@ const monkeyPatchFetch = () => { if (!('fetch' in win) || win.fetch.polyfill) return const oldFetch = win.fetch - win.fetch = function fetch (...args) { - let [url, options] = args - let method = 'GET' - if (options && options.method) { - method = options.method + 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(...args) + oldFetch(...arguments) .then(response => { handleFetchSuccess(response, method, url) resolve(response) diff --git a/packages/plugin-network-breadcrumbs/test/network-breadcrumbs.test.js b/packages/plugin-network-breadcrumbs/test/network-breadcrumbs.test.js index 9b7074daf6..e3fb7e35bb 100644 --- a/packages/plugin-network-breadcrumbs/test/network-breadcrumbs.test.js +++ b/packages/plugin-network-breadcrumbs/test/network-breadcrumbs.test.js @@ -27,6 +27,7 @@ XMLHttpRequest.prototype.removeEventListener = function (evt, listener) { if (listener === this._listeners[evt]) delete this._listeners[evt] } +// mock fetch function fetch (url, options, fail, status) { return new Promise((resolve, reject) => { if (fail) { @@ -37,6 +38,12 @@ function fetch (url, options, fail, status) { }) } +// mock (fetch) Request +function Request (url, opts) { + this.url = url + this.method = (opts && opts.method) || 'GET' +} + describe('plugin: network breadcrumbs', () => { afterEach(() => { // undo the global side effects @@ -179,6 +186,208 @@ describe('plugin: network breadcrumbs', () => { }) }) + it('should handle a fetch(url, { method: null })', (done) => { + const window = { XMLHttpRequest, fetch } + + const client = new Client(VALID_NOTIFIER) + client.setOptions({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) + client.configure() + client.use(plugin, () => [], window) + + window.fetch('/', { method: null }, false, 405).then(() => { + expect(client.breadcrumbs.length).toBe(1) + expect(client.breadcrumbs[0]).toEqual(jasmine.objectContaining({ + type: 'request', + name: 'fetch() failed', + metaData: { + status: 405, + request: 'null /' + } + })) + done() + }) + }) + + it('should handle a fetch() request supplied with a Request object', (done) => { + const window = { XMLHttpRequest, fetch } + + const client = new Client(VALID_NOTIFIER) + client.setOptions({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) + client.configure() + client.use(plugin, () => [], window) + + const request = new Request('/') + + window.fetch(request, {}, false, 200).then(() => { + expect(client.breadcrumbs.length).toBe(1) + expect(client.breadcrumbs[0]).toEqual(jasmine.objectContaining({ + type: 'request', + name: 'fetch() succeeded', + metaData: { + status: 200, + request: 'GET /' + } + })) + done() + }) + }) + + 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(VALID_NOTIFIER) + client.setOptions({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) + client.configure() + client.use(plugin, () => [], window) + + const request = new Request('/', { method: 'PUT' }) + + window.fetch(request, {}, false, 200).then(() => { + expect(client.breadcrumbs.length).toBe(1) + expect(client.breadcrumbs[0]).toEqual(jasmine.objectContaining({ + type: 'request', + name: 'fetch() succeeded', + metaData: { + status: 200, + request: 'PUT /' + } + })) + done() + }) + }) + + it('should handle fetch(null)', (done) => { + const window = { XMLHttpRequest, fetch } + + const client = new Client(VALID_NOTIFIER) + client.setOptions({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) + client.configure() + client.use(plugin, () => [], window) + + window.fetch(null, {}, false, 404).then(() => { + expect(client.breadcrumbs.length).toBe(1) + expect(client.breadcrumbs[0]).toEqual(jasmine.objectContaining({ + type: 'request', + name: 'fetch() failed', + metaData: { + status: 404, + request: 'GET null' + } + })) + done() + }) + }) + + it('should handle fetch(url, null)', (done) => { + const window = { XMLHttpRequest, fetch } + + const client = new Client(VALID_NOTIFIER) + client.setOptions({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) + client.configure() + client.use(plugin, () => [], window) + + window.fetch('/', null, false, 200).then(() => { + expect(client.breadcrumbs.length).toBe(1) + expect(client.breadcrumbs[0]).toEqual(jasmine.objectContaining({ + type: 'request', + name: 'fetch() succeeded', + metaData: { + status: 200, + request: 'GET /' + } + })) + done() + }) + }) + + it('should handle fetch(undefined)', (done) => { + const window = { XMLHttpRequest, fetch } + + const client = new Client(VALID_NOTIFIER) + client.setOptions({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) + client.configure() + client.use(plugin, () => [], window) + + window.fetch(undefined, {}, false, 404).then(() => { + expect(client.breadcrumbs.length).toBe(1) + expect(client.breadcrumbs[0]).toEqual(jasmine.objectContaining({ + type: 'request', + name: 'fetch() failed', + metaData: { + status: 404, + request: 'GET undefined' + } + })) + done() + }) + }) + + it('should handle a fetch(request, { method })', (done) => { + const window = { XMLHttpRequest, fetch } + + const client = new Client(VALID_NOTIFIER) + client.setOptions({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) + client.configure() + client.use(plugin, () => [], window) + + window.fetch(new Request('/foo', { method: 'GET' }), { method: 'PUT' }, false, 200).then(() => { + expect(client.breadcrumbs.length).toBe(1) + expect(client.breadcrumbs[0]).toEqual(jasmine.objectContaining({ + type: 'request', + name: 'fetch() succeeded', + metaData: { + status: 200, + request: 'PUT /foo' + } + })) + done() + }) + }) + + it('should handle a fetch(request, { method: null })', (done) => { + const window = { XMLHttpRequest, fetch } + + const client = new Client(VALID_NOTIFIER) + client.setOptions({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) + client.configure() + client.use(plugin, () => [], window) + + window.fetch(new Request('/foo'), { method: null }, false, 405).then(() => { + expect(client.breadcrumbs.length).toBe(1) + expect(client.breadcrumbs[0]).toEqual(jasmine.objectContaining({ + type: 'request', + name: 'fetch() failed', + metaData: { + status: 405, + request: 'null /foo' + } + })) + done() + }) + }) + + it('should handle a fetch(request, { method: undefined })', (done) => { + const window = { XMLHttpRequest, fetch } + + const client = new Client(VALID_NOTIFIER) + client.setOptions({ apiKey: 'aaaa-aaaa-aaaa-aaaa' }) + client.configure() + client.use(plugin, () => [], window) + + window.fetch(new Request('/foo'), { method: undefined }, false, 200).then(() => { + expect(client.breadcrumbs.length).toBe(1) + expect(client.breadcrumbs[0]).toEqual(jasmine.objectContaining({ + type: 'request', + name: 'fetch() succeeded', + metaData: { + status: 200, + request: 'GET /foo' + } + })) + done() + }) + }) + it('should leave a breadcrumb when a fetch() has a failed response', (done) => { const window = { XMLHttpRequest, fetch }