[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