Skip to content

Commit b019c76

Browse files
committed
refactor: hoist replay header parsing code to browser utils
1 parent 5b4e333 commit b019c76

File tree

4 files changed

+73
-34
lines changed

4 files changed

+73
-34
lines changed

packages/browser-utils/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ export { fetch, setTimeout, clearCachedImplementation, getNativeImplementation }
2828

2929
export { addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY } from './instrument/xhr';
3030

31-
export { getBodyString, getFetchRequestArgBody, serializeFormData } from './networkUtils';
31+
export {
32+
getBodyString,
33+
getFetchRequestArgBody,
34+
serializeFormData,
35+
parseXhrResponseHeaders,
36+
getFetchResponseHeaders,
37+
filterAllowedHeaders,
38+
} from './networkUtils';
3239

3340
export { resourceTimingToSpanAttributes } from './metrics/resourceTiming';
3441

packages/browser-utils/src/networkUtils.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,58 @@ export function getFetchRequestArgBody(fetchArgs: unknown[] = []): RequestInit['
5454

5555
return (fetchArgs[1] as RequestInit).body;
5656
}
57+
58+
/**
59+
* Parses XMLHttpRequest response headers into a Record.
60+
* Extracted from replay internals to be reusable.
61+
*/
62+
export function parseXhrResponseHeaders(xhr: XMLHttpRequest): Record<string, string> {
63+
const headers = xhr.getAllResponseHeaders();
64+
65+
if (!headers) {
66+
return {};
67+
}
68+
69+
return headers.split('\r\n').reduce((acc: Record<string, string>, line: string) => {
70+
const [key, value] = line.split(': ') as [string, string | undefined];
71+
if (value) {
72+
acc[key.toLowerCase()] = value;
73+
}
74+
return acc;
75+
}, {});
76+
}
77+
78+
/**
79+
* Gets specific headers from a Headers object (Fetch API).
80+
* Extracted from replay internals to be reusable.
81+
*/
82+
export function getFetchResponseHeaders(headers: Headers, allowedHeaders: string[]): Record<string, string> {
83+
const allHeaders: Record<string, string> = {};
84+
85+
allowedHeaders.forEach(header => {
86+
const value = headers.get(header);
87+
if (value) {
88+
allHeaders[header.toLowerCase()] = value;
89+
}
90+
});
91+
92+
return allHeaders;
93+
}
94+
95+
/**
96+
* Filters headers based on an allowed list.
97+
* Extracted from replay internals to be reusable.
98+
*/
99+
export function filterAllowedHeaders(
100+
headers: Record<string, string>,
101+
allowedHeaders: string[],
102+
): Record<string, string> {
103+
return Object.entries(headers).reduce((filteredHeaders: Record<string, string>, [key, value]) => {
104+
const normalizedKey = key.toLowerCase();
105+
// Avoid putting empty strings into the headers
106+
if (allowedHeaders.includes(normalizedKey) && value) {
107+
filteredHeaders[normalizedKey] = value;
108+
}
109+
return filteredHeaders;
110+
}, {});
111+
}

packages/replay-internal/src/coreHandlers/util/fetchUtils.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { Breadcrumb, FetchBreadcrumbData } from '@sentry/core';
22
import type { FetchHint, NetworkMetaWarning } from '@sentry-internal/browser-utils';
3-
import { getBodyString, getFetchRequestArgBody, setTimeout } from '@sentry-internal/browser-utils';
3+
import {
4+
getBodyString,
5+
getFetchRequestArgBody,
6+
getFetchResponseHeaders,
7+
setTimeout,
8+
} from '@sentry-internal/browser-utils';
49
import { DEBUG_BUILD } from '../../debug-build';
510
import type {
611
ReplayContainer,
@@ -139,7 +144,7 @@ export async function _getResponseInfo(
139144
return buildSkippedNetworkRequestOrResponse(responseBodySize);
140145
}
141146

142-
const headers = response ? getAllHeaders(response.headers, networkResponseHeaders) : {};
147+
const headers = response ? getFetchResponseHeaders(response.headers, networkResponseHeaders) : {};
143148

144149
if (!response || (!networkCaptureBodies && responseBodySize !== undefined)) {
145150
return buildNetworkRequestOrResponse(headers, responseBodySize, undefined);
@@ -215,18 +220,6 @@ async function _parseFetchResponseBody(response: Response): Promise<[string | un
215220
}
216221
}
217222

218-
function getAllHeaders(headers: Headers, allowedHeaders: string[]): Record<string, string> {
219-
const allHeaders: Record<string, string> = {};
220-
221-
allowedHeaders.forEach(header => {
222-
if (headers.get(header)) {
223-
allHeaders[header] = headers.get(header) as string;
224-
}
225-
});
226-
227-
return allHeaders;
228-
}
229-
230223
function getRequestHeaders(fetchArgs: unknown[], allowedHeaders: string[]): Record<string, string> {
231224
if (fetchArgs.length === 1 && typeof fetchArgs[0] !== 'string') {
232225
return getHeadersFromOptions(fetchArgs[0] as Request | RequestInit, allowedHeaders);
@@ -254,7 +247,7 @@ function getHeadersFromOptions(
254247
}
255248

256249
if (headers instanceof Headers) {
257-
return getAllHeaders(headers, allowedHeaders);
250+
return getFetchResponseHeaders(headers, allowedHeaders);
258251
}
259252

260253
// We do not support this, as it is not really documented (anymore?)

packages/replay-internal/src/coreHandlers/util/xhrUtils.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Breadcrumb, XhrBreadcrumbData } from '@sentry/core';
22
import type { NetworkMetaWarning, XhrHint } from '@sentry-internal/browser-utils';
3-
import { getBodyString, SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils';
3+
import { getBodyString, parseXhrResponseHeaders, SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils';
44
import { DEBUG_BUILD } from '../../debug-build';
55
import type { ReplayContainer, ReplayNetworkOptions, ReplayNetworkRequestData } from '../../types';
66
import { debug } from '../../util/logger';
@@ -104,7 +104,7 @@ function _prepareXhrData(
104104
const networkRequestHeaders = xhrInfo
105105
? getAllowedHeaders(xhrInfo.request_headers, options.networkRequestHeaders)
106106
: {};
107-
const networkResponseHeaders = getAllowedHeaders(getResponseHeaders(xhr), options.networkResponseHeaders);
107+
const networkResponseHeaders = getAllowedHeaders(parseXhrResponseHeaders(xhr), options.networkResponseHeaders);
108108

109109
const [requestBody, requestWarning] = options.networkCaptureBodies ? getBodyString(input, debug) : [undefined];
110110
const [responseBody, responseWarning] = options.networkCaptureBodies ? _getXhrResponseBody(xhr) : [undefined];
@@ -123,22 +123,6 @@ function _prepareXhrData(
123123
};
124124
}
125125

126-
function getResponseHeaders(xhr: XMLHttpRequest): Record<string, string> {
127-
const headers = xhr.getAllResponseHeaders();
128-
129-
if (!headers) {
130-
return {};
131-
}
132-
133-
return headers.split('\r\n').reduce((acc: Record<string, string>, line: string) => {
134-
const [key, value] = line.split(': ') as [string, string | undefined];
135-
if (value) {
136-
acc[key.toLowerCase()] = value;
137-
}
138-
return acc;
139-
}, {});
140-
}
141-
142126
function _getXhrResponseBody(xhr: XMLHttpRequest): [string | undefined, NetworkMetaWarning?] {
143127
// We collect errors that happen, but only log them if we can't get any response body
144128
const errors: unknown[] = [];

0 commit comments

Comments
 (0)