@samitouri / QOS-React-2 / commits / 738aebdbac

[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.

Sebastian Markbåge committed Aug 7, 2025 at 10:39 UTC 738aebdbacff9becd469b888209a08436f87c511
12 files changed +90 -6
packages/react-devtools-shared/src/__tests__/profilingCache-test.js
+3
@@ -862,6 +862,7 @@ describe('ProfilingCache', () => {
862 {
863 "compiledWithForget": false,
864 "displayName": "render()",
865 + "env": null,
866 "hocDisplayNames": null,
867 "id": 1,
868 "key": null,
@@ -903,6 +904,7 @@ describe('ProfilingCache', () => {
904 {
905 "compiledWithForget": false,
906 "displayName": "createRoot()",
907 + "env": null,
908 "hocDisplayNames": null,
909 "id": 1,
910 "key": null,
@@ -943,6 +945,7 @@ describe('ProfilingCache', () => {
945 {
946 "compiledWithForget": false,
947 "displayName": "createRoot()",
948 + "env": null,
949 "hocDisplayNames": null,
950 "id": 1,
951 "key": null,
packages/react-devtools-shared/src/backend/fiber/renderer.js
+6
@@ -4818,6 +4818,7 @@ export function attach(
4818 displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
4819 id: instance.id,
4820 key: fiber.key,
4821 + env: null,
4822 type: getElementTypeForFiber(fiber),
4823 };
4824 } else {
@@ -4826,6 +4827,7 @@ export function attach(
4827 displayName: componentInfo.name || 'Anonymous',
4828 id: instance.id,
4829 key: componentInfo.key == null ? null : componentInfo.key,
4830 + env: componentInfo.env == null ? null : componentInfo.env,
4831 type: ElementTypeVirtual,
4832 };
4833 }
@@ -5451,6 +5453,8 @@ export function attach(
5453 // List of owners
5454 owners,
5455
5456 + env: null,
5457 +
5458 rootType,
5459 rendererPackageName: renderer.rendererPackageName,
5460 rendererVersion: renderer.version,
@@ -5554,6 +5558,8 @@ export function attach(
5558 // List of owners
5559 owners,
5560
5561 + env: componentInfo.env == null ? null : componentInfo.env,
5562 +
5563 rootType,
5564 rendererPackageName: renderer.rendererPackageName,
5565 rendererVersion: renderer.version,
packages/react-devtools-shared/src/backend/legacy/renderer.js
+3
@@ -795,6 +795,7 @@ export function attach(
795 displayName: getData(owner).displayName || 'Unknown',
796 id: getID(owner),
797 key: element.key,
798 + env: null,
799 type: getElementType(owner),
800 });
801 if (owner._currentElement) {
@@ -857,6 +858,8 @@ export function attach(
858 // List of owners
859 owners,
860
861 + env: null,
862 +
863 rootType: null,
864 rendererPackageName: null,
865 rendererVersion: null,
packages/react-devtools-shared/src/backend/types.js
+5
@@ -256,6 +256,7 @@ export type SerializedElement = {
256 displayName: string | null,
257 id: number,
258 key: number | string | null,
259 + env: null | string,
260 type: ElementType,
261 };
262
@@ -301,6 +302,10 @@ export type InspectedElement = {
302
303 // List of owners
304 owners: Array<SerializedElement> | null,
305 +
306 + // Environment name that this component executed in or null for the client
307 + env: string | null,
308 +
309 source: ReactFunctionLocation | null,
310
311 type: ElementType,
packages/react-devtools-shared/src/backendAPI.js
+2
@@ -255,6 +255,7 @@ export function convertInspectedElementBackendToFrontend(
255 id,
256 type,
257 owners,
258 + env,
259 source,
260 context,
261 hooks,
@@ -299,6 +300,7 @@ export function convertInspectedElementBackendToFrontend(
300 owners === null
301 ? null
302 : owners.map(backendToFrontendSerializedElementMapper),
303 + env,
304 context: hydrateHelper(context),
305 hooks: hydrateHelper(hooks),
306 props: hydrateHelper(props),
packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.js
+6 -1
@@ -16,18 +16,21 @@ import styles from './ElementBadges.css';
16
17 type Props = {
18 hocDisplayNames: Array<string> | null,
19 + environmentName: string | null,
20 compiledWithForget: boolean,
21 className?: string,
22 };
23
24 export default function ElementBadges({
25 compiledWithForget,
26 + environmentName,
27 hocDisplayNames,
28 className = '',
29 }: Props): React.Node {
30 if (
31 !compiledWithForget &&
30 - (hocDisplayNames == null || hocDisplayNames.length === 0)
32 + (hocDisplayNames == null || hocDisplayNames.length === 0) &&
33 + environmentName == null
34 ) {
35 return null;
36 }
@@ -36,6 +39,8 @@ export default function ElementBadges({
39 <div className={`${styles.Root} ${className}`}>
40 {compiledWithForget && <ForgetBadge indexable={false} />}
41
42 + {environmentName != null ? <Badge>{environmentName}</Badge> : null}
43 +
44 {hocDisplayNames != null && hocDisplayNames.length > 0 && (
45 <Badge>{hocDisplayNames[0]}</Badge>
46 )}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
+30 -2
@@ -150,13 +150,28 @@ function SuspendedByRow({
150 </Button>
151 {isOpen && (
152 <div className={styles.CollapsableContent}>
153 - {showIOStack && <StackTraceView stack={ioInfo.stack} />}
153 + {showIOStack && (
154 + <StackTraceView
155 + stack={ioInfo.stack}
156 + environmentName={
157 + ioOwner !== null && ioOwner.env === ioInfo.env
158 + ? null
159 + : ioInfo.env
160 + }
161 + />
162 + )}
163 {(showIOStack || !showAwaitStack) &&
164 ioOwner !== null &&
165 ioOwner.id !== inspectedElement.id ? (
166 <OwnerView
167 key={ioOwner.id}
168 displayName={ioOwner.displayName || 'Anonymous'}
169 + environmentName={
170 + ioOwner.env === inspectedElement.env &&
171 + ioOwner.env === ioInfo.env
172 + ? null
173 + : ioOwner.env
174 + }
175 hocDisplayNames={ioOwner.hocDisplayNames}
176 compiledWithForget={ioOwner.compiledWithForget}
177 id={ioOwner.id}
@@ -168,12 +183,25 @@ function SuspendedByRow({
183 <>
184 <div className={styles.SmallHeader}>awaited at:</div>
185 {asyncInfo.stack !== null && asyncInfo.stack.length > 0 && (
171 - <StackTraceView stack={asyncInfo.stack} />
186 + <StackTraceView
187 + stack={asyncInfo.stack}
188 + environmentName={
189 + asyncOwner !== null && asyncOwner.env === asyncInfo.env
190 + ? null
191 + : asyncInfo.env
192 + }
193 + />
194 )}
195 {asyncOwner !== null && asyncOwner.id !== inspectedElement.id ? (
196 <OwnerView
197 key={asyncOwner.id}
198 displayName={asyncOwner.displayName || 'Anonymous'}
199 + environmentName={
200 + asyncOwner.env === inspectedElement.env &&
201 + asyncOwner.env === asyncInfo.env
202 + ? null
203 + : asyncOwner.env
204 + }
205 hocDisplayNames={asyncOwner.hocDisplayNames}
206 compiledWithForget={asyncOwner.compiledWithForget}
207 id={asyncOwner.id}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+3
@@ -174,6 +174,9 @@ export default function InspectedElementView({
174 key={owner.id}
175 displayName={owner.displayName || 'Anonymous'}
176 hocDisplayNames={owner.hocDisplayNames}
177 + environmentName={
178 + inspectedElement.env === owner.env ? null : owner.env
179 + }
180 compiledWithForget={owner.compiledWithForget}
181 id={owner.id}
182 isInStore={store.containsElement(owner.id)}
packages/react-devtools-shared/src/devtools/views/Components/OwnerView.js
+3
@@ -20,6 +20,7 @@ import styles from './OwnerView.css';
20 type OwnerViewProps = {
21 displayName: string,
22 hocDisplayNames: Array<string> | null,
23 + environmentName: string | null,
24 compiledWithForget: boolean,
25 id: number,
26 isInStore: boolean,
@@ -27,6 +28,7 @@ type OwnerViewProps = {
28
29 export default function OwnerView({
30 displayName,
31 + environmentName,
32 hocDisplayNames,
33 compiledWithForget,
34 id,
@@ -65,6 +67,7 @@ export default function OwnerView({
67 <ElementBadges
68 hocDisplayNames={hocDisplayNames}
69 compiledWithForget={compiledWithForget}
70 + environmentName={environmentName}
71 />
72 </span>
73 </Button>
packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js
+2
@@ -220,6 +220,7 @@ function ElementsDropdown({owners, selectOwner}: ElementsDropdownProps) {
220
221 <ElementBadges
222 hocDisplayNames={owner.hocDisplayNames}
223 + environmentName={owner.env}
224 compiledWithForget={owner.compiledWithForget}
225 className={styles.BadgesBlock}
226 />
@@ -268,6 +269,7 @@ function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
269
270 <ElementBadges
271 hocDisplayNames={hocDisplayNames}
272 + environmentName={owner.env}
273 compiledWithForget={compiledWithForget}
274 className={styles.BadgesBlock}
275 />
packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js
+23 -3
@@ -12,6 +12,8 @@ import {use, useContext} from 'react';
12
13 import useOpenResource from '../useOpenResource';
14
15 +import ElementBadges from './ElementBadges';
16 +
17 import styles from './StackTraceView.css';
18
19 import type {
@@ -28,9 +30,13 @@ import formatLocationForDisplay from './formatLocationForDisplay';
30
31 type CallSiteViewProps = {
32 callSite: ReactCallSite,
33 + environmentName: null | string,
34 };
35
33 -export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
36 +export function CallSiteView({
37 + callSite,
38 + environmentName,
39 +}: CallSiteViewProps): React.Node {
40 const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
41
42 const [virtualFunctionName, virtualURL, virtualLine, virtualColumn] =
@@ -64,19 +70,33 @@ export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
70 title={url + ':' + line}>
71 {formatLocationForDisplay(url, line, column)}
72 </span>
73 + <ElementBadges environmentName={environmentName} />
74 </div>
75 );
76 }
77
78 type Props = {
79 stack: ReactStackTrace,
80 + environmentName: null | string,
81 };
82
75 -export default function StackTraceView({stack}: Props): React.Node {
83 +export default function StackTraceView({
84 + stack,
85 + environmentName,
86 +}: Props): React.Node {
87 return (
88 <div className={styles.StackTraceView}>
89 {stack.map((callSite, index) => (
79 - <CallSiteView key={index} callSite={callSite} />
90 + <CallSiteView
91 + key={index}
92 + callSite={callSite}
93 + environmentName={
94 + // Badge last row
95 + // TODO: If we start ignore listing the last row, we should badge the last
96 + // non-ignored row.
97 + index === stack.length - 1 ? environmentName : null
98 + }
99 + />
100 ))}
101 </div>
102 );
packages/react-devtools-shared/src/frontend/types.js
+4
@@ -208,6 +208,7 @@ export type SerializedElement = {
208 displayName: string | null,
209 id: number,
210 key: number | string | null,
211 + env: null | string,
212 hocDisplayNames: Array<string> | null,
213 compiledWithForget: boolean,
214 type: ElementType,
@@ -265,6 +266,9 @@ export type InspectedElement = {
266 // List of owners
267 owners: Array<SerializedElement> | null,
268
269 + // Environment name that this component executed in or null for the client
270 + env: string | null,
271 +
272 // Location of component in source code.
273 source: ReactFunctionLocation | null,
274