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); + } } diff --git a/packages/browser-utils/test/getNativeImplementation.test.ts b/packages/browser-utils/test/getNativeImplementation.test.ts new file mode 100644 index 000000000000..cb7dc59011e0 --- /dev/null +++ b/packages/browser-utils/test/getNativeImplementation.test.ts @@ -0,0 +1,53 @@ +/** + * @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', 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; + + // 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); + + await vi.waitFor(() => expect(callback).toHaveBeenCalledTimes(2)); + }); +});