From d147439ec4c9d48e20308ea454756bde151ac2b8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 16 Mar 2023 17:56:12 +0100 Subject: [PATCH 1/5] fix(replay): Ensure network breadcrumbs have all data --- .../suites/replay/requests/subject.js | 2 +- .../utils/replayEventTemplates.ts | 15 +- .../browser/src/integrations/breadcrumbs.ts | 73 ++-- .../coreHandlers/extendNetworkBreadcrumbs.ts | 181 ---------- .../replay/src/coreHandlers/handleFetch.ts | 19 +- .../coreHandlers/handleNetworkBreadcrumbs.ts | 284 +++++++++++++++ packages/replay/src/coreHandlers/handleXhr.ts | 19 +- .../replay/src/util/addGlobalListeners.ts | 8 +- .../extendNetworkBreadcrumbs.test.ts | 63 ---- .../handleNetworkBreadcrumbs.test.ts | 341 ++++++++++++++++++ packages/types/src/breadcrumb.ts | 31 ++ packages/types/src/index.ts | 9 +- 12 files changed, 729 insertions(+), 316 deletions(-) delete mode 100644 packages/replay/src/coreHandlers/extendNetworkBreadcrumbs.ts create mode 100644 packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts delete mode 100644 packages/replay/test/unit/coreHandlers/extendNetworkBreadcrumbs.test.ts create mode 100644 packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts diff --git a/packages/browser-integration-tests/suites/replay/requests/subject.js b/packages/browser-integration-tests/suites/replay/requests/subject.js index 7e5b305eabc3..a58f304fc687 100644 --- a/packages/browser-integration-tests/suites/replay/requests/subject.js +++ b/packages/browser-integration-tests/suites/replay/requests/subject.js @@ -6,7 +6,7 @@ document.getElementById('go-background').addEventListener('click', () => { }); document.getElementById('fetch').addEventListener('click', () => { - fetch('https://example.com'); + fetch('https://example.com', { method: 'POST', body: 'foo' }); }); document.getElementById('xhr').addEventListener('click', () => { diff --git a/packages/browser-integration-tests/utils/replayEventTemplates.ts b/packages/browser-integration-tests/utils/replayEventTemplates.ts index c417624f9b9a..8891f5aaeb6d 100644 --- a/packages/browser-integration-tests/utils/replayEventTemplates.ts +++ b/packages/browser-integration-tests/utils/replayEventTemplates.ts @@ -132,23 +132,26 @@ export const expectedFPPerformanceSpan = { export const expectedFetchPerformanceSpan = { op: 'resource.fetch', - description: expect.any(String), + description: 'https://example.com', startTimestamp: expect.any(Number), endTimestamp: expect.any(Number), data: { - method: expect.any(String), - statusCode: expect.any(Number), + method: 'POST', + statusCode: 200, + responseBodySize: 11, + requestBodySize: 3, }, }; export const expectedXHRPerformanceSpan = { op: 'resource.xhr', - description: expect.any(String), + description: 'https://example.com', startTimestamp: expect.any(Number), endTimestamp: expect.any(Number), data: { - method: expect.any(String), - statusCode: expect.any(Number), + method: 'GET', + statusCode: 200, + responseBodySize: 11, }, }; diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index ce79b89ea78d..cfd833ef759a 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -2,6 +2,12 @@ /* eslint-disable max-lines */ import { getCurrentHub } from '@sentry/core'; import type { Event as SentryEvent, HandlerDataFetch, HandlerDataXhr, Integration } from '@sentry/types'; +import type { + FetchBreadcrumbData, + FetchBreadcrumbHint, + XhrBreadcrumbData, + XhrBreadcrumbHint, +} from '@sentry/types/build/types/breadcrumb'; import { addInstrumentationHandler, getEventDescription, @@ -217,27 +223,35 @@ function _consoleBreadcrumb(handlerData: HandlerData & { args: unknown[]; level: * Creates breadcrumbs from XHR API calls */ function _xhrBreadcrumb(handlerData: HandlerData & HandlerDataXhr): void { + const { startTimestamp, endTimestamp } = handlerData; + // We only capture complete, non-sentry requests - if (!handlerData.endTimestamp || !handlerData.xhr.__sentry_xhr__) { + if (!startTimestamp || !endTimestamp || !handlerData.xhr.__sentry_xhr__) { return; } const { method, url, status_code, body } = handlerData.xhr.__sentry_xhr__; + const data: XhrBreadcrumbData = { + method, + url, + status_code, + }; + + const hint: XhrBreadcrumbHint = { + xhr: handlerData.xhr, + input: body, + startTimestamp, + endTimestamp, + }; + getCurrentHub().addBreadcrumb( { category: 'xhr', - data: { - method, - url, - status_code, - }, + data, type: 'http', }, - { - xhr: handlerData.xhr, - input: body, - }, + hint, ); } @@ -245,8 +259,10 @@ function _xhrBreadcrumb(handlerData: HandlerData & HandlerDataXhr): void { * Creates breadcrumbs from fetch API calls */ function _fetchBreadcrumb(handlerData: HandlerData & HandlerDataFetch & { response?: Response }): void { + const { startTimestamp, endTimestamp } = handlerData; + // We only capture complete fetch requests - if (!handlerData.endTimestamp) { + if (!endTimestamp) { return; } @@ -256,32 +272,41 @@ function _fetchBreadcrumb(handlerData: HandlerData & HandlerDataFetch & { respon } if (handlerData.error) { + const data: FetchBreadcrumbData = handlerData.fetchData; + const hint: FetchBreadcrumbHint = { + data: handlerData.error, + input: handlerData.args, + startTimestamp, + endTimestamp, + }; + getCurrentHub().addBreadcrumb( { category: 'fetch', - data: handlerData.fetchData, + data, level: 'error', type: 'http', }, - { - data: handlerData.error, - input: handlerData.args, - }, + hint, ); } else { + const data: FetchBreadcrumbData = { + ...handlerData.fetchData, + status_code: handlerData.response && handlerData.response.status, + }; + const hint: FetchBreadcrumbHint = { + input: handlerData.args, + response: handlerData.response, + startTimestamp, + endTimestamp, + }; getCurrentHub().addBreadcrumb( { category: 'fetch', - data: { - ...handlerData.fetchData, - status_code: handlerData.response && handlerData.response.status, - }, + data, type: 'http', }, - { - input: handlerData.args, - response: handlerData.response, - }, + hint, ); } } diff --git a/packages/replay/src/coreHandlers/extendNetworkBreadcrumbs.ts b/packages/replay/src/coreHandlers/extendNetworkBreadcrumbs.ts deleted file mode 100644 index bc1acb1c7c72..000000000000 --- a/packages/replay/src/coreHandlers/extendNetworkBreadcrumbs.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { getCurrentHub } from '@sentry/core'; -import type { - Breadcrumb, - BreadcrumbHint, - HandlerDataFetch, - SentryWrappedXMLHttpRequest, - TextEncoderInternal, -} from '@sentry/types'; -import { logger } from '@sentry/utils'; - -type RequestBody = null | Blob | BufferSource | FormData | URLSearchParams | string; - -interface ExtendedNetworkBreadcrumbsOptions { - textEncoder: TextEncoderInternal; -} - -/** - * This will enrich the xhr/fetch breadcrumbs with additional information. - * - * This adds: - * * request_body_size - * * response_body_size - * - * to the breadcrumb data. - */ -export function extendNetworkBreadcrumbs(): void { - const client = getCurrentHub().getClient(); - - try { - const textEncoder = new TextEncoder(); - - const options: ExtendedNetworkBreadcrumbsOptions = { - textEncoder, - }; - - if (client && client.on) { - client.on('beforeAddBreadcrumb', (breadcrumb, hint) => _beforeNetworkBreadcrumb(options, breadcrumb, hint)); - } - } catch { - // Do nothing - } -} - -function _beforeNetworkBreadcrumb( - options: ExtendedNetworkBreadcrumbsOptions, - breadcrumb: Breadcrumb, - hint?: BreadcrumbHint, -): void { - if (!breadcrumb.data) { - return; - } - - try { - if (breadcrumb.category === 'xhr' && hint && hint.xhr) { - _enrichXhrBreadcrumb( - breadcrumb as Breadcrumb & { data: object }, - { - xhr: hint.xhr as XMLHttpRequest & SentryWrappedXMLHttpRequest, - body: hint.input as RequestBody, - }, - options, - ); - } - - if (breadcrumb.category === 'fetch' && hint) { - _enrichFetchBreadcrumb( - breadcrumb as Breadcrumb & { data: object }, - { - input: hint.input as HandlerDataFetch['args'], - response: hint.response as Response, - }, - options, - ); - } - } catch (e) { - __DEBUG_BUILD__ && logger.warn('Error when enriching network breadcrumb'); - } -} - -function _enrichXhrBreadcrumb( - breadcrumb: Breadcrumb & { data: object }, - hint: { xhr: XMLHttpRequest & SentryWrappedXMLHttpRequest; body?: RequestBody }, - options: ExtendedNetworkBreadcrumbsOptions, -): void { - const { xhr, body } = hint; - - const reqSize = getBodySize(body, options.textEncoder); - const resSize = xhr.getResponseHeader('content-length') - ? parseContentSizeHeader(xhr.getResponseHeader('content-length')) - : getBodySize(xhr.response, options.textEncoder); - - if (reqSize !== undefined) { - breadcrumb.data.request_body_size = reqSize; - } - if (resSize !== undefined) { - breadcrumb.data.response_body_size = resSize; - } -} - -function _enrichFetchBreadcrumb( - breadcrumb: Breadcrumb & { data: object }, - hint: { - input: HandlerDataFetch['args']; - response: Response; - }, - options: ExtendedNetworkBreadcrumbsOptions, -): void { - const { input, response } = hint; - - const body = getFetchBody(input); - const reqSize = getBodySize(body, options.textEncoder); - const resSize = response ? parseContentSizeHeader(response.headers.get('content-length')) : undefined; - - if (reqSize !== undefined) { - breadcrumb.data.request_body_size = reqSize; - } - if (resSize !== undefined) { - breadcrumb.data.response_body_size = resSize; - } -} - -/** only exported for tests */ -export function getBodySize( - body: RequestInit['body'], - textEncoder: TextEncoder | TextEncoderInternal, -): number | undefined { - if (!body) { - return undefined; - } - - try { - if (typeof body === 'string') { - return textEncoder.encode(body).length; - } - - if (body instanceof URLSearchParams) { - return textEncoder.encode(body.toString()).length; - } - - if (body instanceof FormData) { - // This is a bit simplified, but gives us a decent estimate - // This converts e.g. { name: 'Anne Smith', age: 13 } to 'name=Anne+Smith&age=13' - // @ts-ignore passing FormData to URLSearchParams actually works - const formDataStr = new URLSearchParams(body).toString(); - return textEncoder.encode(formDataStr).length; - } - - if (body instanceof Blob) { - return body.size; - } - - if (body instanceof ArrayBuffer) { - return body.byteLength; - } - - // Currently unhandled types: ArrayBufferView, ReadableStream - } catch { - // just return undefined - } - - return undefined; -} - -/** only exported for tests */ -export function parseContentSizeHeader(header: string | null | undefined): number | undefined { - if (!header) { - return undefined; - } - - const size = parseInt(header, 10); - return isNaN(size) ? undefined : size; -} - -function getFetchBody(fetchArgs: unknown[] = []): RequestInit['body'] | undefined { - // We only support getting the body from the fetch options - if (fetchArgs.length !== 2 || typeof fetchArgs[1] !== 'object') { - return undefined; - } - - return (fetchArgs[1] as RequestInit).body; -} diff --git a/packages/replay/src/coreHandlers/handleFetch.ts b/packages/replay/src/coreHandlers/handleFetch.ts index 4ba163d5c5c9..f7ab52b16376 100644 --- a/packages/replay/src/coreHandlers/handleFetch.ts +++ b/packages/replay/src/coreHandlers/handleFetch.ts @@ -1,8 +1,7 @@ import type { HandlerDataFetch } from '@sentry/types'; import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; -import { createPerformanceSpans } from '../util/createPerformanceSpans'; -import { shouldFilterRequest } from '../util/shouldFilterRequest'; +import { addPerformanceEntryBreadcrumb } from './handleNetworkBreadcrumbs'; /** only exported for tests */ export function handleFetch(handlerData: HandlerDataFetch): null | ReplayPerformanceEntry { @@ -39,20 +38,6 @@ export function handleFetchSpanListener(replay: ReplayContainer): (handlerData: const result = handleFetch(handlerData); - if (result === null) { - return; - } - - if (shouldFilterRequest(replay, result.name)) { - return; - } - - replay.addUpdate(() => { - createPerformanceSpans(replay, [result]); - // Returning true will cause `addUpdate` to not flush - // We do not want network requests to cause a flush. This will prevent - // recurring/polling requests from keeping the replay session alive. - return true; - }); + addPerformanceEntryBreadcrumb(replay, result); }; } diff --git a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts new file mode 100644 index 000000000000..72cc3e9b4f46 --- /dev/null +++ b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts @@ -0,0 +1,284 @@ +import { getCurrentHub } from '@sentry/core'; +import type { + Breadcrumb, + BreadcrumbHint, + FetchBreadcrumbData, + FetchBreadcrumbHint, + HandlerDataFetch, + HandlerDataXhr, + SentryWrappedXMLHttpRequest, + TextEncoderInternal, + XhrBreadcrumbData, + XhrBreadcrumbHint, +} from '@sentry/types'; +import { addInstrumentationHandler, logger } from '@sentry/utils'; + +import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; +import { createPerformanceSpans } from '../util/createPerformanceSpans'; +import { shouldFilterRequest } from '../util/shouldFilterRequest'; +import { handleFetchSpanListener } from './handleFetch'; +import { handleXhrSpanListener } from './handleXhr'; + +type RequestBody = null | Blob | BufferSource | FormData | URLSearchParams | string; + +type XhrHint = XhrBreadcrumbHint & { xhr: XMLHttpRequest & SentryWrappedXMLHttpRequest; input?: RequestBody }; +type FetchHint = FetchBreadcrumbHint & { + input: HandlerDataFetch['args']; + response: Response; +}; + +interface ExtendedNetworkBreadcrumbsOptions { + replay: ReplayContainer; + textEncoder: TextEncoderInternal; +} + +/** + * This will enrich the xhr/fetch breadcrumbs with additional information. + * + * This adds: + * * request_body_size + * * response_body_size + * + * to the breadcrumb data. + */ +export function handleNetworkBreadcrumbs(replay: ReplayContainer): void { + const client = getCurrentHub().getClient(); + + try { + const textEncoder = new TextEncoder(); + + const handleFetch = handleFetchSpanListener(replay); + const handleXhr = handleXhrSpanListener(replay); + + const options: ExtendedNetworkBreadcrumbsOptions = { + replay, + textEncoder, + }; + + if (client && client.on) { + client.on('beforeAddBreadcrumb', (breadcrumb, hint) => handleNetworkBreadcrumb(options, breadcrumb, hint)); + } else { + // Fallback behavior + addInstrumentationHandler('fetch', handleFetch); + addInstrumentationHandler('xhr', handleXhr); + } + } catch { + // Do nothing + } +} + +/** just exported for tests */ +export function handleNetworkBreadcrumb( + options: ExtendedNetworkBreadcrumbsOptions, + breadcrumb: Breadcrumb, + hint?: BreadcrumbHint, +): void { + if (!breadcrumb.data) { + return; + } + + try { + if (_isXhrBreadcrumb(breadcrumb) && _isXhrHint(hint)) { + // Enriches the breadcrumb overall + _enrichXhrBreadcrumb(breadcrumb, hint, options); + + // Create a replay performance entry from this breadcrumb + const result = _makeReplayBreadcrumb('resource.xhr', breadcrumb, hint); + addPerformanceEntryBreadcrumb(options.replay, result); + } + + if (_isFetchBreadcrumb(breadcrumb) && _isFetchHint(hint)) { + // Enriches the breadcrumb overall + _enrichFetchBreadcrumb(breadcrumb, hint, options); + + // Create a replay performance entry from this breadcrumb + const result = _makeReplayBreadcrumb('resource.fetch', breadcrumb, hint); + addPerformanceEntryBreadcrumb(options.replay, result); + } + } catch (e) { + __DEBUG_BUILD__ && logger.warn('Error when enriching network breadcrumb'); + } +} + +function _makeReplayBreadcrumb( + type: string, + breadcrumb: Breadcrumb & { data: FetchBreadcrumbData | XhrBreadcrumbData }, + hint: FetchBreadcrumbHint | XhrBreadcrumbHint, +): ReplayPerformanceEntry | null { + const { startTimestamp, endTimestamp } = hint; + + if (!endTimestamp) { + return null; + } + + const { + url, + method, + status_code: statusCode, + request_body_size: requestBodySize, + response_body_size: responseBodySize, + } = breadcrumb.data; + + if (url === undefined) { + return null; + } + + const result: ReplayPerformanceEntry & { data: object } = { + type, + start: startTimestamp / 1000, + end: endTimestamp / 1000, + name: url, + data: { + method, + statusCode, + }, + }; + + if (requestBodySize) { + result.data.requestBodySize = requestBodySize; + } + if (responseBodySize) { + result.data.responseBodySize = responseBodySize; + } + + return result; +} + +function _enrichXhrBreadcrumb( + breadcrumb: Breadcrumb & { data: XhrBreadcrumbData }, + hint: XhrHint, + options: ExtendedNetworkBreadcrumbsOptions, +): void { + const { xhr, input } = hint; + + const reqSize = getBodySize(input, options.textEncoder); + const resSize = xhr.getResponseHeader('content-length') + ? parseContentSizeHeader(xhr.getResponseHeader('content-length')) + : getBodySize(xhr.response, options.textEncoder); + + if (reqSize !== undefined) { + breadcrumb.data.request_body_size = reqSize; + } + if (resSize !== undefined) { + breadcrumb.data.response_body_size = resSize; + } +} + +function _enrichFetchBreadcrumb( + breadcrumb: Breadcrumb & { data: FetchBreadcrumbData }, + hint: FetchHint, + options: ExtendedNetworkBreadcrumbsOptions, +): void { + const { input, response } = hint; + + const body = getFetchBody(input); + const reqSize = getBodySize(body, options.textEncoder); + const resSize = response ? parseContentSizeHeader(response.headers.get('content-length')) : undefined; + + if (reqSize !== undefined) { + breadcrumb.data.request_body_size = reqSize; + } + if (resSize !== undefined) { + breadcrumb.data.response_body_size = resSize; + } +} + +/** only exported for tests */ +export function getBodySize( + body: RequestInit['body'], + textEncoder: TextEncoder | TextEncoderInternal, +): number | undefined { + if (!body) { + return undefined; + } + + try { + if (typeof body === 'string') { + return textEncoder.encode(body).length; + } + + if (body instanceof URLSearchParams) { + return textEncoder.encode(body.toString()).length; + } + + if (body instanceof FormData) { + // This is a bit simplified, but gives us a decent estimate + // This converts e.g. { name: 'Anne Smith', age: 13 } to 'name=Anne+Smith&age=13' + // @ts-ignore passing FormData to URLSearchParams actually works + const formDataStr = new URLSearchParams(body).toString(); + return textEncoder.encode(formDataStr).length; + } + + if (body instanceof Blob) { + return body.size; + } + + if (body instanceof ArrayBuffer) { + return body.byteLength; + } + + // Currently unhandled types: ArrayBufferView, ReadableStream + } catch { + // just return undefined + } + + return undefined; +} + +/** only exported for tests */ +export function parseContentSizeHeader(header: string | null | undefined): number | undefined { + if (!header) { + return undefined; + } + + const size = parseInt(header, 10); + return isNaN(size) ? undefined : size; +} + +function getFetchBody(fetchArgs: unknown[] = []): RequestInit['body'] | undefined { + // We only support getting the body from the fetch options + if (fetchArgs.length !== 2 || typeof fetchArgs[1] !== 'object') { + return undefined; + } + + return (fetchArgs[1] as RequestInit).body; +} + +/** Add a performance entry breadcrumb */ +export function addPerformanceEntryBreadcrumb(replay: ReplayContainer, result: ReplayPerformanceEntry | null): void { + if (!replay.isEnabled()) { + return; + } + + if (result === null) { + return; + } + + if (shouldFilterRequest(replay, result.name)) { + return; + } + + replay.addUpdate(() => { + createPerformanceSpans(replay, [result]); + // Returning true will cause `addUpdate` to not flush + // We do not want network requests to cause a flush. This will prevent + // recurring/polling requests from keeping the replay session alive. + return true; + }); +} + +function _isXhrBreadcrumb(breadcrumb: Breadcrumb): breadcrumb is Breadcrumb & { data: XhrBreadcrumbData } { + return breadcrumb.category === 'xhr'; +} + +function _isFetchBreadcrumb(breadcrumb: Breadcrumb): breadcrumb is Breadcrumb & { data: FetchBreadcrumbData } { + return breadcrumb.category === 'fetch'; +} + +function _isXhrHint(hint?: BreadcrumbHint): hint is XhrHint { + return hint && hint.xhr; +} + +function _isFetchHint(hint?: BreadcrumbHint): hint is FetchHint { + return hint && hint.response; +} diff --git a/packages/replay/src/coreHandlers/handleXhr.ts b/packages/replay/src/coreHandlers/handleXhr.ts index 1e39ec409ea8..3a3decdfd9ea 100644 --- a/packages/replay/src/coreHandlers/handleXhr.ts +++ b/packages/replay/src/coreHandlers/handleXhr.ts @@ -1,8 +1,7 @@ import type { HandlerDataXhr } from '@sentry/types'; import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; -import { createPerformanceSpans } from '../util/createPerformanceSpans'; -import { shouldFilterRequest } from '../util/shouldFilterRequest'; +import { addPerformanceEntryBreadcrumb } from './handleNetworkBreadcrumbs'; /** only exported for tests */ export function handleXhr(handlerData: HandlerDataXhr): ReplayPerformanceEntry | null { @@ -49,20 +48,6 @@ export function handleXhrSpanListener(replay: ReplayContainer): (handlerData: Ha const result = handleXhr(handlerData); - if (result === null) { - return; - } - - if (shouldFilterRequest(replay, result.name)) { - return; - } - - replay.addUpdate(() => { - createPerformanceSpans(replay, [result]); - // Returning true will cause `addUpdate` to not flush - // We do not want network requests to cause a flush. This will prevent - // recurring/polling requests from keeping the replay session alive. - return true; - }); + addPerformanceEntryBreadcrumb(replay, result); }; } diff --git a/packages/replay/src/util/addGlobalListeners.ts b/packages/replay/src/util/addGlobalListeners.ts index ea685c49241a..341e8dccf3ca 100644 --- a/packages/replay/src/util/addGlobalListeners.ts +++ b/packages/replay/src/util/addGlobalListeners.ts @@ -2,14 +2,12 @@ import type { BaseClient } from '@sentry/core'; import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; import { addInstrumentationHandler } from '@sentry/utils'; -import { extendNetworkBreadcrumbs } from '../coreHandlers/extendNetworkBreadcrumbs'; import { handleAfterSendEvent } from '../coreHandlers/handleAfterSendEvent'; import { handleDomListener } from '../coreHandlers/handleDom'; -import { handleFetchSpanListener } from '../coreHandlers/handleFetch'; import { handleGlobalEventListener } from '../coreHandlers/handleGlobalEvent'; import { handleHistorySpanListener } from '../coreHandlers/handleHistory'; +import { handleNetworkBreadcrumbs } from '../coreHandlers/handleNetworkBreadcrumbs'; import { handleScopeListener } from '../coreHandlers/handleScope'; -import { handleXhrSpanListener } from '../coreHandlers/handleXhr'; import type { ReplayContainer } from '../types'; /** @@ -24,8 +22,6 @@ export function addGlobalListeners(replay: ReplayContainer): void { scope.addScopeListener(handleScopeListener(replay)); } addInstrumentationHandler('dom', handleDomListener(replay)); - addInstrumentationHandler('fetch', handleFetchSpanListener(replay)); - addInstrumentationHandler('xhr', handleXhrSpanListener(replay)); addInstrumentationHandler('history', handleHistorySpanListener(replay)); // If a custom client has no hooks yet, we continue to use the "old" implementation @@ -40,5 +36,5 @@ export function addGlobalListeners(replay: ReplayContainer): void { (client as BaseClient).on('afterSendEvent', handleAfterSendEvent(replay)); } - extendNetworkBreadcrumbs(); + handleNetworkBreadcrumbs(replay); } diff --git a/packages/replay/test/unit/coreHandlers/extendNetworkBreadcrumbs.test.ts b/packages/replay/test/unit/coreHandlers/extendNetworkBreadcrumbs.test.ts deleted file mode 100644 index a8d6885d43c2..000000000000 --- a/packages/replay/test/unit/coreHandlers/extendNetworkBreadcrumbs.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { TextEncoder } from 'util'; - -import { getBodySize, parseContentSizeHeader } from '../../../src/coreHandlers/extendNetworkBreadcrumbs'; - -describe('Unit | coreHandlers | extendNetworkBreadcrumbs', () => { - describe('parseContentSizeHeader()', () => { - it.each([ - [undefined, undefined], - [null, undefined], - ['', undefined], - ['12', 12], - ['abc', undefined], - ])('works with %s header value', (headerValue, size) => { - expect(parseContentSizeHeader(headerValue)).toBe(size); - }); - }); - - describe('getBodySize()', () => { - const textEncoder = new TextEncoder(); - - it('works with empty body', () => { - expect(getBodySize(undefined, textEncoder)).toBe(undefined); - expect(getBodySize(null, textEncoder)).toBe(undefined); - expect(getBodySize('', textEncoder)).toBe(undefined); - }); - - it('works with string body', () => { - expect(getBodySize('abcd', textEncoder)).toBe(4); - // Emojis are correctly counted as mutliple characters - expect(getBodySize('With emoji: 😈', textEncoder)).toBe(16); - }); - - it('works with URLSearchParams', () => { - const params = new URLSearchParams(); - params.append('name', 'Jane'); - params.append('age', '42'); - params.append('emoji', '😈'); - - expect(getBodySize(params, textEncoder)).toBe(35); - }); - - it('works with FormData', () => { - const formData = new FormData(); - formData.append('name', 'Jane'); - formData.append('age', '42'); - formData.append('emoji', '😈'); - - expect(getBodySize(formData, textEncoder)).toBe(35); - }); - - it('works with Blob', () => { - const blob = new Blob(['Hello world: 😈'], { type: 'text/html' }); - - expect(getBodySize(blob, textEncoder)).toBe(30); - }); - - it('works with ArrayBuffer', () => { - const arrayBuffer = new ArrayBuffer(8); - - expect(getBodySize(arrayBuffer, textEncoder)).toBe(8); - }); - }); -}); diff --git a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts new file mode 100644 index 000000000000..25910eac1bf2 --- /dev/null +++ b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts @@ -0,0 +1,341 @@ +import { TextEncoder } from 'util'; + +import { + getBodySize, + parseContentSizeHeader, + handleNetworkBreadcrumb, +} from '../../../src/coreHandlers/handleNetworkBreadcrumbs'; +import { + Breadcrumb, + BreadcrumbHint, + TextEncoderInternal, + XhrBreadcrumbData, + XhrBreadcrumbHint, + FetchBreadcrumbHint, +} from '@sentry/types'; +import { setupReplayContainer } from '../../utils/setupReplayContainer'; +import { ReplayContainer } from '../../../src/types'; +import { EventBufferArray } from '../../../src/eventBuffer/EventBufferArray'; +import { BASE_TIMESTAMP } from '../..'; + +jest.useFakeTimers(); + +describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => { + describe('parseContentSizeHeader()', () => { + it.each([ + [undefined, undefined], + [null, undefined], + ['', undefined], + ['12', 12], + ['abc', undefined], + ])('works with %s header value', (headerValue, size) => { + expect(parseContentSizeHeader(headerValue)).toBe(size); + }); + }); + + describe('getBodySize()', () => { + const textEncoder = new TextEncoder(); + + it('works with empty body', () => { + expect(getBodySize(undefined, textEncoder)).toBe(undefined); + expect(getBodySize(null, textEncoder)).toBe(undefined); + expect(getBodySize('', textEncoder)).toBe(undefined); + }); + + it('works with string body', () => { + expect(getBodySize('abcd', textEncoder)).toBe(4); + // Emojis are correctly counted as mutliple characters + expect(getBodySize('With emoji: 😈', textEncoder)).toBe(16); + }); + + it('works with URLSearchParams', () => { + const params = new URLSearchParams(); + params.append('name', 'Jane'); + params.append('age', '42'); + params.append('emoji', '😈'); + + expect(getBodySize(params, textEncoder)).toBe(35); + }); + + it('works with FormData', () => { + const formData = new FormData(); + formData.append('name', 'Jane'); + formData.append('age', '42'); + formData.append('emoji', '😈'); + + expect(getBodySize(formData, textEncoder)).toBe(35); + }); + + it('works with Blob', () => { + const blob = new Blob(['Hello world: 😈'], { type: 'text/html' }); + + expect(getBodySize(blob, textEncoder)).toBe(30); + }); + + it('works with ArrayBuffer', () => { + const arrayBuffer = new ArrayBuffer(8); + + expect(getBodySize(arrayBuffer, textEncoder)).toBe(8); + }); + }); + + describe('handleNetworkBreadcrumb()', () => { + let options: { + replay: ReplayContainer; + textEncoder: TextEncoderInternal; + }; + + beforeEach(() => { + jest.setSystemTime(BASE_TIMESTAMP); + + options = { + textEncoder: new TextEncoder(), + replay: setupReplayContainer(), + }; + + jest.runAllTimers(); + }); + + it('ignores breadcrumb without data', () => { + const breadcrumb: Breadcrumb = {}; + const hint: BreadcrumbHint = {}; + handleNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({}); + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([]); + }); + + it('ignores non-network breadcrumbs', () => { + const breadcrumb: Breadcrumb = { + category: 'foo', + data: {}, + }; + const hint: BreadcrumbHint = {}; + handleNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({ + category: 'foo', + data: {}, + }); + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([]); + }); + + it('handles full xhr breadcrumb', async () => { + const breadcrumb: Breadcrumb = { + category: 'xhr', + data: { + method: 'GET', + url: 'https://example.com', + status_code: 200, + }, + }; + const xhr = new XMLHttpRequest(); + Object.defineProperty(xhr, 'response', { + value: 'test response', + }); + const hint: XhrBreadcrumbHint = { + xhr, + input: 'test input', + startTimestamp: BASE_TIMESTAMP + 1000, + endTimestamp: BASE_TIMESTAMP + 2000, + }; + handleNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({ + category: 'xhr', + data: { + method: 'GET', + request_body_size: 10, + response_body_size: 13, + status_code: 200, + url: 'https://example.com', + }, + }); + + jest.runAllTimers(); + + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([ + { + type: 5, + timestamp: (BASE_TIMESTAMP + 1000) / 1000, + data: { + tag: 'performanceSpan', + payload: { + data: { + method: 'GET', + requestBodySize: 10, + responseBodySize: 13, + statusCode: 200, + }, + description: 'https://example.com', + endTimestamp: (BASE_TIMESTAMP + 2000) / 1000, + op: 'resource.xhr', + startTimestamp: (BASE_TIMESTAMP + 1000) / 1000, + }, + }, + }, + ]); + }); + + it('handles minimal xhr breadcrumb', async () => { + const breadcrumb: Breadcrumb = { + category: 'xhr', + data: { + url: 'https://example.com', + status_code: 200, + }, + }; + const xhr = new XMLHttpRequest(); + + const hint: XhrBreadcrumbHint = { + xhr, + input: undefined, + startTimestamp: BASE_TIMESTAMP + 1000, + endTimestamp: BASE_TIMESTAMP + 2000, + }; + handleNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({ + category: 'xhr', + data: { + status_code: 200, + url: 'https://example.com', + }, + }); + + jest.runAllTimers(); + + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([ + { + type: 5, + timestamp: (BASE_TIMESTAMP + 1000) / 1000, + data: { + tag: 'performanceSpan', + payload: { + data: { + statusCode: 200, + }, + description: 'https://example.com', + endTimestamp: (BASE_TIMESTAMP + 2000) / 1000, + op: 'resource.xhr', + startTimestamp: (BASE_TIMESTAMP + 1000) / 1000, + }, + }, + }, + ]); + }); + + it('handles full fetch breadcrumb', async () => { + const breadcrumb: Breadcrumb = { + category: 'fetch', + data: { + method: 'GET', + url: 'https://example.com', + status_code: 200, + }, + }; + + const mockResponse = { + headers: { + get: () => '13', + }, + } as unknown as Response; + + const hint: FetchBreadcrumbHint = { + input: ['GET', { body: 'test input' }], + response: mockResponse, + startTimestamp: BASE_TIMESTAMP + 1000, + endTimestamp: BASE_TIMESTAMP + 2000, + }; + handleNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({ + category: 'fetch', + data: { + method: 'GET', + request_body_size: 10, + response_body_size: 13, + status_code: 200, + url: 'https://example.com', + }, + }); + + jest.runAllTimers(); + + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([ + { + type: 5, + timestamp: (BASE_TIMESTAMP + 1000) / 1000, + data: { + tag: 'performanceSpan', + payload: { + data: { + method: 'GET', + requestBodySize: 10, + responseBodySize: 13, + statusCode: 200, + }, + description: 'https://example.com', + endTimestamp: (BASE_TIMESTAMP + 2000) / 1000, + op: 'resource.fetch', + startTimestamp: (BASE_TIMESTAMP + 1000) / 1000, + }, + }, + }, + ]); + }); + + it('handles minimal fetch breadcrumb', async () => { + const breadcrumb: Breadcrumb = { + category: 'fetch', + data: { + url: 'https://example.com', + status_code: 200, + }, + }; + + const mockResponse = { + headers: { + get: () => '', + }, + } as unknown as Response; + + const hint: FetchBreadcrumbHint = { + input: [], + response: mockResponse, + startTimestamp: BASE_TIMESTAMP + 1000, + endTimestamp: BASE_TIMESTAMP + 2000, + }; + handleNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({ + category: 'fetch', + data: { + status_code: 200, + url: 'https://example.com', + }, + }); + + jest.runAllTimers(); + + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([ + { + type: 5, + timestamp: (BASE_TIMESTAMP + 1000) / 1000, + data: { + tag: 'performanceSpan', + payload: { + data: { + statusCode: 200, + }, + description: 'https://example.com', + endTimestamp: (BASE_TIMESTAMP + 2000) / 1000, + op: 'resource.fetch', + startTimestamp: (BASE_TIMESTAMP + 1000) / 1000, + }, + }, + }, + ]); + }); + }); +}); diff --git a/packages/types/src/breadcrumb.ts b/packages/types/src/breadcrumb.ts index b8e2552a2f34..9f9b36bd6dcb 100644 --- a/packages/types/src/breadcrumb.ts +++ b/packages/types/src/breadcrumb.ts @@ -16,3 +16,34 @@ export interface Breadcrumb { export interface BreadcrumbHint { [key: string]: any; } + +export interface FetchBreadcrumbData { + method: string; + url: string; + status_code?: number; + request_body_size?: number; + response_body_size?: number; +} + +export interface XhrBreadcrumbData { + method?: string; + url?: string; + status_code?: number; + request_body_size?: number; + response_body_size?: number; +} + +export interface FetchBreadcrumbHint { + input: any[]; + data?: unknown; + response?: unknown; + startTimestamp: number; + endTimestamp: number; +} + +export interface XhrBreadcrumbHint { + xhr: unknown; + input: unknown; + startTimestamp: number; + endTimestamp: number; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 6cb59b9931f8..e62fe6390ac8 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,5 +1,12 @@ export type { Attachment } from './attachment'; -export type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; +export type { + Breadcrumb, + BreadcrumbHint, + FetchBreadcrumbData, + XhrBreadcrumbData, + FetchBreadcrumbHint, + XhrBreadcrumbHint, +} from './breadcrumb'; export type { Client } from './client'; export type { ClientReport, Outcome, EventDropReason } from './clientreport'; export type { Context, Contexts, DeviceContext, OsContext, AppContext, CultureContext, TraceContext } from './context'; From 755e29bda4bed876bd385265327a75964de685e8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 17 Mar 2023 08:20:12 +0100 Subject: [PATCH 2/5] PR feedback --- packages/replay/src/coreHandlers/handleFetch.ts | 4 ++-- .../replay/src/coreHandlers/handleNetworkBreadcrumbs.ts | 6 +++--- packages/replay/src/coreHandlers/handleXhr.ts | 4 ++-- packages/replay/src/util/addGlobalListeners.ts | 3 +-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/replay/src/coreHandlers/handleFetch.ts b/packages/replay/src/coreHandlers/handleFetch.ts index f7ab52b16376..935bff56bea7 100644 --- a/packages/replay/src/coreHandlers/handleFetch.ts +++ b/packages/replay/src/coreHandlers/handleFetch.ts @@ -1,7 +1,7 @@ import type { HandlerDataFetch } from '@sentry/types'; import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; -import { addPerformanceEntryBreadcrumb } from './handleNetworkBreadcrumbs'; +import { addNetworkBreadcrumb } from './handleNetworkBreadcrumbs'; /** only exported for tests */ export function handleFetch(handlerData: HandlerDataFetch): null | ReplayPerformanceEntry { @@ -38,6 +38,6 @@ export function handleFetchSpanListener(replay: ReplayContainer): (handlerData: const result = handleFetch(handlerData); - addPerformanceEntryBreadcrumb(replay, result); + addNetworkBreadcrumb(replay, result); }; } diff --git a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts index 72cc3e9b4f46..b01c959b6c4a 100644 --- a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts +++ b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts @@ -84,7 +84,7 @@ export function handleNetworkBreadcrumb( // Create a replay performance entry from this breadcrumb const result = _makeReplayBreadcrumb('resource.xhr', breadcrumb, hint); - addPerformanceEntryBreadcrumb(options.replay, result); + addNetworkBreadcrumb(options.replay, result); } if (_isFetchBreadcrumb(breadcrumb) && _isFetchHint(hint)) { @@ -93,7 +93,7 @@ export function handleNetworkBreadcrumb( // Create a replay performance entry from this breadcrumb const result = _makeReplayBreadcrumb('resource.fetch', breadcrumb, hint); - addPerformanceEntryBreadcrumb(options.replay, result); + addNetworkBreadcrumb(options.replay, result); } } catch (e) { __DEBUG_BUILD__ && logger.warn('Error when enriching network breadcrumb'); @@ -245,7 +245,7 @@ function getFetchBody(fetchArgs: unknown[] = []): RequestInit['body'] | undefine } /** Add a performance entry breadcrumb */ -export function addPerformanceEntryBreadcrumb(replay: ReplayContainer, result: ReplayPerformanceEntry | null): void { +export function addNetworkBreadcrumb(replay: ReplayContainer, result: ReplayPerformanceEntry | null): void { if (!replay.isEnabled()) { return; } diff --git a/packages/replay/src/coreHandlers/handleXhr.ts b/packages/replay/src/coreHandlers/handleXhr.ts index 3a3decdfd9ea..c42bc0bbdceb 100644 --- a/packages/replay/src/coreHandlers/handleXhr.ts +++ b/packages/replay/src/coreHandlers/handleXhr.ts @@ -1,7 +1,7 @@ import type { HandlerDataXhr } from '@sentry/types'; import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; -import { addPerformanceEntryBreadcrumb } from './handleNetworkBreadcrumbs'; +import { addNetworkBreadcrumb } from './handleNetworkBreadcrumbs'; /** only exported for tests */ export function handleXhr(handlerData: HandlerDataXhr): ReplayPerformanceEntry | null { @@ -48,6 +48,6 @@ export function handleXhrSpanListener(replay: ReplayContainer): (handlerData: Ha const result = handleXhr(handlerData); - addPerformanceEntryBreadcrumb(replay, result); + addNetworkBreadcrumb(replay, result); }; } diff --git a/packages/replay/src/util/addGlobalListeners.ts b/packages/replay/src/util/addGlobalListeners.ts index 341e8dccf3ca..fc68f322d090 100644 --- a/packages/replay/src/util/addGlobalListeners.ts +++ b/packages/replay/src/util/addGlobalListeners.ts @@ -23,6 +23,7 @@ export function addGlobalListeners(replay: ReplayContainer): void { } addInstrumentationHandler('dom', handleDomListener(replay)); addInstrumentationHandler('history', handleHistorySpanListener(replay)); + handleNetworkBreadcrumbs(replay); // If a custom client has no hooks yet, we continue to use the "old" implementation const hasHooks = !!(client && client.on); @@ -35,6 +36,4 @@ export function addGlobalListeners(replay: ReplayContainer): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any (client as BaseClient).on('afterSendEvent', handleAfterSendEvent(replay)); } - - handleNetworkBreadcrumbs(replay); } From ae054000a1dfbeb1a9f54f398b39f5c75a0e7512 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 17 Mar 2023 09:13:58 +0100 Subject: [PATCH 3/5] fix linting & circular dependency check --- .../src/coreHandlers/addNetworkBreadcrumb.ts | 26 ++++++++++++++++++ .../replay/src/coreHandlers/handleFetch.ts | 2 +- .../coreHandlers/handleNetworkBreadcrumbs.ts | 27 +------------------ packages/replay/src/coreHandlers/handleXhr.ts | 2 +- .../handleNetworkBreadcrumbs.test.ts | 23 ++++++++-------- 5 files changed, 40 insertions(+), 40 deletions(-) create mode 100644 packages/replay/src/coreHandlers/addNetworkBreadcrumb.ts diff --git a/packages/replay/src/coreHandlers/addNetworkBreadcrumb.ts b/packages/replay/src/coreHandlers/addNetworkBreadcrumb.ts new file mode 100644 index 000000000000..19f5a17a1c9f --- /dev/null +++ b/packages/replay/src/coreHandlers/addNetworkBreadcrumb.ts @@ -0,0 +1,26 @@ +import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; +import { createPerformanceSpans } from '../util/createPerformanceSpans'; +import { shouldFilterRequest } from '../util/shouldFilterRequest'; + +/** Add a performance entry breadcrumb */ +export function addNetworkBreadcrumb(replay: ReplayContainer, result: ReplayPerformanceEntry | null): void { + if (!replay.isEnabled()) { + return; + } + + if (result === null) { + return; + } + + if (shouldFilterRequest(replay, result.name)) { + return; + } + + replay.addUpdate(() => { + createPerformanceSpans(replay, [result]); + // Returning true will cause `addUpdate` to not flush + // We do not want network requests to cause a flush. This will prevent + // recurring/polling requests from keeping the replay session alive. + return true; + }); +} diff --git a/packages/replay/src/coreHandlers/handleFetch.ts b/packages/replay/src/coreHandlers/handleFetch.ts index 935bff56bea7..2569706d7c33 100644 --- a/packages/replay/src/coreHandlers/handleFetch.ts +++ b/packages/replay/src/coreHandlers/handleFetch.ts @@ -1,7 +1,7 @@ import type { HandlerDataFetch } from '@sentry/types'; import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; -import { addNetworkBreadcrumb } from './handleNetworkBreadcrumbs'; +import { addNetworkBreadcrumb } from './addNetworkBreadcrumb'; /** only exported for tests */ export function handleFetch(handlerData: HandlerDataFetch): null | ReplayPerformanceEntry { diff --git a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts index b01c959b6c4a..f62c7a7e720d 100644 --- a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts +++ b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts @@ -5,7 +5,6 @@ import type { FetchBreadcrumbData, FetchBreadcrumbHint, HandlerDataFetch, - HandlerDataXhr, SentryWrappedXMLHttpRequest, TextEncoderInternal, XhrBreadcrumbData, @@ -14,8 +13,7 @@ import type { import { addInstrumentationHandler, logger } from '@sentry/utils'; import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; -import { createPerformanceSpans } from '../util/createPerformanceSpans'; -import { shouldFilterRequest } from '../util/shouldFilterRequest'; +import { addNetworkBreadcrumb } from './addNetworkBreadcrumb'; import { handleFetchSpanListener } from './handleFetch'; import { handleXhrSpanListener } from './handleXhr'; @@ -244,29 +242,6 @@ function getFetchBody(fetchArgs: unknown[] = []): RequestInit['body'] | undefine return (fetchArgs[1] as RequestInit).body; } -/** Add a performance entry breadcrumb */ -export function addNetworkBreadcrumb(replay: ReplayContainer, result: ReplayPerformanceEntry | null): void { - if (!replay.isEnabled()) { - return; - } - - if (result === null) { - return; - } - - if (shouldFilterRequest(replay, result.name)) { - return; - } - - replay.addUpdate(() => { - createPerformanceSpans(replay, [result]); - // Returning true will cause `addUpdate` to not flush - // We do not want network requests to cause a flush. This will prevent - // recurring/polling requests from keeping the replay session alive. - return true; - }); -} - function _isXhrBreadcrumb(breadcrumb: Breadcrumb): breadcrumb is Breadcrumb & { data: XhrBreadcrumbData } { return breadcrumb.category === 'xhr'; } diff --git a/packages/replay/src/coreHandlers/handleXhr.ts b/packages/replay/src/coreHandlers/handleXhr.ts index c42bc0bbdceb..f78c41fc831e 100644 --- a/packages/replay/src/coreHandlers/handleXhr.ts +++ b/packages/replay/src/coreHandlers/handleXhr.ts @@ -1,7 +1,7 @@ import type { HandlerDataXhr } from '@sentry/types'; import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; -import { addNetworkBreadcrumb } from './handleNetworkBreadcrumbs'; +import { addNetworkBreadcrumb } from './addNetworkBreadcrumb'; /** only exported for tests */ export function handleXhr(handlerData: HandlerDataXhr): ReplayPerformanceEntry | null { diff --git a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts index 25910eac1bf2..d0a7b5cedca1 100644 --- a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts +++ b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts @@ -1,22 +1,21 @@ +import type { + Breadcrumb, + BreadcrumbHint, + FetchBreadcrumbHint, + TextEncoderInternal, + XhrBreadcrumbHint, +} from '@sentry/types'; import { TextEncoder } from 'util'; +import { BASE_TIMESTAMP } from '../..'; import { getBodySize, - parseContentSizeHeader, handleNetworkBreadcrumb, + parseContentSizeHeader, } from '../../../src/coreHandlers/handleNetworkBreadcrumbs'; -import { - Breadcrumb, - BreadcrumbHint, - TextEncoderInternal, - XhrBreadcrumbData, - XhrBreadcrumbHint, - FetchBreadcrumbHint, -} from '@sentry/types'; +import type { EventBufferArray } from '../../../src/eventBuffer/EventBufferArray'; +import type { ReplayContainer } from '../../../src/types'; import { setupReplayContainer } from '../../utils/setupReplayContainer'; -import { ReplayContainer } from '../../../src/types'; -import { EventBufferArray } from '../../../src/eventBuffer/EventBufferArray'; -import { BASE_TIMESTAMP } from '../..'; jest.useFakeTimers(); From d768343b7328b080aeb3f8b8165797b84b1151ff Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 17 Mar 2023 12:05:19 +0100 Subject: [PATCH 4/5] rename --- .../replay/src/coreHandlers/handleNetworkBreadcrumbs.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts index f62c7a7e720d..e6f21378b866 100644 --- a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts +++ b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts @@ -81,7 +81,7 @@ export function handleNetworkBreadcrumb( _enrichXhrBreadcrumb(breadcrumb, hint, options); // Create a replay performance entry from this breadcrumb - const result = _makeReplayBreadcrumb('resource.xhr', breadcrumb, hint); + const result = _makeNetworkReplayBreadcrumb('resource.xhr', breadcrumb, hint); addNetworkBreadcrumb(options.replay, result); } @@ -90,7 +90,7 @@ export function handleNetworkBreadcrumb( _enrichFetchBreadcrumb(breadcrumb, hint, options); // Create a replay performance entry from this breadcrumb - const result = _makeReplayBreadcrumb('resource.fetch', breadcrumb, hint); + const result = _makeNetworkReplayBreadcrumb('resource.fetch', breadcrumb, hint); addNetworkBreadcrumb(options.replay, result); } } catch (e) { @@ -98,7 +98,7 @@ export function handleNetworkBreadcrumb( } } -function _makeReplayBreadcrumb( +function _makeNetworkReplayBreadcrumb( type: string, breadcrumb: Breadcrumb & { data: FetchBreadcrumbData | XhrBreadcrumbData }, hint: FetchBreadcrumbHint | XhrBreadcrumbHint, From e9ce530c8f29fcb58a0b35cb77f1525e127ea1b9 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 17 Mar 2023 13:45:17 +0100 Subject: [PATCH 5/5] avoid unecessary setup --- .../replay/src/coreHandlers/handleNetworkBreadcrumbs.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts index e6f21378b866..0ee72f9edc9c 100644 --- a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts +++ b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts @@ -45,9 +45,6 @@ export function handleNetworkBreadcrumbs(replay: ReplayContainer): void { try { const textEncoder = new TextEncoder(); - const handleFetch = handleFetchSpanListener(replay); - const handleXhr = handleXhrSpanListener(replay); - const options: ExtendedNetworkBreadcrumbsOptions = { replay, textEncoder, @@ -57,8 +54,8 @@ export function handleNetworkBreadcrumbs(replay: ReplayContainer): void { client.on('beforeAddBreadcrumb', (breadcrumb, hint) => handleNetworkBreadcrumb(options, breadcrumb, hint)); } else { // Fallback behavior - addInstrumentationHandler('fetch', handleFetch); - addInstrumentationHandler('xhr', handleXhr); + addInstrumentationHandler('fetch', handleFetchSpanListener(replay)); + addInstrumentationHandler('xhr', handleXhrSpanListener(replay)); } } catch { // Do nothing