Skip to content

Commit c064e16

Browse files
committed
[Flight] Emit debug info for a Server Component (#28272)
This adds a new DEV-only row type `D` for DebugInfo. If we see this in prod, that's an error. It can contain extra debug information about the Server Components (or Promises) that were compiled away during the server render. It's DEV-only since this can contain sensitive information (similar to errors) and since it'll be a lot of data, but it's worth using the same stream for simplicity rather than a side-channel. In this first pass it's just the Server Component's name but I'll keep adding more debug info to the stream, and it won't always just be a Server Component's stack frame. Each row can get more debug rows data streaming in as it resolves and renders multiple server components in a row. The data structure is just a side-channel and it would be perfectly fine to ignore the D rows and it would behave the same as prod. With this data structure though the data is associated with the row ID / chunk, so you can't have inline meta data. This means that an inline Server Component that doesn't get an ID otherwise will need to be outlined. The way I outline Server Components is using a direct reference where it's synchronous though so on the client side it behaves the same (i.e. there's no lazy wrapper in this case). In most cases the `_debugInfo` is on the Promises that we yield and we also expose this on the `React.Lazy` wrappers. In the case where it's a synchronous render it might attach this data to Elements or Arrays (fragments) too. In a future PR I'll wire this information up with Fiber to stash it in the Fiber data structures so that DevTools can pick it up. This property and the information in it is not limited to Server Components. The name of the property that we look for probably shouldn't be `_debugInfo` since it's semi-public. Should consider the name we use for that. If it's a synchronous render that returns a string or number (text node) then we don't have anywhere to attach them to. We could add a `React.Lazy` wrapper for those but I chose to prioritize keeping the data structure untouched. Can be useful if you use Server Components to render data instead of React Nodes. DiffTrain build for [b229f54](b229f54)
1 parent 82dd48d commit c064e16

14 files changed

+218
-29
lines changed

compiled/facebook-www/JSXDEVRuntime-dev.classic.js

+7
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,13 @@ if (__DEV__) {
10541054
enumerable: false,
10551055
writable: true,
10561056
value: false
1057+
}); // debugInfo contains Server Component debug information.
1058+
1059+
Object.defineProperty(element, "_debugInfo", {
1060+
configurable: false,
1061+
enumerable: false,
1062+
writable: true,
1063+
value: null
10571064
});
10581065

10591066
if (Object.freeze) {

compiled/facebook-www/JSXDEVRuntime-dev.modern.js

+7
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,13 @@ if (__DEV__) {
10541054
enumerable: false,
10551055
writable: true,
10561056
value: false
1057+
}); // debugInfo contains Server Component debug information.
1058+
1059+
Object.defineProperty(element, "_debugInfo", {
1060+
configurable: false,
1061+
enumerable: false,
1062+
writable: true,
1063+
value: null
10571064
});
10581065

10591066
if (Object.freeze) {

compiled/facebook-www/REVISION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
37d901e2b81e12d40df7012c6f8681b8272d2555
1+
b229f540e2da91370611945f9875e00a96196df6

compiled/facebook-www/React-dev.classic.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ if (__DEV__) {
2424
) {
2525
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2626
}
27-
var ReactVersion = "18.3.0-www-classic-965940b9";
27+
var ReactVersion = "18.3.0-www-classic-28532002";
2828

2929
// ATTENTION
3030
// When adding new symbols to this file,
@@ -772,6 +772,13 @@ if (__DEV__) {
772772
enumerable: false,
773773
writable: true,
774774
value: false
775+
}); // debugInfo contains Server Component debug information.
776+
777+
Object.defineProperty(element, "_debugInfo", {
778+
configurable: false,
779+
enumerable: false,
780+
writable: true,
781+
value: null
775782
});
776783

777784
if (Object.freeze) {
@@ -1808,6 +1815,13 @@ if (__DEV__) {
18081815
enumerable: false,
18091816
writable: true,
18101817
value: false
1818+
}); // debugInfo contains Server Component debug information.
1819+
1820+
Object.defineProperty(element, "_debugInfo", {
1821+
configurable: false,
1822+
enumerable: false,
1823+
writable: true,
1824+
value: null
18111825
});
18121826

18131827
if (Object.freeze) {

compiled/facebook-www/React-dev.modern.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ if (__DEV__) {
2424
) {
2525
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2626
}
27-
var ReactVersion = "18.3.0-www-modern-b425cc4f";
27+
var ReactVersion = "18.3.0-www-modern-f7a0584c";
2828

2929
// ATTENTION
3030
// When adding new symbols to this file,
@@ -772,6 +772,13 @@ if (__DEV__) {
772772
enumerable: false,
773773
writable: true,
774774
value: false
775+
}); // debugInfo contains Server Component debug information.
776+
777+
Object.defineProperty(element, "_debugInfo", {
778+
configurable: false,
779+
enumerable: false,
780+
writable: true,
781+
value: null
775782
});
776783

777784
if (Object.freeze) {
@@ -1808,6 +1815,13 @@ if (__DEV__) {
18081815
enumerable: false,
18091816
writable: true,
18101817
value: false
1818+
}); // debugInfo contains Server Component debug information.
1819+
1820+
Object.defineProperty(element, "_debugInfo", {
1821+
configurable: false,
1822+
enumerable: false,
1823+
writable: true,
1824+
value: null
18111825
});
18121826

18131827
if (Object.freeze) {

compiled/facebook-www/React-prod.modern.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -562,4 +562,4 @@ exports.useSyncExternalStore = function (
562562
exports.useTransition = function () {
563563
return ReactCurrentDispatcher.current.useTransition();
564564
};
565-
exports.version = "18.3.0-www-modern-2c5233b3";
565+
exports.version = "18.3.0-www-modern-97aa8a4c";

compiled/facebook-www/ReactDOMTesting-prod.modern.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -17066,7 +17066,7 @@ Internals.Events = [
1706617066
var devToolsConfig$jscomp$inline_1784 = {
1706717067
findFiberByHostInstance: getClosestInstanceFromNode,
1706817068
bundleType: 0,
17069-
version: "18.3.0-www-modern-5ed7b4c6",
17069+
version: "18.3.0-www-modern-da44ba18",
1707017070
rendererPackageName: "react-dom"
1707117071
};
1707217072
var internals$jscomp$inline_2157 = {
@@ -17097,7 +17097,7 @@ var internals$jscomp$inline_2157 = {
1709717097
scheduleRoot: null,
1709817098
setRefreshHandler: null,
1709917099
getCurrentFiber: null,
17100-
reconcilerVersion: "18.3.0-www-modern-5ed7b4c6"
17100+
reconcilerVersion: "18.3.0-www-modern-da44ba18"
1710117101
};
1710217102
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
1710317103
var hook$jscomp$inline_2158 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
@@ -17525,4 +17525,4 @@ exports.useFormStatus = function () {
1752517525
return ReactCurrentDispatcher$2.current.useHostTransitionStatus();
1752617526
throw Error(formatProdErrorMessage(248));
1752717527
};
17528-
exports.version = "18.3.0-www-modern-5ed7b4c6";
17528+
exports.version = "18.3.0-www-modern-da44ba18";

compiled/facebook-www/ReactFlightDOMClient-dev.modern.js

+64-2
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,18 @@ if (__DEV__) {
233233
var RESOLVED_MODEL = "resolved_model";
234234
var RESOLVED_MODULE = "resolved_module";
235235
var INITIALIZED = "fulfilled";
236-
var ERRORED = "rejected"; // $FlowFixMe[missing-this-annot]
236+
var ERRORED = "rejected"; // Dev-only
237+
// $FlowFixMe[missing-this-annot]
237238

238239
function Chunk(status, value, reason, response) {
239240
this.status = status;
240241
this.value = value;
241242
this.reason = reason;
242243
this._response = response;
244+
245+
{
246+
this._debugInfo = null;
247+
}
243248
} // We subclass Promise.prototype so that we get other methods like .catch
244249

245250
Chunk.prototype = Object.create(Promise.prototype); // TODO: This doesn't return a new Promise chain unlike the real .then
@@ -537,6 +542,13 @@ if (__DEV__) {
537542
enumerable: false,
538543
writable: true,
539544
value: true // This element has already been validated on the server.
545+
}); // debugInfo contains Server Component debug information.
546+
547+
Object.defineProperty(element, "_debugInfo", {
548+
configurable: false,
549+
enumerable: false,
550+
writable: true,
551+
value: null
540552
});
541553
}
542554

@@ -549,6 +561,13 @@ if (__DEV__) {
549561
_payload: chunk,
550562
_init: readChunk
551563
};
564+
565+
{
566+
// Ensure we have a live array to track future debug info.
567+
var chunkDebugInfo = chunk._debugInfo || (chunk._debugInfo = []);
568+
lazyType._debugInfo = chunkDebugInfo;
569+
}
570+
552571
return lazyType;
553572
}
554573

@@ -768,7 +787,35 @@ if (__DEV__) {
768787

769788
switch (_chunk2.status) {
770789
case INITIALIZED:
771-
return _chunk2.value;
790+
var chunkValue = _chunk2.value;
791+
792+
if (_chunk2._debugInfo) {
793+
// If we have a direct reference to an object that was rendered by a synchronous
794+
// server component, it might have some debug info about how it was rendered.
795+
// We forward this to the underlying object. This might be a React Element or
796+
// an Array fragment.
797+
// If this was a string / number return value we lose the debug info. We choose
798+
// that tradeoff to allow sync server components to return plain values and not
799+
// use them as React Nodes necessarily. We could otherwise wrap them in a Lazy.
800+
if (
801+
typeof chunkValue === "object" &&
802+
chunkValue !== null &&
803+
(Array.isArray(chunkValue) ||
804+
chunkValue.$$typeof === REACT_ELEMENT_TYPE) &&
805+
!chunkValue._debugInfo
806+
) {
807+
// We should maybe use a unique symbol for arrays but this is a React owned array.
808+
// $FlowFixMe[prop-missing]: This should be added to elements.
809+
Object.defineProperty(chunkValue, "_debugInfo", {
810+
configurable: false,
811+
enumerable: false,
812+
writable: true,
813+
value: _chunk2._debugInfo
814+
});
815+
}
816+
}
817+
818+
return chunkValue;
772819

773820
case PENDING:
774821
case BLOCKED:
@@ -925,6 +972,12 @@ if (__DEV__) {
925972
dispatchHint(code, hintModel);
926973
}
927974

975+
function resolveDebugInfo(response, id, debugInfo) {
976+
var chunk = getChunk(response, id);
977+
var chunkDebugInfo = chunk._debugInfo || (chunk._debugInfo = []);
978+
chunkDebugInfo.push(debugInfo);
979+
}
980+
928981
function processFullRow(response, id, tag, buffer, chunk) {
929982
var stringDecoder = response._stringDecoder;
930983
var row = "";
@@ -972,6 +1025,15 @@ if (__DEV__) {
9721025
return;
9731026
}
9741027

1028+
case 68: /* "D" */
1029+
{
1030+
{
1031+
var debugInfo = JSON.parse(row);
1032+
resolveDebugInfo(response, id, debugInfo);
1033+
return;
1034+
}
1035+
}
1036+
9751037
case 80:
9761038
/* "P" */
9771039
// Fallthrough

compiled/facebook-www/ReactFlightDOMClient-prod.modern.js

+4
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,10 @@ function startReadingFromStream(response, stream) {
506506
new Chunk("fulfilled", rowTag, null, rowLength)
507507
);
508508
break;
509+
case 68:
510+
throw Error(
511+
"Failed to read a RSC payload created by a development version of React on the server while using a production version on the client. Always use matching versions on the server and the client."
512+
);
509513
default:
510514
(i = rowLength._chunks),
511515
(offset = i.get(rowID))

compiled/facebook-www/ReactFlightDOMServer-dev.modern.js

+70-4
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,10 @@ if (__DEV__) {
581581
thenableState = prevThenableState;
582582
}
583583
function getThenableStateAfterSuspending() {
584-
var state = thenableState;
584+
// If you use() to Suspend this should always exist but if you throw a Promise instead,
585+
// which is not really supported anymore, it will be empty. We use the empty set as a
586+
// marker to know if this was a replay of the same component or first attempt.
587+
var state = thenableState || createThenableState();
585588
thenableState = null;
586589
return state;
587590
}
@@ -1314,6 +1317,23 @@ if (__DEV__) {
13141317
// component suspends again, the thenable state will be restored.
13151318
var prevThenableState = task.thenableState;
13161319
task.thenableState = null;
1320+
1321+
{
1322+
if (debugID === null) {
1323+
// We don't have a chunk to assign debug info. We need to outline this
1324+
// component to assign it an ID.
1325+
return outlineTask(request, task);
1326+
} else if (prevThenableState !== null);
1327+
else {
1328+
// This is a new component in the same task so we can emit more debug info.
1329+
var componentName = Component.displayName || Component.name || "";
1330+
request.pendingChunks++;
1331+
emitDebugChunk(request, debugID, {
1332+
name: componentName
1333+
});
1334+
}
1335+
}
1336+
13171337
prepareToUseHooksForComponent(prevThenableState); // The secondArg is always undefined in Server Components since refs error early.
13181338

13191339
var secondArg = undefined;
@@ -1421,6 +1441,28 @@ if (__DEV__) {
14211441
// or anything else too which we also get implicitly.
14221442

14231443
return element;
1444+
} // The chunk ID we're currently rendering that we can assign debug data to.
1445+
1446+
var debugID = null;
1447+
1448+
function outlineTask(request, task) {
1449+
var newTask = createTask(
1450+
request,
1451+
task.model, // the currently rendering element
1452+
task.keyPath, // unlike outlineModel this one carries along context
1453+
task.implicitSlot,
1454+
request.abortableTasks
1455+
);
1456+
retryTask(request, newTask);
1457+
1458+
if (newTask.status === COMPLETED) {
1459+
// We completed synchronously so we can refer to this by reference. This
1460+
// makes it behaves the same as prod during deserialization.
1461+
return serializeByValueID(newTask.id);
1462+
} // This didn't complete synchronously so it wouldn't have even if we didn't
1463+
// outline it, so this would reduce to a lazy reference even in prod.
1464+
1465+
return serializeLazyID(newTask.id);
14241466
}
14251467

14261468
function renderElement(request, task, type, key, ref, props) {
@@ -1445,7 +1487,7 @@ if (__DEV__) {
14451487
if (isClientReference(type)) {
14461488
// This is a reference to a Client Component.
14471489
return renderClientElement(task, type, key, props);
1448-
} // This is a server-side component.
1490+
} // This is a Server Component.
14491491

14501492
return renderFunctionComponent(request, task, key, type, props);
14511493
} else if (typeof type === "string") {
@@ -2257,6 +2299,13 @@ if (__DEV__) {
22572299
request.completedRegularChunks.push(processedChunk);
22582300
}
22592301

2302+
function emitDebugChunk(request, id, debugInfo) {
2303+
var json = stringify(debugInfo);
2304+
var row = serializeRowHeader("D", id) + json + "\n";
2305+
var processedChunk = stringToChunk(row);
2306+
request.completedRegularChunks.push(processedChunk);
2307+
}
2308+
22602309
var emptyRoot = {};
22612310

22622311
function retryTask(request, task) {
@@ -2265,11 +2314,18 @@ if (__DEV__) {
22652314
return;
22662315
}
22672316

2317+
var prevDebugID = debugID;
2318+
22682319
try {
22692320
// Track the root so we know that we have to emit this object even though it
22702321
// already has an ID. This is needed because we might see this object twice
22712322
// in the same toJSON if it is cyclic.
2272-
modelRoot = task.model; // We call the destructive form that mutates this task. That way if something
2323+
modelRoot = task.model;
2324+
2325+
if (true) {
2326+
// Track the ID of the current task so we can assign debug info to this id.
2327+
debugID = task.id;
2328+
} // We call the destructive form that mutates this task. That way if something
22732329
// suspends again, we can reuse the same task instead of spawning a new one.
22742330

22752331
var resolvedModel = renderModelDestructive(
@@ -2278,7 +2334,13 @@ if (__DEV__) {
22782334
emptyRoot,
22792335
"",
22802336
task.model
2281-
); // Track the root again for the resolved object.
2337+
);
2338+
2339+
if (true) {
2340+
// We're now past rendering this task and future renders will spawn new tasks for their
2341+
// debug info.
2342+
debugID = null;
2343+
} // Track the root again for the resolved object.
22822344

22832345
modelRoot = resolvedModel; // The keyPath resets at any terminal child node.
22842346

@@ -2326,6 +2388,10 @@ if (__DEV__) {
23262388
task.status = ERRORED;
23272389
var digest = logRecoverableError(request, x);
23282390
emitErrorChunk(request, task.id, digest, x);
2391+
} finally {
2392+
{
2393+
debugID = prevDebugID;
2394+
}
23292395
}
23302396
}
23312397

0 commit comments

Comments
 (0)