Skip to content

Commit 71236c9

Browse files
authored
[DevTools] Include the description derived from the promise (facebook#34017)
Stacked on facebook#34016. This is using the same thing we already do for the performance track to provide a description of the I/O based on the content of the resolved Promise. E.g. a Response's URL. <img width="375" height="388" alt="Screenshot 2025-07-28 at 1 09 49 AM" src="https://github.com/user-attachments/assets/f3fdc40f-4e21-4e83-b49e-21c7ec975137" />
1 parent 7ee7571 commit 71236c9

File tree

8 files changed

+158
-68
lines changed

8 files changed

+158
-68
lines changed

packages/react-client/src/ReactFlightPerformanceTrack.js

Lines changed: 2 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
addObjectToProperties,
2323
} from 'shared/ReactPerformanceTrackProperties';
2424

25+
import {getIODescription} from 'shared/ReactIODescription';
26+
2527
const supportsUserTiming =
2628
enableProfilerTimer &&
2729
typeof console !== 'undefined' &&
@@ -300,70 +302,6 @@ function getIOColor(
300302
}
301303
}
302304

303-
function getIODescription(value: any): string {
304-
if (!__DEV__) {
305-
return '';
306-
}
307-
try {
308-
switch (typeof value) {
309-
case 'object':
310-
// Test the object for a bunch of common property names that are useful identifiers.
311-
// While we only have the return value here, it should ideally be a name that
312-
// describes the arguments requested.
313-
if (value === null) {
314-
return '';
315-
} else if (value instanceof Error) {
316-
// eslint-disable-next-line react-internal/safe-string-coercion
317-
return String(value.message);
318-
} else if (typeof value.url === 'string') {
319-
return value.url;
320-
} else if (typeof value.command === 'string') {
321-
return value.command;
322-
} else if (
323-
typeof value.request === 'object' &&
324-
typeof value.request.url === 'string'
325-
) {
326-
return value.request.url;
327-
} else if (
328-
typeof value.response === 'object' &&
329-
typeof value.response.url === 'string'
330-
) {
331-
return value.response.url;
332-
} else if (
333-
typeof value.id === 'string' ||
334-
typeof value.id === 'number' ||
335-
typeof value.id === 'bigint'
336-
) {
337-
// eslint-disable-next-line react-internal/safe-string-coercion
338-
return String(value.id);
339-
} else if (typeof value.name === 'string') {
340-
return value.name;
341-
} else {
342-
const str = value.toString();
343-
if (str.startWith('[object ') || str.length < 5 || str.length > 500) {
344-
// This is probably not a useful description.
345-
return '';
346-
}
347-
return str;
348-
}
349-
case 'string':
350-
if (value.length < 5 || value.length > 500) {
351-
return '';
352-
}
353-
return value;
354-
case 'number':
355-
case 'bigint':
356-
// eslint-disable-next-line react-internal/safe-string-coercion
357-
return String(value);
358-
default:
359-
// Not useful descriptors.
360-
return '';
361-
}
362-
} catch (x) {
363-
return '';
364-
}
365-
}
366-
367305
function getIOLongName(
368306
ioInfo: ReactIOInfo,
369307
description: string,

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent
105105
import is from 'shared/objectIs';
106106
import hasOwnProperty from 'shared/hasOwnProperty';
107107

108+
import {getIODescription} from 'shared/ReactIODescription';
109+
108110
import {
109111
getStackByFiberInDevAndProd,
110112
getOwnerStackByFiberInDev,
@@ -4116,9 +4118,26 @@ export function attach(
41164118
parentInstance,
41174119
asyncInfo.owner,
41184120
);
4121+
const value: any = ioInfo.value;
4122+
let resolvedValue = undefined;
4123+
if (
4124+
typeof value === 'object' &&
4125+
value !== null &&
4126+
typeof value.then === 'function'
4127+
) {
4128+
switch (value.status) {
4129+
case 'fulfilled':
4130+
resolvedValue = value.value;
4131+
break;
4132+
case 'rejected':
4133+
resolvedValue = value.reason;
4134+
break;
4135+
}
4136+
}
41194137
return {
41204138
awaited: {
41214139
name: ioInfo.name,
4140+
description: getIODescription(resolvedValue),
41224141
start: ioInfo.start,
41234142
end: ioInfo.end,
41244143
value: ioInfo.value == null ? null : ioInfo.value,

packages/react-devtools-shared/src/backend/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ export type PathMatch = {
235235
// Serialized version of ReactIOInfo
236236
export type SerializedIOInfo = {
237237
name: string,
238+
description: string,
238239
start: number,
239240
end: number,
240241
value: null | Promise<mixed>,

packages/react-devtools-shared/src/backendAPI.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ function backendToFrontendSerializedAsyncInfo(
218218
return {
219219
awaited: {
220220
name: ioInfo.name,
221+
description: ioInfo.description,
221222
start: ioInfo.start,
222223
end: ioInfo.end,
223224
value: ioInfo.value,

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,25 @@
7575
color: var(--color-expand-collapse-toggle);
7676
}
7777

78-
.CollapsableHeaderTitle {
79-
flex: 1 1 auto;
78+
.CollapsableHeaderTitle, .CollapsableHeaderDescription, .CollapsableHeaderSeparator, .CollapsableHeaderFiller {
8079
font-family: var(--font-family-monospace);
8180
font-size: var(--font-size-monospace-normal);
8281
text-align: left;
82+
white-space: nowrap;
83+
}
84+
.CollapsableHeaderTitle {
85+
flex: 0 1 auto;
86+
overflow: hidden;
87+
text-overflow: ellipsis;
88+
}
89+
90+
.CollapsableHeaderSeparator {
91+
flex: 0 0 auto;
92+
white-space: pre;
93+
}
94+
95+
.CollapsableHeaderFiller {
96+
flex: 1 0 0;
8397
}
8498

8599
.CollapsableContent {
@@ -108,4 +122,4 @@
108122

109123
.TimeBarSpanErrored {
110124
background-color: var(--color-timespan-background-errored);
111-
}
125+
}

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,37 @@ type RowProps = {
3838
maxTime: number,
3939
};
4040

41+
function getShortDescription(name: string, description: string): string {
42+
const descMaxLength = 30 - name.length;
43+
if (descMaxLength > 1) {
44+
const l = description.length;
45+
if (l > 0 && l <= descMaxLength) {
46+
// We can fit the full description
47+
return description;
48+
} else if (
49+
description.startsWith('http://') ||
50+
description.startsWith('https://') ||
51+
description.startsWith('/')
52+
) {
53+
// Looks like a URL. Let's see if we can extract something shorter.
54+
// We don't have to do a full parse so let's try something cheaper.
55+
let queryIdx = description.indexOf('?');
56+
if (queryIdx === -1) {
57+
queryIdx = description.length;
58+
}
59+
if (description.charCodeAt(queryIdx - 1) === 47 /* "/" */) {
60+
// Ends with slash. Look before that.
61+
queryIdx--;
62+
}
63+
const slashIdx = description.lastIndexOf('/', queryIdx - 1);
64+
// This may now be either the file name or the host.
65+
// Include the slash to make it more obvious what we trimmed.
66+
return '…' + description.slice(slashIdx, queryIdx);
67+
}
68+
}
69+
return '';
70+
}
71+
4172
function SuspendedByRow({
4273
bridge,
4374
element,
@@ -50,6 +81,9 @@ function SuspendedByRow({
5081
}: RowProps) {
5182
const [isOpen, setIsOpen] = useState(false);
5283
const name = asyncInfo.awaited.name;
84+
const description = asyncInfo.awaited.description;
85+
const longName = description === '' ? name : name + ' (' + description + ')';
86+
const shortDescription = getShortDescription(name, description);
5387
let stack;
5488
let owner;
5589
if (asyncInfo.stack === null || asyncInfo.stack.length === 0) {
@@ -83,12 +117,22 @@ function SuspendedByRow({
83117
<Button
84118
className={styles.CollapsableHeader}
85119
onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
86-
title={name + ' — ' + (end - start).toFixed(2) + ' ms'}>
120+
title={longName + ' — ' + (end - start).toFixed(2) + ' ms'}>
87121
<ButtonIcon
88122
className={styles.CollapsableHeaderIcon}
89123
type={isOpen ? 'expanded' : 'collapsed'}
90124
/>
91125
<span className={styles.CollapsableHeaderTitle}>{name}</span>
126+
{shortDescription === '' ? null : (
127+
<>
128+
<span className={styles.CollapsableHeaderSeparator}>{' ('}</span>
129+
<span className={styles.CollapsableHeaderTitle}>
130+
{shortDescription}
131+
</span>
132+
<span className={styles.CollapsableHeaderSeparator}>{') '}</span>
133+
</>
134+
)}
135+
<div className={styles.CollapsableHeaderFiller} />
92136
<div className={styles.TimeBarContainer}>
93137
<div
94138
className={

packages/react-devtools-shared/src/frontend/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export type Element = {
187187
// Serialized version of ReactIOInfo
188188
export type SerializedIOInfo = {
189189
name: string,
190+
description: string,
190191
start: number,
191192
end: number,
192193
value: null | Promise<mixed>,

packages/shared/ReactIODescription.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
export function getIODescription(value: any): string {
11+
if (!__DEV__) {
12+
return '';
13+
}
14+
try {
15+
switch (typeof value) {
16+
case 'object':
17+
// Test the object for a bunch of common property names that are useful identifiers.
18+
// While we only have the return value here, it should ideally be a name that
19+
// describes the arguments requested.
20+
if (value === null) {
21+
return '';
22+
} else if (value instanceof Error) {
23+
// eslint-disable-next-line react-internal/safe-string-coercion
24+
return String(value.message);
25+
} else if (typeof value.url === 'string') {
26+
return value.url;
27+
} else if (typeof value.command === 'string') {
28+
return value.command;
29+
} else if (
30+
typeof value.request === 'object' &&
31+
typeof value.request.url === 'string'
32+
) {
33+
return value.request.url;
34+
} else if (
35+
typeof value.response === 'object' &&
36+
typeof value.response.url === 'string'
37+
) {
38+
return value.response.url;
39+
} else if (
40+
typeof value.id === 'string' ||
41+
typeof value.id === 'number' ||
42+
typeof value.id === 'bigint'
43+
) {
44+
// eslint-disable-next-line react-internal/safe-string-coercion
45+
return String(value.id);
46+
} else if (typeof value.name === 'string') {
47+
return value.name;
48+
} else {
49+
const str = value.toString();
50+
if (str.startWith('[object ') || str.length < 5 || str.length > 500) {
51+
// This is probably not a useful description.
52+
return '';
53+
}
54+
return str;
55+
}
56+
case 'string':
57+
if (value.length < 5 || value.length > 500) {
58+
return '';
59+
}
60+
return value;
61+
case 'number':
62+
case 'bigint':
63+
// eslint-disable-next-line react-internal/safe-string-coercion
64+
return String(value);
65+
default:
66+
// Not useful descriptors.
67+
return '';
68+
}
69+
} catch (x) {
70+
return '';
71+
}
72+
}

0 commit comments

Comments
 (0)