Skip to content

Commit

Permalink
fix(replay): Add additional safeguards for capturing network bodies (#…
Browse files Browse the repository at this point in the history
…9506)

This adds additional safeguards around fetch/xhr body capturing for
replay.
I added additional try-catch in all places that depend on
`networkCaptureBodies`.

This also types the fetch/xhr hints as `Partial` to ensure we guard
against any of the things not actually being defined, to be on the safe
side.

Hopefully fixes
#9339
  • Loading branch information
mydea committed Nov 10, 2023
1 parent 1d50eef commit 04e7be9
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 36 deletions.
24 changes: 13 additions & 11 deletions packages/replay/src/coreHandlers/util/fetchUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
*/
export async function captureFetchBreadcrumbToReplay(
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
hint: FetchHint,
hint: Partial<FetchHint>,
options: ReplayNetworkOptions & {
textEncoder: TextEncoderInternal;
replay: ReplayContainer;
Expand All @@ -50,12 +50,12 @@ export async function captureFetchBreadcrumbToReplay(
*/
export function enrichFetchBreadcrumb(
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
hint: FetchHint,
hint: Partial<FetchHint>,
options: { textEncoder: TextEncoderInternal },
): void {
const { input, response } = hint;

const body = _getFetchRequestArgBody(input);
const body = input ? _getFetchRequestArgBody(input) : undefined;
const reqSize = getBodySize(body, options.textEncoder);

const resSize = response ? parseContentLengthHeader(response.headers.get('content-length')) : undefined;
Expand All @@ -70,12 +70,13 @@ export function enrichFetchBreadcrumb(

async function _prepareFetchData(
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
hint: FetchHint,
hint: Partial<FetchHint>,
options: ReplayNetworkOptions & {
textEncoder: TextEncoderInternal;
},
): Promise<ReplayNetworkRequestData> {
const { startTimestamp, endTimestamp } = hint;
const now = Date.now();
const { startTimestamp = now, endTimestamp = now } = hint;

const {
url,
Expand Down Expand Up @@ -106,10 +107,10 @@ async function _prepareFetchData(

function _getRequestInfo(
{ networkCaptureBodies, networkRequestHeaders }: ReplayNetworkOptions,
input: FetchHint['input'],
input: FetchHint['input'] | undefined,
requestBodySize?: number,
): ReplayNetworkRequestOrResponse | undefined {
const headers = getRequestHeaders(input, networkRequestHeaders);
const headers = input ? getRequestHeaders(input, networkRequestHeaders) : {};

if (!networkCaptureBodies) {
return buildNetworkRequestOrResponse(headers, requestBodySize, undefined);
Expand All @@ -130,16 +131,16 @@ async function _getResponseInfo(
}: ReplayNetworkOptions & {
textEncoder: TextEncoderInternal;
},
response: Response,
response: Response | undefined,
responseBodySize?: number,
): Promise<ReplayNetworkRequestOrResponse | undefined> {
if (!captureDetails && responseBodySize !== undefined) {
return buildSkippedNetworkRequestOrResponse(responseBodySize);
}

const headers = getAllHeaders(response.headers, networkResponseHeaders);
const headers = response ? getAllHeaders(response.headers, networkResponseHeaders) : {};

if (!networkCaptureBodies && responseBodySize !== undefined) {
if (!response || (!networkCaptureBodies && responseBodySize !== undefined)) {
return buildNetworkRequestOrResponse(headers, responseBodySize, undefined);
}

Expand All @@ -163,7 +164,8 @@ async function _getResponseInfo(
}

return buildNetworkRequestOrResponse(headers, size, undefined);
} catch {
} catch (error) {
__DEBUG_BUILD__ && logger.warn('[Replay] Failed to serialize response body', error);
// fallback
return buildNetworkRequestOrResponse(headers, responseBodySize, undefined);
}
Expand Down
23 changes: 13 additions & 10 deletions packages/replay/src/coreHandlers/util/networkUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { TextEncoderInternal } from '@sentry/types';
import { dropUndefinedKeys, stringMatchesSomePattern } from '@sentry/utils';
import { dropUndefinedKeys, logger, stringMatchesSomePattern } from '@sentry/utils';

import { NETWORK_BODY_MAX_SIZE, WINDOW } from '../../constants';
import type {
Expand Down Expand Up @@ -62,16 +62,20 @@ export function parseContentLengthHeader(header: string | null | undefined): num

/** Get the string representation of a body. */
export function getBodyString(body: unknown): string | undefined {
if (typeof body === 'string') {
return body;
}
try {
if (typeof body === 'string') {
return body;
}

if (body instanceof URLSearchParams) {
return body.toString();
}
if (body instanceof URLSearchParams) {
return body.toString();
}

if (body instanceof FormData) {
return _serializeFormData(body);
if (body instanceof FormData) {
return _serializeFormData(body);
}
} catch {
__DEBUG_BUILD__ && logger.warn('[Replay] Failed to serialize body', body);
}

return undefined;
Expand Down Expand Up @@ -199,7 +203,6 @@ function normalizeNetworkBody(body: string | undefined): {
}

const exceedsSizeLimit = body.length > NETWORK_BODY_MAX_SIZE;

const isProbablyJson = _strIsProbablyJson(body);

if (exceedsSizeLimit) {
Expand Down
44 changes: 29 additions & 15 deletions packages/replay/src/coreHandlers/util/xhrUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
*/
export async function captureXhrBreadcrumbToReplay(
breadcrumb: Breadcrumb & { data: XhrBreadcrumbData },
hint: XhrHint,
hint: Partial<XhrHint>,
options: ReplayNetworkOptions & { replay: ReplayContainer },
): Promise<void> {
try {
Expand All @@ -41,11 +41,15 @@ export async function captureXhrBreadcrumbToReplay(
*/
export function enrichXhrBreadcrumb(
breadcrumb: Breadcrumb & { data: XhrBreadcrumbData },
hint: XhrHint,
hint: Partial<XhrHint>,
options: { textEncoder: TextEncoderInternal },
): void {
const { xhr, input } = hint;

if (!xhr) {
return;
}

const reqSize = getBodySize(input, options.textEncoder);
const resSize = xhr.getResponseHeader('content-length')
? parseContentLengthHeader(xhr.getResponseHeader('content-length'))
Expand All @@ -61,10 +65,11 @@ export function enrichXhrBreadcrumb(

function _prepareXhrData(
breadcrumb: Breadcrumb & { data: XhrBreadcrumbData },
hint: XhrHint,
hint: Partial<XhrHint>,
options: ReplayNetworkOptions,
): ReplayNetworkRequestData | null {
const { startTimestamp, endTimestamp, input, xhr } = hint;
const now = Date.now();
const { startTimestamp = now, endTimestamp = now, input, xhr } = hint;

const {
url,
Expand All @@ -78,7 +83,7 @@ function _prepareXhrData(
return null;
}

if (!urlMatches(url, options.networkDetailAllowUrls) || urlMatches(url, options.networkDetailDenyUrls)) {
if (!xhr || !urlMatches(url, options.networkDetailAllowUrls) || urlMatches(url, options.networkDetailDenyUrls)) {
const request = buildSkippedNetworkRequestOrResponse(requestBodySize);
const response = buildSkippedNetworkRequestOrResponse(responseBodySize);
return {
Expand All @@ -98,16 +103,11 @@ function _prepareXhrData(
: {};
const networkResponseHeaders = getAllowedHeaders(getResponseHeaders(xhr), options.networkResponseHeaders);

const request = buildNetworkRequestOrResponse(
networkRequestHeaders,
requestBodySize,
options.networkCaptureBodies ? getBodyString(input) : undefined,
);
const response = buildNetworkRequestOrResponse(
networkResponseHeaders,
responseBodySize,
options.networkCaptureBodies ? hint.xhr.responseText : undefined,
);
const requestBody = options.networkCaptureBodies ? getBodyString(input) : undefined;
const responseBody = options.networkCaptureBodies ? _getXhrResponseBody(xhr) : undefined;

const request = buildNetworkRequestOrResponse(networkRequestHeaders, requestBodySize, requestBody);
const response = buildNetworkRequestOrResponse(networkResponseHeaders, responseBodySize, responseBody);

return {
startTimestamp,
Expand All @@ -133,3 +133,17 @@ function getResponseHeaders(xhr: XMLHttpRequest): Record<string, string> {
return acc;
}, {});
}

function _getXhrResponseBody(xhr: XMLHttpRequest): string | undefined {
try {
return xhr.responseText;
} catch {} // eslint-disable-line no-empty

// Try to manually parse the response body, if responseText fails
try {
const response = xhr.response;
return getBodyString(response);
} catch {} // eslint-disable-line no-empty

return undefined;
}

0 comments on commit 04e7be9

Please sign in to comment.