Skip to content

Commit 52912a1

Browse files
authored
[DevTools] Add ignore-listed stack frame disclosure (react#36828)
1 parent 3c3bcea commit 52912a1

5 files changed

Lines changed: 415 additions & 133 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,8 @@
143143
padding-left: 1.25rem;
144144
margin-top: 0.25rem;
145145
}
146+
147+
.SuspendedBySkeleton {
148+
padding: 0.25rem;
149+
padding-left: 1.25rem;
150+
}

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

Lines changed: 198 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@
99

1010
import {copy} from 'clipboard-js';
1111
import * as React from 'react';
12-
import {useState, useTransition} from 'react';
12+
import {use, useContext, useState, useTransition} from 'react';
1313
import Button from '../Button';
1414
import ButtonIcon from '../ButtonIcon';
1515
import KeyValue from './KeyValue';
1616
import {serializeDataForCopy, pluralize} from '../utils';
1717
import Store from '../../store';
1818
import styles from './InspectedElementSharedStyles.css';
1919
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
20-
import StackTraceView from './StackTraceView';
20+
import FetchFileWithCachingContext from './FetchFileWithCachingContext';
21+
import StackTraceView, {IgnoreListToggleButton} from './StackTraceView';
2122
import OwnerView from './OwnerView';
2223
import {meta} from '../../../hydration';
24+
import Skeleton from './Skeleton';
2325
import useInferredName from '../useInferredName';
26+
import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
2427

2528
import {getClassNameForEnvironment} from '../SuspenseTab/SuspenseEnvironmentColors.js';
2629

@@ -29,6 +32,8 @@ import type {
2932
SerializedAsyncInfo,
3033
} from 'react-devtools-shared/src/frontend/types';
3134
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
35+
import type {ReactStackTrace} from 'shared/ReactTypes';
36+
import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
3237

3338
import {
3439
UNKNOWN_SUSPENDERS_NONE,
@@ -94,6 +99,78 @@ function formatBytes(bytes: number) {
9499
return (bytes / 1_000_000_000).toFixed(1) + ' gB';
95100
}
96101

102+
type StackTraceGroupProps = {
103+
children: (showIgnoreList: boolean) => React.Node,
104+
ioStack: null | ReactStackTrace,
105+
asyncInfoStack: null | ReactStackTrace,
106+
};
107+
108+
function StackTraceGroup({
109+
children,
110+
ioStack,
111+
asyncInfoStack,
112+
}: StackTraceGroupProps): React.Node {
113+
const [showIgnoreList, setShowIgnoreList] = useState(false);
114+
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
115+
116+
const ioStackHasIgnoredFrames =
117+
ioStack !== null &&
118+
ioStack.some(callSite => {
119+
const [, virtualURL, virtualLine, virtualColumn] = callSite;
120+
121+
// symbolicated output is cached
122+
const symbolicatedCallSite: null | SourceMappedLocation =
123+
fetchFileWithCaching !== null
124+
? use(
125+
symbolicateSourceWithCache(
126+
fetchFileWithCaching,
127+
virtualURL,
128+
virtualLine,
129+
virtualColumn,
130+
),
131+
)
132+
: null;
133+
134+
return symbolicatedCallSite !== null && symbolicatedCallSite.ignored;
135+
});
136+
137+
const asyncInfoStackHasIgnoredFrames =
138+
asyncInfoStack !== null &&
139+
asyncInfoStack.some(callSite => {
140+
const [, virtualURL, virtualLine, virtualColumn] = callSite;
141+
142+
// symbolicated output is cached
143+
const symbolicatedCallSite: null | SourceMappedLocation =
144+
fetchFileWithCaching !== null
145+
? use(
146+
symbolicateSourceWithCache(
147+
fetchFileWithCaching,
148+
virtualURL,
149+
virtualLine,
150+
virtualColumn,
151+
),
152+
)
153+
: null;
154+
155+
return symbolicatedCallSite !== null && symbolicatedCallSite.ignored;
156+
});
157+
158+
const hasIgnoredFrames =
159+
ioStackHasIgnoredFrames || asyncInfoStackHasIgnoredFrames;
160+
161+
return (
162+
<>
163+
{children(showIgnoreList)}
164+
{hasIgnoredFrames && (
165+
<IgnoreListToggleButton
166+
onClick={() => setShowIgnoreList(prev => !prev)}
167+
showIgnoreList={showIgnoreList}
168+
/>
169+
)}
170+
</>
171+
);
172+
}
173+
97174
function SuspendedByRow({
98175
bridge,
99176
element,
@@ -203,102 +280,126 @@ function SuspendedByRow({
203280
</Button>
204281
{isOpen && (
205282
<div className={styles.CollapsableContent}>
206-
{showIOStack && (
207-
<StackTraceView
208-
stack={ioInfo.stack}
209-
environmentName={
210-
ioOwner !== null && ioOwner.env === ioInfo.env
211-
? null
212-
: ioInfo.env
213-
}
214-
/>
215-
)}
216-
{ioOwner !== null &&
217-
ioOwner.id !== inspectedElement.id &&
218-
(showIOStack ||
219-
!showAwaitStack ||
220-
asyncOwner === null ||
221-
ioOwner.id !== asyncOwner.id) ? (
222-
<OwnerView
223-
key={ioOwner.id}
224-
displayName={ioOwner.displayName || 'Anonymous'}
225-
environmentName={
226-
ioOwner.env === inspectedElement.env &&
227-
ioOwner.env === ioInfo.env
228-
? null
229-
: ioOwner.env
230-
}
231-
hocDisplayNames={ioOwner.hocDisplayNames}
232-
compiledWithForget={ioOwner.compiledWithForget}
233-
id={ioOwner.id}
234-
isInStore={store.containsElement(ioOwner.id)}
235-
type={ioOwner.type}
236-
/>
237-
) : null}
238-
{showAwaitStack ? (
239-
<>
240-
<div className={styles.SmallHeader}>awaited at:</div>
241-
{asyncInfo.stack !== null && asyncInfo.stack.length > 0 && (
242-
<StackTraceView
243-
stack={asyncInfo.stack}
244-
environmentName={
245-
asyncOwner !== null && asyncOwner.env === asyncInfo.env
246-
? null
247-
: asyncInfo.env
248-
}
249-
/>
283+
<React.Suspense
284+
fallback={
285+
<div className={styles.SuspendedBySkeleton}>
286+
<Skeleton height={16} width="40%" />
287+
</div>
288+
}>
289+
<StackTraceGroup
290+
ioStack={showIOStack ? ioInfo.stack : null}
291+
asyncInfoStack={showAwaitStack ? asyncInfo.stack : null}>
292+
{(showIgnoreList: boolean) => (
293+
<>
294+
{showIOStack && (
295+
<StackTraceView
296+
stack={ioInfo.stack}
297+
environmentName={
298+
ioOwner !== null && ioOwner.env === ioInfo.env
299+
? null
300+
: ioInfo.env
301+
}
302+
showIgnoreList={showIgnoreList}
303+
/>
304+
)}
305+
{ioOwner !== null &&
306+
ioOwner.id !== inspectedElement.id &&
307+
(showIOStack ||
308+
!showAwaitStack ||
309+
asyncOwner === null ||
310+
ioOwner.id !== asyncOwner.id) ? (
311+
<OwnerView
312+
key={ioOwner.id}
313+
displayName={ioOwner.displayName || 'Anonymous'}
314+
environmentName={
315+
ioOwner.env === inspectedElement.env &&
316+
ioOwner.env === ioInfo.env
317+
? null
318+
: ioOwner.env
319+
}
320+
hocDisplayNames={ioOwner.hocDisplayNames}
321+
compiledWithForget={ioOwner.compiledWithForget}
322+
id={ioOwner.id}
323+
isInStore={store.containsElement(ioOwner.id)}
324+
type={ioOwner.type}
325+
/>
326+
) : null}
327+
{showAwaitStack ? (
328+
<>
329+
<div className={styles.SmallHeader}>awaited at:</div>
330+
{asyncInfo.stack !== null &&
331+
asyncInfo.stack.length > 0 && (
332+
<StackTraceView
333+
stack={asyncInfo.stack}
334+
environmentName={
335+
asyncOwner !== null &&
336+
asyncOwner.env === asyncInfo.env
337+
? null
338+
: asyncInfo.env
339+
}
340+
showIgnoreList={showIgnoreList}
341+
/>
342+
)}
343+
{asyncOwner !== null &&
344+
asyncOwner.id !== inspectedElement.id ? (
345+
<OwnerView
346+
key={asyncOwner.id}
347+
displayName={asyncOwner.displayName || 'Anonymous'}
348+
environmentName={
349+
asyncOwner.env === inspectedElement.env &&
350+
asyncOwner.env === asyncInfo.env
351+
? null
352+
: asyncOwner.env
353+
}
354+
hocDisplayNames={asyncOwner.hocDisplayNames}
355+
compiledWithForget={asyncOwner.compiledWithForget}
356+
id={asyncOwner.id}
357+
isInStore={store.containsElement(asyncOwner.id)}
358+
type={asyncOwner.type}
359+
/>
360+
) : null}
361+
</>
362+
) : null}
363+
<div className={styles.PreviewContainer}>
364+
<KeyValue
365+
alphaSort={true}
366+
bridge={bridge}
367+
canDeletePaths={false}
368+
canEditValues={false}
369+
canRenamePaths={false}
370+
depth={1}
371+
element={element}
372+
hidden={false}
373+
inspectedElement={inspectedElement}
374+
name={
375+
isFulfilled
376+
? 'awaited value'
377+
: isRejected
378+
? 'rejected with'
379+
: 'pending value'
380+
}
381+
path={
382+
isFulfilled
383+
? [index, 'awaited', 'value', 'value']
384+
: isRejected
385+
? [index, 'awaited', 'value', 'reason']
386+
: [index, 'awaited', 'value']
387+
}
388+
pathRoot="suspendedBy"
389+
store={store}
390+
value={
391+
isFulfilled
392+
? value.value
393+
: isRejected
394+
? value.reason
395+
: value
396+
}
397+
/>
398+
</div>
399+
</>
250400
)}
251-
{asyncOwner !== null && asyncOwner.id !== inspectedElement.id ? (
252-
<OwnerView
253-
key={asyncOwner.id}
254-
displayName={asyncOwner.displayName || 'Anonymous'}
255-
environmentName={
256-
asyncOwner.env === inspectedElement.env &&
257-
asyncOwner.env === asyncInfo.env
258-
? null
259-
: asyncOwner.env
260-
}
261-
hocDisplayNames={asyncOwner.hocDisplayNames}
262-
compiledWithForget={asyncOwner.compiledWithForget}
263-
id={asyncOwner.id}
264-
isInStore={store.containsElement(asyncOwner.id)}
265-
type={asyncOwner.type}
266-
/>
267-
) : null}
268-
</>
269-
) : null}
270-
<div className={styles.PreviewContainer}>
271-
<KeyValue
272-
alphaSort={true}
273-
bridge={bridge}
274-
canDeletePaths={false}
275-
canEditValues={false}
276-
canRenamePaths={false}
277-
depth={1}
278-
element={element}
279-
hidden={false}
280-
inspectedElement={inspectedElement}
281-
name={
282-
isFulfilled
283-
? 'awaited value'
284-
: isRejected
285-
? 'rejected with'
286-
: 'pending value'
287-
}
288-
path={
289-
isFulfilled
290-
? [index, 'awaited', 'value', 'value']
291-
: isRejected
292-
? [index, 'awaited', 'value', 'reason']
293-
: [index, 'awaited', 'value']
294-
}
295-
pathRoot="suspendedBy"
296-
store={store}
297-
value={
298-
isFulfilled ? value.value : isRejected ? value.reason : value
299-
}
300-
/>
301-
</div>
401+
</StackTraceGroup>
402+
</React.Suspense>
302403
</div>
303404
)}
304405
</div>

0 commit comments

Comments
 (0)