From 772269a0480ca09a604ccc75c3d84d56956b0c81 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 22 May 2024 11:15:36 -0230 Subject: [PATCH] feat(replay): Use unwrapped `setTimeout` to avoid e.g. angular change detection (#11924) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR makes sure we use the native, unwrapped `setTimeout` implementation of the browser. Some environments, e.g. Angular, monkey patch this for their change detection, leading to performance issues (possibly). We have already changed this in rrweb, but we also have some usage of this in replay itself. This PR _should_ work fine, however all test fail today because we heavily use `jest.useFakeTimers()`, which basically monkey patches fetch too. So with this change, we do not use the patched timers, leading to everything blowing up 🤯 Based on https://github.com/getsentry/sentry-javascript/pull/11864 Depends on https://github.com/getsentry/sentry-javascript/pull/11899 --------- Co-authored-by: Francesco Novy --- .../src/getNativeImplementation.ts | 131 ++++++++++++++++++ packages/browser-utils/src/index.ts | 7 +- packages/browser-utils/src/instrument/dom.ts | 2 +- .../browser-utils/src/instrument/history.ts | 2 +- packages/browser-utils/src/instrument/xhr.ts | 2 +- .../src/metrics/browserMetrics.ts | 2 +- packages/browser-utils/src/metrics/utils.ts | 2 +- .../src/metrics/web-vitals/getINP.ts | 2 +- .../src/metrics/web-vitals/getLCP.ts | 2 +- .../web-vitals/lib/getNavigationEntry.ts | 2 +- .../web-vitals/lib/getVisibilityWatcher.ts | 2 +- .../src/metrics/web-vitals/lib/initMetric.ts | 2 +- .../src/metrics/web-vitals/lib/onHidden.ts | 2 +- .../metrics/web-vitals/lib/whenActivated.ts | 2 +- .../src/metrics/web-vitals/onTTFB.ts | 2 +- .../browser-utils/src/{metrics => }/types.ts | 0 .../test/browser/browserMetrics.test.ts | 2 +- packages/browser/.eslintrc.js | 2 +- packages/browser/src/transports/fetch.ts | 10 +- packages/browser/src/transports/utils.ts | 91 ------------ packages/browser/{src => test}/loader.js | 0 .../test/unit/transports/fetch.test.ts | 13 +- packages/browser/tsconfig.json | 2 +- .../src/coreHandlers/handleAfterSendEvent.ts | 1 + .../src/coreHandlers/handleClick.ts | 1 + .../src/coreHandlers/util/fetchUtils.ts | 1 + packages/replay-internal/src/util/debounce.ts | 2 + packages/replay-internal/src/util/log.ts | 1 + .../replay-internal/src/util/sendReplay.ts | 1 + .../coreHandlers/handleAfterSendEvent.test.ts | 5 +- .../test/integration/flush.test.ts | 7 +- .../unit/coreHandlers/handleClick.test.ts | 7 +- .../handleNetworkBreadcrumbs.test.ts | 3 +- .../util/addBreadcrumbEvent.test.ts | 9 +- .../unit/coreHandlers/util/fetchUtils.test.ts | 27 ++-- .../coreHandlers/util/networkUtils.test.ts | 5 +- .../unit/util/createPerformanceEntry.test.ts | 4 +- .../test/unit/util/debounce.test.ts | 7 +- .../test/unit/util/throttle.test.ts | 3 +- .../test/utils/use-fake-timers.ts | 7 + 40 files changed, 221 insertions(+), 154 deletions(-) create mode 100644 packages/browser-utils/src/getNativeImplementation.ts rename packages/browser-utils/src/{metrics => }/types.ts (100%) delete mode 100644 packages/browser/src/transports/utils.ts rename packages/browser/{src => test}/loader.js (100%) diff --git a/packages/browser-utils/src/getNativeImplementation.ts b/packages/browser-utils/src/getNativeImplementation.ts new file mode 100644 index 000000000000..e06fb69f561e --- /dev/null +++ b/packages/browser-utils/src/getNativeImplementation.ts @@ -0,0 +1,131 @@ +import { logger } from '@sentry/utils'; +import { DEBUG_BUILD } from './debug-build'; +import { WINDOW } from './types'; + +/** + * We generally want to use window.fetch / window.setTimeout. + * However, in some cases this may be wrapped (e.g. by Zone.js for Angular), + * so we try to get an unpatched version of this from a sandboxed iframe. + */ + +interface CacheableImplementations { + setTimeout: typeof WINDOW.setTimeout; + fetch: typeof WINDOW.fetch; +} + +const cachedImplementations: Partial = {}; + +/** + * isNative checks if the given function is a native implementation + */ +// eslint-disable-next-line @typescript-eslint/ban-types +function isNative(func: Function): boolean { + return func && /^function\s+\w+\(\)\s+\{\s+\[native code\]\s+\}$/.test(func.toString()); +} + +/** + * Get the native implementation of a browser function. + * + * This can be used to ensure we get an unwrapped version of a function, in cases where a wrapped function can lead to problems. + * + * The following methods can be retrieved: + * - `setTimeout`: This can be wrapped by e.g. Angular, causing change detection to be triggered. + * - `fetch`: This can be wrapped by e.g. ad-blockers, causing an infinite loop when a request is blocked. + */ +export function getNativeImplementation( + name: T, +): CacheableImplementations[T] { + const cached = cachedImplementations[name]; + if (cached) { + return cached; + } + + let impl = WINDOW[name] as CacheableImplementations[T]; + + // Fast path to avoid DOM I/O + if (isNative(impl)) { + return (cachedImplementations[name] = impl.bind(WINDOW) as CacheableImplementations[T]); + } + + const document = WINDOW.document; + // eslint-disable-next-line deprecation/deprecation + if (document && typeof document.createElement === 'function') { + try { + const sandbox = document.createElement('iframe'); + sandbox.hidden = true; + document.head.appendChild(sandbox); + const contentWindow = sandbox.contentWindow; + if (contentWindow && contentWindow[name]) { + impl = contentWindow[name] as CacheableImplementations[T]; + } + document.head.removeChild(sandbox); + } catch (e) { + // Could not create sandbox iframe, just use window.xxx + DEBUG_BUILD && logger.warn(`Could not create sandbox iframe for ${name} check, bailing to window.${name}: `, e); + } + } + + // Sanity check: This _should_ not happen, but if it does, we just skip caching... + // This can happen e.g. in tests where fetch may not be available in the env, or similar. + if (!impl) { + return impl; + } + + return (cachedImplementations[name] = impl.bind(WINDOW) as CacheableImplementations[T]); +} + +/** Clear a cached implementation. */ +export function clearCachedImplementation(name: keyof CacheableImplementations): void { + cachedImplementations[name] = undefined; +} + +/** + * A special usecase for incorrectly wrapped Fetch APIs in conjunction with ad-blockers. + * Whenever someone wraps the Fetch API and returns the wrong promise chain, + * this chain becomes orphaned and there is no possible way to capture it's rejections + * other than allowing it bubble up to this very handler. eg. + * + * const f = window.fetch; + * window.fetch = function () { + * const p = f.apply(this, arguments); + * + * p.then(function() { + * console.log('hi.'); + * }); + * + * return p; + * } + * + * `p.then(function () { ... })` is producing a completely separate promise chain, + * however, what's returned is `p` - the result of original `fetch` call. + * + * This mean, that whenever we use the Fetch API to send our own requests, _and_ + * some ad-blocker blocks it, this orphaned chain will _always_ reject, + * effectively causing another event to be captured. + * This makes a whole process become an infinite loop, which we need to somehow + * deal with, and break it in one way or another. + * + * To deal with this issue, we are making sure that we _always_ use the real + * browser Fetch API, instead of relying on what `window.fetch` exposes. + * The only downside to this would be missing our own requests as breadcrumbs, + * but because we are already not doing this, it should be just fine. + * + * Possible failed fetch error messages per-browser: + * + * Chrome: Failed to fetch + * Edge: Failed to Fetch + * Firefox: NetworkError when attempting to fetch resource + * Safari: resource blocked by content blocker + */ +export function fetch(...rest: Parameters): ReturnType { + return getNativeImplementation('fetch')(...rest); +} + +/** + * Get an unwrapped `setTimeout` method. + * This ensures that even if e.g. Angular wraps `setTimeout`, we get the native implementation, + * avoiding triggering change detection. + */ +export function setTimeout(...rest: Parameters): ReturnType { + return getNativeImplementation('setTimeout')(...rest); +} diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts index 53e2f24068d6..7e7c4d0a387a 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -18,7 +18,6 @@ export { addClickKeypressInstrumentationHandler } from './instrument/dom'; export { addHistoryInstrumentationHandler } from './instrument/history'; -export { - addXhrInstrumentationHandler, - SENTRY_XHR_DATA_KEY, -} from './instrument/xhr'; +export { fetch, setTimeout, clearCachedImplementation, getNativeImplementation } from './getNativeImplementation'; + +export { addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY } from './instrument/xhr'; diff --git a/packages/browser-utils/src/instrument/dom.ts b/packages/browser-utils/src/instrument/dom.ts index 5e813f23eb67..d08350a48766 100644 --- a/packages/browser-utils/src/instrument/dom.ts +++ b/packages/browser-utils/src/instrument/dom.ts @@ -1,7 +1,7 @@ import type { HandlerDataDom } from '@sentry/types'; import { addHandler, addNonEnumerableProperty, fill, maybeInstrument, triggerHandlers, uuid4 } from '@sentry/utils'; -import { WINDOW } from '../metrics/types'; +import { WINDOW } from '../types'; type SentryWrappedTarget = HTMLElement & { _sentryId?: string }; diff --git a/packages/browser-utils/src/instrument/history.ts b/packages/browser-utils/src/instrument/history.ts index f791bcef5389..acb31dfc455d 100644 --- a/packages/browser-utils/src/instrument/history.ts +++ b/packages/browser-utils/src/instrument/history.ts @@ -1,6 +1,6 @@ import type { HandlerDataHistory } from '@sentry/types'; import { addHandler, fill, maybeInstrument, supportsHistory, triggerHandlers } from '@sentry/utils'; -import { WINDOW } from '../metrics/types'; +import { WINDOW } from '../types'; let lastHref: string | undefined; diff --git a/packages/browser-utils/src/instrument/xhr.ts b/packages/browser-utils/src/instrument/xhr.ts index c3fa2ec6a62c..5b7a9c261092 100644 --- a/packages/browser-utils/src/instrument/xhr.ts +++ b/packages/browser-utils/src/instrument/xhr.ts @@ -1,7 +1,7 @@ import type { HandlerDataXhr, SentryWrappedXMLHttpRequest, WrappedFunction } from '@sentry/types'; import { addHandler, fill, isString, maybeInstrument, timestampInSeconds, triggerHandlers } from '@sentry/utils'; -import { WINDOW } from '../metrics/types'; +import { WINDOW } from '../types'; export const SENTRY_XHR_DATA_KEY = '__sentry_xhr_v3__'; diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index deca3765e404..877b0612fb06 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -6,6 +6,7 @@ import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logge import { spanToJSON } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; +import { WINDOW } from '../types'; import { addClsInstrumentationHandler, addFidInstrumentationHandler, @@ -13,7 +14,6 @@ import { addPerformanceInstrumentationHandler, addTtfbInstrumentationHandler, } from './instrument'; -import { WINDOW } from './types'; import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils'; import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher'; diff --git a/packages/browser-utils/src/metrics/utils.ts b/packages/browser-utils/src/metrics/utils.ts index 1927f0e9a971..d46cb2cfe35c 100644 --- a/packages/browser-utils/src/metrics/utils.ts +++ b/packages/browser-utils/src/metrics/utils.ts @@ -1,7 +1,7 @@ import type { SentrySpan } from '@sentry/core'; import { spanToJSON, startInactiveSpan, withActiveSpan } from '@sentry/core'; import type { Span, SpanTimeInput, StartSpanOptions } from '@sentry/types'; -import { WINDOW } from './types'; +import { WINDOW } from '../types'; /** * Checks if a given value is a valid measurement value. diff --git a/packages/browser-utils/src/metrics/web-vitals/getINP.ts b/packages/browser-utils/src/metrics/web-vitals/getINP.ts index 5c4a185aa92f..fa2d90da3371 100644 --- a/packages/browser-utils/src/metrics/web-vitals/getINP.ts +++ b/packages/browser-utils/src/metrics/web-vitals/getINP.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { WINDOW } from '../types'; +import { WINDOW } from '../../types'; import { bindReporter } from './lib/bindReporter'; import { initMetric } from './lib/initMetric'; import { observe } from './lib/observe'; diff --git a/packages/browser-utils/src/metrics/web-vitals/getLCP.ts b/packages/browser-utils/src/metrics/web-vitals/getLCP.ts index db1bd90fb71d..b50358c98d61 100644 --- a/packages/browser-utils/src/metrics/web-vitals/getLCP.ts +++ b/packages/browser-utils/src/metrics/web-vitals/getLCP.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { WINDOW } from '../types'; +import { WINDOW } from '../../types'; import { bindReporter } from './lib/bindReporter'; import { getActivationStart } from './lib/getActivationStart'; import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts b/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts index 2fa455e3fbba..63cfa04b3ad4 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { WINDOW } from '../../types'; +import { WINDOW } from '../../../types'; import type { NavigationTimingPolyfillEntry } from '../types'; export const getNavigationEntry = (): PerformanceNavigationTiming | NavigationTimingPolyfillEntry | undefined => { diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts b/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts index 6fe3755f6f59..c254ad1259d9 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { WINDOW } from '../../types'; +import { WINDOW } from '../../../types'; let firstHiddenTime = -1; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts b/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts index 386333b7eb2d..fee96d83bf33 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { WINDOW } from '../../types'; +import { WINDOW } from '../../../types'; import type { MetricType } from '../types'; import { generateUniqueID } from './generateUniqueID'; import { getActivationStart } from './getActivationStart'; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts b/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts index 9f81c7369007..9f65196d27a2 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { WINDOW } from '../../types'; +import { WINDOW } from '../../../types'; export interface OnHiddenCallback { (event: Event): void; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts b/packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts index 183a8566aeb4..8463a1d199ef 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { WINDOW } from '../../types'; +import { WINDOW } from '../../../types'; export const whenActivated = (callback: () => void) => { if (WINDOW.document && WINDOW.document.prerendering) { diff --git a/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts b/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts index 993af7ca074e..85f9b99bc0f4 100644 --- a/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts +++ b/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { WINDOW } from '../types'; +import { WINDOW } from '../../types'; import { bindReporter } from './lib/bindReporter'; import { getActivationStart } from './lib/getActivationStart'; import { getNavigationEntry } from './lib/getNavigationEntry'; diff --git a/packages/browser-utils/src/metrics/types.ts b/packages/browser-utils/src/types.ts similarity index 100% rename from packages/browser-utils/src/metrics/types.ts rename to packages/browser-utils/src/types.ts diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index b6d6aaa087aa..555dfde1c92f 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -11,7 +11,7 @@ import { import type { Span } from '@sentry/types'; import type { ResourceEntry } from '../../src/metrics/browserMetrics'; import { _addMeasureSpans, _addResourceSpans } from '../../src/metrics/browserMetrics'; -import { WINDOW } from '../../src/metrics/types'; +import { WINDOW } from '../../src/types'; import { TestClient, getDefaultClientOptions } from '../utils/TestClient'; const mockWindowLocation = { diff --git a/packages/browser/.eslintrc.js b/packages/browser/.eslintrc.js index 765edcb14960..fec08079889a 100644 --- a/packages/browser/.eslintrc.js +++ b/packages/browser/.eslintrc.js @@ -2,6 +2,6 @@ module.exports = { env: { browser: true, }, - ignorePatterns: ['test/integration/**', 'src/loader.js'], + ignorePatterns: ['test/integration/**', 'test/loader.js'], extends: ['../../.eslintrc.js'], }; diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts index 305afb9fc0ec..52ba6d71154c 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -1,17 +1,17 @@ +import { clearCachedImplementation, getNativeImplementation } from '@sentry-internal/browser-utils'; import { createTransport } from '@sentry/core'; import type { Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; import { rejectedSyncPromise } from '@sentry/utils'; +import type { WINDOW } from '../helpers'; import type { BrowserTransportOptions } from './types'; -import type { FetchImpl } from './utils'; -import { clearCachedFetchImplementation, getNativeFetchImplementation } from './utils'; /** * Creates a Transport that uses the Fetch API to send events to Sentry. */ export function makeFetchTransport( options: BrowserTransportOptions, - nativeFetch: FetchImpl | undefined = getNativeFetchImplementation(), + nativeFetch: typeof WINDOW.fetch | undefined = getNativeImplementation('fetch'), ): Transport { let pendingBodySize = 0; let pendingCount = 0; @@ -42,7 +42,7 @@ export function makeFetchTransport( }; if (!nativeFetch) { - clearCachedFetchImplementation(); + clearCachedImplementation('fetch'); return rejectedSyncPromise('No fetch implementation available'); } @@ -59,7 +59,7 @@ export function makeFetchTransport( }; }); } catch (e) { - clearCachedFetchImplementation(); + clearCachedImplementation('fetch'); pendingBodySize -= requestSize; pendingCount--; return rejectedSyncPromise(e); diff --git a/packages/browser/src/transports/utils.ts b/packages/browser/src/transports/utils.ts deleted file mode 100644 index 053a0d0dd483..000000000000 --- a/packages/browser/src/transports/utils.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { isNativeFetch, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from '../debug-build'; -import { WINDOW } from '../helpers'; - -let cachedFetchImpl: FetchImpl | undefined = undefined; - -export type FetchImpl = typeof fetch; - -/** - * A special usecase for incorrectly wrapped Fetch APIs in conjunction with ad-blockers. - * Whenever someone wraps the Fetch API and returns the wrong promise chain, - * this chain becomes orphaned and there is no possible way to capture it's rejections - * other than allowing it bubble up to this very handler. eg. - * - * const f = window.fetch; - * window.fetch = function () { - * const p = f.apply(this, arguments); - * - * p.then(function() { - * console.log('hi.'); - * }); - * - * return p; - * } - * - * `p.then(function () { ... })` is producing a completely separate promise chain, - * however, what's returned is `p` - the result of original `fetch` call. - * - * This mean, that whenever we use the Fetch API to send our own requests, _and_ - * some ad-blocker blocks it, this orphaned chain will _always_ reject, - * effectively causing another event to be captured. - * This makes a whole process become an infinite loop, which we need to somehow - * deal with, and break it in one way or another. - * - * To deal with this issue, we are making sure that we _always_ use the real - * browser Fetch API, instead of relying on what `window.fetch` exposes. - * The only downside to this would be missing our own requests as breadcrumbs, - * but because we are already not doing this, it should be just fine. - * - * Possible failed fetch error messages per-browser: - * - * Chrome: Failed to fetch - * Edge: Failed to Fetch - * Firefox: NetworkError when attempting to fetch resource - * Safari: resource blocked by content blocker - */ -export function getNativeFetchImplementation(): FetchImpl | undefined { - if (cachedFetchImpl) { - return cachedFetchImpl; - } - - /* eslint-disable @typescript-eslint/unbound-method */ - - // Fast path to avoid DOM I/O - if (isNativeFetch(WINDOW.fetch)) { - return (cachedFetchImpl = WINDOW.fetch.bind(WINDOW)); - } - - const document = WINDOW.document; - let fetchImpl = WINDOW.fetch; - // eslint-disable-next-line deprecation/deprecation - if (document && typeof document.createElement === 'function') { - try { - const sandbox = document.createElement('iframe'); - sandbox.hidden = true; - document.head.appendChild(sandbox); - const contentWindow = sandbox.contentWindow; - if (contentWindow && contentWindow.fetch) { - fetchImpl = contentWindow.fetch; - } - document.head.removeChild(sandbox); - } catch (e) { - DEBUG_BUILD && logger.warn('Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ', e); - } - } - - try { - return (cachedFetchImpl = fetchImpl.bind(WINDOW)); - } catch (e) { - // empty - } - - return undefined; - /* eslint-enable @typescript-eslint/unbound-method */ -} - -/** Clears cached fetch impl */ -export function clearCachedFetchImplementation(): void { - cachedFetchImpl = undefined; -} diff --git a/packages/browser/src/loader.js b/packages/browser/test/loader.js similarity index 100% rename from packages/browser/src/loader.js rename to packages/browser/test/loader.js diff --git a/packages/browser/test/unit/transports/fetch.test.ts b/packages/browser/test/unit/transports/fetch.test.ts index c80688eb11c3..ae8a5d43a6e0 100644 --- a/packages/browser/test/unit/transports/fetch.test.ts +++ b/packages/browser/test/unit/transports/fetch.test.ts @@ -3,7 +3,6 @@ import { createEnvelope, serializeEnvelope } from '@sentry/utils'; import { makeFetchTransport } from '../../../src/transports/fetch'; import type { BrowserTransportOptions } from '../../../src/transports/types'; -import type { FetchImpl } from '../../../src/transports/utils'; const DEFAULT_FETCH_TRANSPORT_OPTIONS: BrowserTransportOptions = { url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', @@ -37,7 +36,7 @@ describe('NewFetchTransport', () => { status: 200, text: () => Promise.resolve({}), }), - ) as unknown as FetchImpl; + ) as unknown as typeof window.fetch; const transport = makeFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); expect(mockFetch).toHaveBeenCalledTimes(0); @@ -63,7 +62,7 @@ describe('NewFetchTransport', () => { status: 200, text: () => Promise.resolve({}), }), - ) as unknown as FetchImpl; + ) as unknown as typeof window.fetch; const transport = makeFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); expect(headers.get).toHaveBeenCalledTimes(0); @@ -81,7 +80,7 @@ describe('NewFetchTransport', () => { status: 200, text: () => Promise.resolve({}), }), - ) as unknown as FetchImpl; + ) as unknown as typeof window.fetch; const REQUEST_OPTIONS: RequestInit = { referrerPolicy: 'strict-origin', @@ -102,8 +101,8 @@ describe('NewFetchTransport', () => { }); }); - it('handles when `getNativeFetchImplementation` is undefined', async () => { - const mockFetch = jest.fn(() => undefined) as unknown as FetchImpl; + it('handles when `getNativetypeof window.fetchementation` is undefined', async () => { + const mockFetch = jest.fn(() => undefined) as unknown as typeof window.fetch; const transport = makeFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); expect(mockFetch).toHaveBeenCalledTimes(0); @@ -118,7 +117,7 @@ describe('NewFetchTransport', () => { status: 200, text: () => Promise.resolve({}), }), - ) as unknown as FetchImpl; + ) as unknown as typeof window.fetch; const REQUEST_OPTIONS: RequestInit = { referrerPolicy: 'strict-origin', diff --git a/packages/browser/tsconfig.json b/packages/browser/tsconfig.json index bf45a09f2d71..f88d8939acf7 100644 --- a/packages/browser/tsconfig.json +++ b/packages/browser/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*"], + "include": ["src/**/*", "test/loader.js"], "compilerOptions": { // package-specific options diff --git a/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts b/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts index 14e92d404fc7..74a4f8869d0f 100644 --- a/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts +++ b/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts @@ -1,3 +1,4 @@ +import { setTimeout } from '@sentry-internal/browser-utils'; import type { ErrorEvent, Event, TransactionEvent, TransportMakeRequestResponse } from '@sentry/types'; import type { ReplayContainer } from '../types'; diff --git a/packages/replay-internal/src/coreHandlers/handleClick.ts b/packages/replay-internal/src/coreHandlers/handleClick.ts index dab5d04657fc..da07474deebb 100644 --- a/packages/replay-internal/src/coreHandlers/handleClick.ts +++ b/packages/replay-internal/src/coreHandlers/handleClick.ts @@ -1,3 +1,4 @@ +import { setTimeout } from '@sentry-internal/browser-utils'; import { IncrementalSource, MouseInteractions, record } from '@sentry-internal/rrweb'; import type { Breadcrumb } from '@sentry/types'; diff --git a/packages/replay-internal/src/coreHandlers/util/fetchUtils.ts b/packages/replay-internal/src/coreHandlers/util/fetchUtils.ts index 399206ad659a..b5c2c3c36305 100644 --- a/packages/replay-internal/src/coreHandlers/util/fetchUtils.ts +++ b/packages/replay-internal/src/coreHandlers/util/fetchUtils.ts @@ -1,3 +1,4 @@ +import { setTimeout } from '@sentry-internal/browser-utils'; import type { Breadcrumb, FetchBreadcrumbData } from '@sentry/types'; import { logger } from '@sentry/utils'; diff --git a/packages/replay-internal/src/util/debounce.ts b/packages/replay-internal/src/util/debounce.ts index 88057c14a4c5..78437b7d9403 100644 --- a/packages/replay-internal/src/util/debounce.ts +++ b/packages/replay-internal/src/util/debounce.ts @@ -1,3 +1,5 @@ +import { setTimeout } from '@sentry-internal/browser-utils'; + type DebouncedCallback = { (): void | unknown; flush: () => void | unknown; diff --git a/packages/replay-internal/src/util/log.ts b/packages/replay-internal/src/util/log.ts index 3d16137bbbc0..c847a093f02f 100644 --- a/packages/replay-internal/src/util/log.ts +++ b/packages/replay-internal/src/util/log.ts @@ -1,3 +1,4 @@ +import { setTimeout } from '@sentry-internal/browser-utils'; import { addBreadcrumb } from '@sentry/core'; import { logger } from '@sentry/utils'; diff --git a/packages/replay-internal/src/util/sendReplay.ts b/packages/replay-internal/src/util/sendReplay.ts index 18c256987453..973c3fb9a556 100644 --- a/packages/replay-internal/src/util/sendReplay.ts +++ b/packages/replay-internal/src/util/sendReplay.ts @@ -1,3 +1,4 @@ +import { setTimeout } from '@sentry-internal/browser-utils'; import { captureException, setContext } from '@sentry/core'; import { RETRY_BASE_INTERVAL, RETRY_MAX_COUNT, UNABLE_TO_SEND_REPLAY } from '../constants'; diff --git a/packages/replay-internal/test/integration/coreHandlers/handleAfterSendEvent.test.ts b/packages/replay-internal/test/integration/coreHandlers/handleAfterSendEvent.test.ts index afab6ac6030a..8edc813aaad2 100644 --- a/packages/replay-internal/test/integration/coreHandlers/handleAfterSendEvent.test.ts +++ b/packages/replay-internal/test/integration/coreHandlers/handleAfterSendEvent.test.ts @@ -1,5 +1,8 @@ import { vi } from 'vitest'; import type { MockInstance } from 'vitest'; +import { useFakeTimers } from '../../utils/use-fake-timers'; + +useFakeTimers(); import { getClient } from '@sentry/core'; import type { ErrorEvent, Event } from '@sentry/types'; @@ -10,9 +13,7 @@ import type { ReplayContainer } from '../../../src/replay'; import { Error } from '../../fixtures/error'; import { Transaction } from '../../fixtures/transaction'; import { resetSdkMock } from '../../mocks/resetSdkMock'; -import { useFakeTimers } from '../../utils/use-fake-timers'; -useFakeTimers(); let replay: ReplayContainer; describe('Integration | coreHandlers | handleAfterSendEvent', () => { diff --git a/packages/replay-internal/test/integration/flush.test.ts b/packages/replay-internal/test/integration/flush.test.ts index bb00da732df3..31fd8a91a5e2 100644 --- a/packages/replay-internal/test/integration/flush.test.ts +++ b/packages/replay-internal/test/integration/flush.test.ts @@ -1,6 +1,10 @@ import { vi } from 'vitest'; import type { MockedFunction } from 'vitest'; +import { useFakeTimers } from '../utils/use-fake-timers'; + +useFakeTimers(); + import * as SentryBrowserUtils from '@sentry-internal/browser-utils'; import * as SentryUtils from '@sentry/utils'; @@ -14,9 +18,6 @@ import * as SendReplay from '../../src/util/sendReplay'; import { BASE_TIMESTAMP, mockRrweb, mockSdk } from '../index'; import type { DomHandler } from '../types'; import { getTestEventCheckout } from '../utils/getTestEvent'; -import { useFakeTimers } from '../utils/use-fake-timers'; - -useFakeTimers(); type MockSendReplay = MockedFunction; type MockAddPerformanceEntries = MockedFunction; diff --git a/packages/replay-internal/test/unit/coreHandlers/handleClick.test.ts b/packages/replay-internal/test/unit/coreHandlers/handleClick.test.ts index 07ef281800c1..d143eaa5a5a7 100644 --- a/packages/replay-internal/test/unit/coreHandlers/handleClick.test.ts +++ b/packages/replay-internal/test/unit/coreHandlers/handleClick.test.ts @@ -1,11 +1,12 @@ +import { useFakeTimers } from '../../utils/use-fake-timers'; + +useFakeTimers(); + import type { Breadcrumb } from '@sentry/types'; import { BASE_TIMESTAMP } from '../..'; import { ClickDetector, ignoreElement } from '../../../src/coreHandlers/handleClick'; import type { ReplayContainer } from '../../../src/types'; - -vi.useFakeTimers(); - describe('Unit | coreHandlers | handleClick', () => { describe('ClickDetector', () => { beforeEach(() => { diff --git a/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts b/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts index ef81feef3a8e..ddf638c8498f 100644 --- a/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts +++ b/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts @@ -13,8 +13,9 @@ import { beforeAddNetworkBreadcrumb } from '../../../src/coreHandlers/handleNetw import type { EventBufferArray } from '../../../src/eventBuffer/EventBufferArray'; import type { ReplayContainer, ReplayNetworkOptions } from '../../../src/types'; import { setupReplayContainer } from '../../utils/setupReplayContainer'; +import { useFakeTimers } from '../../utils/use-fake-timers'; -vi.useFakeTimers(); +useFakeTimers(); async function waitForReplayEventBuffer() { // Need one Promise.resolve() per await in the util functions diff --git a/packages/replay-internal/test/unit/coreHandlers/util/addBreadcrumbEvent.test.ts b/packages/replay-internal/test/unit/coreHandlers/util/addBreadcrumbEvent.test.ts index 1979556b3dc9..d82eb5f183c6 100644 --- a/packages/replay-internal/test/unit/coreHandlers/util/addBreadcrumbEvent.test.ts +++ b/packages/replay-internal/test/unit/coreHandlers/util/addBreadcrumbEvent.test.ts @@ -2,8 +2,9 @@ import { BASE_TIMESTAMP } from '../../..'; import { addBreadcrumbEvent } from '../../../../src/coreHandlers/util/addBreadcrumbEvent'; import type { EventBufferArray } from '../../../../src/eventBuffer/EventBufferArray'; import { setupReplayContainer } from '../../../utils/setupReplayContainer'; +import { useFakeTimers } from '../../../utils/use-fake-timers'; -vi.useFakeTimers(); +useFakeTimers(); describe('Unit | coreHandlers | util | addBreadcrumbEvent', function () { beforeEach(function () { @@ -23,12 +24,6 @@ describe('Unit | coreHandlers | util | addBreadcrumbEvent', function () { const replay = setupReplayContainer(); addBreadcrumbEvent(replay, breadcrumb); - await undefined; - await undefined; - await undefined; - await undefined; - await undefined; - expect((replay.eventBuffer as EventBufferArray).events).toEqual([ { type: 5, diff --git a/packages/replay-internal/test/unit/coreHandlers/util/fetchUtils.test.ts b/packages/replay-internal/test/unit/coreHandlers/util/fetchUtils.test.ts index 6d78a2583774..73a23e00babe 100644 --- a/packages/replay-internal/test/unit/coreHandlers/util/fetchUtils.test.ts +++ b/packages/replay-internal/test/unit/coreHandlers/util/fetchUtils.test.ts @@ -1,3 +1,9 @@ +import { useFakeTimers } from '../../../utils/use-fake-timers'; + +useFakeTimers(); + +import { vi } from 'vitest'; + import { _getResponseInfo } from '../../../../src/coreHandlers/util/fetchUtils'; describe('Unit | coreHandlers | util | fetchUtils', () => { @@ -112,15 +118,18 @@ describe('Unit | coreHandlers | util | fetchUtils', () => { text: () => new Promise(resolve => setTimeout(() => resolve('text body'), 1000)), } as unknown as Response; - const res = await _getResponseInfo( - true, - { - networkCaptureBodies: true, - networkResponseHeaders: [], - }, - response, - undefined, - ); + const [res] = await Promise.all([ + _getResponseInfo( + true, + { + networkCaptureBodies: true, + networkResponseHeaders: [], + }, + response, + undefined, + ), + vi.runAllTimersAsync(), + ]); expect(res).toEqual({ _meta: { warnings: ['BODY_PARSE_ERROR'] }, diff --git a/packages/replay-internal/test/unit/coreHandlers/util/networkUtils.test.ts b/packages/replay-internal/test/unit/coreHandlers/util/networkUtils.test.ts index 4a330081507a..33ea345ec609 100644 --- a/packages/replay-internal/test/unit/coreHandlers/util/networkUtils.test.ts +++ b/packages/replay-internal/test/unit/coreHandlers/util/networkUtils.test.ts @@ -1,5 +1,3 @@ -import { vi } from 'vitest'; - import { NETWORK_BODY_MAX_SIZE } from '../../../../src/constants'; import { buildNetworkRequestOrResponse, @@ -8,8 +6,9 @@ import { getFullUrl, parseContentLengthHeader, } from '../../../../src/coreHandlers/util/networkUtils'; +import { useFakeTimers } from '../../../utils/use-fake-timers'; -vi.useFakeTimers(); +useFakeTimers(); describe('Unit | coreHandlers | util | networkUtils', () => { describe('parseContentLengthHeader()', () => { diff --git a/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts b/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts index fd086eea6fa0..176de1c2d32e 100644 --- a/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts +++ b/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts @@ -1,6 +1,8 @@ import { vi } from 'vitest'; -vi.useFakeTimers(); +import { useFakeTimers } from '../../utils/use-fake-timers'; + +useFakeTimers(); vi.setSystemTime(new Date('2023-01-01')); vi.mock('@sentry/utils', async () => ({ diff --git a/packages/replay-internal/test/unit/util/debounce.test.ts b/packages/replay-internal/test/unit/util/debounce.test.ts index 59eb9e2a84fa..ae8ad4262053 100644 --- a/packages/replay-internal/test/unit/util/debounce.test.ts +++ b/packages/replay-internal/test/unit/util/debounce.test.ts @@ -1,7 +1,12 @@ +import { useFakeTimers } from '../../utils/use-fake-timers'; + +useFakeTimers(); + +import { vi } from 'vitest'; + import { debounce } from '../../../src/util/debounce'; describe('Unit | util | debounce', () => { - vi.useFakeTimers(); it('delay the execution of the passed callback function by the passed minDelay', () => { const callback = vi.fn(); const debouncedCallback = debounce(callback, 100); diff --git a/packages/replay-internal/test/unit/util/throttle.test.ts b/packages/replay-internal/test/unit/util/throttle.test.ts index 797558dea904..00d394d5b769 100644 --- a/packages/replay-internal/test/unit/util/throttle.test.ts +++ b/packages/replay-internal/test/unit/util/throttle.test.ts @@ -1,7 +1,8 @@ import { BASE_TIMESTAMP } from '../..'; import { SKIPPED, THROTTLED, throttle } from '../../../src/util/throttle'; +import { useFakeTimers } from '../../utils/use-fake-timers'; -vi.useFakeTimers(); +useFakeTimers(); describe('Unit | util | throttle', () => { it('executes when not hitting the limit', () => { diff --git a/packages/replay-internal/test/utils/use-fake-timers.ts b/packages/replay-internal/test/utils/use-fake-timers.ts index 57fda295cb59..2d5d43bdf338 100644 --- a/packages/replay-internal/test/utils/use-fake-timers.ts +++ b/packages/replay-internal/test/utils/use-fake-timers.ts @@ -1,5 +1,12 @@ import { vi } from 'vitest'; +vi.mock('@sentry-internal/browser-utils', async () => ({ + ...(await vi.importActual('@sentry-internal/browser-utils')), + setTimeout: (...args) => { + return setTimeout.call(global, ...args); + }, +})); + export function useFakeTimers(): void { vi.useFakeTimers(); }