From 738aebdbacff9becd469b888209a08436f87c511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 7 Aug 2025 10:39:08 -0400 Subject: [PATCH] [DevTools] Add Badge to Owners and sometimes stack traces (#34106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #34101. This adds a badge to owners if they are different from the currently selected component's environment. Screenshot 2025-08-04 at 5 15 02 PM 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. --- .../src/__tests__/profilingCache-test.js | 3 ++ .../src/backend/fiber/renderer.js | 6 ++++ .../src/backend/legacy/renderer.js | 3 ++ .../src/backend/types.js | 5 +++ .../react-devtools-shared/src/backendAPI.js | 2 ++ .../views/Components/ElementBadges.js | 7 +++- .../Components/InspectedElementSuspendedBy.js | 32 +++++++++++++++++-- .../views/Components/InspectedElementView.js | 3 ++ .../devtools/views/Components/OwnerView.js | 3 ++ .../devtools/views/Components/OwnersStack.js | 2 ++ .../views/Components/StackTraceView.js | 26 +++++++++++++-- .../src/frontend/types.js | 4 +++ 12 files changed, 90 insertions(+), 6 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js index c0f0804f41..795f37183a 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js @@ -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, diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index aab9476db3..b26da3530b 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -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, diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index d2b846bee2..6153e08832 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -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, diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 6324e63da1..585654252d 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -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 | null, + + // Environment name that this component executed in or null for the client + env: string | null, + source: ReactFunctionLocation | null, type: ElementType, diff --git a/packages/react-devtools-shared/src/backendAPI.js b/packages/react-devtools-shared/src/backendAPI.js index d6aa18cd31..a27e70c26d 100644 --- a/packages/react-devtools-shared/src/backendAPI.js +++ b/packages/react-devtools-shared/src/backendAPI.js @@ -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), diff --git a/packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.js b/packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.js index a829ad0153..5a3355c60c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.js @@ -16,18 +16,21 @@ import styles from './ElementBadges.css'; type Props = { hocDisplayNames: Array | 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({
{compiledWithForget && } + {environmentName != null ? {environmentName} : null} + {hocDisplayNames != null && hocDisplayNames.length > 0 && ( {hocDisplayNames[0]} )} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js index a1b76e49b6..c24dd881e9 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js @@ -150,13 +150,28 @@ function SuspendedByRow({ {isOpen && (
- {showIOStack && } + {showIOStack && ( + + )} {(showIOStack || !showAwaitStack) && ioOwner !== null && ioOwner.id !== inspectedElement.id ? (
awaited at:
{asyncInfo.stack !== null && asyncInfo.stack.length > 0 && ( - + )} {asyncOwner !== null && asyncOwner.id !== inspectedElement.id ? ( | 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({ diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js index 0fa5c0910b..09bdb96af0 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js @@ -220,6 +220,7 @@ function ElementsDropdown({owners, selectOwner}: ElementsDropdownProps) { @@ -268,6 +269,7 @@ function ElementView({isSelected, owner, selectOwner}: ElementViewProps) { diff --git a/packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js b/packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js index 4352ad6a82..fdbdba702d 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js @@ -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)} +
); } 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 (
{stack.map((callSite, index) => ( - + ))}
); diff --git a/packages/react-devtools-shared/src/frontend/types.js b/packages/react-devtools-shared/src/frontend/types.js index 622205dd67..e4a4c5400b 100644 --- a/packages/react-devtools-shared/src/frontend/types.js +++ b/packages/react-devtools-shared/src/frontend/types.js @@ -208,6 +208,7 @@ export type SerializedElement = { displayName: string | null, id: number, key: number | string | null, + env: null | string, hocDisplayNames: Array | null, compiledWithForget: boolean, type: ElementType, @@ -265,6 +266,9 @@ export type InspectedElement = { // List of owners owners: Array | 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,