Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions packages/core/src/integrations/functiontostring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ 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';

let originalFunctionToString: () => void;

const INTEGRATION_NAME = 'FunctionToString' as const;

const SETUP_CLIENTS = new WeakMap<Client, boolean>();
Expand All @@ -16,17 +13,29 @@ const _functionToStringIntegration = (() => {
name: INTEGRATION_NAME,
setupOnce() {
// eslint-disable-next-line @typescript-eslint/unbound-method
originalFunctionToString = Function.prototype.toString;
const originalFunctionToString = Function.prototype.toString;

// 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 = function (this: WrappedFunction, ...args: unknown[]): string {
const originalFunction = getOriginalFunction(this);
const context =
SETUP_CLIENTS.has(getClient() as Client) && originalFunction !== undefined ? originalFunction : this;
return originalFunctionToString.apply(context, args);
};
Function.prototype.toString = new Proxy(originalFunctionToString, {
apply(target, thisArg, args) {
const originalFunction = getOriginalFunction(thisArg);
let context = thisArg;

try {
if (SETUP_CLIENTS.has(getClient()!) && originalFunction) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unguarded non-null assertion

Low Severity

New non-null assertion on getClient()! has no comment explaining why a safer type is impossible. Elsewhere in this package the usual pattern is to bind getClient() and null-check before use, which also avoids the assertion. Flagged because it was mentioned in the PR review guidelines rules file.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 69701ca. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this is fine, if we do has(undefined) it will simply return false

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.
}

return Reflect.apply(target, context, args);
},
});
} catch {
// ignore errors here, just don't patch this
}
Expand Down
33 changes: 32 additions & 1 deletion packages/core/test/lib/integrations/functiontostring.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import { afterAll, beforeEach, describe, expect, it } from 'vitest';
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as currentScopes from '../../../src/currentScopes';
import { fill, getClient, getCurrentScope, setCurrentClient } from '../../../src';
import { functionToStringIntegration } from '../../../src/integrations/functiontostring';
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';

vi.mock('../../../src/currentScopes', async importOriginal => {
const actual = await importOriginal<typeof currentScopes>();
return { ...actual, getClient: vi.fn(actual.getClient) };
});

describe('FunctionToString', () => {
beforeEach(() => {
const testClient = new TestClient(getDefaultTestClientOptions({}));
setCurrentClient(testClient);
});

afterEach(() => {
vi.mocked(currentScopes.getClient).mockClear();
});

afterAll(() => {
getCurrentScope().setClient(undefined);
vi.restoreAllMocks();
});

it('it works as expected', () => {
Expand Down Expand Up @@ -55,4 +66,24 @@ describe('FunctionToString', () => {

expect(foo.bar.toString()).not.toBe(originalFunction);
});

it('falls back to native toString and does not throw when the carrier read throws', () => {
const foo = {
bar(wat: boolean): boolean {
return wat;
},
};
const nativeString = foo.bar.toString();

const fts = functionToStringIntegration();
getClient()?.addIntegration(fts);

// Simulate a `SecurityError` thrown while reading the Sentry carrier off a cross-origin `WindowProxy`.
vi.mocked(currentScopes.getClient).mockImplementation(() => {
throw new Error('SecurityError: Blocked a frame from accessing a cross-origin frame.');
});

expect(() => foo.bar.toString()).not.toThrow();
expect(foo.bar.toString()).toBe(nativeString);
});
});
Loading