diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js
index 755551047535b..f08f7a110bf61 100644
--- a/fixtures/flight/src/index.js
+++ b/fixtures/flight/src/index.js
@@ -1,5 +1,5 @@
import * as React from 'react';
-import {use, Suspense, useState, startTransition} from 'react';
+import {use, Suspense, useState, startTransition, Profiler} from 'react';
import ReactDOM from 'react-dom/client';
import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
@@ -54,14 +54,20 @@ async function hydrateApp() {
}
);
- ReactDOM.hydrateRoot(document, , {
- // TODO: This part doesn't actually work because the server only returns
- // form state during the request that submitted the form. Which means it
- // the state needs to be transported as part of the HTML stream. We intend
- // to add a feature to Fizz for this, but for now it's up to the
- // metaframework to implement correctly.
- formState: formState,
- });
+ ReactDOM.hydrateRoot(
+ document,
+
+
+ ,
+ {
+ // TODO: This part doesn't actually work because the server only returns
+ // form state during the request that submitted the form. Which means it
+ // the state needs to be transported as part of the HTML stream. We intend
+ // to add a feature to Fizz for this, but for now it's up to the
+ // metaframework to implement correctly.
+ formState: formState,
+ }
+ );
}
// Remove this line to simulate MPA behavior
diff --git a/fixtures/ssr/src/index.js b/fixtures/ssr/src/index.js
index f6457ce570674..bac5be6ec62e2 100644
--- a/fixtures/ssr/src/index.js
+++ b/fixtures/ssr/src/index.js
@@ -1,6 +1,12 @@
import React from 'react';
+import {Profiler} from 'react';
import {hydrateRoot} from 'react-dom/client';
import App from './components/App';
-hydrateRoot(document, );
+hydrateRoot(
+ document,
+
+
+
+);
diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js
index 30d340ac56700..7c424d45b3183 100644
--- a/packages/react-client/src/ReactFlightClient.js
+++ b/packages/react-client/src/ReactFlightClient.js
@@ -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;
}
}
}
diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js
index d2860e407cc65..123878526d3eb 100644
--- a/packages/react-client/src/ReactFlightPerformanceTrack.js
+++ b/packages/react-client/src/ReactFlightPerformanceTrack.js
@@ -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,