Repository: facebook/react
Pull request: [Flight] Serialize rate limited objects in console logs as marker an increase limit

This marker can then be emitted as a getter. When this object gets accessed we use a special error to let the user know what is going on.

<img width="1350" alt="Screenshot 2024-07-08 at 10 13 46 PM" src="https://github.com/facebook/react/assets/63648/e3eb698f-e02d-4394-a051-ba9ac3236480">

When you click the `...`:
<img width="1357" alt="Screenshot 2024-07-08 at 10 13 56 PM" src="https://github.com/facebook/react/assets/63648/4b8ce1cf-d762-49a4-97b9-aeeb1aa8722c">

I also increased the object limit in console logs. It was arbitrarily set very low before.

These limits are per message. So if you have a loop of many logs it can quickly add up a lot of strain on the server memory and the client. This is trying to find some tradeoff. Unfortunately we don't really do much deduping in these logs so with cyclic objects it ends up maximizing the limit and then siblings aren't logged.

Ideally we should be able to lazy load them but that requires a lot of plumbing to wire up so if we can avoid it we should try to. If we want to that though one idea is to use the getter to do a sync XHR to load more data but the server needs to retain the objects in memory for an unknown amount of time. The client could maybe send a signal to retain them until a weakref clean up but even then it kind of needs a heartbeat to let the server know the client is still alive. That's a lot of complexity. There's probably more we can do to optimize deduping and other parts of the protocol to make it possible to have even higher limits.

Changed files:
filename: packages/react-client/src/ReactFlightClient.js
patch: @@ -1231,6 +1231,29 @@ function parseModelString(
         }
         // Fallthrough
       }
+      case 'Y': {
+        if (__DEV__) {
+          // In DEV mode we encode omitted objects in logs as a getter that throws
+          // so that when you try to access it on the client, you know why that
+          // happened.
+          Object.defineProperty(parentObject, key, {
+            get: function () {
+              // We intentionally don't throw an error object here because it looks better
+              // without the stack in the console which isn't useful anyway.
+              // eslint-disable-next-line no-throw-literal
+              throw (
+                'This object has been omitted by React in the console log ' +
+                'to avoid sending too much data from the server. Try logging smaller ' +
+                'or more specific objects.'
+              );
+            },
+            enumerable: true,
+            configurable: false,
+          });
+          return null;
+        }
+        // Fallthrough
+      }
       default: {
         // We assume that anything else is a reference ID.
         const ref = value.slice(1);

filename: packages/react-server/src/ReactFlightServer.js
patch: @@ -1851,6 +1851,10 @@ function serializeSymbolReference(name: string): string {
   return '$S' + name;
 }
 
+function serializeLimitedObject(): string {
+  return '$Y';
+}
+
 function serializeNumber(number: number): string | number {
   if (Number.isFinite(number)) {
     if (number === 0 && 1 / number === -Infinity) {
@@ -3181,10 +3185,10 @@ function renderConsoleValue(
       }
     }
 
-    if (counter.objectCount > 20) {
+    if (counter.objectCount > 500) {
       // We've reached our max number of objects to serialize across the wire so we serialize this
-      // object but no properties inside of it, as a place holder.
-      return Array.isArray(value) ? [] : {};
+      // as a marker so that the client can error when this is accessed by the console.
+      return serializeLimitedObject();
     }
 
     counter.objectCount++;