From c8a79440a683b26e061bdd2f5bbe863af181f153 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 22 Apr 2024 10:36:20 +0200 Subject: [PATCH 1/2] feat(replay): Ensure to use unwrapped `setTimeout` method This moves some code around in `browser-utils` so we can-reuse the logic for getting the unwrapped fetch implementation to also get the unwrapped `setTimeout` implementation. E.g. Angular wraps this for change detection, which can lead to performance degration. --- .../src/getNativeImplementation.ts | 118 ++++++++++++++++++ packages/browser-utils/src/index.ts | 2 + 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 +- .../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 + 28 files changed, 153 insertions(+), 118 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..781d1b40f56e --- /dev/null +++ b/packages/browser-utils/src/getNativeImplementation.ts @@ -0,0 +1,118 @@ +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 = {}; + +/** + * 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; + } + + const document = WINDOW.document; + let impl = WINDOW[name] as CacheableImplementations[T]; + // 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 { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete cachedImplementations[name]; +} + +/** + * 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..403115af01a2 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -18,6 +18,8 @@ export { addClickKeypressInstrumentationHandler } from './instrument/dom'; export { addHistoryInstrumentationHandler } from './instrument/history'; +export { fetch, setTimeout, clearCachedImplementation, getNativeImplementation } from './getNativeImplementation'; + export { addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY, 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 c504c8dce5f6..0f799900b878 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, 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..6a12e723b7f9 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/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'; From 04680e05319c009b8b390f6cd6bd297cbaba5533 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 22 Apr 2024 11:09:41 +0200 Subject: [PATCH 2/2] WIP --- .../browser-utils/src/getNativeImplementation.ts | 12 ++++++++++++ packages/browser-utils/src/index.ts | 8 +++++++- .../test/unit/coreHandlers/handleClick.test.ts | 3 ++- .../coreHandlers/handleNetworkBreadcrumbs.test.ts | 3 ++- .../coreHandlers/util/addBreadcrumbEvent.test.ts | 3 ++- .../test/unit/coreHandlers/util/networkUtils.test.ts | 3 ++- .../test/unit/util/createPerformanceEntry.test.ts | 5 ++++- .../replay-internal/test/unit/util/debounce.test.ts | 3 ++- .../replay-internal/test/unit/util/throttle.test.ts | 3 ++- .../replay-internal/test/utils/use-fake-timers.ts | 5 +++++ 10 files changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/browser-utils/src/getNativeImplementation.ts b/packages/browser-utils/src/getNativeImplementation.ts index 781d1b40f56e..34537ef5dd44 100644 --- a/packages/browser-utils/src/getNativeImplementation.ts +++ b/packages/browser-utils/src/getNativeImplementation.ts @@ -66,6 +66,18 @@ export function clearCachedImplementation(name: keyof CacheableImplementations): delete cachedImplementations[name]; } +/** + * Sets a cached implementation. + * This should NOT be used, and is only here + * @hidden + */ +export function setCachedImplementation( + name: T, + impl: CacheableImplementations[T], +): void { + cachedImplementations[name] = impl; +} + /** * 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, diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts index 403115af01a2..492e4848fc82 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -18,7 +18,13 @@ export { addClickKeypressInstrumentationHandler } from './instrument/dom'; export { addHistoryInstrumentationHandler } from './instrument/history'; -export { fetch, setTimeout, clearCachedImplementation, getNativeImplementation } from './getNativeImplementation'; +export { + fetch, + setTimeout, + clearCachedImplementation, + getNativeImplementation, + setCachedImplementation, +} from './getNativeImplementation'; export { addXhrInstrumentationHandler, diff --git a/packages/replay-internal/test/unit/coreHandlers/handleClick.test.ts b/packages/replay-internal/test/unit/coreHandlers/handleClick.test.ts index ae52d2076293..679e36a77c6b 100644 --- a/packages/replay-internal/test/unit/coreHandlers/handleClick.test.ts +++ b/packages/replay-internal/test/unit/coreHandlers/handleClick.test.ts @@ -3,8 +3,9 @@ import type { Breadcrumb } from '@sentry/types'; import { BASE_TIMESTAMP } from '../..'; import { ClickDetector, ignoreElement } from '../../../src/coreHandlers/handleClick'; import type { ReplayContainer } from '../../../src/types'; +import { useFakeTimers } from '../../utils/use-fake-timers'; -jest.useFakeTimers(); +useFakeTimers(); describe('Unit | coreHandlers | handleClick', () => { describe('ClickDetector', () => { diff --git a/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts b/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts index b3522c0ceb6c..a853967e46f6 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'; -jest.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 45483ebab5b4..d0450b95b12d 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'; -jest.useFakeTimers(); +useFakeTimers(); describe('Unit | coreHandlers | util | addBreadcrumbEvent', function () { beforeEach(function () { 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 8f240c8ea7a7..33ea345ec609 100644 --- a/packages/replay-internal/test/unit/coreHandlers/util/networkUtils.test.ts +++ b/packages/replay-internal/test/unit/coreHandlers/util/networkUtils.test.ts @@ -6,8 +6,9 @@ import { getFullUrl, parseContentLengthHeader, } from '../../../../src/coreHandlers/util/networkUtils'; +import { useFakeTimers } from '../../../utils/use-fake-timers'; -jest.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 636eb3aded9d..2001f2542eed 100644 --- a/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts +++ b/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts @@ -1,4 +1,7 @@ -jest.useFakeTimers().setSystemTime(new Date('2023-01-01')); +import { useFakeTimers } from '../../utils/use-fake-timers'; + +useFakeTimers(); +jest.setSystemTime(new Date('2023-01-01')); jest.mock('@sentry/utils', () => ({ ...jest.requireActual('@sentry/utils'), diff --git a/packages/replay-internal/test/unit/util/debounce.test.ts b/packages/replay-internal/test/unit/util/debounce.test.ts index d52f9d456b49..9998a0f6fd8b 100644 --- a/packages/replay-internal/test/unit/util/debounce.test.ts +++ b/packages/replay-internal/test/unit/util/debounce.test.ts @@ -1,7 +1,8 @@ import { debounce } from '../../../src/util/debounce'; +import { useFakeTimers } from '../../utils/use-fake-timers'; describe('Unit | util | debounce', () => { - jest.useFakeTimers(); + useFakeTimers(); it('delay the execution of the passed callback function by the passed minDelay', () => { const callback = jest.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 a242bde73398..5031f5650132 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'; -jest.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 ef7c92dc8101..a68738603e89 100644 --- a/packages/replay-internal/test/utils/use-fake-timers.ts +++ b/packages/replay-internal/test/utils/use-fake-timers.ts @@ -1,8 +1,13 @@ +import { clearCachedImplementation, setCachedImplementation } from '@sentry-internal/browser-utils'; + export function useFakeTimers(): void { + clearCachedImplementation('setTimeout'); const _setInterval = setInterval; const _clearInterval = clearInterval; jest.useFakeTimers(); + setCachedImplementation('setTimeout', window.setTimeout.bind(window)); + let interval: any; beforeAll(function () { interval = _setInterval(() => jest.advanceTimersByTime(20), 20);