From ae7b10674d113416a1da35d0f01b9c053e123a5b Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 9 Nov 2023 13:00:16 +0100 Subject: [PATCH 1/3] fix(replay): Add additional safeguards for capturing network bodies --- .../src/coreHandlers/util/fetchUtils.ts | 21 ++++----- .../src/coreHandlers/util/networkUtils.ts | 21 ++++----- .../replay/src/coreHandlers/util/xhrUtils.ts | 44 ++++++++++++------- 3 files changed, 51 insertions(+), 35 deletions(-) diff --git a/packages/replay/src/coreHandlers/util/fetchUtils.ts b/packages/replay/src/coreHandlers/util/fetchUtils.ts index 491fe727b1b8..dae9b6331335 100644 --- a/packages/replay/src/coreHandlers/util/fetchUtils.ts +++ b/packages/replay/src/coreHandlers/util/fetchUtils.ts @@ -26,7 +26,7 @@ import { */ export async function captureFetchBreadcrumbToReplay( breadcrumb: Breadcrumb & { data: FetchBreadcrumbData }, - hint: FetchHint, + hint: Partial, options: ReplayNetworkOptions & { textEncoder: TextEncoderInternal; replay: ReplayContainer; @@ -50,12 +50,12 @@ export async function captureFetchBreadcrumbToReplay( */ export function enrichFetchBreadcrumb( breadcrumb: Breadcrumb & { data: FetchBreadcrumbData }, - hint: FetchHint, + hint: Partial, 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; @@ -70,12 +70,13 @@ export function enrichFetchBreadcrumb( async function _prepareFetchData( breadcrumb: Breadcrumb & { data: FetchBreadcrumbData }, - hint: FetchHint, + hint: Partial, options: ReplayNetworkOptions & { textEncoder: TextEncoderInternal; }, ): Promise { - const { startTimestamp, endTimestamp } = hint; + const now = Date.now(); + const { startTimestamp = now, endTimestamp = now } = hint; const { url, @@ -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); @@ -130,16 +131,16 @@ async function _getResponseInfo( }: ReplayNetworkOptions & { textEncoder: TextEncoderInternal; }, - response: Response, + response: Response | undefined, responseBodySize?: number, ): Promise { 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); } diff --git a/packages/replay/src/coreHandlers/util/networkUtils.ts b/packages/replay/src/coreHandlers/util/networkUtils.ts index 78b4e22efa9b..a78a4434b85b 100644 --- a/packages/replay/src/coreHandlers/util/networkUtils.ts +++ b/packages/replay/src/coreHandlers/util/networkUtils.ts @@ -62,17 +62,19 @@ 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 {} // eslint-disable-line no-empty return undefined; } @@ -199,7 +201,6 @@ function normalizeNetworkBody(body: string | undefined): { } const exceedsSizeLimit = body.length > NETWORK_BODY_MAX_SIZE; - const isProbablyJson = _strIsProbablyJson(body); if (exceedsSizeLimit) { diff --git a/packages/replay/src/coreHandlers/util/xhrUtils.ts b/packages/replay/src/coreHandlers/util/xhrUtils.ts index 8c02a8da6abf..b11f8575e2ad 100644 --- a/packages/replay/src/coreHandlers/util/xhrUtils.ts +++ b/packages/replay/src/coreHandlers/util/xhrUtils.ts @@ -20,7 +20,7 @@ import { */ export async function captureXhrBreadcrumbToReplay( breadcrumb: Breadcrumb & { data: XhrBreadcrumbData }, - hint: XhrHint, + hint: Partial, options: ReplayNetworkOptions & { replay: ReplayContainer }, ): Promise { try { @@ -41,11 +41,15 @@ export async function captureXhrBreadcrumbToReplay( */ export function enrichXhrBreadcrumb( breadcrumb: Breadcrumb & { data: XhrBreadcrumbData }, - hint: XhrHint, + hint: Partial, 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')) @@ -61,10 +65,11 @@ export function enrichXhrBreadcrumb( function _prepareXhrData( breadcrumb: Breadcrumb & { data: XhrBreadcrumbData }, - hint: XhrHint, + hint: Partial, options: ReplayNetworkOptions, ): ReplayNetworkRequestData | null { - const { startTimestamp, endTimestamp, input, xhr } = hint; + const now = Date.now(); + const { startTimestamp = now, endTimestamp = now, input, xhr } = hint; const { url, @@ -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 { @@ -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, @@ -133,3 +133,17 @@ function getResponseHeaders(xhr: XMLHttpRequest): Record { 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; +} From 97767d44a5ca2d05f2e72afe280fbfff3a90dac6 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 10 Nov 2023 10:23:48 +0100 Subject: [PATCH 2/3] add log for body parsing error --- packages/replay/src/coreHandlers/util/networkUtils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/replay/src/coreHandlers/util/networkUtils.ts b/packages/replay/src/coreHandlers/util/networkUtils.ts index a78a4434b85b..f75ecf0f9bc9 100644 --- a/packages/replay/src/coreHandlers/util/networkUtils.ts +++ b/packages/replay/src/coreHandlers/util/networkUtils.ts @@ -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 { @@ -74,7 +74,9 @@ export function getBodyString(body: unknown): string | undefined { if (body instanceof FormData) { return _serializeFormData(body); } - } catch {} // eslint-disable-line no-empty + } catch { + __DEBUG_BUILD__ && logger.warn('[Replay] Failed to serialize body', body); + } return undefined; } From 6d0e4d4c9d27fa62f780661422f2602393c353bd Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 10 Nov 2023 10:30:31 +0100 Subject: [PATCH 3/3] log for failed serialized response body --- packages/replay/src/coreHandlers/util/fetchUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/replay/src/coreHandlers/util/fetchUtils.ts b/packages/replay/src/coreHandlers/util/fetchUtils.ts index dae9b6331335..80c6b4686a70 100644 --- a/packages/replay/src/coreHandlers/util/fetchUtils.ts +++ b/packages/replay/src/coreHandlers/util/fetchUtils.ts @@ -164,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); }