From 3f04354bad7ecc8343824c305e59d398e4a8794e Mon Sep 17 00:00:00 2001 From: Sergiy Dybskiy Date: Sat, 3 Oct 2026 20:00:05 +0000 Subject: [PATCH 1/3] fix(browser-utils): Handle NS_ERROR_NOT_INITIALIZED from setTimeout in Firefox Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> --- .../browser-utils/src/getNativeImplementation.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/browser-utils/src/getNativeImplementation.ts b/packages/browser-utils/src/getNativeImplementation.ts index 0bb5302b12f5..402cb6bac8cc 100644 --- a/packages/browser-utils/src/getNativeImplementation.ts +++ b/packages/browser-utils/src/getNativeImplementation.ts @@ -119,5 +119,15 @@ export function fetch(...rest: Parameters): ReturnType): ReturnType { - return getNativeImplementation('setTimeout')(...rest); + try { + return getNativeImplementation('setTimeout')(...rest); + } catch (e) { + // The implementation retrieved from the (since removed) sandbox iframe may throw when invoked, + // e.g. Firefox throws `NS_ERROR_NOT_INITIALIZED` for timers of a detached window. + // In this case, fall back to (and cache) the window's own setTimeout. + DEBUG_BUILD && debug.warn('Could not use native setTimeout implementation, falling back to window.setTimeout: ', e); + const fallback = WINDOW.setTimeout.bind(WINDOW) as typeof WINDOW.setTimeout; + cachedImplementations.setTimeout = fallback; + return fallback(...rest); + } } From 2a04dc0cf3c3690ebbe9a1320bb96d8895ef1020 Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:15:30 +0000 Subject: [PATCH 2/3] fix(browser-utils): Handle NS_ERROR_NOT_INITIALIZED in setTimeout for Firefox --- .../test/getNativeImplementation.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 packages/browser-utils/test/getNativeImplementation.test.ts diff --git a/packages/browser-utils/test/getNativeImplementation.test.ts b/packages/browser-utils/test/getNativeImplementation.test.ts new file mode 100644 index 000000000000..3f6079ab2312 --- /dev/null +++ b/packages/browser-utils/test/getNativeImplementation.test.ts @@ -0,0 +1,51 @@ +/** + * @vitest-environment jsdom + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { clearCachedImplementation, setTimeout as sentrySetTimeout } from '../src/getNativeImplementation'; +import { WINDOW } from '../src/types'; + +describe('setTimeout', () => { + const originalSetTimeout = WINDOW.setTimeout; + + beforeEach(() => { + clearCachedImplementation('setTimeout'); + }); + + afterEach(() => { + WINDOW.setTimeout = originalSetTimeout; + clearCachedImplementation('setTimeout'); + vi.restoreAllMocks(); + }); + + it('falls back to window.setTimeout if the sandboxed iframe implementation throws', () => { + // Simulate a wrapped (non-native) window.setTimeout, e.g. by a browser extension + const wrappedSetTimeout = vi.fn((...args: Parameters) => originalSetTimeout(...args)); + WINDOW.setTimeout = wrappedSetTimeout as unknown as typeof WINDOW.setTimeout; + + // Simulate Firefox throwing `NS_ERROR_NOT_INITIALIZED` when calling setTimeout of a detached iframe window + const detachedSetTimeout = vi.fn(() => { + const error = new Error(''); + error.name = 'NS_ERROR_NOT_INITIALIZED'; + throw error; + }); + const fakeIframe = { hidden: false, contentWindow: { setTimeout: detachedSetTimeout } }; + + const document = WINDOW.document as Document; + vi.spyOn(document, 'createElement').mockReturnValue(fakeIframe as unknown as HTMLElement); + vi.spyOn(document.head, 'appendChild').mockImplementation(node => node); + vi.spyOn(document.head, 'removeChild').mockImplementation(node => node); + + const callback = vi.fn(); + + expect(() => sentrySetTimeout(callback, 0)).not.toThrow(); + expect(detachedSetTimeout).toHaveBeenCalledTimes(1); + expect(wrappedSetTimeout).toHaveBeenCalledTimes(1); + + // Subsequent calls use the cached fallback and do not hit the broken implementation again + sentrySetTimeout(callback, 0); + expect(detachedSetTimeout).toHaveBeenCalledTimes(1); + expect(wrappedSetTimeout).toHaveBeenCalledTimes(2); + }); +}); From f9eb9e17a8c735d40db2de4c2276f15c79cb0f0b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 5 Oct 2026 13:51:08 +0200 Subject: [PATCH 3/3] test(browser-utils): Assert setTimeout fallback invokes callback Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/browser-utils/test/getNativeImplementation.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/browser-utils/test/getNativeImplementation.test.ts b/packages/browser-utils/test/getNativeImplementation.test.ts index 3f6079ab2312..cb7dc59011e0 100644 --- a/packages/browser-utils/test/getNativeImplementation.test.ts +++ b/packages/browser-utils/test/getNativeImplementation.test.ts @@ -19,7 +19,7 @@ describe('setTimeout', () => { vi.restoreAllMocks(); }); - it('falls back to window.setTimeout if the sandboxed iframe implementation throws', () => { + it('falls back to window.setTimeout if the sandboxed iframe implementation throws', async () => { // Simulate a wrapped (non-native) window.setTimeout, e.g. by a browser extension const wrappedSetTimeout = vi.fn((...args: Parameters) => originalSetTimeout(...args)); WINDOW.setTimeout = wrappedSetTimeout as unknown as typeof WINDOW.setTimeout; @@ -47,5 +47,7 @@ describe('setTimeout', () => { sentrySetTimeout(callback, 0); expect(detachedSetTimeout).toHaveBeenCalledTimes(1); expect(wrappedSetTimeout).toHaveBeenCalledTimes(2); + + await vi.waitFor(() => expect(callback).toHaveBeenCalledTimes(2)); }); });