Skip to content

Commit

Permalink
feat(replay): Capture fetch body size for replay events
Browse files Browse the repository at this point in the history
  • Loading branch information
mydea committed Mar 21, 2023
1 parent 72dca3e commit 9bcd880
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 20 deletions.
68 changes: 56 additions & 12 deletions packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function handleNetworkBreadcrumbs(replay: ReplayContainer): void {
};

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

/** just exported for tests */
export function handleNetworkBreadcrumb(
export function beforeAddNetworkBreadcrumb(
options: ExtendedNetworkBreadcrumbsOptions,
breadcrumb: Breadcrumb,
hint?: BreadcrumbHint,
Expand All @@ -74,27 +74,71 @@ export function handleNetworkBreadcrumb(

try {
if (_isXhrBreadcrumb(breadcrumb) && _isXhrHint(hint)) {
// Enriches the breadcrumb overall
_enrichXhrBreadcrumb(breadcrumb, hint, options);

// Create a replay performance entry from this breadcrumb
const result = _makeNetworkReplayBreadcrumb('resource.xhr', breadcrumb, hint);
addNetworkBreadcrumb(options.replay, result);
_handleXhrBreadcrumb(breadcrumb, hint, options);
}

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

// Create a replay performance entry from this breadcrumb
const result = _makeNetworkReplayBreadcrumb('resource.fetch', breadcrumb, hint);
addNetworkBreadcrumb(options.replay, result);
void _handleFetchBreadcrumb(breadcrumb, hint, options);
}
} catch (e) {
__DEBUG_BUILD__ && logger.warn('Error when enriching network breadcrumb');
}
}

function _handleXhrBreadcrumb(
breadcrumb: Breadcrumb & { data: XhrBreadcrumbData },
hint: XhrHint,
options: ExtendedNetworkBreadcrumbsOptions,
): void {
// Enriches the breadcrumb overall
_enrichXhrBreadcrumb(breadcrumb, hint, options);

// Create a replay performance entry from this breadcrumb
const result = _makeNetworkReplayBreadcrumb('resource.xhr', breadcrumb, hint);
addNetworkBreadcrumb(options.replay, result);
}

async function _handleFetchBreadcrumb(
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
hint: FetchHint,
options: ExtendedNetworkBreadcrumbsOptions,
): Promise<void> {
await _parseFetchResponse(breadcrumb, hint, options);

// Create a replay performance entry from this breadcrumb
const result = _makeNetworkReplayBreadcrumb('resource.fetch', breadcrumb, hint);
addNetworkBreadcrumb(options.replay, result);
}

// This does async operations on the breadcrumb for replay
async function _parseFetchResponse(
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
hint: FetchBreadcrumbHint,
options: ExtendedNetworkBreadcrumbsOptions,
): Promise<void> {
if (breadcrumb.data.response_body_size || !hint.response) {
return;
}

// If no Content-Length header exists, we try to get the size from the response body
try {
// We have to clone this, as the body can only be read once
const response = (hint.response as Response).clone();
const body = await response.text();

if (body.length) {
breadcrumb.data.response_body_size = getBodySize(body, options.textEncoder);
}
} catch {
// just ignore if something fails here
}
}

function _makeNetworkReplayBreadcrumb(
type: string,
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData | XhrBreadcrumbData },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { TextEncoder } from 'util';

import { BASE_TIMESTAMP } from '../..';
import {
beforeAddNetworkBreadcrumb,
getBodySize,
handleNetworkBreadcrumb,
parseContentSizeHeader,
} from '../../../src/coreHandlers/handleNetworkBreadcrumbs';
import type { EventBufferArray } from '../../../src/eventBuffer/EventBufferArray';
Expand Down Expand Up @@ -78,7 +78,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
});
});

describe('handleNetworkBreadcrumb()', () => {
describe('beforeAddNetworkBreadcrumb()', () => {
let options: {
replay: ReplayContainer;
textEncoder: TextEncoderInternal;
Expand All @@ -98,7 +98,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
it('ignores breadcrumb without data', () => {
const breadcrumb: Breadcrumb = {};
const hint: BreadcrumbHint = {};
handleNetworkBreadcrumb(options, breadcrumb, hint);
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);

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

expect(breadcrumb).toEqual({
category: 'foo',
Expand Down Expand Up @@ -138,7 +138,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
handleNetworkBreadcrumb(options, breadcrumb, hint);
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);

expect(breadcrumb).toEqual({
category: 'xhr',
Expand Down Expand Up @@ -192,7 +192,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
handleNetworkBreadcrumb(options, breadcrumb, hint);
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);

expect(breadcrumb).toEqual({
category: 'xhr',
Expand Down Expand Up @@ -246,7 +246,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
handleNetworkBreadcrumb(options, breadcrumb, hint);
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);

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

jest.runAllTimers();
await Promise.resolve();

expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
{
Expand Down Expand Up @@ -305,7 +306,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
handleNetworkBreadcrumb(options, breadcrumb, hint);
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);

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

jest.runAllTimers();
await Promise.resolve();

expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
{
Expand All @@ -336,5 +338,63 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
},
]);
});

it('parses fetch response body if necessary', async () => {
const breadcrumb: Breadcrumb = {
category: 'fetch',
data: {
url: 'https://example.com',
status_code: 200,
},
};

const mockResponse = {
headers: {
get: () => '',
},
clone: () => mockResponse,
text: () => Promise.resolve('test response'),
} as unknown as Response;

const hint: FetchBreadcrumbHint = {
input: [],
response: mockResponse,
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);

expect(breadcrumb).toEqual({
category: 'fetch',
data: {
status_code: 200,
url: 'https://example.com',
},
});

await Promise.resolve();
jest.runAllTimers();
await Promise.resolve();

expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
{
type: 5,
timestamp: (BASE_TIMESTAMP + 1000) / 1000,
data: {
tag: 'performanceSpan',
payload: {
data: {
statusCode: 200,
responseBodySize: 13,
},
description: 'https://example.com',
endTimestamp: (BASE_TIMESTAMP + 2000) / 1000,
op: 'resource.fetch',
startTimestamp: (BASE_TIMESTAMP + 1000) / 1000,
},
},
},
]);
});
});
});

0 comments on commit 9bcd880

Please sign in to comment.