mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
[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" />
This commit is contained in:
parent
7ee7571212
commit
71236c9409
|
|
@ -22,6 +22,8 @@ import {
|
|||
addObjectToProperties,
|
||||
} from 'shared/ReactPerformanceTrackProperties';
|
||||
|
||||
import {getIODescription} from 'shared/ReactIODescription';
|
||||
|
||||
const supportsUserTiming =
|
||||
enableProfilerTimer &&
|
||||
typeof console !== 'undefined' &&
|
||||
|
|
@ -300,70 +302,6 @@ function getIOColor(
|
|||
}
|
||||
}
|
||||
|
||||
function getIODescription(value: any): string {
|
||||
if (!__DEV__) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
switch (typeof value) {
|
||||
case 'object':
|
||||
// Test the object for a bunch of common property names that are useful identifiers.
|
||||
// While we only have the return value here, it should ideally be a name that
|
||||
// describes the arguments requested.
|
||||
if (value === null) {
|
||||
return '';
|
||||
} else if (value instanceof Error) {
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
return String(value.message);
|
||||
} else if (typeof value.url === 'string') {
|
||||
return value.url;
|
||||
} else if (typeof value.command === 'string') {
|
||||
return value.command;
|
||||
} else if (
|
||||
typeof value.request === 'object' &&
|
||||
typeof value.request.url === 'string'
|
||||
) {
|
||||
return value.request.url;
|
||||
} else if (
|
||||
typeof value.response === 'object' &&
|
||||
typeof value.response.url === 'string'
|
||||
) {
|
||||
return value.response.url;
|
||||
} else if (
|
||||
typeof value.id === 'string' ||
|
||||
typeof value.id === 'number' ||
|
||||
typeof value.id === 'bigint'
|
||||
) {
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
return String(value.id);
|
||||
} else if (typeof value.name === 'string') {
|
||||
return value.name;
|
||||
} else {
|
||||
const str = value.toString();
|
||||
if (str.startWith('[object ') || str.length < 5 || str.length > 500) {
|
||||
// This is probably not a useful description.
|
||||
return '';
|
||||
}
|
||||
return str;
|
||||
}
|
||||
case 'string':
|
||||
if (value.length < 5 || value.length > 500) {
|
||||
return '';
|
||||
}
|
||||
return value;
|
||||
case 'number':
|
||||
case 'bigint':
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
return String(value);
|
||||
default:
|
||||
// Not useful descriptors.
|
||||
return '';
|
||||
}
|
||||
} catch (x) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getIOLongName(
|
||||
ioInfo: ReactIOInfo,
|
||||
description: string,
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent
|
|||
import is from 'shared/objectIs';
|
||||
import hasOwnProperty from 'shared/hasOwnProperty';
|
||||
|
||||
import {getIODescription} from 'shared/ReactIODescription';
|
||||
|
||||
import {
|
||||
getStackByFiberInDevAndProd,
|
||||
getOwnerStackByFiberInDev,
|
||||
|
|
@ -4116,9 +4118,26 @@ export function attach(
|
|||
parentInstance,
|
||||
asyncInfo.owner,
|
||||
);
|
||||
const value: any = ioInfo.value;
|
||||
let resolvedValue = undefined;
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
typeof value.then === 'function'
|
||||
) {
|
||||
switch (value.status) {
|
||||
case 'fulfilled':
|
||||
resolvedValue = value.value;
|
||||
break;
|
||||
case 'rejected':
|
||||
resolvedValue = value.reason;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
awaited: {
|
||||
name: ioInfo.name,
|
||||
description: getIODescription(resolvedValue),
|
||||
start: ioInfo.start,
|
||||
end: ioInfo.end,
|
||||
value: ioInfo.value == null ? null : ioInfo.value,
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@ export type PathMatch = {
|
|||
// Serialized version of ReactIOInfo
|
||||
export type SerializedIOInfo = {
|
||||
name: string,
|
||||
description: string,
|
||||
start: number,
|
||||
end: number,
|
||||
value: null | Promise<mixed>,
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ function backendToFrontendSerializedAsyncInfo(
|
|||
return {
|
||||
awaited: {
|
||||
name: ioInfo.name,
|
||||
description: ioInfo.description,
|
||||
start: ioInfo.start,
|
||||
end: ioInfo.end,
|
||||
value: ioInfo.value,
|
||||
|
|
|
|||
|
|
@ -75,11 +75,25 @@
|
|||
color: var(--color-expand-collapse-toggle);
|
||||
}
|
||||
|
||||
.CollapsableHeaderTitle {
|
||||
flex: 1 1 auto;
|
||||
.CollapsableHeaderTitle, .CollapsableHeaderDescription, .CollapsableHeaderSeparator, .CollapsableHeaderFiller {
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.CollapsableHeaderTitle {
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.CollapsableHeaderSeparator {
|
||||
flex: 0 0 auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.CollapsableHeaderFiller {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.CollapsableContent {
|
||||
|
|
@ -108,4 +122,4 @@
|
|||
|
||||
.TimeBarSpanErrored {
|
||||
background-color: var(--color-timespan-background-errored);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,37 @@ type RowProps = {
|
|||
maxTime: number,
|
||||
};
|
||||
|
||||
function getShortDescription(name: string, description: string): string {
|
||||
const descMaxLength = 30 - name.length;
|
||||
if (descMaxLength > 1) {
|
||||
const l = description.length;
|
||||
if (l > 0 && l <= descMaxLength) {
|
||||
// We can fit the full description
|
||||
return description;
|
||||
} else if (
|
||||
description.startsWith('http://') ||
|
||||
description.startsWith('https://') ||
|
||||
description.startsWith('/')
|
||||
) {
|
||||
// Looks like a URL. Let's see if we can extract something shorter.
|
||||
// We don't have to do a full parse so let's try something cheaper.
|
||||
let queryIdx = description.indexOf('?');
|
||||
if (queryIdx === -1) {
|
||||
queryIdx = description.length;
|
||||
}
|
||||
if (description.charCodeAt(queryIdx - 1) === 47 /* "/" */) {
|
||||
// Ends with slash. Look before that.
|
||||
queryIdx--;
|
||||
}
|
||||
const slashIdx = description.lastIndexOf('/', queryIdx - 1);
|
||||
// This may now be either the file name or the host.
|
||||
// Include the slash to make it more obvious what we trimmed.
|
||||
return '…' + description.slice(slashIdx, queryIdx);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function SuspendedByRow({
|
||||
bridge,
|
||||
element,
|
||||
|
|
@ -50,6 +81,9 @@ function SuspendedByRow({
|
|||
}: RowProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const name = asyncInfo.awaited.name;
|
||||
const description = asyncInfo.awaited.description;
|
||||
const longName = description === '' ? name : name + ' (' + description + ')';
|
||||
const shortDescription = getShortDescription(name, description);
|
||||
let stack;
|
||||
let owner;
|
||||
if (asyncInfo.stack === null || asyncInfo.stack.length === 0) {
|
||||
|
|
@ -83,12 +117,22 @@ function SuspendedByRow({
|
|||
<Button
|
||||
className={styles.CollapsableHeader}
|
||||
onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
|
||||
title={name + ' — ' + (end - start).toFixed(2) + ' ms'}>
|
||||
title={longName + ' — ' + (end - start).toFixed(2) + ' ms'}>
|
||||
<ButtonIcon
|
||||
className={styles.CollapsableHeaderIcon}
|
||||
type={isOpen ? 'expanded' : 'collapsed'}
|
||||
/>
|
||||
<span className={styles.CollapsableHeaderTitle}>{name}</span>
|
||||
{shortDescription === '' ? null : (
|
||||
<>
|
||||
<span className={styles.CollapsableHeaderSeparator}>{' ('}</span>
|
||||
<span className={styles.CollapsableHeaderTitle}>
|
||||
{shortDescription}
|
||||
</span>
|
||||
<span className={styles.CollapsableHeaderSeparator}>{') '}</span>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.CollapsableHeaderFiller} />
|
||||
<div className={styles.TimeBarContainer}>
|
||||
<div
|
||||
className={
|
||||
|
|
|
|||
|
|
@ -187,6 +187,7 @@ export type Element = {
|
|||
// Serialized version of ReactIOInfo
|
||||
export type SerializedIOInfo = {
|
||||
name: string,
|
||||
description: string,
|
||||
start: number,
|
||||
end: number,
|
||||
value: null | Promise<mixed>,
|
||||
|
|
|
|||
72
packages/shared/ReactIODescription.js
Normal file
72
packages/shared/ReactIODescription.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export function getIODescription(value: any): string {
|
||||
if (!__DEV__) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
switch (typeof value) {
|
||||
case 'object':
|
||||
// Test the object for a bunch of common property names that are useful identifiers.
|
||||
// While we only have the return value here, it should ideally be a name that
|
||||
// describes the arguments requested.
|
||||
if (value === null) {
|
||||
return '';
|
||||
} else if (value instanceof Error) {
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
return String(value.message);
|
||||
} else if (typeof value.url === 'string') {
|
||||
return value.url;
|
||||
} else if (typeof value.command === 'string') {
|
||||
return value.command;
|
||||
} else if (
|
||||
typeof value.request === 'object' &&
|
||||
typeof value.request.url === 'string'
|
||||
) {
|
||||
return value.request.url;
|
||||
} else if (
|
||||
typeof value.response === 'object' &&
|
||||
typeof value.response.url === 'string'
|
||||
) {
|
||||
return value.response.url;
|
||||
} else if (
|
||||
typeof value.id === 'string' ||
|
||||
typeof value.id === 'number' ||
|
||||
typeof value.id === 'bigint'
|
||||
) {
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
return String(value.id);
|
||||
} else if (typeof value.name === 'string') {
|
||||
return value.name;
|
||||
} else {
|
||||
const str = value.toString();
|
||||
if (str.startWith('[object ') || str.length < 5 || str.length > 500) {
|
||||
// This is probably not a useful description.
|
||||
return '';
|
||||
}
|
||||
return str;
|
||||
}
|
||||
case 'string':
|
||||
if (value.length < 5 || value.length > 500) {
|
||||
return '';
|
||||
}
|
||||
return value;
|
||||
case 'number':
|
||||
case 'bigint':
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
return String(value);
|
||||
default:
|
||||
// Not useful descriptors.
|
||||
return '';
|
||||
}
|
||||
} catch (x) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user