Repository: facebook/react
Pull request: [Flight] Mark Errored Server Components

This is similar to #31876 but for Server Components.

It marks them as errored and puts the error message in the Summary properties.

<img width="1511" alt="Screenshot 2024-12-20 at 5 05 35 PM" src="https://github.com/user-attachments/assets/92f11e42-0e23-41c7-bfd4-09effb25e024" />

This only looks at the current chunk for rejections. That means that there might still be promises deeper that rejected but it's only the immediate return value of the Server Component that's considered a rejection of the component itself.

Changed files:
filename: packages/react-client/src/ReactFlightClient.js
patch: @@ -73,6 +73,7 @@ import {
   markAllTracksInOrder,
   logComponentRender,
   logDedupedComponentRender,
+  logComponentErrored,
 } from './ReactFlightPerformanceTrack';
 
 import {
@@ -2876,6 +2877,7 @@ function flushComponentPerformance(
 
   if (debugInfo) {
     let endTime = 0;
+    let isLastComponent = true;
     for (let i = debugInfo.length - 1; i >= 0; i--) {
       const info = debugInfo[i];
       if (typeof info.time === 'number') {
@@ -2890,17 +2892,37 @@ function flushComponentPerformance(
         const startTimeInfo = debugInfo[i - 1];
         if (typeof startTimeInfo.time === 'number') {
           const startTime = startTimeInfo.time;
-          logComponentRender(
-            componentInfo,
-            trackIdx,
-            startTime,
-            endTime,
-            childrenEndTime,
-            response._rootEnvironmentName,
-          );
+          if (
+            isLastComponent &&
+            root.status === ERRORED &&
+            root.reason !== response._closedReason
+          ) {
+            // If this is the last component to render before this chunk rejected, then conceptually
+            // this component errored. If this was a cancellation then it wasn't this component that
+            // errored.
+            logComponentErrored(
+              componentInfo,
+              trackIdx,
+              startTime,
+              endTime,
+              childrenEndTime,
+              response._rootEnvironmentName,
+              root.reason,
+            );
+          } else {
+            logComponentRender(
+              componentInfo,
+              trackIdx,
+              startTime,
+              endTime,
+              childrenEndTime,
+              response._rootEnvironmentName,
+            );
+          }
           // Track the root most component of the result for deduping logging.
           result.component = componentInfo;
         }
+        isLastComponent = false;
       }
     }
   }

filename: packages/react-client/src/ReactFlightPerformanceTrack.js
patch: @@ -102,6 +102,49 @@ export function logComponentRender(
   }
 }
 
+export function logComponentErrored(
+  componentInfo: ReactComponentInfo,
+  trackIdx: number,
+  startTime: number,
+  endTime: number,
+  childrenEndTime: number,
+  rootEnv: string,
+  error: mixed,
+): void {
+  if (supportsUserTiming) {
+    const properties = [];
+    if (__DEV__) {
+      const message =
+        typeof error === 'object' &&
+        error !== null &&
+        typeof error.message === 'string'
+          ? // eslint-disable-next-line react-internal/safe-string-coercion
+            String(error.message)
+          : // eslint-disable-next-line react-internal/safe-string-coercion
+            String(error);
+      properties.push(['Error', message]);
+    }
+    const env = componentInfo.env;
+    const name = componentInfo.name;
+    const isPrimaryEnv = env === rootEnv;
+    const entryName =
+      isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
+    performance.measure(entryName, {
+      start: startTime < 0 ? 0 : startTime,
+      end: childrenEndTime,
+      detail: {
+        devtools: {
+          color: 'error',
+          track: trackNames[trackIdx],
+          trackGroup: COMPONENTS_TRACK,
+          tooltipText: entryName + ' Errored',
+          properties,
+        },
+      },
+    });
+  }
+}
+
 export function logDedupedComponentRender(
   componentInfo: ReactComponentInfo,
   trackIdx: number,