Skip to content

Commit 809d578

Browse files
authored
fix(react): Avoid String(key) to fix Symbol conversion error (#18982)
Resolves: #18966
1 parent 8db9376 commit 809d578

File tree

4 files changed

+136
-5
lines changed

4 files changed

+136
-5
lines changed

dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import * as React from 'react';
2+
import { withProfiler } from '@sentry/react';
3+
4+
function ProfilerTestComponent() {
5+
return <div id="profiler-test">withProfiler works</div>;
6+
}
7+
ProfilerTestComponent.customStaticMethod = () => 'static method works';
8+
const ProfiledComponent = withProfiler(ProfilerTestComponent);
29

310
const Index = () => {
411
const [caughtError, setCaughtError] = React.useState(false);
@@ -7,6 +14,7 @@ const Index = () => {
714
return (
815
<>
916
<div>
17+
<ProfiledComponent />
1018
<SampleErrorBoundary>
1119
<h1>React 19</h1>
1220
{caughtError && <Throw error="caught" />}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test('withProfiler does not throw Symbol conversion error when String() is patched to simulate minifier', async ({
4+
page,
5+
}) => {
6+
const errors: string[] = [];
7+
8+
// Listen for any page errors (including the Symbol conversion error)
9+
page.on('pageerror', error => {
10+
errors.push(error.message);
11+
});
12+
13+
// Listen for console errors
14+
page.on('console', msg => {
15+
if (msg.type() === 'error') {
16+
errors.push(msg.text());
17+
}
18+
});
19+
20+
await page.addInitScript(() => {
21+
const OriginalString = String;
22+
// @ts-expect-error - intentionally replacing String to simulate minifier behavior
23+
window.String = function (value: unknown) {
24+
if (typeof value === 'symbol') {
25+
throw new TypeError('Cannot convert a Symbol value to a string');
26+
}
27+
return OriginalString(value);
28+
} as StringConstructor;
29+
30+
Object.setPrototypeOf(window.String, OriginalString);
31+
window.String.prototype = OriginalString.prototype;
32+
window.String.fromCharCode = OriginalString.fromCharCode;
33+
window.String.fromCodePoint = OriginalString.fromCodePoint;
34+
window.String.raw = OriginalString.raw;
35+
});
36+
37+
await page.goto('/');
38+
39+
const profilerTest = page.locator('#profiler-test');
40+
await expect(profilerTest).toBeVisible();
41+
await expect(profilerTest).toHaveText('withProfiler works');
42+
43+
const symbolErrors = errors.filter(e => e.includes('Cannot convert a Symbol value to a string'));
44+
expect(symbolErrors).toHaveLength(0);
45+
});

packages/react/src/hoist-non-react-statics.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,12 @@ export function hoistNonReactStatics<
143143
const sourceStatics = getStatics(sourceComponent);
144144

145145
for (const key of keys) {
146-
const keyStr = String(key);
146+
// Use key directly - String(key) throws for Symbols if minified to '' + key (#18966)
147147
if (
148-
!KNOWN_STATICS[keyStr as keyof typeof KNOWN_STATICS] &&
149-
!excludelist?.[keyStr] &&
150-
!sourceStatics?.[keyStr] &&
151-
!targetStatics?.[keyStr] &&
148+
!KNOWN_STATICS[key as keyof typeof KNOWN_STATICS] &&
149+
!(excludelist && excludelist[key as keyof C]) &&
150+
!sourceStatics?.[key as string] &&
151+
!targetStatics?.[key as string] &&
152152
!getOwnPropertyDescriptor(targetComponent, key) // Don't overwrite existing properties
153153
) {
154154
const descriptor = getOwnPropertyDescriptor(sourceComponent, key);

packages/react/test/hoist-non-react-statics.test.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,4 +290,82 @@ describe('hoistNonReactStatics', () => {
290290
expect((Hoc2 as any).originalStatic).toBe('original');
291291
expect((Hoc2 as any).hoc1Static).toBe('hoc1');
292292
});
293+
294+
it('handles Symbol.hasInstance from Function.prototype without throwing', () => {
295+
expect(Object.getOwnPropertySymbols(Function.prototype)).toContain(Symbol.hasInstance);
296+
297+
class Source extends React.Component {
298+
static customStatic = 'value';
299+
}
300+
class Target extends React.Component {}
301+
302+
// This should not throw "Cannot convert a Symbol value to a string"
303+
expect(() => hoistNonReactStatics(Target, Source)).not.toThrow();
304+
expect((Target as any).customStatic).toBe('value');
305+
});
306+
307+
it('handles components with Symbol.hasInstance defined', () => {
308+
class Source extends React.Component {
309+
static customStatic = 'value';
310+
static [Symbol.hasInstance](instance: unknown) {
311+
return instance instanceof Source;
312+
}
313+
}
314+
class Target extends React.Component {}
315+
316+
// This should not throw
317+
expect(() => hoistNonReactStatics(Target, Source)).not.toThrow();
318+
expect((Target as any).customStatic).toBe('value');
319+
// Symbol.hasInstance should be hoisted
320+
expect(typeof (Target as any)[Symbol.hasInstance]).toBe('function');
321+
});
322+
323+
it('does not rely on String() for symbol keys (simulating minifier transformation)', () => {
324+
const sym = Symbol('test');
325+
// eslint-disable-next-line prefer-template
326+
expect(() => '' + (sym as any)).toThrow('Cannot convert a Symbol value to a string');
327+
328+
// But accessing an object with a symbol key should NOT throw
329+
const obj: Record<string, boolean> = { name: true };
330+
expect(obj[sym as any]).toBeUndefined(); // No error, just undefined
331+
332+
// Now test the actual function - it should work because it shouldn't
333+
// need to convert symbols to strings
334+
class Source extends React.Component {
335+
static customStatic = 'value';
336+
}
337+
// Add a symbol property that will be iterated over
338+
(Source as any)[Symbol.for('test.symbol')] = 'symbolValue';
339+
340+
class Target extends React.Component {}
341+
342+
expect(() => hoistNonReactStatics(Target, Source)).not.toThrow();
343+
expect((Target as any).customStatic).toBe('value');
344+
expect((Target as any)[Symbol.for('test.symbol')]).toBe('symbolValue');
345+
});
346+
347+
it('works when String() throws for symbols (simulating aggressive minifier)', () => {
348+
const OriginalString = globalThis.String;
349+
globalThis.String = function (value: unknown) {
350+
if (typeof value === 'symbol') {
351+
throw new TypeError('Cannot convert a Symbol value to a string');
352+
}
353+
return OriginalString(value);
354+
} as StringConstructor;
355+
356+
try {
357+
class Source extends React.Component {
358+
static customStatic = 'value';
359+
}
360+
(Source as any)[Symbol.for('test.symbol')] = 'symbolValue';
361+
362+
class Target extends React.Component {}
363+
364+
expect(() => hoistNonReactStatics(Target, Source)).not.toThrow();
365+
expect((Target as any).customStatic).toBe('value');
366+
expect((Target as any)[Symbol.for('test.symbol')]).toBe('symbolValue');
367+
} finally {
368+
globalThis.String = OriginalString;
369+
}
370+
});
293371
});

0 commit comments

Comments
 (0)