@samitouri / QOS-React / commits / f38c22b244

[Flight] Set Current Owner / Task When Calling console.error or invoking onError/onPostpone (#30206)

Stacked on #30197. This is similar to #30182 and #21610 in Fizz. Track the current owner/stack/task on the task. This tracks it for attribution when serializing child properties. This lets us provide the right owner and createTask when we console.error from inside Flight itself. This also affects the way we print those logs on the client since we need the owner and stack. Now console.errors that originate on the server gets the right stack on the client: <img width="760" alt="Screenshot 2024-07-03 at 6 03 13 PM" src="https://github.com/facebook/react/assets/63648/913300f8-f364-4e66-a19d-362e8d776c64"> Unfortunately, because we don't track the stack we never pop it so it'll keep tracking for serializing sibling properties. We rely on "children" typically being the last property in the common case anyway. However, this can lead to wrong attribution in some cases where the invalid property is a next property (without a wrapping element) and there's a previous element that doesn't. E.g. `<ClientComponent title={<div />} invalid={nonSerializable} />` would use the div as the attribution instead of ClientComponent. I also wrap all of our own console.error, onError and onPostpone in the context of the parent component. It's annoying to have to remember to do this though. We could always wrap the whole rendering in such as context but it would add more overhead since this rarely actually happens. It might make sense to track the whole current task instead to lower the overhead. That's what we do in Fizz. We'd still have to remember to restore the debug task though. I realize now Fizz doesn't do that neither so the debug task isn't wrapping the console.errors that Fizz itself logs. There's something off about that Flight and Fizz implementations don't perfectly align.

Sebastian Markbåge committed Jul 4, 2024 at 12:31 UTC f38c22b244086f62ae5ed851b6ed17029ec44be5
2 files changed +287 -110
packages/react-client/src/__tests__/ReactFlight-test.js
+62 -1
@@ -2761,10 +2761,61 @@ describe('ReactFlight', () => {
2761 );
2762 });
2763
2764 + // @gate __DEV__ && enableOwnerStacks
2765 + it('can get the component owner stacks for onError in dev', async () => {
2766 + const thrownError = new Error('hi');
2767 + let caughtError;
2768 + let ownerStack;
2769 +
2770 + function Foo() {
2771 + return ReactServer.createElement(Bar, null);
2772 + }
2773 + function Bar() {
2774 + return ReactServer.createElement(
2775 + 'div',
2776 + null,
2777 + ReactServer.createElement(Baz, null),
2778 + );
2779 + }
2780 + function Baz() {
2781 + throw thrownError;
2782 + }
2783 +
2784 + ReactNoopFlightServer.render(
2785 + ReactServer.createElement(
2786 + 'div',
2787 + null,
2788 + ReactServer.createElement(Foo, null),
2789 + ),
2790 + {
2791 + onError(error, errorInfo) {
2792 + caughtError = error;
2793 + ownerStack = ReactServer.captureOwnerStack
2794 + ? ReactServer.captureOwnerStack()
2795 + : null;
2796 + },
2797 + },
2798 + );
2799 +
2800 + expect(caughtError).toBe(thrownError);
2801 + expect(normalizeCodeLocInfo(ownerStack)).toBe(
2802 + '\n in Bar (at **)' + '\n in Foo (at **)',
2803 + );
2804 + });
2805 +
2806 // @gate (enableOwnerStacks && enableServerComponentLogs) || !__DEV__
2807 it('should not include component stacks in replayed logs (unless DevTools add them)', () => {
2808 + class MyError extends Error {
2809 + toJSON() {
2810 + return 123;
2811 + }
2812 + }
2813 +
2814 function Foo() {
2767 - return 'hi';
2815 + return ReactServer.createElement('div', null, [
2816 + 'Womp womp: ',
2817 + new MyError('spaghetti'),
2818 + ]);
2819 }
2820
2821 function Bar() {
@@ -2781,11 +2832,18 @@ describe('ReactFlight', () => {
2832 const transport = ReactNoopFlightServer.render(
2833 ReactServer.createElement(App),
2834 );
2835 +
2836 assertConsoleErrorDev([
2837 'Each child in a list should have a unique "key" prop.' +
2838 ' See https://react.dev/link/warning-keys for more information.\n' +
2839 ' in Bar (at **)\n' +
2840 ' in App (at **)',
2841 + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
2842 + ' <div>Womp womp: {Error}</div>\n' +
2843 + ' ^^^^^^^\n' +
2844 + ' in Foo (at **)\n' +
2845 + ' in Bar (at **)\n' +
2846 + ' in App (at **)',
2847 ]);
2848
2849 // Replay logs on the client
@@ -2794,6 +2852,9 @@ describe('ReactFlight', () => {
2852 [
2853 'Each child in a list should have a unique "key" prop.' +
2854 ' See https://react.dev/link/warning-keys for more information.',
2855 + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
2856 + ' <div>Womp womp: {Error}</div>\n' +
2857 + ' ^^^^^^^',
2858 ],
2859 // We should not have a stack in the replay because that should be added either by console.createTask
2860 // or React DevTools on the client. Neither of which we do here.
packages/react-server/src/ReactFlightServer.js
+225 -109
@@ -426,6 +426,9 @@ type Task = {
426 implicitSlot: boolean, // true if the root server component of this sequence had a null key
427 thenableState: ThenableState | null,
428 environmentName: string, // DEV-only. Used to track if the environment for this task changed.
429 + debugOwner: null | ReactComponentInfo, // DEV-only
430 + debugStack: null | string, // DEV-only
431 + debugTask: null | ConsoleTask, // DEV-only
432 };
433
434 interface Reference {}
@@ -577,7 +580,16 @@ function RequestInstance(
580 : environmentName;
581 this.didWarnForKey = null;
582 }
580 - const rootTask = createTask(this, model, null, false, abortSet);
583 + const rootTask = createTask(
584 + this,
585 + model,
586 + null,
587 + false,
588 + abortSet,
589 + null,
590 + null,
591 + null,
592 + );
593 pingedTasks.push(rootTask);
594 }
595
@@ -624,6 +636,9 @@ function serializeThenable(
636 task.keyPath, // the server component sequence continues through Promise-as-a-child.
637 task.implicitSlot,
638 request.abortableTasks,
639 + __DEV__ && enableOwnerStacks ? task.debugOwner : null,
640 + __DEV__ && enableOwnerStacks ? task.debugStack : null,
641 + __DEV__ && enableOwnerStacks ? task.debugTask : null,
642 );
643 if (__DEV__) {
644 // If this came from Flight, forward any debug info into this new row.
@@ -649,10 +664,10 @@ function serializeThenable(
664 (x: any).$$typeof === REACT_POSTPONE_TYPE
665 ) {
666 const postponeInstance: Postpone = (x: any);
652 - logPostpone(request, postponeInstance.message);
667 + logPostpone(request, postponeInstance.message, newTask);
668 emitPostponeChunk(request, newTask.id, postponeInstance);
669 } else {
655 - const digest = logRecoverableError(request, x);
670 + const digest = logRecoverableError(request, x, null);
671 emitErrorChunk(request, newTask.id, digest, x);
672 }
673 return newTask.id;
@@ -708,11 +723,11 @@ function serializeThenable(
723 (reason: any).$$typeof === REACT_POSTPONE_TYPE
724 ) {
725 const postponeInstance: Postpone = (reason: any);
711 - logPostpone(request, postponeInstance.message);
726 + logPostpone(request, postponeInstance.message, newTask);
727 emitPostponeChunk(request, newTask.id, postponeInstance);
728 } else {
729 newTask.status = ERRORED;
715 - const digest = logRecoverableError(request, reason);
730 + const digest = logRecoverableError(request, reason, newTask);
731 emitErrorChunk(request, newTask.id, digest, reason);
732 }
733 request.abortableTasks.delete(newTask);
@@ -753,6 +768,9 @@ function serializeReadableStream(
768 task.keyPath,
769 task.implicitSlot,
770 request.abortableTasks,
771 + __DEV__ && enableOwnerStacks ? task.debugOwner : null,
772 + __DEV__ && enableOwnerStacks ? task.debugStack : null,
773 + __DEV__ && enableOwnerStacks ? task.debugTask : null,
774 );
775 request.abortableTasks.delete(streamTask);
776
@@ -801,10 +819,10 @@ function serializeReadableStream(
819 (reason: any).$$typeof === REACT_POSTPONE_TYPE
820 ) {
821 const postponeInstance: Postpone = (reason: any);
804 - logPostpone(request, postponeInstance.message);
822 + logPostpone(request, postponeInstance.message, streamTask);
823 emitPostponeChunk(request, streamTask.id, postponeInstance);
824 } else {
807 - const digest = logRecoverableError(request, reason);
825 + const digest = logRecoverableError(request, reason, streamTask);
826 emitErrorChunk(request, streamTask.id, digest, reason);
827 }
828 enqueueFlush(request);
@@ -849,6 +867,9 @@ function serializeAsyncIterable(
867 task.keyPath,
868 task.implicitSlot,
869 request.abortableTasks,
870 + __DEV__ && enableOwnerStacks ? task.debugOwner : null,
871 + __DEV__ && enableOwnerStacks ? task.debugStack : null,
872 + __DEV__ && enableOwnerStacks ? task.debugTask : null,
873 );
874 request.abortableTasks.delete(streamTask);
875
@@ -930,10 +951,10 @@ function serializeAsyncIterable(
951 (reason: any).$$typeof === REACT_POSTPONE_TYPE
952 ) {
953 const postponeInstance: Postpone = (reason: any);
933 - logPostpone(request, postponeInstance.message);
954 + logPostpone(request, postponeInstance.message, streamTask);
955 emitPostponeChunk(request, streamTask.id, postponeInstance);
956 } else {
936 - const digest = logRecoverableError(request, reason);
957 + const digest = logRecoverableError(request, reason, streamTask);
958 emitErrorChunk(request, streamTask.id, digest, reason);
959 }
960 enqueueFlush(request);
@@ -1077,15 +1098,43 @@ function callLazyInitInDEV(lazy: LazyComponent<any, any>): any {
1098 return init(payload);
1099 }
1100
1101 +function callWithDebugContextInDEV<A, T>(
1102 + task: Task,
1103 + callback: A => T,
1104 + arg: A,
1105 +): T {
1106 + // We don't have a Server Component instance associated with this callback and
1107 + // the nearest context is likely a Client Component being serialized. We create
1108 + // a fake owner during this callback so we can get the stack trace from it.
1109 + // This also gets sent to the client as the owner for the replaying log.
1110 + const componentDebugInfo: ReactComponentInfo = {
1111 + env: task.environmentName,
1112 + owner: task.debugOwner,
1113 + };
1114 + if (enableOwnerStacks) {
1115 + // $FlowFixMe[cannot-write]
1116 + componentDebugInfo.stack = task.debugStack;
1117 + }
1118 + const debugTask = task.debugTask;
1119 + // We don't need the async component storage context here so we only set the
1120 + // synchronous tracking of owner.
1121 + setCurrentOwner(componentDebugInfo);
1122 + try {
1123 + if (enableOwnerStacks && debugTask) {
1124 + return debugTask.run(callback.bind(null, arg));
1125 + }
1126 + return callback(arg);
1127 + } finally {
1128 + setCurrentOwner(null);
1129 + }
1130 +}
1131 +
1132 function renderFunctionComponent<Props>(
1133 request: Request,
1134 task: Task,
1135 key: null | string,
1136 Component: (p: Props, arg: void) => any,
1137 props: Props,
1086 - owner: null | ReactComponentInfo, // DEV-only
1087 - stack: null | string, // DEV-only
1088 - debugTask: null | ConsoleTask, // DEV-only
1138 validated: number, // DEV-only
1139 ): ReactJSONValue {
1140 // Reset the task's thenable state before continuing, so that if a later
@@ -1117,11 +1166,11 @@ function renderFunctionComponent<Props>(
1166 componentDebugInfo = ({
1167 name: componentName,
1168 env: componentEnv,
1120 - owner: owner,
1169 + owner: task.debugOwner,
1170 }: ReactComponentInfo);
1171 if (enableOwnerStacks) {
1172 // $FlowFixMe[cannot-write]
1124 - componentDebugInfo.stack = stack;
1173 + componentDebugInfo.stack = task.debugStack;
1174 }
1175 // We outline this model eagerly so that we can refer to by reference as an owner.
1176 // If we had a smarter way to dedupe we might not have to do this if there ends up
@@ -1138,7 +1187,7 @@ function renderFunctionComponent<Props>(
1187 key,
1188 validated,
1189 componentDebugInfo,
1141 - debugTask,
1190 + task.debugTask,
1191 );
1192 }
1193 }
@@ -1147,7 +1196,7 @@ function renderFunctionComponent<Props>(
1196 Component,
1197 props,
1198 componentDebugInfo,
1150 - debugTask,
1199 + task.debugTask,
1200 );
1201 } else {
1202 prepareToUseHooksForComponent(prevThenableState, null);
@@ -1222,10 +1271,12 @@ function renderFunctionComponent<Props>(
1271 Object.prototype.toString.call(iterableChild) ===
1272 '[object Generator]';
1273 if (!isGeneratorComponent) {
1225 - console.error(
1226 - 'Returning an Iterator from a Server Component is not supported ' +
1227 - 'since it cannot be looped over more than once. ',
1228 - );
1274 + callWithDebugContextInDEV(task, () => {
1275 + console.error(
1276 + 'Returning an Iterator from a Server Component is not supported ' +
1277 + 'since it cannot be looped over more than once. ',
1278 + );
1279 + });
1280 }
1281 }
1282 }
@@ -1259,10 +1310,12 @@ function renderFunctionComponent<Props>(
1310 Object.prototype.toString.call(iterableChild) ===
1311 '[object AsyncGenerator]';
1312 if (!isGeneratorComponent) {
1262 - console.error(
1263 - 'Returning an AsyncIterator from a Server Component is not supported ' +
1264 - 'since it cannot be looped over more than once. ',
1265 - );
1313 + callWithDebugContextInDEV(task, () => {
1314 + console.error(
1315 + 'Returning an AsyncIterator from a Server Component is not supported ' +
1316 + 'since it cannot be looped over more than once. ',
1317 + );
1318 + });
1319 }
1320 }
1321 }
@@ -1489,8 +1542,6 @@ function renderClientElement(
1542 type: any,
1543 key: null | string,
1544 props: any,
1492 - owner: null | ReactComponentInfo, // DEV-only
1493 - stack: null | string, // DEV-only
1545 validated: number, // DEV-only
1546 ): ReactJSONValue {
1547 // We prepend the terminal client element that actually gets serialized with
@@ -1503,8 +1554,16 @@ function renderClientElement(
1554 }
1555 const element = __DEV__
1556 ? enableOwnerStacks
1506 - ? [REACT_ELEMENT_TYPE, type, key, props, owner, stack, validated]
1507 - : [REACT_ELEMENT_TYPE, type, key, props, owner]
1557 + ? [
1558 + REACT_ELEMENT_TYPE,
1559 + type,
1560 + key,
1561 + props,
1562 + task.debugOwner,
1563 + task.debugStack,
1564 + validated,
1565 + ]
1566 + : [REACT_ELEMENT_TYPE, type, key, props, task.debugOwner]
1567 : [REACT_ELEMENT_TYPE, type, key, props];
1568 if (task.implicitSlot && key !== null) {
1569 // The root Server Component had no key so it was in an implicit slot.
@@ -1531,6 +1590,9 @@ function outlineTask(request: Request, task: Task): ReactJSONValue {
1590 task.keyPath, // unlike outlineModel this one carries along context
1591 task.implicitSlot,
1592 request.abortableTasks,
1593 + __DEV__ && enableOwnerStacks ? task.debugOwner : null,
1594 + __DEV__ && enableOwnerStacks ? task.debugStack : null,
1595 + __DEV__ && enableOwnerStacks ? task.debugTask : null,
1596 );
1597
1598 retryTask(request, newTask);
@@ -1551,9 +1613,6 @@ function renderElement(
1613 key: null | string,
1614 ref: mixed,
1615 props: any,
1554 - owner: null | ReactComponentInfo, // DEV only
1555 - stack: null | string, // DEV only
1556 - debugTask: null | ConsoleTask, // DEV only
1616 validated: number, // DEV only
1617 ): ReactJSONValue {
1618 if (ref !== null && ref !== undefined) {
@@ -1578,17 +1637,7 @@ function renderElement(
1637 !isOpaqueTemporaryReference(type)
1638 ) {
1639 // This is a Server Component.
1581 - return renderFunctionComponent(
1582 - request,
1583 - task,
1584 - key,
1585 - type,
1586 - props,
1587 - owner,
1588 - stack,
1589 - debugTask,
1590 - validated,
1591 - );
1640 + return renderFunctionComponent(request, task, key, type, props, validated);
1641 } else if (type === REACT_FRAGMENT_TYPE && key === null) {
1642 // For key-less fragments, we add a small optimization to avoid serializing
1643 // it as a wrapper.
@@ -1633,9 +1682,6 @@ function renderElement(
1682 key,
1683 ref,
1684 props,
1636 - owner,
1637 - stack,
1638 - debugTask,
1685 validated,
1686 );
1687 }
@@ -1646,9 +1692,6 @@ function renderElement(
1692 key,
1693 type.render,
1694 props,
1649 - owner,
1650 - stack,
1651 - debugTask,
1695 validated,
1696 );
1697 }
@@ -1660,9 +1703,6 @@ function renderElement(
1703 key,
1704 ref,
1705 props,
1663 - owner,
1664 - stack,
1665 - debugTask,
1706 validated,
1707 );
1708 }
@@ -1680,7 +1720,7 @@ function renderElement(
1720 // We don't know if the client will support it or not. This might error on the
1721 // client or error during serialization but the stack will point back to the
1722 // server.
1683 - return renderClientElement(task, type, key, props, owner, stack, validated);
1723 + return renderClientElement(task, type, key, props, validated);
1724 }
1725
1726 function pingTask(request: Request, task: Task): void {
@@ -1698,6 +1738,9 @@ function createTask(
1738 keyPath: null | string,
1739 implicitSlot: boolean,
1740 abortSet: Set<Task>,
1741 + debugOwner: null | ReactComponentInfo, // DEV-only
1742 + debugStack: null | string, // DEV-only
1743 + debugTask: null | ConsoleTask, // DEV-only
1744 ): Task {
1745 request.pendingChunks++;
1746 const id = request.nextChunkId++;
@@ -1735,38 +1778,50 @@ function createTask(
1778 originalValue !== value &&
1779 !(originalValue instanceof Date)
1780 ) {
1738 - if (objectName(originalValue) !== 'Object') {
1739 - const jsxParentType = jsxChildrenParents.get(parent);
1740 - if (typeof jsxParentType === 'string') {
1741 - console.error(
1742 - '%s objects cannot be rendered as text children. Try formatting it using toString().%s',
1743 - objectName(originalValue),
1744 - describeObjectForErrorMessage(parent, parentPropertyName),
1745 - );
1781 + // Call with the server component as the currently rendering component
1782 + // for context.
1783 + callWithDebugContextInDEV(task, () => {
1784 + if (objectName(originalValue) !== 'Object') {
1785 + const jsxParentType = jsxChildrenParents.get(parent);
1786 + if (typeof jsxParentType === 'string') {
1787 + console.error(
1788 + '%s objects cannot be rendered as text children. Try formatting it using toString().%s',
1789 + objectName(originalValue),
1790 + describeObjectForErrorMessage(parent, parentPropertyName),
1791 + );
1792 + } else {
1793 + console.error(
1794 + 'Only plain objects can be passed to Client Components from Server Components. ' +
1795 + '%s objects are not supported.%s',
1796 + objectName(originalValue),
1797 + describeObjectForErrorMessage(parent, parentPropertyName),
1798 + );
1799 + }
1800 } else {
1801 console.error(
1802 'Only plain objects can be passed to Client Components from Server Components. ' +
1749 - '%s objects are not supported.%s',
1750 - objectName(originalValue),
1803 + 'Objects with toJSON methods are not supported. Convert it manually ' +
1804 + 'to a simple value before passing it to props.%s',
1805 describeObjectForErrorMessage(parent, parentPropertyName),
1806 );
1807 }
1754 - } else {
1755 - console.error(
1756 - 'Only plain objects can be passed to Client Components from Server Components. ' +
1757 - 'Objects with toJSON methods are not supported. Convert it manually ' +
1758 - 'to a simple value before passing it to props.%s',
1759 - describeObjectForErrorMessage(parent, parentPropertyName),
1760 - );
1761 - }
1808 + });
1809 }
1810 }
1811 return renderModel(request, task, parent, parentPropertyName, value);
1812 },
1813 thenableState: null,
1767 - }: Omit<Task, 'environmentName'>): any);
1814 + }: Omit<
1815 + Task,
1816 + 'environmentName' | 'debugOwner' | 'debugStack' | 'debugTask',
1817 + >): any);
1818 if (__DEV__) {
1819 task.environmentName = request.environmentName();
1820 + if (enableOwnerStacks) {
1821 + task.debugOwner = debugOwner;
1822 + task.debugStack = debugStack;
1823 + task.debugTask = debugTask;
1824 + }
1825 }
1826 abortSet.add(task);
1827 return task;
@@ -1884,7 +1939,7 @@ function serializeClientReference(
1939 } catch (x) {
1940 request.pendingChunks++;
1941 const errorId = request.nextChunkId++;
1887 - const digest = logRecoverableError(request, x);
1942 + const digest = logRecoverableError(request, x, null);
1943 emitErrorChunk(request, errorId, digest, x);
1944 return serializeByValueID(errorId);
1945 }
@@ -1897,6 +1952,9 @@ function outlineModel(request: Request, value: ReactClientValue): number {
1952 null, // The way we use outlining is for reusing an object.
1953 false, // It makes no sense for that use case to be contextual.
1954 request.abortableTasks,
1955 + null, // TODO: Currently we don't associate any debug information with
1956 + null, // this object on the server. If it ends up erroring, it won't
1957 + null, // have any context on the server but can on the client.
1958 );
1959 retryTask(request, newTask);
1960 return newTask.id;
@@ -1990,6 +2048,9 @@ function serializeBlob(request: Request, blob: Blob): string {
2048 null,
2049 false,
2050 request.abortableTasks,
2051 + null, // TODO: Currently we don't associate any debug information with
2052 + null, // this object on the server. If it ends up erroring, it won't
2053 + null, // have any context on the server but can on the client.
2054 );
2055
2056 const reader = blob.stream().getReader();
@@ -2019,7 +2080,7 @@ function serializeBlob(request: Request, blob: Blob): string {
2080 }
2081 aborted = true;
2082 request.abortListeners.delete(error);
2022 - const digest = logRecoverableError(request, reason);
2083 + const digest = logRecoverableError(request, reason, newTask);
2084 emitErrorChunk(request, newTask.id, digest, reason);
2085 request.abortableTasks.delete(newTask);
2086 enqueueFlush(request);
@@ -2098,6 +2159,9 @@ function renderModel(
2159 task.keyPath,
2160 task.implicitSlot,
2161 request.abortableTasks,
2162 + __DEV__ && enableOwnerStacks ? task.debugOwner : null,
2163 + __DEV__ && enableOwnerStacks ? task.debugStack : null,
2164 + __DEV__ && enableOwnerStacks ? task.debugTask : null,
2165 );
2166 const ping = newTask.ping;
2167 (x: any).then(ping, ping);
@@ -2118,7 +2182,7 @@ function renderModel(
2182 const postponeInstance: Postpone = (x: any);
2183 request.pendingChunks++;
2184 const postponeId = request.nextChunkId++;
2121 - logPostpone(request, postponeInstance.message);
2185 + logPostpone(request, postponeInstance.message, task);
2186 emitPostponeChunk(request, postponeId, postponeInstance);
2187
2188 // Restore the context. We assume that this will be restored by the inner
@@ -2150,7 +2214,7 @@ function renderModel(
2214 // Something errored. We'll still send everything we have up until this point.
2215 request.pendingChunks++;
2216 const errorId = request.nextChunkId++;
2153 - const digest = logRecoverableError(request, x);
2217 + const digest = logRecoverableError(request, x, task);
2218 emitErrorChunk(request, errorId, digest, x);
2219 if (wasReactNode) {
2220 // We'll replace this element with a lazy reference that throws on the client
@@ -2252,6 +2316,23 @@ function renderModelDestructive(
2316 }
2317
2318 // Attempt to render the Server Component.
2319 +
2320 + if (__DEV__) {
2321 + task.debugOwner = element._owner;
2322 + if (enableOwnerStacks) {
2323 + task.debugStack =
2324 + !element._debugStack || typeof element._debugStack === 'string'
2325 + ? element._debugStack
2326 + : filterDebugStack(element._debugStack);
2327 + task.debugTask = element._debugTask;
2328 + }
2329 + // TODO: Pop this. Since we currently don't have a point where we can pop the stack
2330 + // this debug information will be used for errors inside sibling properties that
2331 + // are not elements. Leading to the wrong attribution on the server. We could fix
2332 + // that if we switch to a proper stack instead of JSON.stringify's trampoline.
2333 + // Attribution on the client is still correct since it has a pop.
2334 + }
2335 +
2336 const newChild = renderElement(
2337 request,
2338 task,
@@ -2260,13 +2341,6 @@ function renderModelDestructive(
2341 element.key,
2342 ref,
2343 props,
2263 - __DEV__ ? element._owner : null,
2264 - __DEV__ && enableOwnerStacks
2265 - ? !element._debugStack || typeof element._debugStack === 'string'
2266 - ? element._debugStack
2267 - : filterDebugStack(element._debugStack)
2268 - : null,
2269 - __DEV__ && enableOwnerStacks ? element._debugTask : null,
2344 __DEV__ && enableOwnerStacks ? element._store.validated : 0,
2345 );
2346 if (
@@ -2573,27 +2647,33 @@ function renderModelDestructive(
2647 }
2648
2649 if (objectName(value) !== 'Object') {
2576 - console.error(
2577 - 'Only plain objects can be passed to Client Components from Server Components. ' +
2578 - '%s objects are not supported.%s',
2579 - objectName(value),
2580 - describeObjectForErrorMessage(parent, parentPropertyName),
2581 - );
2650 + callWithDebugContextInDEV(task, () => {
2651 + console.error(
2652 + 'Only plain objects can be passed to Client Components from Server Components. ' +
2653 + '%s objects are not supported.%s',
2654 + objectName(value),
2655 + describeObjectForErrorMessage(parent, parentPropertyName),
2656 + );
2657 + });
2658 } else if (!isSimpleObject(value)) {
2583 - console.error(
2584 - 'Only plain objects can be passed to Client Components from Server Components. ' +
2585 - 'Classes or other objects with methods are not supported.%s',
2586 - describeObjectForErrorMessage(parent, parentPropertyName),
2587 - );
2588 - } else if (Object.getOwnPropertySymbols) {
2589 - const symbols = Object.getOwnPropertySymbols(value);
2590 - if (symbols.length > 0) {
2659 + callWithDebugContextInDEV(task, () => {
2660 console.error(
2661 'Only plain objects can be passed to Client Components from Server Components. ' +
2593 - 'Objects with symbol properties like %s are not supported.%s',
2594 - symbols[0].description,
2662 + 'Classes or other objects with methods are not supported.%s',
2663 describeObjectForErrorMessage(parent, parentPropertyName),
2664 );
2665 + });
2666 + } else if (Object.getOwnPropertySymbols) {
2667 + const symbols = Object.getOwnPropertySymbols(value);
2668 + if (symbols.length > 0) {
2669 + callWithDebugContextInDEV(task, () => {
2670 + console.error(
2671 + 'Only plain objects can be passed to Client Components from Server Components. ' +
2672 + 'Objects with symbol properties like %s are not supported.%s',
2673 + symbols[0].description,
2674 + describeObjectForErrorMessage(parent, parentPropertyName),
2675 + );
2676 + });
2677 }
2678 }
2679 }
@@ -2749,12 +2829,30 @@ function renderModelDestructive(
2829 );
2830 }
2831
2752 -function logPostpone(request: Request, reason: string): void {
2832 +function logPostpone(
2833 + request: Request,
2834 + reason: string,
2835 + task: Task | null, // DEV-only
2836 +): void {
2837 const prevRequest = currentRequest;
2838 + // We clear the request context so that console.logs inside the callback doesn't
2839 + // get forwarded to the client.
2840 currentRequest = null;
2841 try {
2842 const onPostpone = request.onPostpone;
2757 - if (supportsRequestStorage) {
2843 + if (__DEV__ && task !== null) {
2844 + if (supportsRequestStorage) {
2845 + requestStorage.run(
2846 + undefined,
2847 + callWithDebugContextInDEV,
2848 + task,
2849 + onPostpone,
2850 + reason,
2851 + );
2852 + } else {
2853 + callWithDebugContextInDEV(task, onPostpone, reason);
2854 + }
2855 + } else if (supportsRequestStorage) {
2856 // Exit the request context while running callbacks.
2857 requestStorage.run(undefined, onPostpone, reason);
2858 } else {
@@ -2765,13 +2863,31 @@ function logPostpone(request: Request, reason: string): void {
2863 }
2864 }
2865
2768 -function logRecoverableError(request: Request, error: mixed): string {
2866 +function logRecoverableError(
2867 + request: Request,
2868 + error: mixed,
2869 + task: Task | null, // DEV-only
2870 +): string {
2871 const prevRequest = currentRequest;
2872 + // We clear the request context so that console.logs inside the callback doesn't
2873 + // get forwarded to the client.
2874 currentRequest = null;
2875 let errorDigest;
2876 try {
2877 const onError = request.onError;
2774 - if (supportsRequestStorage) {
2878 + if (__DEV__ && task !== null) {
2879 + if (supportsRequestStorage) {
2880 + errorDigest = requestStorage.run(
2881 + undefined,
2882 + callWithDebugContextInDEV,
2883 + task,
2884 + onError,
2885 + error,
2886 + );
2887 + } else {
2888 + errorDigest = callWithDebugContextInDEV(task, onError, error);
2889 + }
2890 + } else if (supportsRequestStorage) {
2891 // Exit the request context while running callbacks.
2892 errorDigest = requestStorage.run(undefined, onError, error);
2893 } else {
@@ -3567,7 +3683,7 @@ function retryTask(request: Request, task: Task): void {
3683 request.abortableTasks.delete(task);
3684 task.status = ERRORED;
3685 const postponeInstance: Postpone = (x: any);
3570 - logPostpone(request, postponeInstance.message);
3686 + logPostpone(request, postponeInstance.message, task);
3687 emitPostponeChunk(request, task.id, postponeInstance);
3688 return;
3689 }
@@ -3584,7 +3700,7 @@ function retryTask(request: Request, task: Task): void {
3700
3701 request.abortableTasks.delete(task);
3702 task.status = ERRORED;
3587 - const digest = logRecoverableError(request, x);
3703 + const digest = logRecoverableError(request, x, task);
3704 emitErrorChunk(request, task.id, digest, x);
3705 } finally {
3706 if (__DEV__) {
@@ -3629,7 +3745,7 @@ function performWork(request: Request): void {
3745 flushCompletedChunks(request, request.destination);
3746 }
3747 } catch (error) {
3632 - logRecoverableError(request, error);
3748 + logRecoverableError(request, error, null);
3749 fatalError(request, error);
3750 } finally {
3751 ReactSharedInternals.H = prevDispatcher;
@@ -3780,7 +3896,7 @@ export function startFlowing(request: Request, destination: Destination): void {
3896 try {
3897 flushCompletedChunks(request, destination);
3898 } catch (error) {
3783 - logRecoverableError(request, error);
3899 + logRecoverableError(request, error, null);
3900 fatalError(request, error);
3901 }
3902 }
@@ -3807,7 +3923,7 @@ export function abort(request: Request, reason: mixed): void {
3923 (reason: any).$$typeof === REACT_POSTPONE_TYPE
3924 ) {
3925 const postponeInstance: Postpone = (reason: any);
3810 - logPostpone(request, postponeInstance.message);
3926 + logPostpone(request, postponeInstance.message, null);
3927 emitPostponeChunk(request, errorId, postponeInstance);
3928 } else {
3929 const error =
@@ -3820,7 +3936,7 @@ export function abort(request: Request, reason: mixed): void {
3936 typeof reason.then === 'function'
3937 ? new Error('The render was aborted by the server with a promise.')
3938 : reason;
3823 - const digest = logRecoverableError(request, error);
3939 + const digest = logRecoverableError(request, error, null);
3940 emitErrorChunk(request, errorId, digest, error);
3941 }
3942 abortableTasks.forEach(task => abortTask(task, request, errorId));
@@ -3858,7 +3974,7 @@ export function abort(request: Request, reason: mixed): void {
3974 flushCompletedChunks(request, request.destination);
3975 }
3976 } catch (error) {
3861 - logRecoverableError(request, error);
3977 + logRecoverableError(request, error, null);
3978 fatalError(request, error);
3979 }
3980 }