[DevTools] Include the description derived from the promise (#34017)
Stacked on #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" />
Sebastian Markbåge committed
Jul 28, 2025 at 15:11 UTC
71236c940952410b4655a673b0fe3f897d4baf04
8 files changed
+158
-68
packages/react-client/src/ReactFlightPerformanceTrack.js
+2
-64
@@ -22,6 +22,8 @@ import {
22
addObjectToProperties,
23
} from 'shared/ReactPerformanceTrackProperties';
24
25
+import {getIODescription} from 'shared/ReactIODescription';
26
+
27
const supportsUserTiming =
28
enableProfilerTimer &&
29
typeof console !== 'undefined' &&
@@ -300,70 +302,6 @@ function getIOColor(
302
}
303
}
304
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
-
305
function getIOLongName(
306
ioInfo: ReactIOInfo,
307
description: string,
packages/react-devtools-shared/src/backend/fiber/renderer.js
+19
@@ -105,6 +105,8 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent
105
import is from 'shared/objectIs';
106
import hasOwnProperty from 'shared/hasOwnProperty';
107
108
+import {getIODescription} from 'shared/ReactIODescription';
109
+
110
import {
111
getStackByFiberInDevAndProd,
112
getOwnerStackByFiberInDev,
@@ -4116,9 +4118,26 @@ export function attach(
4118
parentInstance,
4119
asyncInfo.owner,
4120
);
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
+ }
4137
return {
4138
awaited: {
4139
name: ioInfo.name,
4140
+ description: getIODescription(resolvedValue),
4141
start: ioInfo.start,
4142
end: ioInfo.end,
4143
value: ioInfo.value == null ? null : ioInfo.value,
packages/react-devtools-shared/src/backend/types.js
+1
@@ -235,6 +235,7 @@ export type PathMatch = {
235
// Serialized version of ReactIOInfo
236
export type SerializedIOInfo = {
237
name: string,
238
+ description: string,
239
start: number,
240
end: number,
241
value: null | Promise<mixed>,
packages/react-devtools-shared/src/backendAPI.js
+1
@@ -218,6 +218,7 @@ function backendToFrontendSerializedAsyncInfo(
218
return {
219
awaited: {
220
name: ioInfo.name,
221
+ description: ioInfo.description,
222
start: ioInfo.start,
223
end: ioInfo.end,
224
value: ioInfo.value,
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
+17
-3
@@ -75,11 +75,25 @@
75
color: var(--color-expand-collapse-toggle);
76
}
77
78
-.CollapsableHeaderTitle {
79
- flex: 1 1 auto;
78
+.CollapsableHeaderTitle, .CollapsableHeaderDescription, .CollapsableHeaderSeparator, .CollapsableHeaderFiller {
79
font-family: var(--font-family-monospace);
80
font-size: var(--font-size-monospace-normal);
81
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;
97
}
98
99
.CollapsableContent {
@@ -108,4 +122,4 @@
122
123
.TimeBarSpanErrored {
124
background-color: var(--color-timespan-background-errored);
111
-}
\ No newline at end of file
125
+}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
+45
-1
@@ -38,6 +38,37 @@ type RowProps = {
38
maxTime: number,
39
};
40
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
+
72
function SuspendedByRow({
73
bridge,
74
element,
@@ -50,6 +81,9 @@ function SuspendedByRow({
81
}: RowProps) {
82
const [isOpen, setIsOpen] = useState(false);
83
const name = asyncInfo.awaited.name;
84
+ const description = asyncInfo.awaited.description;
85
+ const longName = description === '' ? name : name + ' (' + description + ')';
86
+ const shortDescription = getShortDescription(name, description);
87
let stack;
88
let owner;
89
if (asyncInfo.stack === null || asyncInfo.stack.length === 0) {
@@ -83,12 +117,22 @@ function SuspendedByRow({
117
<Button
118
className={styles.CollapsableHeader}
119
onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
86
- title={name + ' — ' + (end - start).toFixed(2) + ' ms'}>
120
+ title={longName + ' — ' + (end - start).toFixed(2) + ' ms'}>
121
<ButtonIcon
122
className={styles.CollapsableHeaderIcon}
123
type={isOpen ? 'expanded' : 'collapsed'}
124
/>
125
<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} />
136
<div className={styles.TimeBarContainer}>
137
<div
138
className={
packages/react-devtools-shared/src/frontend/types.js
+1
@@ -187,6 +187,7 @@ export type Element = {
187
// Serialized version of ReactIOInfo
188
export type SerializedIOInfo = {
189
name: string,
190
+ description: string,
191
start: number,
192
end: number,
193
value: null | Promise<mixed>,
packages/shared/ReactIODescription.js
new
+72
@@ -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
+}