Skip to content

Commit 9bcd880

Browse files
committed
feat(replay): Capture fetch body size for replay events
1 parent 72dca3e commit 9bcd880

File tree

2 files changed

+124
-20
lines changed

2 files changed

+124
-20
lines changed

packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function handleNetworkBreadcrumbs(replay: ReplayContainer): void {
5151
};
5252

5353
if (client && client.on) {
54-
client.on('beforeAddBreadcrumb', (breadcrumb, hint) => handleNetworkBreadcrumb(options, breadcrumb, hint));
54+
client.on('beforeAddBreadcrumb', (breadcrumb, hint) => beforeAddNetworkBreadcrumb(options, breadcrumb, hint));
5555
} else {
5656
// Fallback behavior
5757
addInstrumentationHandler('fetch', handleFetchSpanListener(replay));
@@ -63,7 +63,7 @@ export function handleNetworkBreadcrumbs(replay: ReplayContainer): void {
6363
}
6464

6565
/** just exported for tests */
66-
export function handleNetworkBreadcrumb(
66+
export function beforeAddNetworkBreadcrumb(
6767
options: ExtendedNetworkBreadcrumbsOptions,
6868
breadcrumb: Breadcrumb,
6969
hint?: BreadcrumbHint,
@@ -74,27 +74,71 @@ export function handleNetworkBreadcrumb(
7474

7575
try {
7676
if (_isXhrBreadcrumb(breadcrumb) && _isXhrHint(hint)) {
77-
// Enriches the breadcrumb overall
78-
_enrichXhrBreadcrumb(breadcrumb, hint, options);
79-
80-
// Create a replay performance entry from this breadcrumb
81-
const result = _makeNetworkReplayBreadcrumb('resource.xhr', breadcrumb, hint);
82-
addNetworkBreadcrumb(options.replay, result);
77+
_handleXhrBreadcrumb(breadcrumb, hint, options);
8378
}
8479

8580
if (_isFetchBreadcrumb(breadcrumb) && _isFetchHint(hint)) {
86-
// Enriches the breadcrumb overall
81+
// This has to be sync, as we need to ensure the breadcrumb is enriched in the same tick
82+
// Because the hook runs synchronously, and the breadcrumb is afterwards passed on
83+
// So any async mutations to it will not be reflected in the final breadcrumb
8784
_enrichFetchBreadcrumb(breadcrumb, hint, options);
8885

89-
// Create a replay performance entry from this breadcrumb
90-
const result = _makeNetworkReplayBreadcrumb('resource.fetch', breadcrumb, hint);
91-
addNetworkBreadcrumb(options.replay, result);
86+
void _handleFetchBreadcrumb(breadcrumb, hint, options);
9287
}
9388
} catch (e) {
9489
__DEBUG_BUILD__ && logger.warn('Error when enriching network breadcrumb');
9590
}
9691
}
9792

93+
function _handleXhrBreadcrumb(
94+
breadcrumb: Breadcrumb & { data: XhrBreadcrumbData },
95+
hint: XhrHint,
96+
options: ExtendedNetworkBreadcrumbsOptions,
97+
): void {
98+
// Enriches the breadcrumb overall
99+
_enrichXhrBreadcrumb(breadcrumb, hint, options);
100+
101+
// Create a replay performance entry from this breadcrumb
102+
const result = _makeNetworkReplayBreadcrumb('resource.xhr', breadcrumb, hint);
103+
addNetworkBreadcrumb(options.replay, result);
104+
}
105+
106+
async function _handleFetchBreadcrumb(
107+
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
108+
hint: FetchHint,
109+
options: ExtendedNetworkBreadcrumbsOptions,
110+
): Promise<void> {
111+
await _parseFetchResponse(breadcrumb, hint, options);
112+
113+
// Create a replay performance entry from this breadcrumb
114+
const result = _makeNetworkReplayBreadcrumb('resource.fetch', breadcrumb, hint);
115+
addNetworkBreadcrumb(options.replay, result);
116+
}
117+
118+
// This does async operations on the breadcrumb for replay
119+
async function _parseFetchResponse(
120+
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
121+
hint: FetchBreadcrumbHint,
122+
options: ExtendedNetworkBreadcrumbsOptions,
123+
): Promise<void> {
124+
if (breadcrumb.data.response_body_size || !hint.response) {
125+
return;
126+
}
127+
128+
// If no Content-Length header exists, we try to get the size from the response body
129+
try {
130+
// We have to clone this, as the body can only be read once
131+
const response = (hint.response as Response).clone();
132+
const body = await response.text();
133+
134+
if (body.length) {
135+
breadcrumb.data.response_body_size = getBodySize(body, options.textEncoder);
136+
}
137+
} catch {
138+
// just ignore if something fails here
139+
}
140+
}
141+
98142
function _makeNetworkReplayBreadcrumb(
99143
type: string,
100144
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData | XhrBreadcrumbData },

packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { TextEncoder } from 'util';
99

1010
import { BASE_TIMESTAMP } from '../..';
1111
import {
12+
beforeAddNetworkBreadcrumb,
1213
getBodySize,
13-
handleNetworkBreadcrumb,
1414
parseContentSizeHeader,
1515
} from '../../../src/coreHandlers/handleNetworkBreadcrumbs';
1616
import type { EventBufferArray } from '../../../src/eventBuffer/EventBufferArray';
@@ -78,7 +78,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
7878
});
7979
});
8080

81-
describe('handleNetworkBreadcrumb()', () => {
81+
describe('beforeAddNetworkBreadcrumb()', () => {
8282
let options: {
8383
replay: ReplayContainer;
8484
textEncoder: TextEncoderInternal;
@@ -98,7 +98,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
9898
it('ignores breadcrumb without data', () => {
9999
const breadcrumb: Breadcrumb = {};
100100
const hint: BreadcrumbHint = {};
101-
handleNetworkBreadcrumb(options, breadcrumb, hint);
101+
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
102102

103103
expect(breadcrumb).toEqual({});
104104
expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([]);
@@ -110,7 +110,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
110110
data: {},
111111
};
112112
const hint: BreadcrumbHint = {};
113-
handleNetworkBreadcrumb(options, breadcrumb, hint);
113+
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
114114

115115
expect(breadcrumb).toEqual({
116116
category: 'foo',
@@ -138,7 +138,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
138138
startTimestamp: BASE_TIMESTAMP + 1000,
139139
endTimestamp: BASE_TIMESTAMP + 2000,
140140
};
141-
handleNetworkBreadcrumb(options, breadcrumb, hint);
141+
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
142142

143143
expect(breadcrumb).toEqual({
144144
category: 'xhr',
@@ -192,7 +192,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
192192
startTimestamp: BASE_TIMESTAMP + 1000,
193193
endTimestamp: BASE_TIMESTAMP + 2000,
194194
};
195-
handleNetworkBreadcrumb(options, breadcrumb, hint);
195+
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
196196

197197
expect(breadcrumb).toEqual({
198198
category: 'xhr',
@@ -246,7 +246,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
246246
startTimestamp: BASE_TIMESTAMP + 1000,
247247
endTimestamp: BASE_TIMESTAMP + 2000,
248248
};
249-
handleNetworkBreadcrumb(options, breadcrumb, hint);
249+
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
250250

251251
expect(breadcrumb).toEqual({
252252
category: 'fetch',
@@ -260,6 +260,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
260260
});
261261

262262
jest.runAllTimers();
263+
await Promise.resolve();
263264

264265
expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
265266
{
@@ -305,7 +306,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
305306
startTimestamp: BASE_TIMESTAMP + 1000,
306307
endTimestamp: BASE_TIMESTAMP + 2000,
307308
};
308-
handleNetworkBreadcrumb(options, breadcrumb, hint);
309+
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
309310

310311
expect(breadcrumb).toEqual({
311312
category: 'fetch',
@@ -316,6 +317,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
316317
});
317318

318319
jest.runAllTimers();
320+
await Promise.resolve();
319321

320322
expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
321323
{
@@ -336,5 +338,63 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
336338
},
337339
]);
338340
});
341+
342+
it('parses fetch response body if necessary', async () => {
343+
const breadcrumb: Breadcrumb = {
344+
category: 'fetch',
345+
data: {
346+
url: 'https://example.com',
347+
status_code: 200,
348+
},
349+
};
350+
351+
const mockResponse = {
352+
headers: {
353+
get: () => '',
354+
},
355+
clone: () => mockResponse,
356+
text: () => Promise.resolve('test response'),
357+
} as unknown as Response;
358+
359+
const hint: FetchBreadcrumbHint = {
360+
input: [],
361+
response: mockResponse,
362+
startTimestamp: BASE_TIMESTAMP + 1000,
363+
endTimestamp: BASE_TIMESTAMP + 2000,
364+
};
365+
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
366+
367+
expect(breadcrumb).toEqual({
368+
category: 'fetch',
369+
data: {
370+
status_code: 200,
371+
url: 'https://example.com',
372+
},
373+
});
374+
375+
await Promise.resolve();
376+
jest.runAllTimers();
377+
await Promise.resolve();
378+
379+
expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
380+
{
381+
type: 5,
382+
timestamp: (BASE_TIMESTAMP + 1000) / 1000,
383+
data: {
384+
tag: 'performanceSpan',
385+
payload: {
386+
data: {
387+
statusCode: 200,
388+
responseBodySize: 13,
389+
},
390+
description: 'https://example.com',
391+
endTimestamp: (BASE_TIMESTAMP + 2000) / 1000,
392+
op: 'resource.fetch',
393+
startTimestamp: (BASE_TIMESTAMP + 1000) / 1000,
394+
},
395+
},
396+
},
397+
]);
398+
});
339399
});
340400
});

0 commit comments

Comments
 (0)