diff --git a/packages/replay/src/coreHandlers/util/fetchUtils.ts b/packages/replay/src/coreHandlers/util/fetchUtils.ts index 491fe727b1b8..80c6b4686a70 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); } @@ -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); } diff --git a/packages/replay/src/coreHandlers/util/networkUtils.ts b/packages/replay/src/coreHandlers/util/networkUtils.ts index 78b4e22efa9b..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 { @@ -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; @@ -199,7 +203,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; +}