Skip to content

Commit

Permalink
fix(replay): Capture JSON XHR response bodies
Browse files Browse the repository at this point in the history
  • Loading branch information
mydea committed Nov 21, 2023
1 parent 14b8092 commit 9476ae3
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,93 @@ sentryTest('captures JSON response body', async ({ getLocalTestPath, page, brows
]);
});

sentryTest('captures JSON response body when responseType=json', async ({ getLocalTestPath, page, browserName }) => {
// These are a bit flaky on non-chromium browsers
if (shouldSkipReplayTest() || browserName !== 'chromium') {
sentryTest.skip();
}

await page.route('**/foo', route => {
return route.fulfill({
status: 200,
body: JSON.stringify({ res: 'this' }),
headers: {
'Content-Length': '',
},
});
});

await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 'test-id' }),
});
});

const requestPromise = waitForErrorRequest(page);
const replayRequestPromise1 = waitForReplayRequest(page, 0);

const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);

void page.evaluate(() => {
/* eslint-disable */
const xhr = new XMLHttpRequest();

xhr.open('POST', 'http://localhost:7654/foo');
// Setting this to json ensures that xhr.response returns a POJO
xhr.responseType = 'json';
xhr.send();

xhr.addEventListener('readystatechange', function () {
if (xhr.readyState === 4) {
// @ts-expect-error Sentry is a global
setTimeout(() => Sentry.captureException('test error', 0));
}
});
/* eslint-enable */
});

const request = await requestPromise;
const eventData = envelopeRequestParser(request);

expect(eventData.exception?.values).toHaveLength(1);

expect(eventData?.breadcrumbs?.length).toBe(1);
expect(eventData!.breadcrumbs![0]).toEqual({
timestamp: expect.any(Number),
category: 'xhr',
type: 'http',
data: {
method: 'POST',
response_body_size: 14,
status_code: 200,
url: 'http://localhost:7654/foo',
},
});

const replayReq1 = await replayRequestPromise1;
const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([
{
data: {
method: 'POST',
statusCode: 200,
response: {
size: 14,
headers: {},
body: { res: 'this' },
},
},
description: 'http://localhost:7654/foo',
endTimestamp: expect.any(Number),
op: 'resource.xhr',
startTimestamp: expect.any(Number),
},
]);
});

sentryTest('captures non-text response body', async ({ getLocalTestPath, page, browserName }) => {
// These are a bit flaky on non-chromium browsers
if (shouldSkipReplayTest() || browserName !== 'chromium') {
Expand Down
54 changes: 51 additions & 3 deletions packages/replay/src/coreHandlers/util/xhrUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function enrichXhrBreadcrumb(
const reqSize = getBodySize(input, options.textEncoder);
const resSize = xhr.getResponseHeader('content-length')
? parseContentLengthHeader(xhr.getResponseHeader('content-length'))
: getBodySize(xhr.response, options.textEncoder);
: _getBodySize(xhr.response, xhr.responseType, options.textEncoder);

if (reqSize !== undefined) {
breadcrumb.data.request_body_size = reqSize;
Expand Down Expand Up @@ -153,8 +153,7 @@ function _getXhrResponseBody(xhr: XMLHttpRequest): [string | undefined, NetworkM

// Try to manually parse the response body, if responseText fails
try {
const response = xhr.response;
return getBodyString(response);
return _parseXhrResponse(xhr.response, xhr.responseType);
} catch (e) {
errors.push(e);
}
Expand All @@ -163,3 +162,52 @@ function _getXhrResponseBody(xhr: XMLHttpRequest): [string | undefined, NetworkM

return [undefined];
}

/**
* Get the string representation of the XHR response.
* Based on MDN, these are the possible types of the response:
* string
* ArrayBuffer
* Blob
* Document
* POJO
*/
export function _parseXhrResponse(
body: XMLHttpRequest['response'],
responseType: XMLHttpRequest['responseType'],
): [string | undefined, NetworkMetaWarning?] {
logger.log(body, responseType, typeof body);
try {
if (typeof body === 'string') {
return [body];
}

if (body instanceof Document) {
return [body.body.outerHTML];
}

if (responseType === 'json' && body && typeof body === 'object') {
return [JSON.stringify(body)];
}
} catch {
__DEBUG_BUILD__ && logger.warn('[Replay] Failed to serialize body', body);
return [undefined, 'BODY_PARSE_ERROR'];
}

__DEBUG_BUILD__ && logger.info('[Replay] Skipping network body because of body type', body);

return [undefined];
}

function _getBodySize(
body: XMLHttpRequest['response'],
responseType: XMLHttpRequest['responseType'],
textEncoder: TextEncoder | TextEncoderInternal,
): number | undefined {
try {
const bodyStr = responseType === 'json' && body && typeof body === 'object' ? JSON.stringify(body) : body;
return getBodySize(bodyStr, textEncoder);
} catch {
return undefined;
}
}

0 comments on commit 9476ae3

Please sign in to comment.