diff --git a/src/extensions/replay/config.ts b/src/extensions/replay/config.ts index 694858475..b6921640e 100644 --- a/src/extensions/replay/config.ts +++ b/src/extensions/replay/config.ts @@ -4,29 +4,8 @@ import { convertToURL } from '../../utils/request-utils' import { logger } from '../../utils/logger' export const defaultNetworkOptions: NetworkRecordOptions = { - initiatorTypes: [ - 'audio', - 'beacon', - 'body', - 'css', - 'early-hint', - 'embed', - 'fetch', - 'frame', - 'iframe', - 'icon', - 'image', - 'img', - 'input', - 'link', - 'navigation', - 'object', - 'ping', - 'script', - 'track', - 'video', - 'xmlhttprequest', - ], + // by default, no initiator types are ignored + initiatorTypes: undefined, maskRequestFn: (data: CapturedNetworkRequest) => data, recordHeaders: false, recordBody: false, diff --git a/src/loader-recorder-v2.ts b/src/loader-recorder-v2.ts index 65ff7991b..f1bc07482 100644 --- a/src/loader-recorder-v2.ts +++ b/src/loader-recorder-v2.ts @@ -42,6 +42,8 @@ export type NetworkData = { type networkCallback = (data: NetworkData) => void +const _noop = () => {} + const isNavigationTiming = (entry: PerformanceEntry): entry is PerformanceNavigationTiming => entry.entryType === 'navigation' const isResourceTiming = (entry: PerformanceEntry): entry is PerformanceResourceTiming => entry.entryType === 'resource' @@ -60,9 +62,7 @@ export function patch( ): () => void { try { if (!(name in source)) { - return () => { - // - } + return _noop } const original = source[name] as () => unknown @@ -87,9 +87,7 @@ export function patch( source[name] = original } } catch { - return () => { - // - } + return _noop // This can throw if multiple fill happens on a global object like XMLHttpRequest // Fixes https://github.com/getsentry/sentry-javascript/issues/2043 } @@ -105,7 +103,31 @@ export function findLast(array: Array, predicate: (value: T) => boolean): return undefined } -function initPerformanceObserver(cb: networkCallback, win: IWindow, options: Required) { +function filterPerformanceEntries(entries: PerformanceEntryList, options: NetworkRecordOptions, isInitial?: boolean) { + return entries.filter( + (entry): entry is ObservedPerformanceEntry => + isNavigationTiming(entry) || + (isResourceTiming(entry) && + // if initiatorTypes allow list is defined then check it, otherwise allow all + (options.initiatorTypes || [entry.initiatorType]).includes(entry.initiatorType as InitiatorType) && + // if not the initial check, then use the wrapped filter check to see if we should record this entry + !!isInitial + ? true + : wrappedInitiatorFilter(entry, options)) + ) +} + +/** + * if recordBody or recordHeaders is true then we don't want to record fetch or xhr + * in the performance observer as the wrapped functions will do that. + * Otherwise, this filter becomes a noop because we do want to record them + */ +const wrappedInitiatorFilter = (entry: PerformanceEntry | ObservedPerformanceEntry, options: NetworkRecordOptions) => + isResourceTiming(entry) && (options.recordBody || options.recordHeaders) + ? entry.initiatorType !== 'xmlhttprequest' && entry.initiatorType !== 'fetch' + : true + +function initPerformanceObserver(cb: networkCallback, win: IWindow, options: NetworkRecordOptions) { // if we are only observing timings then we could have a single observer for all types, with buffer true, // but we are going to filter by initiatorType _if we are wrapping fetch and xhr as the wrapped functions // will deal with those. @@ -113,13 +135,7 @@ function initPerformanceObserver(cb: networkCallback, win: IWindow, options: Req // these are marked `isInitial` so playback can display them differently if needed // they will never have method/status/headers/body because they are pre-wrapping that provides that if (options.recordInitialRequests) { - const initialPerformanceEntries = win.performance - .getEntries() - .filter( - (entry): entry is ObservedPerformanceEntry => - isNavigationTiming(entry) || - (isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType as InitiatorType)) - ) + const initialPerformanceEntries = filterPerformanceEntries(win.performance.getEntries(), options) cb({ requests: initialPerformanceEntries.flatMap((entry) => prepareRequest(entry, undefined, undefined, {}, true) @@ -128,22 +144,7 @@ function initPerformanceObserver(cb: networkCallback, win: IWindow, options: Req }) } const observer = new win.PerformanceObserver((entries) => { - // if recordBody or recordHeaders is true then we don't want to record fetch or xhr here - // as the wrapped functions will do that. Otherwise, this filter becomes a noop - // because we do want to record them here - const wrappedInitiatorFilter = (entry: ObservedPerformanceEntry) => - options.recordBody || options.recordHeaders - ? entry.initiatorType !== 'xmlhttprequest' && entry.initiatorType !== 'fetch' - : true - - const performanceEntries = entries.getEntries().filter( - (entry): entry is ObservedPerformanceEntry => - isNavigationTiming(entry) || - (isResourceTiming(entry) && - options.initiatorTypes.includes(entry.initiatorType as InitiatorType) && - // TODO if we are _only_ capturing timing we don't want to filter initiator here - wrappedInitiatorFilter(entry)) - ) + const performanceEntries = filterPerformanceEntries(entries.getEntries(), options) cb({ requests: performanceEntries.flatMap((entry) => prepareRequest(entry, undefined, undefined, {})), @@ -247,11 +248,9 @@ function _tryReadXHRBody(body: Document | XMLHttpRequestBodyInit | any | null | return '[SessionReplay] Cannot read body of type ' + toString.call(body) } -function initXhrObserver(cb: networkCallback, win: IWindow, options: Required): listenerHandler { - if (!options.initiatorTypes.includes('xmlhttprequest')) { - return () => { - // - } +function initXhrObserver(cb: networkCallback, win: IWindow, options: NetworkRecordOptions): listenerHandler { + if (options.initiatorTypes && !options.initiatorTypes.includes('xmlhttprequest')) { + return _noop } const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders) const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders) @@ -339,9 +338,7 @@ function initXhrObserver(cb: networkCallback, win: IWindow, options: Required { - // - }) + .catch(_noop) }) originalOpen.call(xhr, method, url, async, username, password) } @@ -457,15 +454,9 @@ async function _tryReadResponseBody(r: Response): Promise { return _tryReadBody(r) } -function initFetchObserver( - cb: networkCallback, - win: IWindow, - options: Required -): listenerHandler { - if (!options.initiatorTypes.includes('fetch')) { - return () => { - // - } +function initFetchObserver(cb: networkCallback, win: IWindow, options: NetworkRecordOptions): listenerHandler { + if (options.initiatorTypes && !options.initiatorTypes.includes('fetch')) { + return _noop } const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders) const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders) @@ -517,9 +508,7 @@ function initFetchObserver( const requests = prepareRequest(entry, req.method, res?.status, networkRequest) cb({ requests }) }) - .catch(() => { - // - }) + .catch(_noop) } } }) @@ -534,9 +523,7 @@ function initNetworkObserver( options: NetworkRecordOptions ): listenerHandler { if (!('performance' in win)) { - return () => { - // - } + return _noop } const networkOptions = ( options ? Object.assign({}, defaultNetworkOptions, options) : defaultNetworkOptions @@ -558,8 +545,8 @@ function initNetworkObserver( const performanceObserver = initPerformanceObserver(cb, win, networkOptions) // only wrap fetch and xhr if headers or body are being recorded - let xhrObserver: listenerHandler = () => {} - let fetchObserver: listenerHandler = () => {} + let xhrObserver: listenerHandler = _noop + let fetchObserver: listenerHandler = _noop if (networkOptions.recordHeaders || networkOptions.recordBody) { xhrObserver = initXhrObserver(cb, win, networkOptions) fetchObserver = initFetchObserver(cb, win, networkOptions) diff --git a/src/types.ts b/src/types.ts index 189a18044..1884d73b1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -401,6 +401,11 @@ export type InitiatorType = | 'xmlhttprequest' export type NetworkRecordOptions = { + // controls which initiator types are recorded + // if not set, all initiator types are recorded + // if the array is empty, no initiator types are recorded + // if 'xmlhttprequest' is not in the array, no XHR requests will be recorded + // if 'fetch' is not in the array, no fetch requests will be recorded initiatorTypes?: InitiatorType[] maskRequestFn?: (data: CapturedNetworkRequest) => CapturedNetworkRequest | undefined recordHeaders?: boolean | { request: boolean; response: boolean }