-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(browser): Add new v7 XHR Transport (#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 #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.
- Loading branch information
Showing
4 changed files
with
176 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { | ||
BaseTransportOptions, | ||
createTransport, | ||
NewTransport, | ||
TransportMakeRequestResponse, | ||
TransportRequest, | ||
} 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 { | ||
headers?: { [key: string]: string }; | ||
} | ||
|
||
/** | ||
* Creates a Transport that uses the XMLHttpRequest API to send events to Sentry. | ||
*/ | ||
export function makeNewXHRTransport(options: XHRTransportOptions): NewTransport { | ||
function makeRequest(request: TransportRequest): PromiseLike<TransportMakeRequestResponse> { | ||
return new SyncPromise<TransportMakeRequestResponse>((resolve, _reject) => { | ||
const xhr = new XMLHttpRequest(); | ||
|
||
xhr.onreadystatechange = (): void => { | ||
if (xhr.readyState === XHR_READYSTATE_DONE) { | ||
const response = { | ||
body: xhr.response, | ||
headers: { | ||
'x-sentry-rate-limits': xhr.getResponseHeader('X-Sentry-Rate-Limits'), | ||
'retry-after': xhr.getResponseHeader('Retry-After'), | ||
}, | ||
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); | ||
}); | ||
} | ||
|
||
return createTransport({ bufferSize: options.bufferSize }, makeRequest); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { EventEnvelope, EventItem } from '@sentry/types'; | ||
import { createEnvelope, serializeEnvelope } from '@sentry/utils'; | ||
|
||
import { makeNewXHRTransport, XHRTransportOptions } from '../../../src/transports/new-xhr'; | ||
|
||
const DEFAULT_XHR_TRANSPORT_OPTIONS: XHRTransportOptions = { | ||
url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', | ||
}; | ||
|
||
const ERROR_ENVELOPE = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ | ||
[{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, | ||
]); | ||
|
||
function createXHRMock() { | ||
const retryAfterSeconds = 10; | ||
|
||
const xhrMock: Partial<XMLHttpRequest> = { | ||
open: jest.fn(), | ||
send: jest.fn(), | ||
setRequestHeader: jest.fn(), | ||
readyState: 4, | ||
status: 200, | ||
response: 'Hello World!', | ||
onreadystatechange: () => {}, | ||
getResponseHeader: jest.fn((header: string) => { | ||
switch (header) { | ||
case 'Retry-After': | ||
return '10'; | ||
case `${retryAfterSeconds}`: | ||
return; | ||
default: | ||
return `${retryAfterSeconds}:error:scope`; | ||
} | ||
}), | ||
}; | ||
|
||
// casting `window` as `any` because XMLHttpRequest is missing in Window (TS-only) | ||
jest.spyOn(window as any, 'XMLHttpRequest').mockImplementation(() => xhrMock as XMLHttpRequest); | ||
|
||
return xhrMock; | ||
} | ||
|
||
describe('NewXHRTransport', () => { | ||
const xhrMock: Partial<XMLHttpRequest> = createXHRMock(); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.restoreAllMocks(); | ||
}); | ||
|
||
it('makes an XHR request to the given URL', async () => { | ||
const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); | ||
expect(xhrMock.open).toHaveBeenCalledTimes(0); | ||
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(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); | ||
}); | ||
}); |