Skip to content

ref(core): Remove redundant iframe check in supportsNativeFetch#19853

Open
HazAT wants to merge 2 commits intodevelopfrom
bundle-size/supports-native-fetch
Open

ref(core): Remove redundant iframe check in supportsNativeFetch#19853
HazAT wants to merge 2 commits intodevelopfrom
bundle-size/supports-native-fetch

Conversation

@HazAT
Copy link
Member

@HazAT HazAT commented Mar 17, 2026

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 if fetch is natively implemented. This is identical logic to getNativeImplementation("fetch") in browser-utils, creating two separate iframe-based checks in the CDN bundle.

The function is only called behind a skipNativeFetchCheck guard:

if (skipNativeFetchCheck && !supportsNativeFetch()) { return; }

In the base CDN bundle, skipNativeFetchCheck is never set to true, 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 if window.fetch exists). The native-vs-polyfill distinction is handled by getNativeImplementation at call sites that need it.

Also simplifies isNativeFunction from /^function\s+\w+\(\)\s+\{\s+\[native code\]\s+\}$/ to /\[native code\]/ — more permissive across different JS engine toString() formats.

Part of #19833.

Co-Authored-By: Claude claude@anthropic.com

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 17, 2026

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 25.45 kB -0.73% -185 B 🔽
@sentry/browser - with treeshaking flags 23.97 kB -0.69% -166 B 🔽
@sentry/browser (incl. Tracing) 42.45 kB -0.41% -171 B 🔽
@sentry/browser (incl. Tracing, Profiling) 47.11 kB -0.36% -170 B 🔽
@sentry/browser (incl. Tracing, Replay) 81.22 kB -0.25% -202 B 🔽
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 70.82 kB -0.25% -173 B 🔽
@sentry/browser (incl. Tracing, Replay with Canvas) 85.92 kB -0.24% -199 B 🔽
@sentry/browser (incl. Tracing, Replay, Feedback) 98.18 kB -0.2% -190 B 🔽
@sentry/browser (incl. Feedback) 42.28 kB -0.39% -165 B 🔽
@sentry/browser (incl. sendFeedback) 30.12 kB -0.62% -185 B 🔽
@sentry/browser (incl. FeedbackAsync) 35.2 kB -0.47% -166 B 🔽
@sentry/browser (incl. Metrics) 26.72 kB -0.75% -200 B 🔽
@sentry/browser (incl. Logs) 26.86 kB -0.76% -205 B 🔽
@sentry/browser (incl. Metrics & Logs) 27.55 kB -0.69% -189 B 🔽
@sentry/react 27.23 kB -0.59% -159 B 🔽
@sentry/react (incl. Tracing) 44.78 kB -0.38% -167 B 🔽
@sentry/vue 29.89 kB -0.64% -191 B 🔽
@sentry/vue (incl. Tracing) 44.3 kB -0.41% -180 B 🔽
@sentry/svelte 25.48 kB -0.73% -186 B 🔽
CDN Bundle 28.16 kB -0.43% -121 B 🔽
CDN Bundle (incl. Tracing) 43.33 kB -0.42% -179 B 🔽
CDN Bundle (incl. Logs, Metrics) 29.02 kB -0.44% -126 B 🔽
CDN Bundle (incl. Tracing, Logs, Metrics) 44.2 kB -0.36% -159 B 🔽
CDN Bundle (incl. Replay, Logs, Metrics) 68.06 kB -0.22% -148 B 🔽
CDN Bundle (incl. Tracing, Replay) 80.21 kB -0.15% -119 B 🔽
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 81.14 kB -0.12% -96 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback) 85.73 kB -0.17% -138 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 86.63 kB -0.16% -136 B 🔽
CDN Bundle - uncompressed 82.07 kB -0.67% -553 B 🔽
CDN Bundle (incl. Tracing) - uncompressed 128 kB -0.44% -564 B 🔽
CDN Bundle (incl. Logs, Metrics) - uncompressed 84.94 kB -0.65% -553 B 🔽
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 130.86 kB -0.43% -564 B 🔽
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 208.56 kB -0.27% -559 B 🔽
CDN Bundle (incl. Tracing, Replay) - uncompressed 244.84 kB -0.24% -570 B 🔽
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 247.69 kB -0.23% -570 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 257.75 kB -0.23% -570 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 260.6 kB -0.22% -570 B 🔽
@sentry/nextjs (client) 47.2 kB -0.35% -162 B 🔽
@sentry/sveltekit (client) 42.88 kB -0.45% -192 B 🔽
@sentry/node-core 56.28 kB -0.12% -62 B 🔽
@sentry/node 173.21 kB +0.04% +52 B 🔺
@sentry/node - without tracing 96.27 kB -0.08% -76 B 🔽
@sentry/aws-serverless 113.28 kB -0.06% -60 B 🔽

View base workflow run

@github-actions
Copy link
Contributor

github-actions bot commented Mar 17, 2026

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.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 8,516 - 11,512 -26%
GET With Sentry 1,628 19% 1,991 -18%
GET With Sentry (error only) 5,884 69% 7,302 -19%
POST Baseline 1,177 - 1,265 -7%
POST With Sentry 540 46% 633 -15%
POST With Sentry (error only) 1,005 85% 1,136 -12%
MYSQL Baseline 3,103 - 3,457 -10%
MYSQL With Sentry 402 13% 431 -7%
MYSQL With Sentry (error only) 2,535 82% 2,862 -11%

View base workflow run

@Lms24 Lms24 force-pushed the bundle-size/supports-native-fetch branch from e2fd3a8 to 39fcc12 Compare March 19, 2026 11:42
@Lms24 Lms24 requested review from a team as code owners March 19, 2026 11:42
@Lms24 Lms24 changed the title refactor(core): Remove redundant iframe check in supportsNativeFetch ref(core): Remove redundant iframe check in supportsNativeFetch Mar 19, 2026
@Lms24 Lms24 changed the title ref(core): Remove redundant iframe check in supportsNativeFetch ref(core): Remove redundant iframe check in supportsNativeFetch Mar 19, 2026
@Lms24 Lms24 self-assigned this Mar 19, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 19, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (remix) Server Timing Headers Trace Propagation by onurtemizkan in #18653

Bug Fixes 🐛

Deps

  • Bump next to 15.5.13/16.1.7 to fix CVE-2026-1525, CVE-202-33036 and related by chargome in #19870
  • Bump devalue 5.6.3 to 5.6.4 to fix CVE-2026-30226 by chargome in #19849
  • Bump file-type to 21.3.2 and @nestjs/common to 11.1.17 by chargome in #19847
  • Bump unhead 2.1.4 to 2.1.12 to fix CVE-2026-31860 and CVE-2026-31873 by chargome in #19848
  • Bump flatted 3.3.1 to 3.4.2 to fix CVE-2026-32141 by chargome in #19842
  • Bump tar 7.5.10 to 7.5.11 to fix CVE-2026-31802 by chargome in #19846
  • Bump hono 4.12.5 to 4.12.7 in cloudflare-hono E2E test app by chargome in #19850
  • Bump undici 6.23.0 to 6.24.1 to fix multiple CVEs by chargome in #19841

Other

  • (cloudflare) Use correct env types for withSentry by JPeer264 in #19836
  • (core) Align error span status message with core SpanStatusType for langchain/google-genai by nicohrubec in #19863
  • (deno) Clear pre-existing OTel global before registering TracerProvider by sergical in #19723
  • (nextjs) Skip tracing for tunnel requests by chargome in #19861
  • (node-core) Recycle propagationContext for each request by Lms24 in #19835
  • (serverless) Add node to metadata by nicohrubec in #19878

Internal Changes 🔧

Core

  • Remove redundant iframe check in supportsNativeFetch by HazAT in #19853
  • Simplify core utility functions for smaller bundle by HazAT in #19854

Other

  • (deps) Bump next from 16.1.5 to 16.1.7 in /dev-packages/e2e-tests/test-applications/nextjs-16 by dependabot in #19851
  • (nextjs) Skip broken ISR tests by chargome in #19871
  • (react) Add gql tests for react router by chargome in #19844
  • (release) Switch from action-prepare-release to Craft by BYK in #18763

🤖 This preview updates automatically when you update the PR.

@Lms24 Lms24 changed the base branch from autoresearch/browser-bundle-size-2026-03-17 to develop March 19, 2026 11:49
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
@Lms24 Lms24 force-pushed the bundle-size/supports-native-fetch branch from 39fcc12 to dff6335 Compare March 19, 2026 12:13
// 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());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

// 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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll revert this. It saves a few bytes but drastically reduces specificity. We have no unit tests that cover this function :(

Comment on lines -111 to -141
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saving old size check for comparison:

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

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 🔽

View base workflow run

Comment on lines 103 to 109
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');
}

/**
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants