From dff63355fdb66922651722868c4f36d4b1e21680 Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Tue, 17 Mar 2026 17:39:31 +0100 Subject: [PATCH 1/4] refactor(core): Remove redundant iframe check in supportsNativeFetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit supportsNativeFetch() created a sandboxed iframe to check if the Fetch API is natively implemented — identical logic to getNativeImplementation('fetch') in browser-utils. The iframe approach adds ~30 lines of DOM manipulation code. The function is only called behind a `skipNativeFetchCheck` guard that is never set to true in the base CDN bundle, making the iframe code dead weight that cannot be tree-shaken by terser (it can't prove the parameter is always falsy across the program). Simplify to delegate to `_isFetchSupported()` which checks if `window.fetch` exists. The actual native-vs-polyfill distinction is already handled by `getNativeImplementation` at the call sites that need it. Also simplifies the `isNativeFunction` regex from an exact whitespace pattern to a simpler `/[native code]/` check, which is more permissive across different JS engine toString() formats. Saves ~200 bytes gzipped on the base browser bundle. Co-Authored-By: Claude claude@anthropic.com --- packages/core/src/utils/supports.ts | 34 ++--------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/packages/core/src/utils/supports.ts b/packages/core/src/utils/supports.ts index 7ac3b4789765..33a7a2d3e3af 100644 --- a/packages/core/src/utils/supports.ts +++ b/packages/core/src/utils/supports.ts @@ -94,7 +94,7 @@ function _isFetchSupported(): boolean { */ // eslint-disable-next-line @typescript-eslint/ban-types export function isNativeFunction(func: Function): boolean { - return func && /^function\s+\w+\(\)\s+\{\s+\[native code\]\s+\}$/.test(func.toString()); + return func && /\[native code\]/.test(func.toString()); } /** @@ -108,37 +108,7 @@ export function supportsNativeFetch(): boolean { return true; } - if (!_isFetchSupported()) { - return false; - } - - // Fast path to avoid DOM I/O - // eslint-disable-next-line @typescript-eslint/unbound-method - if (isNativeFunction(WINDOW.fetch)) { - return true; - } - - // window.fetch is implemented, but is polyfilled or already wrapped (e.g: by a chrome extension) - // so create a "pure" iframe to see if that has native fetch - let result = false; - const doc = WINDOW.document; - // eslint-disable-next-line deprecation/deprecation - if (doc && typeof (doc.createElement as unknown) === 'function') { - try { - const sandbox = doc.createElement('iframe'); - sandbox.hidden = true; - doc.head.appendChild(sandbox); - if (sandbox.contentWindow?.fetch) { - // eslint-disable-next-line @typescript-eslint/unbound-method - result = isNativeFunction(sandbox.contentWindow.fetch); - } - doc.head.removeChild(sandbox); - } catch (err) { - DEBUG_BUILD && debug.warn('Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ', err); - } - } - - return result; + return _isFetchSupported(); } /** From 0f3a8f0b5913bc04915b92a73b8e494e5da53492 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 19 Mar 2026 14:21:36 +0100 Subject: [PATCH 2/4] move getNativeImplementation to core, rework supportsNativeFetch --- .../src/getNativeImplementation.ts | 53 ++----------- packages/browser-utils/src/index.ts | 3 +- packages/core/src/index.ts | 3 +- .../core/src/utils/getNativeImplementation.ts | 77 +++++++++++++++++++ packages/core/src/utils/is.ts | 8 ++ packages/core/src/utils/supports.ts | 17 ++-- 6 files changed, 100 insertions(+), 61 deletions(-) create mode 100644 packages/core/src/utils/getNativeImplementation.ts diff --git a/packages/browser-utils/src/getNativeImplementation.ts b/packages/browser-utils/src/getNativeImplementation.ts index 410d2abf4de0..4705f05f51bf 100644 --- a/packages/browser-utils/src/getNativeImplementation.ts +++ b/packages/browser-utils/src/getNativeImplementation.ts @@ -1,20 +1,11 @@ -import { debug, isNativeFunction } from '@sentry/core'; -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. - */ +import { getNativeImplementation as getNativeImplementationCore } from '@sentry/core'; interface CacheableImplementations { setTimeout: typeof WINDOW.setTimeout; fetch: typeof WINDOW.fetch; } -const cachedImplementations: Partial = {}; - /** * Get the native implementation of a browser function. * @@ -27,48 +18,14 @@ const cachedImplementations: Partial = {}; 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 (isNativeFunction(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?.[name]) { - impl = contentWindow[name] as CacheableImplementations[T]; - } - document.head.removeChild(sandbox); - } catch (e) { - // Could not create sandbox iframe, just use window.xxx - DEBUG_BUILD && debug.warn(`Could not create sandbox iframe for ${name} check, bailing to window.${name}: `, e); - } + const nativeImpl = getNativeImplementationCore(name); + if (nativeImpl) { + return nativeImpl; } // 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; + return WINDOW[name] as CacheableImplementations[T]; } /** diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts index a4d0960b1ccb..eca896d4ab82 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -24,7 +24,8 @@ export { addClickKeypressInstrumentationHandler } from './instrument/dom'; export { addHistoryInstrumentationHandler } from './instrument/history'; -export { fetch, setTimeout, clearCachedImplementation, getNativeImplementation } from './getNativeImplementation'; +export { fetch, setTimeout, getNativeImplementation } from './getNativeImplementation'; +export { clearCachedImplementation } from '@sentry/core'; export { addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY } from './instrument/xhr'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 61865ea7ba3c..ae8339e6fd04 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -216,7 +216,9 @@ export { isSyntheticEvent, isThenable, isVueViewModel, + isNativeFunction, } from './utils/is'; +export { getNativeImplementation, clearCachedImplementation } from './utils/getNativeImplementation'; export { isBrowser } from './utils/isBrowser'; export { CONSOLE_LEVELS, consoleSandbox, debug, originalConsoleMethods } from './utils/debug-logger'; export type { SentryDebugLogger } from './utils/debug-logger'; @@ -259,7 +261,6 @@ export { export { filenameIsInApp, node, nodeStackLineParser } from './utils/node-stack-trace'; export { isMatchingPattern, safeJoin, snipLine, stringMatchesSomePattern, truncate } from './utils/string'; export { - isNativeFunction, supportsDOMError, supportsDOMException, supportsErrorEvent, diff --git a/packages/core/src/utils/getNativeImplementation.ts b/packages/core/src/utils/getNativeImplementation.ts new file mode 100644 index 000000000000..ef9c58b1f576 --- /dev/null +++ b/packages/core/src/utils/getNativeImplementation.ts @@ -0,0 +1,77 @@ +import { DEBUG_BUILD } from '../debug-build'; +import { GLOBAL_OBJ } from './worldwide'; +import { debug } from './debug-logger'; +import { isNativeFunction } from './is'; + +/** + * 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. + */ +const WINDOW = GLOBAL_OBJ as unknown as Window; + +interface CacheableImplementations { + setTimeout: typeof WINDOW.setTimeout; + fetch: typeof WINDOW.fetch; +} + +const cachedImplementations: Partial = {}; + +/** + * Get the native implementation of a 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 main use case is for browser-based native function lookup but it can also be used for other + * runtimes, just with a less sophisticated mechanism. + * + * 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] | undefined { + const cached = cachedImplementations[name]; + if (cached) { + return cached; + } + + let impl = WINDOW[name] as CacheableImplementations[T] | undefined; + + // Fast path to avoid DOM I/O + if (impl && isNativeFunction(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?.[name]) { + impl = contentWindow[name] as CacheableImplementations[T]; + } + document.head.removeChild(sandbox); + } catch (e) { + // Could not create sandbox iframe, just use window.xxx + DEBUG_BUILD && debug.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 undefined; + } + + return (cachedImplementations[name] = impl.bind(WINDOW) as CacheableImplementations[T]); +} + +/** Clear a cached implementation. */ +export function clearCachedImplementation(name: keyof CacheableImplementations): void { + cachedImplementations[name] = undefined; +} diff --git a/packages/core/src/utils/is.ts b/packages/core/src/utils/is.ts index 4c8589934800..eb0bbe628cbf 100644 --- a/packages/core/src/utils/is.ts +++ b/packages/core/src/utils/is.ts @@ -214,3 +214,11 @@ export function isVueViewModel(wat: unknown): wat is VueViewModel | VNode { export function isRequest(request: unknown): request is Request { return typeof Request !== 'undefined' && isInstanceOf(request, Request); } + +/** + * isNative checks if the given function is a native implementation + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export function isNativeFunction(func: Function): boolean { + return func && /^function\s+\w+\(\)\s+\{\s+\[native code\]\s+\}$/.test(func.toString()); +} diff --git a/packages/core/src/utils/supports.ts b/packages/core/src/utils/supports.ts index 33a7a2d3e3af..bae6c220dc36 100644 --- a/packages/core/src/utils/supports.ts +++ b/packages/core/src/utils/supports.ts @@ -1,5 +1,4 @@ -import { DEBUG_BUILD } from '../debug-build'; -import { debug } from './debug-logger'; +import { getNativeImplementation } from './getNativeImplementation'; import { GLOBAL_OBJ } from './worldwide'; const WINDOW = GLOBAL_OBJ as unknown as Window; @@ -89,14 +88,6 @@ function _isFetchSupported(): boolean { } } -/** - * isNative checks if the given function is a native implementation - */ -// eslint-disable-next-line @typescript-eslint/ban-types -export function isNativeFunction(func: Function): boolean { - return func && /\[native code\]/.test(func.toString()); -} - /** * Tells whether current environment supports Fetch API natively * {@link supportsNativeFetch}. @@ -108,7 +99,11 @@ export function supportsNativeFetch(): boolean { return true; } - return _isFetchSupported(); + if (!_isFetchSupported()) { + return false; + } + + return !!getNativeImplementation('fetch'); } /** From 6ded375b10346a4965111a45c068abee30989bc7 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 19 Mar 2026 17:39:31 +0100 Subject: [PATCH 3/4] try other approach, extract iframe implementation retrieval --- .../src/getNativeImplementation.ts | 37 +++++++-- packages/browser-utils/src/index.ts | 3 +- packages/core/src/index.ts | 2 +- .../core/src/utils/getNativeImplementation.ts | 77 ------------------- .../getNativeImplementationFromIframe.ts | 32 ++++++++ packages/core/src/utils/supports.ts | 13 +++- 6 files changed, 76 insertions(+), 88 deletions(-) delete mode 100644 packages/core/src/utils/getNativeImplementation.ts create mode 100644 packages/core/src/utils/getNativeImplementationFromIframe.ts diff --git a/packages/browser-utils/src/getNativeImplementation.ts b/packages/browser-utils/src/getNativeImplementation.ts index 4705f05f51bf..fbef452f2842 100644 --- a/packages/browser-utils/src/getNativeImplementation.ts +++ b/packages/browser-utils/src/getNativeImplementation.ts @@ -1,11 +1,19 @@ +import { getNativeImplementationFromIframe, isNativeFunction } from '@sentry/core'; import { WINDOW } from './types'; -import { getNativeImplementation as getNativeImplementationCore } from '@sentry/core'; + +/** + * 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. * @@ -18,14 +26,31 @@ interface CacheableImplementations { export function getNativeImplementation( name: T, ): CacheableImplementations[T] { - const nativeImpl = getNativeImplementationCore(name); - if (nativeImpl) { - return nativeImpl; + const cached = cachedImplementations[name]; + if (cached) { + return cached; } - // Sanity check: This _should_ not happen, but if it does, we just skip caching... + let impl = WINDOW[name] as CacheableImplementations[T]; + + // Fast path to avoid DOM I/O + if (isNativeFunction(impl)) { + return (cachedImplementations[name] = impl.bind(WINDOW) as CacheableImplementations[T]); + } + + // Sanity check: the ?? fallback _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. - return WINDOW[name] as CacheableImplementations[T]; + impl = getNativeImplementationFromIframe(name) ?? impl; + 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; } /** diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts index eca896d4ab82..7f7841050552 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -24,8 +24,7 @@ export { addClickKeypressInstrumentationHandler } from './instrument/dom'; export { addHistoryInstrumentationHandler } from './instrument/history'; -export { fetch, setTimeout, getNativeImplementation } from './getNativeImplementation'; -export { clearCachedImplementation } from '@sentry/core'; +export { fetch, setTimeout, getNativeImplementation, clearCachedImplementation } from './getNativeImplementation'; export { addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY } from './instrument/xhr'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ae8339e6fd04..282371163698 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -218,7 +218,7 @@ export { isVueViewModel, isNativeFunction, } from './utils/is'; -export { getNativeImplementation, clearCachedImplementation } from './utils/getNativeImplementation'; +export { getNativeImplementationFromIframe } from './utils/getNativeImplementationFromIframe'; export { isBrowser } from './utils/isBrowser'; export { CONSOLE_LEVELS, consoleSandbox, debug, originalConsoleMethods } from './utils/debug-logger'; export type { SentryDebugLogger } from './utils/debug-logger'; diff --git a/packages/core/src/utils/getNativeImplementation.ts b/packages/core/src/utils/getNativeImplementation.ts deleted file mode 100644 index ef9c58b1f576..000000000000 --- a/packages/core/src/utils/getNativeImplementation.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { DEBUG_BUILD } from '../debug-build'; -import { GLOBAL_OBJ } from './worldwide'; -import { debug } from './debug-logger'; -import { isNativeFunction } from './is'; - -/** - * 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. - */ -const WINDOW = GLOBAL_OBJ as unknown as Window; - -interface CacheableImplementations { - setTimeout: typeof WINDOW.setTimeout; - fetch: typeof WINDOW.fetch; -} - -const cachedImplementations: Partial = {}; - -/** - * Get the native implementation of a 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 main use case is for browser-based native function lookup but it can also be used for other - * runtimes, just with a less sophisticated mechanism. - * - * 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] | undefined { - const cached = cachedImplementations[name]; - if (cached) { - return cached; - } - - let impl = WINDOW[name] as CacheableImplementations[T] | undefined; - - // Fast path to avoid DOM I/O - if (impl && isNativeFunction(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?.[name]) { - impl = contentWindow[name] as CacheableImplementations[T]; - } - document.head.removeChild(sandbox); - } catch (e) { - // Could not create sandbox iframe, just use window.xxx - DEBUG_BUILD && debug.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 undefined; - } - - return (cachedImplementations[name] = impl.bind(WINDOW) as CacheableImplementations[T]); -} - -/** Clear a cached implementation. */ -export function clearCachedImplementation(name: keyof CacheableImplementations): void { - cachedImplementations[name] = undefined; -} diff --git a/packages/core/src/utils/getNativeImplementationFromIframe.ts b/packages/core/src/utils/getNativeImplementationFromIframe.ts new file mode 100644 index 000000000000..8e3efede136f --- /dev/null +++ b/packages/core/src/utils/getNativeImplementationFromIframe.ts @@ -0,0 +1,32 @@ +import { DEBUG_BUILD } from '../debug-build'; +import { GLOBAL_OBJ } from './worldwide'; +import { debug } from './debug-logger'; + +const WINDOW = GLOBAL_OBJ as unknown as Window; + +interface CacheableImplementations { + setTimeout: typeof WINDOW.setTimeout; + fetch: typeof WINDOW.fetch; +} + +export function getNativeImplementationFromIframe(name: T) { + let impl = undefined; + 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?.[name]) { + impl = contentWindow[name] as CacheableImplementations[T]; + } + document.head.removeChild(sandbox); + } catch (e) { + // Could not create sandbox iframe, just use window.xxx + DEBUG_BUILD && debug.warn(`Could not create sandbox iframe for ${name} check, bailing to window.${name}: `, e); + } + } + return impl; +} diff --git a/packages/core/src/utils/supports.ts b/packages/core/src/utils/supports.ts index bae6c220dc36..ceb752b4a7fa 100644 --- a/packages/core/src/utils/supports.ts +++ b/packages/core/src/utils/supports.ts @@ -1,4 +1,5 @@ -import { getNativeImplementation } from './getNativeImplementation'; +import { getNativeImplementationFromIframe } from './getNativeImplementationFromIframe'; +import { isNativeFunction } from './is'; import { GLOBAL_OBJ } from './worldwide'; const WINDOW = GLOBAL_OBJ as unknown as Window; @@ -103,7 +104,15 @@ export function supportsNativeFetch(): boolean { return false; } - return !!getNativeImplementation('fetch'); + // Fast path to avoid DOM I/O + // eslint-disable-next-line @typescript-eslint/unbound-method + if (isNativeFunction(WINDOW.fetch)) { + return true; + } + + const nativeImpl = getNativeImplementationFromIframe('fetch'); + + return nativeImpl ? isNativeFunction(nativeImpl) : false; } /** From 24e35f3dc5adfbf96e9f38a386673db7f6ae51cf Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 19 Mar 2026 18:30:46 +0100 Subject: [PATCH 4/4] fix last issue? --- packages/browser-utils/src/getNativeImplementation.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/browser-utils/src/getNativeImplementation.ts b/packages/browser-utils/src/getNativeImplementation.ts index fbef452f2842..70a0baa11e59 100644 --- a/packages/browser-utils/src/getNativeImplementation.ts +++ b/packages/browser-utils/src/getNativeImplementation.ts @@ -38,14 +38,14 @@ export function getNativeImplementation