ref(core): Remove redundant iframe check in supportsNativeFetch#19853
ref(core): Remove redundant iframe check in supportsNativeFetch#19853
supportsNativeFetch#19853Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
size-limit report 📦
|
node-overhead report 🧳Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.
|
e2fd3a8 to
39fcc12
Compare
supportsNativeFetch
Semver Impact of This PR🟢 Patch (bug fixes) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨
Bug Fixes 🐛Deps
Other
Internal Changes 🔧Core
Other
🤖 This preview updates automatically when you update the PR. |
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
39fcc12 to
dff6335
Compare
packages/core/src/utils/supports.ts
Outdated
| // 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()); |
There was a problem hiding this comment.
Bug: The relaxed regex in isNativeFunction can misidentify a wrapped fetch as native, causing getNativeImplementation to return a version that can trigger an infinite loop with ad-blockers.
Severity: HIGH
Suggested Fix
Revert the regex in isNativeFunction to a stricter version that validates the function's structure, not just the presence of the [native code] substring. The previous regex /^function\s+\w+\(\)\s+\{\s+\[native code\]\s+\}$/ or a similarly strict alternative should be used to ensure only truly native functions are identified.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: packages/core/src/utils/supports.ts#L97
Potential issue: The `isNativeFunction` utility was changed to use a permissive regex
that only checks for the substring `[native code]`. This causes wrapped or polyfilled
functions, such as a `fetch` implementation modified by an ad-blocker or framework, to
be incorrectly identified as native. Consequently, `getNativeImplementation('fetch')`
returns this wrapped version instead of a truly native one. If Sentry then uses this
wrapped `fetch` for its own event reporting and an ad-blocker intercepts the request, it
can trigger an infinite loop, freezing the user's application. This bypasses a specific
safety mechanism designed to prevent this exact scenario.
Did we get this right? 👍 / 👎 to inform future reviews.
packages/core/src/utils/supports.ts
Outdated
| // 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()); |
There was a problem hiding this comment.
I'll revert this. It saves a few bytes but drastically reduces specificity. We have no unit tests that cover this function :(
| 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; |
There was a problem hiding this comment.
Unfortunately, this is a behaviour change to the original function. Supporting a fetch implementation is not the same as supporting/having the native one. I'm going to experiment with moving getNativeImplementation to core so that we can actually reuse the iframe lookup but I'm not sure how much bundle size this still saves.
There was a problem hiding this comment.
Saving old size check for comparison:
size-limit report 📦
| Path | Size | % Change | Change |
|---|---|---|---|
| @sentry/browser | 25.4 kB | -0.92% | -234 B 🔽 |
| @sentry/browser - with treeshaking flags | 23.94 kB | -0.84% | -201 B 🔽 |
| @sentry/browser (incl. Tracing) | 42.4 kB | -0.52% | -220 B 🔽 |
| @sentry/browser (incl. Tracing, Profiling) | 47.06 kB | -0.48% | -223 B 🔽 |
| @sentry/browser (incl. Tracing, Replay) | 81.18 kB | -0.31% | -245 B 🔽 |
| @sentry/browser (incl. Tracing, Replay) - with treeshaking flags | 70.78 kB | -0.3% | -211 B 🔽 |
| @sentry/browser (incl. Tracing, Replay with Canvas) | 85.88 kB | -0.29% | -245 B 🔽 |
| @sentry/browser (incl. Tracing, Replay, Feedback) | 98.14 kB | -0.24% | -235 B 🔽 |
| @sentry/browser (incl. Feedback) | 42.22 kB | -0.53% | -221 B 🔽 |
| @sentry/browser (incl. sendFeedback) | 30.07 kB | -0.78% | -234 B 🔽 |
| @sentry/browser (incl. FeedbackAsync) | 35.13 kB | -0.66% | -232 B 🔽 |
| @sentry/browser (incl. Metrics) | 26.68 kB | -0.92% | -246 B 🔽 |
| @sentry/browser (incl. Logs) | 26.82 kB | -0.91% | -245 B 🔽 |
| @sentry/browser (incl. Metrics & Logs) | 27.51 kB | -0.83% | -228 B 🔽 |
| @sentry/react | 27.19 kB | -0.75% | -203 B 🔽 |
| @sentry/react (incl. Tracing) | 44.73 kB | -0.5% | -224 B 🔽 |
| @sentry/vue | 29.86 kB | -0.73% | -218 B 🔽 |
| @sentry/vue (incl. Tracing) | 44.26 kB | -0.51% | -226 B 🔽 |
| @sentry/svelte | 25.43 kB | -0.93% | -238 B 🔽 |
| CDN Bundle | 28.11 kB | -0.61% | -172 B 🔽 |
| CDN Bundle (incl. Tracing) | 43.29 kB | -0.49% | -213 B 🔽 |
| CDN Bundle (incl. Logs, Metrics) | 28.97 kB | -0.62% | -178 B 🔽 |
| CDN Bundle (incl. Tracing, Logs, Metrics) | 44.16 kB | -0.44% | -195 B 🔽 |
| CDN Bundle (incl. Replay, Logs, Metrics) | 68.03 kB | -0.27% | -184 B 🔽 |
| CDN Bundle (incl. Tracing, Replay) | 80.15 kB | -0.23% | -179 B 🔽 |
| CDN Bundle (incl. Tracing, Replay, Logs, Metrics) | 81.04 kB | -0.24% | -193 B 🔽 |
| CDN Bundle (incl. Tracing, Replay, Feedback) | 85.66 kB | -0.25% | -211 B 🔽 |
| CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) | 86.55 kB | -0.25% | -215 B 🔽 |
| CDN Bundle - uncompressed | 81.97 kB | -0.8% | -655 B 🔽 |
| CDN Bundle (incl. Tracing) - uncompressed | 127.89 kB | -0.52% | -666 B 🔽 |
| CDN Bundle (incl. Logs, Metrics) - uncompressed | 84.83 kB | -0.77% | -655 B 🔽 |
| CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed | 130.76 kB | -0.51% | -666 B 🔽 |
| CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed | 208.47 kB | -0.32% | -655 B 🔽 |
| CDN Bundle (incl. Tracing, Replay) - uncompressed | 244.75 kB | -0.28% | -666 B 🔽 |
| CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed | 247.6 kB | -0.27% | -666 B 🔽 |
| CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed | 257.66 kB | -0.26% | -666 B 🔽 |
| CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed | 260.5 kB | -0.26% | -666 B 🔽 |
| @sentry/nextjs (client) | 47.16 kB | -0.44% | -204 B 🔽 |
| @sentry/sveltekit (client) | 42.82 kB | -0.6% | -255 B 🔽 |
| @sentry/node-core | 56.28 kB | -0.12% | -63 B 🔽 |
| @sentry/node | 173.21 kB | +0.03% | +51 B 🔺 |
| @sentry/node - without tracing | 96.27 kB | -0.08% | -76 B 🔽 |
| @sentry/aws-serverless | 113.28 kB | -0.06% | -62 B 🔽 |
| 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 !!getNativeImplementation('fetch'); | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
Bug: supportsNativeFetch() now incorrectly returns true for polyfilled fetch implementations, causing instrumentation to wrap non-native fetch, which can lead to infinite loops.
Severity: HIGH
Suggested Fix
Modify getNativeImplementation to not return the polyfilled implementation as a fallback. It should return undefined or null if a truly native implementation cannot be found, ensuring supportsNativeFetch() correctly returns false for polyfills.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: packages/core/src/utils/supports.ts#L103-L109
Potential issue: The function `getNativeImplementation('fetch')` was changed to return a
polyfilled `WINDOW.fetch` as a fallback if a native version cannot be found. As a
result, `supportsNativeFetch()`, which checks the truthiness of this result, now
incorrectly returns `true` for polyfills. This causes the `httpclient` integration to
instrument non-native `fetch` implementations, which can trigger an infinite loop when
another wrapper (like an ad-blocker) is also present. This is a regression that
reintroduces a bug the original code was designed to prevent.

Summary
Remove ~30 lines of iframe-based DOM manipulation from
supportsNativeFetch(). Saves ~200 bytes gzipped — the single biggest win in this bundle size effort.Problem
supportsNativeFetch()created a sandboxed iframe to check iffetchis natively implemented. This is identical logic togetNativeImplementation("fetch")inbrowser-utils, creating two separate iframe-based checks in the CDN bundle.The function is only called behind a
skipNativeFetchCheckguard:In the base CDN bundle,
skipNativeFetchCheckis never set totrue, making the iframe code dead weight. Terser cannot tree-shake it because it cannot prove the parameter is always falsy.Solution
Simplify
supportsNativeFetch()to just delegate to_isFetchSupported()(checks ifwindow.fetchexists). The native-vs-polyfill distinction is handled bygetNativeImplementationat call sites that need it.Also simplifies
isNativeFunctionfrom/^function\s+\w+\(\)\s+\{\s+\[native code\]\s+\}$/to/\[native code\]/— more permissive across different JS enginetoString()formats.Part of #19833.
Co-Authored-By: Claude claude@anthropic.com