[DevTools] Add Badge to Owners and sometimes stack traces (#34106)

Stacked on #34101.

This adds a badge to owners if they are different from the currently
selected component's environment.

<img width="590" height="566" alt="Screenshot 2025-08-04 at 5 15 02 PM"
src="https://github.com/user-attachments/assets/e898254f-1b4c-498e-8713-978d90545340"
/>

We also add one to the end of stack traces if the stack trace has a
different environment than the owner which can happen when you call a
function (without rendering a component) into a third party environment
but the owner component was in the first party.

One awkward thing is that Suspense boundaries are always in the client
environment so their Server Components are always badged.
This commit is contained in:
Sebastian Markbåge
2025-08-07 10:39:08 -04:00
committed by GitHub
parent 4c9c109cea
commit 738aebdbac
12 changed files with 90 additions and 6 deletions

View File

@@ -862,6 +862,7 @@ describe('ProfilingCache', () => {
{
"compiledWithForget": false,
"displayName": "render()",
"env": null,
"hocDisplayNames": null,
"id": 1,
"key": null,
@@ -903,6 +904,7 @@ describe('ProfilingCache', () => {
{
"compiledWithForget": false,
"displayName": "createRoot()",
"env": null,
"hocDisplayNames": null,
"id": 1,
"key": null,
@@ -943,6 +945,7 @@ describe('ProfilingCache', () => {
{
"compiledWithForget": false,
"displayName": "createRoot()",
"env": null,
"hocDisplayNames": null,
"id": 1,
"key": null,

View File

@@ -4818,6 +4818,7 @@ export function attach(
displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
id: instance.id,
key: fiber.key,
env: null,
type: getElementTypeForFiber(fiber),
};
} else {
@@ -4826,6 +4827,7 @@ export function attach(
displayName: componentInfo.name || 'Anonymous',
id: instance.id,
key: componentInfo.key == null ? null : componentInfo.key,
env: componentInfo.env == null ? null : componentInfo.env,
type: ElementTypeVirtual,
};
}
@@ -5451,6 +5453,8 @@ export function attach(
// List of owners
owners,
env: null,
rootType,
rendererPackageName: renderer.rendererPackageName,
rendererVersion: renderer.version,
@@ -5554,6 +5558,8 @@ export function attach(
// List of owners
owners,
env: componentInfo.env == null ? null : componentInfo.env,
rootType,
rendererPackageName: renderer.rendererPackageName,
rendererVersion: renderer.version,

View File

@@ -795,6 +795,7 @@ export function attach(
displayName: getData(owner).displayName || 'Unknown',
id: getID(owner),
key: element.key,
env: null,
type: getElementType(owner),
});
if (owner._currentElement) {
@@ -857,6 +858,8 @@ export function attach(
// List of owners
owners,
env: null,
rootType: null,
rendererPackageName: null,
rendererVersion: null,

View File

@@ -256,6 +256,7 @@ export type SerializedElement = {
displayName: string | null,
id: number,
key: number | string | null,
env: null | string,
type: ElementType,
};
@@ -301,6 +302,10 @@ export type InspectedElement = {
// List of owners
owners: Array<SerializedElement> | null,
// Environment name that this component executed in or null for the client
env: string | null,
source: ReactFunctionLocation | null,
type: ElementType,

View File

@@ -255,6 +255,7 @@ export function convertInspectedElementBackendToFrontend(
id,
type,
owners,
env,
source,
context,
hooks,
@@ -299,6 +300,7 @@ export function convertInspectedElementBackendToFrontend(
owners === null
? null
: owners.map(backendToFrontendSerializedElementMapper),
env,
context: hydrateHelper(context),
hooks: hydrateHelper(hooks),
props: hydrateHelper(props),

View File

@@ -16,18 +16,21 @@ import styles from './ElementBadges.css';
type Props = {
hocDisplayNames: Array<string> | null,
environmentName: string | null,
compiledWithForget: boolean,
className?: string,
};
export default function ElementBadges({
compiledWithForget,
environmentName,
hocDisplayNames,
className = '',
}: Props): React.Node {
if (
!compiledWithForget &&
(hocDisplayNames == null || hocDisplayNames.length === 0)
(hocDisplayNames == null || hocDisplayNames.length === 0) &&
environmentName == null
) {
return null;
}
@@ -36,6 +39,8 @@ export default function ElementBadges({
<div className={`${styles.Root} ${className}`}>
{compiledWithForget && <ForgetBadge indexable={false} />}
{environmentName != null ? <Badge>{environmentName}</Badge> : null}
{hocDisplayNames != null && hocDisplayNames.length > 0 && (
<Badge>{hocDisplayNames[0]}</Badge>
)}

View File

@@ -150,13 +150,28 @@ function SuspendedByRow({
</Button>
{isOpen && (
<div className={styles.CollapsableContent}>
{showIOStack && <StackTraceView stack={ioInfo.stack} />}
{showIOStack && (
<StackTraceView
stack={ioInfo.stack}
environmentName={
ioOwner !== null && ioOwner.env === ioInfo.env
? null
: ioInfo.env
}
/>
)}
{(showIOStack || !showAwaitStack) &&
ioOwner !== null &&
ioOwner.id !== inspectedElement.id ? (
<OwnerView
key={ioOwner.id}
displayName={ioOwner.displayName || 'Anonymous'}
environmentName={
ioOwner.env === inspectedElement.env &&
ioOwner.env === ioInfo.env
? null
: ioOwner.env
}
hocDisplayNames={ioOwner.hocDisplayNames}
compiledWithForget={ioOwner.compiledWithForget}
id={ioOwner.id}
@@ -168,12 +183,25 @@ function SuspendedByRow({
<>
<div className={styles.SmallHeader}>awaited at:</div>
{asyncInfo.stack !== null && asyncInfo.stack.length > 0 && (
<StackTraceView stack={asyncInfo.stack} />
<StackTraceView
stack={asyncInfo.stack}
environmentName={
asyncOwner !== null && asyncOwner.env === asyncInfo.env
? null
: asyncInfo.env
}
/>
)}
{asyncOwner !== null && asyncOwner.id !== inspectedElement.id ? (
<OwnerView
key={asyncOwner.id}
displayName={asyncOwner.displayName || 'Anonymous'}
environmentName={
asyncOwner.env === inspectedElement.env &&
asyncOwner.env === asyncInfo.env
? null
: asyncOwner.env
}
hocDisplayNames={asyncOwner.hocDisplayNames}
compiledWithForget={asyncOwner.compiledWithForget}
id={asyncOwner.id}

View File

@@ -174,6 +174,9 @@ export default function InspectedElementView({
key={owner.id}
displayName={owner.displayName || 'Anonymous'}
hocDisplayNames={owner.hocDisplayNames}
environmentName={
inspectedElement.env === owner.env ? null : owner.env
}
compiledWithForget={owner.compiledWithForget}
id={owner.id}
isInStore={store.containsElement(owner.id)}

View File

@@ -20,6 +20,7 @@ import styles from './OwnerView.css';
type OwnerViewProps = {
displayName: string,
hocDisplayNames: Array<string> | null,
environmentName: string | null,
compiledWithForget: boolean,
id: number,
isInStore: boolean,
@@ -27,6 +28,7 @@ type OwnerViewProps = {
export default function OwnerView({
displayName,
environmentName,
hocDisplayNames,
compiledWithForget,
id,
@@ -65,6 +67,7 @@ export default function OwnerView({
<ElementBadges
hocDisplayNames={hocDisplayNames}
compiledWithForget={compiledWithForget}
environmentName={environmentName}
/>
</span>
</Button>

View File

@@ -220,6 +220,7 @@ function ElementsDropdown({owners, selectOwner}: ElementsDropdownProps) {
<ElementBadges
hocDisplayNames={owner.hocDisplayNames}
environmentName={owner.env}
compiledWithForget={owner.compiledWithForget}
className={styles.BadgesBlock}
/>
@@ -268,6 +269,7 @@ function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
<ElementBadges
hocDisplayNames={hocDisplayNames}
environmentName={owner.env}
compiledWithForget={compiledWithForget}
className={styles.BadgesBlock}
/>

View File

@@ -12,6 +12,8 @@ import {use, useContext} from 'react';
import useOpenResource from '../useOpenResource';
import ElementBadges from './ElementBadges';
import styles from './StackTraceView.css';
import type {
@@ -28,9 +30,13 @@ import formatLocationForDisplay from './formatLocationForDisplay';
type CallSiteViewProps = {
callSite: ReactCallSite,
environmentName: null | string,
};
export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
export function CallSiteView({
callSite,
environmentName,
}: CallSiteViewProps): React.Node {
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
const [virtualFunctionName, virtualURL, virtualLine, virtualColumn] =
@@ -64,19 +70,33 @@ export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
title={url + ':' + line}>
{formatLocationForDisplay(url, line, column)}
</span>
<ElementBadges environmentName={environmentName} />
</div>
);
}
type Props = {
stack: ReactStackTrace,
environmentName: null | string,
};
export default function StackTraceView({stack}: Props): React.Node {
export default function StackTraceView({
stack,
environmentName,
}: Props): React.Node {
return (
<div className={styles.StackTraceView}>
{stack.map((callSite, index) => (
<CallSiteView key={index} callSite={callSite} />
<CallSiteView
key={index}
callSite={callSite}
environmentName={
// Badge last row
// TODO: If we start ignore listing the last row, we should badge the last
// non-ignored row.
index === stack.length - 1 ? environmentName : null
}
/>
))}
</div>
);

View File

@@ -208,6 +208,7 @@ export type SerializedElement = {
displayName: string | null,
id: number,
key: number | string | null,
env: null | string,
hocDisplayNames: Array<string> | null,
compiledWithForget: boolean,
type: ElementType,
@@ -265,6 +266,9 @@ export type InspectedElement = {
// List of owners
owners: Array<SerializedElement> | null,
// Environment name that this component executed in or null for the client
env: string | null,
// Location of component in source code.
source: ReactFunctionLocation | null,