diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts index 86844843664f..c915e55d8c3f 100644 --- a/packages/core/src/integrations/functiontostring.ts +++ b/packages/core/src/integrations/functiontostring.ts @@ -2,6 +2,7 @@ import type { Client } from '../client'; import { getClient } from '../currentScopes'; import { defineIntegration } from '../integration'; import type { IntegrationFn } from '../types/integration'; +import type { WrappedFunction } from '../types/wrappedfunction'; import { getOriginalFunction } from '../utils/object'; const INTEGRATION_NAME = 'FunctionToString' as const; @@ -18,24 +19,22 @@ const _functionToStringIntegration = (() => { // intrinsics (like Function.prototype) might be immutable in some environments // e.g. Node with --frozen-intrinsics, XS (an embedded JavaScript engine) or SES (a JavaScript proposal) try { - Function.prototype.toString = new Proxy(originalFunctionToString, { - apply(target, thisArg, args) { - const originalFunction = getOriginalFunction(thisArg); - let context = thisArg; + Function.prototype.toString = function (this: WrappedFunction, ...args: unknown[]): string { + const originalFunction = getOriginalFunction(this); + let unwrappedFunction: WrappedFunction | undefined; - try { - if (SETUP_CLIENTS.has(getClient()!) && originalFunction) { - context = originalFunction; - } - } catch { - // Reading the Sentry carrier off `getClient()` can throw a `SecurityError` when `this` (or the global - // object) is a `WindowProxy` whose browsing context was navigated cross-origin. The native - // `toString` never throws here, so fall back to it to avoid turning harmless introspection into noise. + try { + if (SETUP_CLIENTS.has(getClient() as Client) && originalFunction !== undefined) { + unwrappedFunction = originalFunction; } + } catch { + // Reading the Sentry carrier off `getClient()` can throw a `SecurityError` when `this` (or the global + // object) is a `WindowProxy` whose browsing context was navigated cross-origin. The native + // `toString` never throws here, so fall back to it to avoid turning harmless introspection into noise. + } - return Reflect.apply(target, context, args); - }, - }); + return originalFunctionToString.apply(unwrappedFunction ?? this, args); + }; } catch { // ignore errors here, just don't patch this } diff --git a/packages/core/test/lib/integrations/functiontostring.test.ts b/packages/core/test/lib/integrations/functiontostring.test.ts index 1e992cbd93cb..99b54fa1c3ff 100644 --- a/packages/core/test/lib/integrations/functiontostring.test.ts +++ b/packages/core/test/lib/integrations/functiontostring.test.ts @@ -17,6 +17,7 @@ describe('FunctionToString', () => { afterEach(() => { vi.mocked(currentScopes.getClient).mockClear(); + vi.restoreAllMocks(); }); afterAll(() => { @@ -67,6 +68,22 @@ describe('FunctionToString', () => { expect(foo.bar.toString()).not.toBe(originalFunction); }); + it('does not recurse when Reflect.apply performs a function toString check', () => { + function inspectedFunction(): void {} + + const fts = functionToStringIntegration(); + getClient()?.addIntegration(fts); + const expected = inspectedFunction.toString(); + const originalReflectApply = Reflect.apply; + + vi.spyOn(Reflect, 'apply').mockImplementation((target, thisArgument, argumentsList) => { + target.toString(); + return originalReflectApply(target, thisArgument, argumentsList); + }); + + expect(inspectedFunction.toString()).toBe(expected); + }); + it('falls back to native toString and does not throw when the carrier read throws', () => { const foo = { bar(wat: boolean): boolean {