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,