@samitouri / QOS-React-1 / commits / 400e822277

Remove Component Stack from React Logged Warnings and Error Reporting (#30308)

React transpiles some of its own `console.error` calls into a helper that appends component stacks to those calls. However, this doesn't cover user space `console.error` calls - which includes React helpers that React has moved into third parties like createClass and prop-types. The idea is that any user space component can add a warning just like React can which is why React DevTools adds them too if they don't already exist. Having them appended in both places is tricky because now you have to know whether to remove them from React's logs. Similarly it's often common for server-side frameworks to forget to cover the `console.error` logs from other sources since React DevTools isn't active there. However, it's also annoying to get component stacks clogging the terminal - depending on where the log came from. In the future `console.createTask()` will cover this use case natively and when available we don't append them at all. The new strategy relies on either: - React DevTools existing to add them to React logs as well as third parties. - `console.createTask` being supported and surfaced. - A third party framework showing the component stack either in an Error Dialog or appended to terminal output. For a third party to be able to implement this they need to be able to get the component stack. To get the component stack from within a `console.error` call you need to use the `React.captureOwnerStack()` helper which is only available in `enableOwnerStacks` flag. However, it's possible to polyfill with parent stacks using internals as a stop gap. There's a question of whether React 19 should just go out with `enableOwnerStacks` to expose this but regardless I think it's best it doesn't include component stacks from the runtime for consistency. In practice it's not really a regression though because typically either of the other options exists and error dialogs don't implement `console.error` overrides anyway yet. SSR terminals might miss them but they'd only have them in DEV warnings to begin with an a subset of React warnings. Typically those are either going to happen on the client anyway or replayed. Our tests are written to assert that component stacks work in various scenarios all over the place. To ensure that this keeps working I implement a "polyfill" that is similar to that expected a server framework might do - in `assertConsoleErrorDev` and `toErrorDev`. This PR doesn't yet change www or RN since they have their own forks of consoleWithStackDev for now.

Sebastian Markbåge committed Jul 12, 2024 at 13:02 UTC 400e82227747f6b60fecbc3b43f7515b4fd89d8c
12 files changed +544 -183
packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
+370 -72
@@ -866,16 +866,40 @@ describe('ReactInternalTestUtils console assertions', () => {
866 const message = expectToThrowFailure(() => {
867 expect(root).toMatchRenderedOutput(<div>foobarbaz</div>);
868 });
869 - expect(message).toMatchInlineSnapshot(`
870 - "asserConsoleLogsCleared(expected)
869 + if (!__DEV__) {
870 + expect(message).toMatchInlineSnapshot(`
871 + "asserConsoleLogsCleared(expected)
872
872 - console.log was called without assertConsoleLogDev:
873 - + Not asserted
874 - + Not asserted
875 - + Not asserted
873 + console.log was called without assertConsoleLogDev:
874 + + Not asserted
875 + + Not asserted
876 + + Not asserted
877
877 - You must call one of the assertConsoleDev helpers between each act call."
878 - `);
878 + You must call one of the assertConsoleDev helpers between each act call."
879 + `);
880 + } else if (gate(flags => flags.enableOwnerStacks)) {
881 + expect(message).toMatchInlineSnapshot(`
882 + "asserConsoleLogsCleared(expected)
883 +
884 + console.log was called without assertConsoleLogDev:
885 + + Not asserted
886 + + Not asserted
887 + + Not asserted
888 +
889 + You must call one of the assertConsoleDev helpers between each act call."
890 + `);
891 + } else {
892 + expect(message).toMatchInlineSnapshot(`
893 + "asserConsoleLogsCleared(expected)
894 +
895 + console.log was called without assertConsoleLogDev:
896 + + Not asserted
897 + + Not asserted
898 + + Not asserted
899 +
900 + You must call one of the assertConsoleDev helpers between each act call."
901 + `);
902 + }
903
904 expect(root).toMatchRenderedOutput(<div>foobarbaz</div>);
905 });
@@ -922,16 +946,52 @@ describe('ReactInternalTestUtils console assertions', () => {
946 });
947 });
948
925 - expect(message).toMatchInlineSnapshot(`
926 - "asserConsoleLogsCleared(expected)
949 + if (!__DEV__) {
950 + expect(message).toMatchInlineSnapshot(`
951 + "asserConsoleLogsCleared(expected)
952
928 - console.warn was called without assertConsoleWarnDev:
929 - + A
930 - + B
931 - + C
953 + console.warn was called without assertConsoleWarnDev:
954 + + A
955 + + B
956 + + C
957
933 - You must call one of the assertConsoleDev helpers between each act call."
934 - `);
958 + You must call one of the assertConsoleDev helpers between each act call."
959 + `);
960 + } else if (gate(flags => flags.enableOwnerStacks)) {
961 + expect(message).toMatchInlineSnapshot(`
962 + "asserConsoleLogsCleared(expected)
963 +
964 + console.warn was called without assertConsoleWarnDev:
965 + + A%s,
966 + + in App (at **)
967 + + B%s,
968 + + in App (at **)
969 + + C%s,
970 + + in App (at **)
971 +
972 + You must call one of the assertConsoleDev helpers between each act call."
973 + `);
974 + } else {
975 + expect(message).toMatchInlineSnapshot(`
976 + "asserConsoleLogsCleared(expected)
977 +
978 + console.warn was called without assertConsoleWarnDev:
979 + + A%s,
980 + + in Yield (at **)
981 + + in div (at **)
982 + + in App (at **)
983 + + B%s,
984 + + in Yield (at **)
985 + + in div (at **)
986 + + in App (at **)
987 + + C%s,
988 + + in Yield (at **)
989 + + in div (at **)
990 + + in App (at **)
991 +
992 + You must call one of the assertConsoleDev helpers between each act call."
993 + `);
994 + }
995 });
996
997 it('fails if act is called without any assertConsoleDev helpers', async () => {
@@ -962,26 +1022,94 @@ describe('ReactInternalTestUtils console assertions', () => {
1022 });
1023 });
1024
965 - expect(message).toMatchInlineSnapshot(`
966 - "asserConsoleLogsCleared(expected)
1025 + if (!__DEV__) {
1026 + expect(message).toMatchInlineSnapshot(`
1027 + "asserConsoleLogsCleared(expected)
1028
968 - console.log was called without assertConsoleLogDev:
969 - + A
970 - + B
971 - + C
1029 + console.log was called without assertConsoleLogDev:
1030 + + A
1031 + + B
1032 + + C
1033
973 - console.warn was called without assertConsoleWarnDev:
974 - + A
975 - + B
976 - + C
1034 + console.warn was called without assertConsoleWarnDev:
1035 + + A
1036 + + B
1037 + + C
1038
978 - console.error was called without assertConsoleErrorDev:
979 - + A
980 - + B
981 - + C
1039 + console.error was called without assertConsoleErrorDev:
1040 + + A
1041 + + B
1042 + + C
1043
983 - You must call one of the assertConsoleDev helpers between each act call."
984 - `);
1044 + You must call one of the assertConsoleDev helpers between each act call."
1045 + `);
1046 + } else if (gate(flags => flags.enableOwnerStacks)) {
1047 + expect(message).toMatchInlineSnapshot(`
1048 + "asserConsoleLogsCleared(expected)
1049 +
1050 + console.log was called without assertConsoleLogDev:
1051 + + A
1052 + + B
1053 + + C
1054 +
1055 + console.warn was called without assertConsoleWarnDev:
1056 + + A%s,
1057 + + in App (at **)
1058 + + B%s,
1059 + + in App (at **)
1060 + + C%s,
1061 + + in App (at **)
1062 +
1063 + console.error was called without assertConsoleErrorDev:
1064 + + A%s,
1065 + + in App (at **)
1066 + + B%s,
1067 + + in App (at **)
1068 + + C%s,
1069 + + in App (at **)
1070 +
1071 + You must call one of the assertConsoleDev helpers between each act call."
1072 + `);
1073 + } else {
1074 + expect(message).toMatchInlineSnapshot(`
1075 + "asserConsoleLogsCleared(expected)
1076 +
1077 + console.log was called without assertConsoleLogDev:
1078 + + A
1079 + + B
1080 + + C
1081 +
1082 + console.warn was called without assertConsoleWarnDev:
1083 + + A%s,
1084 + + in Yield (at **)
1085 + + in div (at **)
1086 + + in App (at **)
1087 + + B%s,
1088 + + in Yield (at **)
1089 + + in div (at **)
1090 + + in App (at **)
1091 + + C%s,
1092 + + in Yield (at **)
1093 + + in div (at **)
1094 + + in App (at **)
1095 +
1096 + console.error was called without assertConsoleErrorDev:
1097 + + A%s,
1098 + + in Yield (at **)
1099 + + in div (at **)
1100 + + in App (at **)
1101 + + B%s,
1102 + + in Yield (at **)
1103 + + in div (at **)
1104 + + in App (at **)
1105 + + C%s,
1106 + + in Yield (at **)
1107 + + in div (at **)
1108 + + in App (at **)
1109 +
1110 + You must call one of the assertConsoleDev helpers between each act call."
1111 + `);
1112 + }
1113 });
1114
1115 // @gate __DEV__
@@ -1804,16 +1932,49 @@ describe('ReactInternalTestUtils console assertions', () => {
1932 const message = expectToThrowFailure(() => {
1933 expect(root).toMatchRenderedOutput(<div>foobarbaz</div>);
1934 });
1807 - expect(message).toMatchInlineSnapshot(`
1808 - "asserConsoleLogsCleared(expected)
1935 + if (!__DEV__) {
1936 + expect(message).toMatchInlineSnapshot(`
1937 + "asserConsoleLogsCleared(expected)
1938
1810 - console.warn was called without assertConsoleWarnDev:
1811 - + Not asserted
1812 - + Not asserted
1813 - + Not asserted
1939 + console.warn was called without assertConsoleWarnDev:
1940 + + Not asserted
1941 + + Not asserted
1942 + + Not asserted
1943
1815 - You must call one of the assertConsoleDev helpers between each act call."
1816 - `);
1944 + You must call one of the assertConsoleDev helpers between each act call."
1945 + `);
1946 + } else if (gate(flags => flags.enableOwnerStacks)) {
1947 + expect(message).toMatchInlineSnapshot(`
1948 + "asserConsoleLogsCleared(expected)
1949 +
1950 + console.warn was called without assertConsoleWarnDev:
1951 + + Not asserted%s,
1952 + + in Yield (at **)
1953 + + Not asserted%s,
1954 + + in Yield (at **)
1955 + + Not asserted%s,
1956 + + in Yield (at **)
1957 +
1958 + You must call one of the assertConsoleDev helpers between each act call."
1959 + `);
1960 + } else {
1961 + expect(message).toMatchInlineSnapshot(`
1962 + "asserConsoleLogsCleared(expected)
1963 +
1964 + console.warn was called without assertConsoleWarnDev:
1965 + + Not asserted%s,
1966 + + in Yield (at **)
1967 + + in div (at **)
1968 + + Not asserted%s,
1969 + + in Yield (at **)
1970 + + in div (at **)
1971 + + Not asserted%s,
1972 + + in Yield (at **)
1973 + + in div (at **)
1974 +
1975 + You must call one of the assertConsoleDev helpers between each act call."
1976 + `);
1977 + }
1978
1979 expect(root).toMatchRenderedOutput(<div>foobarbaz</div>);
1980 });
@@ -1860,16 +2021,52 @@ describe('ReactInternalTestUtils console assertions', () => {
2021 });
2022 });
2023
1863 - expect(message).toMatchInlineSnapshot(`
1864 - "asserConsoleLogsCleared(expected)
2024 + if (!__DEV__) {
2025 + expect(message).toMatchInlineSnapshot(`
2026 + "asserConsoleLogsCleared(expected)
2027
1866 - console.error was called without assertConsoleErrorDev:
1867 - + A
1868 - + B
1869 - + C
2028 + console.error was called without assertConsoleErrorDev:
2029 + + A
2030 + + B
2031 + + C
2032
1871 - You must call one of the assertConsoleDev helpers between each act call."
1872 - `);
2033 + You must call one of the assertConsoleDev helpers between each act call."
2034 + `);
2035 + } else if (gate(flags => flags.enableOwnerStacks)) {
2036 + expect(message).toMatchInlineSnapshot(`
2037 + "asserConsoleLogsCleared(expected)
2038 +
2039 + console.error was called without assertConsoleErrorDev:
2040 + + A%s,
2041 + + in App (at **)
2042 + + B%s,
2043 + + in App (at **)
2044 + + C%s,
2045 + + in App (at **)
2046 +
2047 + You must call one of the assertConsoleDev helpers between each act call."
2048 + `);
2049 + } else {
2050 + expect(message).toMatchInlineSnapshot(`
2051 + "asserConsoleLogsCleared(expected)
2052 +
2053 + console.error was called without assertConsoleErrorDev:
2054 + + A%s,
2055 + + in Yield (at **)
2056 + + in div (at **)
2057 + + in App (at **)
2058 + + B%s,
2059 + + in Yield (at **)
2060 + + in div (at **)
2061 + + in App (at **)
2062 + + C%s,
2063 + + in Yield (at **)
2064 + + in div (at **)
2065 + + in App (at **)
2066 +
2067 + You must call one of the assertConsoleDev helpers between each act call."
2068 + `);
2069 + }
2070 });
2071
2072 it('fails if act is called without any assertConsoleDev helpers', async () => {
@@ -1900,26 +2097,94 @@ describe('ReactInternalTestUtils console assertions', () => {
2097 });
2098 });
2099
1903 - expect(message).toMatchInlineSnapshot(`
1904 - "asserConsoleLogsCleared(expected)
2100 + if (!__DEV__) {
2101 + expect(message).toMatchInlineSnapshot(`
2102 + "asserConsoleLogsCleared(expected)
2103
1906 - console.log was called without assertConsoleLogDev:
1907 - + A
1908 - + B
1909 - + C
2104 + console.log was called without assertConsoleLogDev:
2105 + + A
2106 + + B
2107 + + C
2108
1911 - console.warn was called without assertConsoleWarnDev:
1912 - + A
1913 - + B
1914 - + C
2109 + console.warn was called without assertConsoleWarnDev:
2110 + + A
2111 + + B
2112 + + C
2113
1916 - console.error was called without assertConsoleErrorDev:
1917 - + A
1918 - + B
1919 - + C
2114 + console.error was called without assertConsoleErrorDev:
2115 + + A
2116 + + B
2117 + + C
2118
1921 - You must call one of the assertConsoleDev helpers between each act call."
1922 - `);
2119 + You must call one of the assertConsoleDev helpers between each act call."
2120 + `);
2121 + } else if (gate(flags => flags.enableOwnerStacks)) {
2122 + expect(message).toMatchInlineSnapshot(`
2123 + "asserConsoleLogsCleared(expected)
2124 +
2125 + console.log was called without assertConsoleLogDev:
2126 + + A
2127 + + B
2128 + + C
2129 +
2130 + console.warn was called without assertConsoleWarnDev:
2131 + + A%s,
2132 + + in App (at **)
2133 + + B%s,
2134 + + in App (at **)
2135 + + C%s,
2136 + + in App (at **)
2137 +
2138 + console.error was called without assertConsoleErrorDev:
2139 + + A%s,
2140 + + in App (at **)
2141 + + B%s,
2142 + + in App (at **)
2143 + + C%s,
2144 + + in App (at **)
2145 +
2146 + You must call one of the assertConsoleDev helpers between each act call."
2147 + `);
2148 + } else {
2149 + expect(message).toMatchInlineSnapshot(`
2150 + "asserConsoleLogsCleared(expected)
2151 +
2152 + console.log was called without assertConsoleLogDev:
2153 + + A
2154 + + B
2155 + + C
2156 +
2157 + console.warn was called without assertConsoleWarnDev:
2158 + + A%s,
2159 + + in Yield (at **)
2160 + + in div (at **)
2161 + + in App (at **)
2162 + + B%s,
2163 + + in Yield (at **)
2164 + + in div (at **)
2165 + + in App (at **)
2166 + + C%s,
2167 + + in Yield (at **)
2168 + + in div (at **)
2169 + + in App (at **)
2170 +
2171 + console.error was called without assertConsoleErrorDev:
2172 + + A%s,
2173 + + in Yield (at **)
2174 + + in div (at **)
2175 + + in App (at **)
2176 + + B%s,
2177 + + in Yield (at **)
2178 + + in div (at **)
2179 + + in App (at **)
2180 + + C%s,
2181 + + in Yield (at **)
2182 + + in div (at **)
2183 + + in App (at **)
2184 +
2185 + You must call one of the assertConsoleDev helpers between each act call."
2186 + `);
2187 + }
2188 });
2189
2190 // @gate __DEV__
@@ -2786,16 +3051,49 @@ describe('ReactInternalTestUtils console assertions', () => {
3051 const message = expectToThrowFailure(() => {
3052 expect(root).toMatchRenderedOutput(<div>foobarbaz</div>);
3053 });
2789 - expect(message).toMatchInlineSnapshot(`
2790 - "asserConsoleLogsCleared(expected)
3054 + if (!__DEV__) {
3055 + expect(message).toMatchInlineSnapshot(`
3056 + "asserConsoleLogsCleared(expected)
3057
2792 - console.error was called without assertConsoleErrorDev:
2793 - + Not asserted
2794 - + Not asserted
2795 - + Not asserted
3058 + console.error was called without assertConsoleErrorDev:
3059 + + Not asserted
3060 + + Not asserted
3061 + + Not asserted
3062
2797 - You must call one of the assertConsoleDev helpers between each act call."
2798 - `);
3063 + You must call one of the assertConsoleDev helpers between each act call."
3064 + `);
3065 + } else if (gate(flags => flags.enableOwnerStacks)) {
3066 + expect(message).toMatchInlineSnapshot(`
3067 + "asserConsoleLogsCleared(expected)
3068 +
3069 + console.error was called without assertConsoleErrorDev:
3070 + + Not asserted%s,
3071 + + in Yield (at **)
3072 + + Not asserted%s,
3073 + + in Yield (at **)
3074 + + Not asserted%s,
3075 + + in Yield (at **)
3076 +
3077 + You must call one of the assertConsoleDev helpers between each act call."
3078 + `);
3079 + } else {
3080 + expect(message).toMatchInlineSnapshot(`
3081 + "asserConsoleLogsCleared(expected)
3082 +
3083 + console.error was called without assertConsoleErrorDev:
3084 + + Not asserted%s,
3085 + + in Yield (at **)
3086 + + in div (at **)
3087 + + Not asserted%s,
3088 + + in Yield (at **)
3089 + + in div (at **)
3090 + + Not asserted%s,
3091 + + in Yield (at **)
3092 + + in div (at **)
3093 +
3094 + You must call one of the assertConsoleDev helpers between each act call."
3095 + `);
3096 + }
3097
3098 expect(root).toMatchRenderedOutput(<div>foobarbaz</div>);
3099 });
packages/internal-test-utils/consoleMock.js
+43 -2
@@ -44,6 +44,34 @@ const patchConsoleMethod = (
44 return;
45 }
46
47 + // Append Component Stacks. Simulates a framework or DevTools appending them.
48 + if (
49 + typeof format === 'string' &&
50 + (methodName === 'error' || methodName === 'warn')
51 + ) {
52 + const React = require('react');
53 + if (React.captureOwnerStack) {
54 + // enableOwnerStacks enabled. When it's always on, we can assume this case.
55 + const stack = React.captureOwnerStack();
56 + if (stack) {
57 + format += '%s';
58 + args.push(stack);
59 + }
60 + } else {
61 + // Otherwise we have to use internals to emulate parent stacks.
62 + const ReactSharedInternals =
63 + React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ||
64 + React.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
65 + if (ReactSharedInternals && ReactSharedInternals.getCurrentStack) {
66 + const stack = ReactSharedInternals.getCurrentStack();
67 + if (stack !== '') {
68 + format += '%s';
69 + args.push(stack);
70 + }
71 + }
72 + }
73 + }
74 +
75 // Capture the call stack now so we can warn about it later.
76 // The call stack has helpful information for the test author.
77 // Don't throw yet though b'c it might be accidentally caught and suppressed.
@@ -204,7 +232,7 @@ export function assertConsoleLogsCleared() {
232 if (warnings.length > 0) {
233 message += `\nconsole.warn was called without assertConsoleWarnDev:\n${diff(
234 '',
207 - warnings.join('\n'),
235 + warnings.map(normalizeComponentStack).join('\n'),
236 {
237 omitAnnotationLines: true,
238 },
@@ -213,7 +241,7 @@ export function assertConsoleLogsCleared() {
241 if (errors.length > 0) {
242 message += `\nconsole.error was called without assertConsoleErrorDev:\n${diff(
243 '',
216 - errors.join('\n'),
244 + errors.map(normalizeComponentStack).join('\n'),
245 {
246 omitAnnotationLines: true,
247 },
@@ -249,6 +277,19 @@ function normalizeCodeLocInfo(str) {
277 });
278 }
279
280 +function normalizeComponentStack(entry) {
281 + if (
282 + typeof entry[0] === 'string' &&
283 + entry[0].endsWith('%s') &&
284 + isLikelyAComponentStack(entry[entry.length - 1])
285 + ) {
286 + const clone = entry.slice(0);
287 + clone[clone.length - 1] = normalizeCodeLocInfo(entry[entry.length - 1]);
288 + return clone;
289 + }
290 + return entry;
291 +}
292 +
293 const isLikelyAComponentStack = message =>
294 typeof message === 'string' &&
295 (message.indexOf('<component stack>') > -1 ||
packages/react-client/src/__tests__/ReactFlight-test.js
+21 -5
@@ -1436,12 +1436,23 @@ describe('ReactFlight', () => {
1436
1437 it('should warn in DEV a child is missing keys on server component', () => {
1438 function NoKey({children}) {
1439 - return <div key="this has a key but parent doesn't" />;
1439 + return ReactServer.createElement('div', {
1440 + key: "this has a key but parent doesn't",
1441 + });
1442 }
1443 expect(() => {
1444 + // While we're on the server we need to have the Server version active to track component stacks.
1445 + jest.resetModules();
1446 + jest.mock('react', () => ReactServer);
1447 const transport = ReactNoopFlightServer.render(
1443 - <div>{Array(6).fill(<NoKey />)}</div>,
1448 + ReactServer.createElement(
1449 + 'div',
1450 + null,
1451 + Array(6).fill(ReactServer.createElement(NoKey)),
1452 + ),
1453 );
1454 + jest.resetModules();
1455 + jest.mock('react', () => React);
1456 ReactNoopFlightClient.read(transport);
1457 }).toErrorDev('Each child in a list should have a unique "key" prop.');
1458 });
@@ -2814,7 +2825,7 @@ describe('ReactFlight', () => {
2825 });
2826
2827 // @gate (enableOwnerStacks && enableServerComponentLogs) || !__DEV__
2817 - it('should not include component stacks in replayed logs (unless DevTools add them)', () => {
2828 + it('should include only one component stack in replayed logs (if DevTools or polyfill adds them)', () => {
2829 class MyError extends Error {
2830 toJSON() {
2831 return 123;
@@ -2839,6 +2850,9 @@ describe('ReactFlight', () => {
2850 return ReactServer.createElement(Bar);
2851 }
2852
2853 + // While we're on the server we need to have the Server version active to track component stacks.
2854 + jest.resetModules();
2855 + jest.mock('react', () => ReactServer);
2856 const transport = ReactNoopFlightServer.render(
2857 ReactServer.createElement(App),
2858 );
@@ -2857,6 +2871,8 @@ describe('ReactFlight', () => {
2871 ]);
2872
2873 // Replay logs on the client
2874 + jest.resetModules();
2875 + jest.mock('react', () => React);
2876 ReactNoopFlightClient.read(transport);
2877 assertConsoleErrorDev(
2878 [
@@ -2866,8 +2882,8 @@ describe('ReactFlight', () => {
2882 ' <div>Womp womp: {Error}</div>\n' +
2883 ' ^^^^^^^',
2884 ],
2869 - // We should not have a stack in the replay because that should be added either by console.createTask
2870 - // or React DevTools on the client. Neither of which we do here.
2885 + // We should have a stack in the replay but we don't yet set the owner from the Flight replaying
2886 + // so our simulated polyfill doesn't end up getting any component stacks yet.
2887 {withoutStack: true},
2888 );
2889 });
packages/react-dom/src/__tests__/ReactDOMConsoleErrorReporting-test.js
+12 -6
@@ -143,7 +143,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
143 expect.stringContaining('%s'),
144 expect.stringContaining('An error occurred in the <Foo> component'),
145 expect.stringContaining('Consider adding an error boundary'),
146 - expect.stringContaining('Foo'),
146 + // The component stack is not added without the polyfill/devtools.
147 + // expect.stringContaining('Foo'),
148 ],
149 ]);
150 } else {
@@ -208,7 +209,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
209 'The above error occurred in the <Foo> component',
210 ),
211 expect.stringContaining('ErrorBoundary'),
211 - expect.stringContaining('Foo'),
212 + // The component stack is not added without the polyfill/devtools.
213 + // expect.stringContaining('Foo'),
214 ],
215 ]);
216 } else {
@@ -274,7 +276,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
276 expect.stringContaining('%s'),
277 expect.stringContaining('An error occurred in the <Foo> component'),
278 expect.stringContaining('Consider adding an error boundary'),
277 - expect.stringContaining('Foo'),
279 + // The component stack is not added without the polyfill/devtools.
280 + // expect.stringContaining('Foo'),
281 ],
282 ]);
283 } else {
@@ -344,7 +347,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
347 'The above error occurred in the <Foo> component',
348 ),
349 expect.stringContaining('ErrorBoundary'),
347 - expect.stringContaining('Foo'),
350 + // The component stack is not added without the polyfill/devtools.
351 + // expect.stringContaining('Foo'),
352 ],
353 ]);
354 } else {
@@ -410,7 +414,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
414 expect.stringContaining('%s'),
415 expect.stringContaining('An error occurred in the <Foo> component'),
416 expect.stringContaining('Consider adding an error boundary'),
413 - expect.stringContaining('Foo'),
417 + // The component stack is not added without the polyfill/devtools.
418 + // expect.stringContaining('Foo'),
419 ],
420 ]);
421 } else {
@@ -478,7 +483,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
483 'The above error occurred in the <Foo> component',
484 ),
485 expect.stringContaining('ErrorBoundary'),
481 - expect.stringContaining('Foo'),
486 + // The component stack is not added without the polyfill/devtools.
487 + // expect.stringContaining('Foo'),
488 ],
489 ]);
490 } else {
packages/react-dom/src/__tests__/ReactDOMConsoleErrorReportingLegacy-test.js
+12 -6
@@ -162,7 +162,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
162 // Addendum by React:
163 expect.stringContaining('An error occurred in the <Foo> component'),
164 expect.stringContaining('Consider adding an error boundary'),
165 - expect.stringContaining('Foo'),
165 + // The component stack is not added without the polyfill/devtools.
166 + // expect.stringContaining('Foo'),
167 ],
168 ]);
169
@@ -239,7 +240,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
240 'The above error occurred in the <Foo> component',
241 ),
242 expect.stringContaining('ErrorBoundary'),
242 - expect.stringContaining('Foo'),
243 + // The component stack is not added without the polyfill/devtools.
244 + // expect.stringContaining('Foo'),
245 ],
246 ]);
247 } else {
@@ -309,7 +311,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
311 // Addendum by React:
312 expect.stringContaining('An error occurred in the <Foo> component'),
313 expect.stringContaining('Consider adding an error boundary'),
312 - expect.stringContaining('Foo'),
314 + // The component stack is not added without the polyfill/devtools.
315 + // expect.stringContaining('Foo'),
316 ],
317 ]);
318
@@ -390,7 +393,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
393 'The above error occurred in the <Foo> component',
394 ),
395 expect.stringContaining('ErrorBoundary'),
393 - expect.stringContaining('Foo'),
396 + // The component stack is not added without the polyfill/devtools.
397 + // expect.stringContaining('Foo'),
398 ],
399 ]);
400 } else {
@@ -460,7 +464,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
464 // Addendum by React:
465 expect.stringContaining('An error occurred in the <Foo> component'),
466 expect.stringContaining('Consider adding an error boundary'),
463 - expect.stringContaining('Foo'),
467 + // The component stack is not added without the polyfill/devtools.
468 + // expect.stringContaining('Foo'),
469 ],
470 ]);
471
@@ -540,7 +545,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
545 'The above error occurred in the <Foo> component',
546 ),
547 expect.stringContaining('ErrorBoundary'),
543 - expect.stringContaining('Foo'),
548 + // The component stack is not added without the polyfill/devtools.
549 + // expect.stringContaining('Foo'),
550 ],
551 ]);
552 } else {
packages/react-dom/src/__tests__/ReactUpdates-test.js
+9 -8
@@ -1857,12 +1857,14 @@ describe('ReactUpdates', () => {
1857 }
1858
1859 let error = null;
1860 - let stack = null;
1860 + let ownerStack = null;
1861 let nativeStack = null;
1862 const originalConsoleError = console.error;
1863 - console.error = (e, s) => {
1863 + console.error = e => {
1864 error = e;
1865 - stack = s;
1865 + ownerStack = gate(flags => flags.enableOwnerStacks)
1866 + ? React.captureOwnerStack()
1867 + : null;
1868 nativeStack = new Error().stack;
1869 Scheduler.log('stop');
1870 };
@@ -1878,12 +1880,11 @@ describe('ReactUpdates', () => {
1880 expect(error).toContain('Maximum update depth exceeded');
1881 // The currently executing effect should be on the native stack
1882 expect(nativeStack).toContain('at myEffect');
1881 - if (!gate(flags => flags.enableOwnerStacks)) {
1882 - // The currently running component's name is not in the owner
1883 - // stack because it's just its JSX callsite.
1884 - expect(stack).toContain('at NonTerminating');
1883 + if (gate(flags => flags.enableOwnerStacks)) {
1884 + expect(ownerStack).toContain('at App');
1885 + } else {
1886 + expect(ownerStack).toBe(null);
1887 }
1886 - expect(stack).toContain('at App');
1888 });
1889
1890 it('can have nested updates if they do not cross the limit', async () => {
packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js
+39 -35
@@ -91,15 +91,16 @@ describe('ReactIncrementalErrorLogging', () => {
91 'Consider adding an error boundary to your tree ' +
92 'to customize error handling behavior.',
93 ),
94 - expect.stringMatching(
95 - new RegExp(
96 - gate(flags => flags.enableOwnerStacks)
97 - ? '\\s+(in|at) ErrorThrowingComponent'
98 - : '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
99 - '\\s+(in|at) span(.*)\n' +
100 - '\\s+(in|at) div(.*)',
101 - ),
102 - ),
94 + // The component stack is not added without the polyfill/devtools.
95 + // expect.stringMatching(
96 + // new RegExp(
97 + // gate(flags => flags.enableOwnerStacks)
98 + // ? '\\s+(in|at) ErrorThrowingComponent'
99 + // : '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
100 + // '\\s+(in|at) span(.*)\n' +
101 + // '\\s+(in|at) div(.*)',
102 + // ),
103 + // ),
104 );
105 }
106 });
@@ -139,15 +140,16 @@ describe('ReactIncrementalErrorLogging', () => {
140 'Consider adding an error boundary to your tree ' +
141 'to customize error handling behavior.',
142 ),
142 - expect.stringMatching(
143 - new RegExp(
144 - gate(flags => flags.enableOwnerStacks)
145 - ? '\\s+(in|at) ErrorThrowingComponent'
146 - : '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
147 - '\\s+(in|at) span(.*)\n' +
148 - '\\s+(in|at) div(.*)',
149 - ),
150 - ),
143 + // The component stack is not added without the polyfill/devtools.
144 + // expect.stringMatching(
145 + // new RegExp(
146 + // gate(flags => flags.enableOwnerStacks)
147 + // ? '\\s+(in|at) ErrorThrowingComponent'
148 + // : '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
149 + // '\\s+(in|at) span(.*)\n' +
150 + // '\\s+(in|at) div(.*)',
151 + // ),
152 + // ),
153 );
154 }
155 });
@@ -199,16 +201,17 @@ describe('ReactIncrementalErrorLogging', () => {
201 'React will try to recreate this component tree from scratch ' +
202 'using the error boundary you provided, ErrorBoundary.',
203 ),
202 - expect.stringMatching(
203 - new RegExp(
204 - gate(flags => flags.enableOwnerStacks)
205 - ? '\\s+(in|at) ErrorThrowingComponent'
206 - : '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
207 - '\\s+(in|at) span(.*)\n' +
208 - '\\s+(in|at) ErrorBoundary(.*)\n' +
209 - '\\s+(in|at) div(.*)',
210 - ),
211 - ),
204 + // The component stack is not added without the polyfill/devtools.
205 + // expect.stringMatching(
206 + // new RegExp(
207 + // gate(flags => flags.enableOwnerStacks)
208 + // ? '\\s+(in|at) ErrorThrowingComponent'
209 + // : '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
210 + // '\\s+(in|at) span(.*)\n' +
211 + // '\\s+(in|at) ErrorBoundary(.*)\n' +
212 + // '\\s+(in|at) div(.*)',
213 + // ),
214 + // ),
215 );
216 } else {
217 expect(logCapturedErrorCalls[0]).toEqual(
@@ -282,13 +285,14 @@ describe('ReactIncrementalErrorLogging', () => {
285 'React will try to recreate this component tree from scratch ' +
286 'using the error boundary you provided, ErrorBoundary.',
287 ),
285 - expect.stringMatching(
286 - gate(flag => flag.enableOwnerStacks)
287 - ? new RegExp('\\s+(in|at) Foo')
288 - : new RegExp(
289 - '\\s+(in|at) Foo (.*)\n' + '\\s+(in|at) ErrorBoundary(.*)',
290 - ),
291 - ),
288 + // The component stack is not added without the polyfill/devtools.
289 + // expect.stringMatching(
290 + // gate(flag => flag.enableOwnerStacks)
291 + // ? new RegExp('\\s+(in|at) Foo')
292 + // : new RegExp(
293 + // '\\s+(in|at) Foo (.*)\n' + '\\s+(in|at) ErrorBoundary(.*)',
294 + // ),
295 + // ),
296 );
297 } else {
298 expect(console.error).toHaveBeenCalledWith(
packages/react-server/src/ReactFlightServer.js
+2 -20
@@ -99,8 +99,6 @@ import {resolveOwner, setCurrentOwner} from './flight/ReactFlightCurrentOwner';
99
100 import {getOwnerStackByComponentInfoInDev} from './flight/ReactFlightComponentStack';
101
102 -import {isWritingAppendedStack} from 'shared/consoleWithStackDev';
103 -
102 import {
103 getIteratorFn,
104 REACT_ELEMENT_TYPE,
@@ -267,9 +265,8 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
265 'name',
266 );
267 const wrapperMethod = function (this: typeof console) {
270 - let args = arguments;
268 const request = resolveRequest();
272 - if (methodName === 'assert' && args[0]) {
269 + if (methodName === 'assert' && arguments[0]) {
270 // assert doesn't emit anything unless first argument is falsy so we can skip it.
271 } else if (request !== null) {
272 // Extract the stack. Not all console logs print the full stack but they have at
@@ -281,22 +278,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
278 // refer to previous logs in debug info to associate them with a component.
279 const id = request.nextChunkId++;
280 const owner: null | ReactComponentInfo = resolveOwner();
284 - if (
285 - isWritingAppendedStack &&
286 - (methodName === 'error' || methodName === 'warn') &&
287 - args.length > 1 &&
288 - typeof args[0] === 'string' &&
289 - args[0].endsWith('%s')
290 - ) {
291 - // This looks like we've appended the component stack to the error from our own logs.
292 - // We don't want those added to the replayed logs since those have the opportunity to add
293 - // their own stacks or use console.createTask on the client as needed.
294 - // TODO: Remove this special case once we remove consoleWithStackDev.
295 - // $FlowFixMe[method-unbinding]
296 - args = Array.prototype.slice.call(args, 0, args.length - 1);
297 - args[0] = args[0].slice(0, args[0].length - 2);
298 - }
299 - emitConsoleChunk(request, id, methodName, owner, stack, args);
281 + emitConsoleChunk(request, id, methodName, owner, stack, arguments);
282 }
283 // $FlowFixMe[prop-missing]
284 return originalMethod.apply(this, arguments);
packages/react/src/__tests__/createReactClassIntegration-test.js
+1 -1
@@ -804,7 +804,7 @@ describe('create-react-class-integration', () => {
804 'MyComponent: isMounted is deprecated. Instead, make sure to ' +
805 'clean up subscriptions and pending requests in componentWillUnmount ' +
806 'to prevent memory leaks.',
807 - {withoutStack: true},
807 + // This now has a component stack even though it's part of a third-party library.
808 );
809
810 // Dedupe
packages/shared/consoleWithStackDev.js
+7 -24
@@ -5,9 +5,6 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import ReactSharedInternals from 'shared/ReactSharedInternals';
9 -import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
10 -
8 export function setSuppressWarning(newSuppressWarning) {
9 // TODO: Noop. Delete.
10 }
@@ -19,39 +16,25 @@ export function setSuppressWarning(newSuppressWarning) {
16 // they are left as they are instead.
17
18 export function warn(format, ...args) {
22 - printWarning('warn', format, args, new Error('react-stack-top-frame'));
19 + if (__DEV__) {
20 + printWarning('warn', format, args);
21 + }
22 }
23
24 export function error(format, ...args) {
26 - printWarning('error', format, args, new Error('react-stack-top-frame'));
25 + if (__DEV__) {
26 + printWarning('error', format, args);
27 + }
28 }
29
29 -// eslint-disable-next-line react-internal/no-production-logging
30 -const supportsCreateTask = __DEV__ && enableOwnerStacks && !!console.createTask;
31 -
32 -export let isWritingAppendedStack = false;
33 -
34 -function printWarning(level, format, args, currentStack) {
30 +function printWarning(level, format, args) {
31 // When changing this logic, you might want to also
32 // update consoleWithStackDev.www.js as well.
33 if (__DEV__) {
38 - if (!supportsCreateTask && ReactSharedInternals.getCurrentStack) {
39 - // We only add the current stack to the console when createTask is not supported.
40 - // Since createTask requires DevTools to be open to work, this means that stacks
41 - // can be lost while DevTools isn't open but we can't detect this.
42 - const stack = ReactSharedInternals.getCurrentStack(currentStack);
43 - if (stack !== '') {
44 - isWritingAppendedStack = true;
45 - format += '%s';
46 - args = args.concat([stack]);
47 - }
48 - }
49 -
34 args.unshift(format);
35 // We intentionally don't use spread (or .apply) directly because it
36 // breaks IE9: https://github.com/facebook/react/issues/13610
37 // eslint-disable-next-line react-internal/no-production-logging
38 Function.prototype.apply.call(console[level], console, args);
55 - isWritingAppendedStack = false;
39 }
40 }
packages/shared/forks/consoleWithStackDev.rn.js
-4
@@ -36,14 +36,11 @@ export function error(format, ...args) {
36 }
37 }
38
39 -export let isWritingAppendedStack = false;
40 -
39 function printWarning(level, format, args) {
40 if (__DEV__) {
41 if (ReactSharedInternals.getCurrentStack) {
42 const stack = ReactSharedInternals.getCurrentStack();
43 if (stack !== '') {
46 - isWritingAppendedStack = true;
44 format += '%s';
45 args = args.concat([stack]);
46 }
@@ -54,6 +51,5 @@ function printWarning(level, format, args) {
51 // breaks IE9: https://github.com/facebook/react/issues/13610
52 // eslint-disable-next-line react-internal/no-production-logging
53 Function.prototype.apply.call(console[level], console, args);
57 - isWritingAppendedStack = false;
54 }
55 }
scripts/jest/matchers/toWarnDev.js
+28
@@ -80,6 +80,34 @@ const createMatcherFor = (consoleMethod, matcherName) =>
80 return;
81 }
82
83 + // Append Component Stacks. Simulates a framework or DevTools appending them.
84 + if (
85 + typeof format === 'string' &&
86 + (consoleMethod === 'error' || consoleMethod === 'warn')
87 + ) {
88 + const React = require('react');
89 + if (React.captureOwnerStack) {
90 + // enableOwnerStacks enabled. When it's always on, we can assume this case.
91 + const stack = React.captureOwnerStack();
92 + if (stack) {
93 + format += '%s';
94 + args.push(stack);
95 + }
96 + } else {
97 + // Otherwise we have to use internals to emulate parent stacks.
98 + const ReactSharedInternals =
99 + React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ||
100 + React.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
101 + if (ReactSharedInternals && ReactSharedInternals.getCurrentStack) {
102 + const stack = ReactSharedInternals.getCurrentStack();
103 + if (stack !== '') {
104 + format += '%s';
105 + args.push(stack);
106 + }
107 + }
108 + }
109 + }
110 +
111 const message = util.format(format, ...args);
112 const normalizedMessage = normalizeCodeLocInfo(message);
113