@samitouri / QOS-React-2 / commits / 71236c9409

[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 +}