diff --git a/packages/browser/src/transports/new-xhr.ts b/packages/browser/src/transports/new-xhr.ts index 7373b946a505..7f9cad6cdfa1 100644 --- a/packages/browser/src/transports/new-xhr.ts +++ b/packages/browser/src/transports/new-xhr.ts @@ -7,9 +7,17 @@ import { } from '@sentry/core'; import { SyncPromise } from '@sentry/utils'; +/** + * The DONE ready state for XmlHttpRequest + * + * Defining it here as a constant b/c XMLHttpRequest.DONE is not always defined + * (e.g. during testing, it is `undefined`) + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState} + */ +const XHR_READYSTATE_DONE = 4; + export interface XHRTransportOptions extends BaseTransportOptions { - // TODO choose whatever is preferred here (I like record more for easier readability) - //headers?: { [key: string]: string }; headers?: Record; } @@ -22,8 +30,7 @@ export function makeNewXHRTransport(options: XHRTransportOptions): NewTransport const xhr = new XMLHttpRequest(); xhr.onreadystatechange = (): void => { - // TODO make 4 a constant - if (xhr.readyState === 4) { + if (xhr.readyState === XHR_READYSTATE_DONE) { const response = { body: xhr.response, headers: { @@ -33,17 +40,18 @@ export function makeNewXHRTransport(options: XHRTransportOptions): NewTransport reason: xhr.statusText, statusCode: xhr.status, }; - resolve(response); } }; xhr.open('POST', options.url); + for (const header in options.headers) { if (Object.prototype.hasOwnProperty.call(options.headers, header)) { xhr.setRequestHeader(header, options.headers[header]); } } + xhr.send(request.body); }); } diff --git a/packages/browser/test/unit/transports/new-xhr.test.ts b/packages/browser/test/unit/transports/new-xhr.test.ts index 4deee14f3d46..a7c67e7b2396 100644 --- a/packages/browser/test/unit/transports/new-xhr.test.ts +++ b/packages/browser/test/unit/transports/new-xhr.test.ts @@ -21,7 +21,7 @@ function createXHRMock() { status: 200, response: 'Hello World!', onreadystatechange: () => {}, - getResponseHeader: (header: string) => { + getResponseHeader: jest.fn((header: string) => { switch (header) { case 'Retry-After': return '10'; @@ -30,7 +30,7 @@ function createXHRMock() { default: return `${retryAfterSeconds}:error:scope`; } - }, + }), }; // casting `window` as `any` because XMLHttpRequest is missing in Window (TS-only) @@ -56,16 +56,53 @@ describe('NewXHRTransport', () => { expect(xhrMock.setRequestHeader).toHaveBeenCalledTimes(0); expect(xhrMock.send).toHaveBeenCalledTimes(0); + await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange(null)]); + + expect(xhrMock.open).toHaveBeenCalledTimes(1); + expect(xhrMock.open).toHaveBeenCalledWith('POST', DEFAULT_XHR_TRANSPORT_OPTIONS.url); + expect(xhrMock.send).toHaveBeenCalledTimes(1); + expect(xhrMock.send).toHaveBeenCalledWith(serializeEnvelope(ERROR_ENVELOPE)); + }); + + it('returns the correct response', async () => { + const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); + const [res] = await Promise.all([ transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange(null), ]); - expect(xhrMock.open).toHaveBeenCalledTimes(1); - expect(xhrMock.open).toHaveBeenCalledWith('POST', DEFAULT_XHR_TRANSPORT_OPTIONS.url); - expect(xhrMock.send).toBeCalledWith(serializeEnvelope(ERROR_ENVELOPE)); - expect(res).toBeDefined(); expect(res.status).toEqual('success'); }); + + it('sets rate limit response headers', async () => { + const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); + + await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange(null)]); + + expect(xhrMock.getResponseHeader).toHaveBeenCalledTimes(2); + expect(xhrMock.getResponseHeader).toHaveBeenCalledWith('X-Sentry-Rate-Limits'); + expect(xhrMock.getResponseHeader).toHaveBeenCalledWith('Retry-After'); + }); + + it('sets custom request headers', async () => { + const headers = { + referrerPolicy: 'strict-origin', + keepalive: 'true', + referrer: 'http://example.org', + }; + const options: XHRTransportOptions = { + ...DEFAULT_XHR_TRANSPORT_OPTIONS, + headers, + }; + + const transport = makeNewXHRTransport(options); + await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange(null)]); + + expect(xhrMock.setRequestHeader).toHaveBeenCalledTimes(3); + expect(xhrMock.setRequestHeader).toHaveBeenCalledWith('referrerPolicy', headers.referrerPolicy); + expect(xhrMock.setRequestHeader).toHaveBeenCalledWith('keepalive', headers.keepalive); + expect(xhrMock.setRequestHeader).toHaveBeenCalledWith('referrer', headers.referrer); + }); });