8000 feat(browser): Add new v7 XHR Transport (#4803) · michax/sentry-javascript@fa58281 · GitHub
[go: up one dir, main page]

Skip to content

Commit fa58281

Browse files
authored
feat(browser): Add new v7 XHR Transport (getsentry#4803)
* add the new XHR Transport creation functionality. The function makeNewXHRTransport(...) creates a Transport that is based on the browser's XMLHttpRequest API. It is used as a fallback if the Fetch API is not available (IE11...). The creation function is similar to the new Fetch Transport creation introduced in getsentry#4765. * in addition to the transport creation function, this PR also adds tests which verify the correct calls to the XMLHttpRequest API. Furthermore, the tests check for correct request/response header setting.
1 parent 0cf3014 commit fa58281

File tree

4 files changed

+176
-1
lines changed

4 files changed

+176
-1
lines changed

packages/browser/src/backend.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Event, EventHint, Options, Severity, Transport, TransportOptions } from
33
import { supportsFetch } from '@sentry/utils';
44

55
import { eventFromException, eventFromMessage } from './eventbuilder';
6-
import { FetchTransport, makeNewFetchTransport, XHRTransport } from './transports';
6+
import { FetchTransport, makeNewFetchTransport, makeNewXHRTransport, XHRTransport } from './transports';
77

88
/**
99
* Configuration options for the Sentry Browser SDK.
@@ -77,6 +77,11 @@ export class BrowserBackend extends BaseBackend<BrowserOptions> {
7777
this._newTransport = makeNewFetchTransport({ requestOptions, url });
7878
return new FetchTransport(transportOptions);
7979
}
80+
81+
this._newTransport = makeNewXHRTransport({
82+
url,
83+
headers: transportOptions.headers,
84+
});
8085
return new XHRTransport(transportOptions);
8186
}
8287
}

packages/browser/src/transports/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { FetchTransport } from './fetch';
33
export { XHRTransport } from './xhr';
44

55
export { makeNewFetchTransport } from './new-fetch';
6+
export { makeNewXHRTransport } from './new-xhr';
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {
2+
BaseTransportOptions,
3+
createTransport,
4+
NewTransport,
5+
TransportMakeRequestResponse,
6+
TransportRequest,
7+
} from '@sentry/core';
8+
import { SyncPromise } from '@sentry/utils';
9+
10+
/**
11+
* The DONE ready state for XmlHttpRequest
12+
*
13+
* Defining it here as a constant b/c XMLHttpRequest.DONE is not always defined
14+
* (e.g. during testing, it is `undefined`)
15+
*
16+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState}
17+
*/
18+
const XHR_READYSTATE_DONE = 4;
19+
20+
export interface XHRTransportOptions extends BaseTransportOptions {
21+
headers?: { [key: string]: string };
22+
}
23+
24+
/**
25+
* Creates a Transport that uses the XMLHttpRequest API to send events to Sentry.
26+
*/
27+
export function makeNewXHRTransport(options: XHRTransportOptions): NewTransport {
28+
function makeRequest(request: TransportRequest): PromiseLike<TransportMakeRequestResponse> {
29+
return new SyncPromise<TransportMakeRequestResponse>((resolve, _reject) => {
30+
const xhr = new XMLHttpRequest();
31+
32+
xhr.onreadystatechange = (): void => {
33+
if (xhr.readyState === XHR_READYSTATE_DONE) {
34+
const response = {
35+
body: xhr.response,
36+
headers: {
37+
'x-sentry-rate-limits': xhr.getResponseHeader('X-Sentry-Rate-Limits'),
38+
'retry-after': xhr.getResponseHeader('Retry-After'),
39+
},
40+
reason: xhr.statusText,
41+
statusCode: xhr.status,
42+
};
43+
resolve(response);
44+
}
45+
};
46+
47+
xhr.open('POST', options.url);
48+
49+
for (const header in options.headers) {
50+
if (Object.prototype.hasOwnProperty.call(options.headers, header)) {
51+
xhr.setRequestHeader(header, options.headers[header]);
52+
}
53+
}
54+
55+
xhr.send(request.body);
56+
});
57+
}
58+
59+
return createTransport({ bufferSize: options.bufferSize }, makeRequest);
60+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { EventEnvelope, EventItem } from '@sentry/types';
2+
import { createEnvelope, serializeEnvelope } from '@sentry/utils';
3+
4+
import { makeNewXHRTransport, XHRTransportOptions } from '../../../src/transports/new-xhr';
5+
6+
const DEFAULT_XHR_TRANSPORT_OPTIONS: XHRTransportOptions = {
7+
url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7',
8+
};
9+
10+
const ERROR_ENVELOPE = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [
11+
[{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem,
12+
]);
13+
14+
function createXHRMock() {
15+
const retryAfterSeconds = 10;
16+
17+
const xhrMock: Partial<XMLHttpRequest> = {
18+
open: jest.fn(),
19+
send: jest.fn(),
20+
setRequestHeader: jest.fn(),
21+
readyState: 4,
22+
status: 200,
23+
response: 'Hello World!',
24+
onreadystatechange: () => {},
25+
getResponseHeader: jest.fn((header: string) => {
26+
switch (header) {
27+
case 'Retry-After':
28+
return '10';
29+
case `${retryAfterSeconds}`:
30+
return;
31+
default:
32+
return `${retryAfterSeconds}:error:scope`;
33+
}
34+
}),
35+
};
36+
37+
// casting `window` as `any` because XMLHttpRequest is missing in Window (TS-only)
38+
jest.spyOn(window as any, 'XMLHttpRequest').mockImplementation(() => xhrMock as XMLHttpRequest);
39+
40+
return xhrMock;
41+
}
42+
43+
describe('NewXHRTransport', () => {
44+
const xhrMock: Partial<XMLHttpRequest> = createXHRMock();
45+
46+
afterEach(() => {
47+
jest.clearAllMocks();
48+
});
49+
50+
afterAll(() => {
51+
jest.restoreAllMocks();
52+
});
53+
54+
it('makes an XHR request to the given URL', async () => {
55+
const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS);
56+
expect(xhrMock.open).toHaveBeenCalledTimes(0);
57+
expect(xhrMock.setRequestHeader).toHaveBeenCalledTimes(0);
58+
expect(xhrMock.send).toHaveBeenCalledTimes(0);
59+
60+
await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange(null)]);
61+
62+
expect(xhrMock.open).toHaveBeenCalledTimes(1);
63+
expect(xhrMock.open).toHaveBeenCalledWith('POST', DEFAULT_XHR_TRANSPORT_OPTIONS.url);
64+
expect(xhrMock.send).toHaveBeenCalledTimes(1);
65+
expect(xhrMock.send).toHaveBeenCalledWith(serializeEnvelope(ERROR_ENVELOPE));
66+
});
67+
68+
it('returns the correct response', async () => {
69+
const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS);
70+
71+
const [res] = await Promise.all([
72+
transport.send(ERROR_ENVELOPE),
73+
(xhrMock as XMLHttpRequest).onreadystatechange(null),
74+
]);
75+
76+
expect(res).toBeDefined();
77+
expect(res.status).toEqual('success');
78+
});
79+
80+
it('sets rate limit response headers', async () => {
81+
const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS);
82+
83+
await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange(null)]);
84+
85+
expect(xhrMock.getResponseHeader).toHaveBeenCalledTimes(2);
86+
expect(xhrMock.getResponseHeader).toHaveBeenCalledWith('X-Sentry-Rate-Limits');
87+
expect(xhrMock.getResponseHeader).toHaveBeenCalledWith('Retry-After');
88+
});
89+
90+
it('sets custom request headers', async () => {
91+
const headers = {
92+
referrerPolicy: 'strict-origin',
93+
keepalive: 'true',
94+
referrer: 'http://example.org',
95+
};
96+
const options: XHRTransportOptions = {
97+
...DEFAULT_XHR_TRANSPORT_OPTIONS,
98+
headers,
99+
};
100+
101+
const transport = makeNewXHRTransport(options);
102+
await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange(null)]);
103+
104+
expect(xhrMock.setRequestHeader).toHaveBeenCalledTimes(3);
105+
expect(xhrMock.setRequestHeader).toHaveBeenCalledWith('referrerPolicy', headers.referrerPolicy);
106+
expect(xhrMock.setRequestHeader).toHaveBeenCalledWith('keepalive', headers.keepalive);
107+
expect(xhrMock.setRequestHeader).toHaveBeenCalledWith('referrer', headers.referrer);
108+
});
109+
});

0 commit comments

Comments
 (0)
0