[DevTools] Add ignore-listed stack frame disclosure (#36828)
Jiwon Choi committed
Jun 25, 2026 at 15:57 UTC
52912a14531dad54d7844c83bc0e35f2c6a74c11
5 files changed
+415
-133
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
+5
@@ -143,3 +143,8 @@
143
padding-left: 1.25rem;
144
margin-top: 0.25rem;
145
}
146
+
147
+.SuspendedBySkeleton {
148
+ padding: 0.25rem;
149
+ padding-left: 1.25rem;
150
+}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
+198
-97
@@ -9,7 +9,7 @@
9
10
import {copy} from 'clipboard-js';
11
import * as React from 'react';
12
-import {useState, useTransition} from 'react';
12
+import {use, useContext, useState, useTransition} from 'react';
13
import Button from '../Button';
14
import ButtonIcon from '../ButtonIcon';
15
import KeyValue from './KeyValue';
@@ -17,10 +17,13 @@ import {serializeDataForCopy, pluralize} from '../utils';
17
import Store from '../../store';
18
import styles from './InspectedElementSharedStyles.css';
19
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';
22
import OwnerView from './OwnerView';
23
import {meta} from '../../../hydration';
24
+import Skeleton from './Skeleton';
25
import useInferredName from '../useInferredName';
26
+import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
27
28
import {getClassNameForEnvironment} from '../SuspenseTab/SuspenseEnvironmentColors.js';
29
@@ -29,6 +32,8 @@ import type {
32
SerializedAsyncInfo,
33
} from 'react-devtools-shared/src/frontend/types';
34
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';
37
38
import {
39
UNKNOWN_SUSPENDERS_NONE,
@@ -94,6 +99,78 @@ function formatBytes(bytes: number) {
99
return (bytes / 1_000_000_000).toFixed(1) + ' gB';
100
}
101
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
+
174
function SuspendedByRow({
175
bridge,
176
element,
@@ -203,102 +280,126 @@ function SuspendedByRow({
280
</Button>
281
{isOpen && (
282
<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
+ </>
400
)}
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>
403
</div>
404
)}
405
</div>
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+125
-28
@@ -8,7 +8,7 @@
8
*/
9
10
import * as React from 'react';
11
-import {Fragment, useContext} from 'react';
11
+import {Fragment, useContext, useState, use} from 'react';
12
import {BridgeContext, StoreContext} from '../context';
13
import InspectedElementBadges from './InspectedElementBadges';
14
import InspectedElementContextTree from './InspectedElementContextTree';
@@ -19,15 +19,17 @@ import InspectedElementStateTree from './InspectedElementStateTree';
19
import InspectedElementStyleXPlugin from './InspectedElementStyleXPlugin';
20
import InspectedElementSuspendedBy from './InspectedElementSuspendedBy';
21
import NativeStyleEditor from './NativeStyleEditor';
22
+import FetchFileWithCachingContext from './FetchFileWithCachingContext';
23
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
24
import InspectedElementSourcePanel from './InspectedElementSourcePanel';
24
-import StackTraceView from './StackTraceView';
25
+import StackTraceView, {IgnoreListToggleButton} from './StackTraceView';
26
import OwnerView from './OwnerView';
27
import Skeleton from './Skeleton';
28
import {
29
ElementTypeSuspense,
30
ElementTypeActivity,
31
} from 'react-devtools-shared/src/frontend/types';
32
+import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
33
34
import styles from './InspectedElementView.css';
35
@@ -39,6 +41,83 @@ import type {HookNames} from 'react-devtools-shared/src/frontend/types';
41
import type {ToggleParseHookNames} from './InspectedElementContext';
42
import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
43
44
+type StackTraceGroupProps = {
45
+ children: (showIgnoreList: boolean) => React.Node,
46
+ componentStack: InspectedElement['stack'],
47
+ owners: InspectedElement['owners'],
48
+};
49
+
50
+function StackTraceGroup({
51
+ children,
52
+ componentStack,
53
+ owners,
54
+}: StackTraceGroupProps): React.Node {
55
+ const [showIgnoreList, setShowIgnoreList] = useState(false);
56
+ const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
57
+
58
+ const componentStackHasIgnoredFrames =
59
+ componentStack !== null &&
60
+ componentStack.some(callSite => {
61
+ const [, virtualURL, virtualLine, virtualColumn] = callSite;
62
+
63
+ // symbolicated output is cached
64
+ const symbolicatedCallSite: null | SourceMappedLocation =
65
+ fetchFileWithCaching !== null
66
+ ? use(
67
+ symbolicateSourceWithCache(
68
+ fetchFileWithCaching,
69
+ virtualURL,
70
+ virtualLine,
71
+ virtualColumn,
72
+ ),
73
+ )
74
+ : null;
75
+
76
+ return symbolicatedCallSite !== null && symbolicatedCallSite.ignored;
77
+ });
78
+
79
+ const ownerStacksHaveIgnoredFrames =
80
+ owners !== null &&
81
+ owners.some(owner => {
82
+ return (
83
+ owner.stack !== null &&
84
+ owner.stack.some(callSite => {
85
+ const [, virtualURL, virtualLine, virtualColumn] = callSite;
86
+
87
+ // symbolicated output is cached
88
+ const symbolicatedCallSite: null | SourceMappedLocation =
89
+ fetchFileWithCaching !== null
90
+ ? use(
91
+ symbolicateSourceWithCache(
92
+ fetchFileWithCaching,
93
+ virtualURL,
94
+ virtualLine,
95
+ virtualColumn,
96
+ ),
97
+ )
98
+ : null;
99
+
100
+ return symbolicatedCallSite !== null && symbolicatedCallSite.ignored;
101
+ })
102
+ );
103
+ });
104
+
105
+ const hasIgnoredFrames =
106
+ componentStackHasIgnoredFrames || ownerStacksHaveIgnoredFrames;
107
+
108
+ return (
109
+ <>
110
+ {children(showIgnoreList)}
111
+ {hasIgnoredFrames && (
112
+ <IgnoreListToggleButton
113
+ onClick={() => setShowIgnoreList(prev => !prev)}
114
+ showIgnoreList={showIgnoreList}
115
+ />
116
+ )}
117
+ </>
118
+ );
119
+}
120
+
121
type Props = {
122
element: Element,
123
hookNames: HookNames | null,
@@ -189,33 +268,51 @@ export default function InspectedElementView({
268
<Skeleton height={16} width="40%" />
269
</div>
270
}>
192
- {showStack ? <StackTraceView stack={stack} /> : null}
193
- {showOwnersList &&
194
- owners?.map(owner => (
195
- <Fragment key={owner.id}>
196
- <OwnerView
197
- displayName={owner.displayName || 'Anonymous'}
198
- hocDisplayNames={owner.hocDisplayNames}
199
- environmentName={
200
- inspectedElement.env === owner.env ? null : owner.env
201
- }
202
- compiledWithForget={owner.compiledWithForget}
203
- id={owner.id}
204
- isInStore={store.containsElement(owner.id)}
205
- type={owner.type}
206
- />
207
- {owner.stack != null && owner.stack.length > 0 ? (
208
- <StackTraceView stack={owner.stack} />
271
+ <StackTraceGroup componentStack={stack} owners={owners}>
272
+ {(showIgnoreList: boolean) => (
273
+ <>
274
+ {showStack ? (
275
+ <StackTraceView
276
+ stack={stack}
277
+ showIgnoreList={showIgnoreList}
278
+ />
279
) : null}
210
- </Fragment>
211
- ))}
212
-
213
- {rootType !== null && (
214
- <div className={styles.OwnersMetaField}>{rootType}</div>
215
- )}
216
- {rendererLabel !== null && (
217
- <div className={styles.OwnersMetaField}>{rendererLabel}</div>
218
- )}
280
+ {showOwnersList &&
281
+ owners?.map(owner => (
282
+ <Fragment key={owner.id}>
283
+ <OwnerView
284
+ displayName={owner.displayName || 'Anonymous'}
285
+ hocDisplayNames={owner.hocDisplayNames}
286
+ environmentName={
287
+ inspectedElement.env === owner.env
288
+ ? null
289
+ : owner.env
290
+ }
291
+ compiledWithForget={owner.compiledWithForget}
292
+ id={owner.id}
293
+ isInStore={store.containsElement(owner.id)}
294
+ type={owner.type}
295
+ />
296
+ {owner.stack != null && owner.stack.length > 0 ? (
297
+ <StackTraceView
298
+ stack={owner.stack}
299
+ showIgnoreList={showIgnoreList}
300
+ />
301
+ ) : null}
302
+ </Fragment>
303
+ ))}
304
+
305
+ {rootType !== null && (
306
+ <div className={styles.OwnersMetaField}>{rootType}</div>
307
+ )}
308
+ {rendererLabel !== null && (
309
+ <div className={styles.OwnersMetaField}>
310
+ {rendererLabel}
311
+ </div>
312
+ )}
313
+ </>
314
+ )}
315
+ </StackTraceGroup>
316
</React.Suspense>
317
</div>
318
)}
packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.css
+19
@@ -8,10 +8,18 @@
8
white-space-collapse: preserve;
9
}
10
11
+.IgnoredCallSite {
12
+ opacity: 0.5;
13
+}
14
+
15
.IgnoredCallSite, .BuiltInCallSite {
16
display: none;
17
}
18
19
+.IgnoredCallSite.CallSite {
20
+ display: flex;
21
+}
22
+
23
.CallSite + .BuiltInCallSite {
24
display: block;
25
}
@@ -33,3 +41,14 @@
41
margin-left: 0.25rem;
42
}
43
44
+.IgnoreListToggleContainer {
45
+ padding-left: 1rem;
46
+}
47
+
48
+.IgnoreListToggleButton {
49
+ background: none;
50
+ color: var(--color-link);
51
+ font: inherit;
52
+ font-style: italic;
53
+ text-decoration: underline;
54
+}
packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js
+68
-8
@@ -10,6 +10,7 @@
10
import * as React from 'react';
11
import {use, useContext} from 'react';
12
13
+import Button from '../Button';
14
import useOpenResource from '../useOpenResource';
15
16
import ElementBadges from './ElementBadges';
@@ -29,11 +30,13 @@ import formatLocationForDisplay from './formatLocationForDisplay';
30
type CallSiteViewProps = {
31
callSite: ReactCallSite,
32
environmentName: null | string,
33
+ showIgnoreList: boolean,
34
};
35
36
export function CallSiteView({
37
callSite,
38
environmentName,
39
+ showIgnoreList,
40
}: CallSiteViewProps): React.Node {
41
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
42
@@ -60,16 +63,14 @@ export function CallSiteView({
63
symbolicatedCallSite !== null ? symbolicatedCallSite.location : callSite;
64
const ignored =
65
symbolicatedCallSite !== null ? symbolicatedCallSite.ignored : false;
63
- // TODO: Make an option to be able to toggle the display of ignore listed rows.
64
- // Ideally this UI should be higher than a single Stack Trace so that there's not
65
- // multiple buttons in a single inspection taking up space.
66
67
const isBuiltIn = url === '' || url.startsWith('<anonymous>'); // This looks like a fake anonymous through eval.
68
return (
69
<div
70
className={
71
ignored
72
- ? styles.IgnoredCallSite
72
+ ? styles.IgnoredCallSite +
73
+ (showIgnoreList ? ' ' + styles.CallSite : '')
74
: isBuiltIn
75
? styles.BuiltInCallSite
76
: styles.CallSite
@@ -94,15 +95,75 @@ export function CallSiteView({
95
);
96
}
97
98
+type IgnoreListToggleButtonProps = {
99
+ onClick: () => void,
100
+ showIgnoreList: boolean,
101
+};
102
+
103
+export function IgnoreListToggleButton({
104
+ onClick,
105
+ showIgnoreList,
106
+}: IgnoreListToggleButtonProps): React.Node {
107
+ const label = showIgnoreList
108
+ ? 'Hide ignore-listed frames'
109
+ : 'Show ignore-listed frames';
110
+
111
+ return (
112
+ <div className={styles.IgnoreListToggleContainer}>
113
+ <Button
114
+ className={styles.IgnoreListToggleButton}
115
+ onClick={onClick}
116
+ title={label}>
117
+ {label}
118
+ </Button>
119
+ </div>
120
+ );
121
+}
122
+
123
type Props = {
124
stack: ReactStackTrace,
125
environmentName: null | string,
126
+ showIgnoreList: boolean,
127
};
128
129
export default function StackTraceView({
130
stack,
131
environmentName,
132
+ showIgnoreList,
133
}: Props): React.Node {
134
+ const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
135
+
136
+ let lastMeaningfulFrameIndex = -1;
137
+ // Reverse loop to find the last non-ignored, non-built-in index
138
+ for (let index = stack.length - 1; index >= 0; index--) {
139
+ const callSite = stack[index];
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
+ const [, url] =
155
+ symbolicatedCallSite !== null ? symbolicatedCallSite.location : callSite;
156
+ const ignored =
157
+ symbolicatedCallSite !== null ? symbolicatedCallSite.ignored : false;
158
+ // This looks like a fake anonymous through eval.
159
+ const isBuiltIn = url === '' || url.startsWith('<anonymous>');
160
+
161
+ if (!ignored && !isBuiltIn) {
162
+ lastMeaningfulFrameIndex = index;
163
+ break;
164
+ }
165
+ }
166
+
167
return (
168
<div className={styles.StackTraceView}>
169
{stack.map((callSite, index) => (
@@ -110,11 +171,10 @@ export default function StackTraceView({
171
key={index}
172
callSite={callSite}
173
environmentName={
113
- // Badge last row
114
- // TODO: If we start ignore listing the last row, we should badge the last
115
- // non-ignored row.
116
- index === stack.length - 1 ? environmentName : null
174
+ // Badge last meaningful frame (non-ignored, non-built-in)
175
+ index === lastMeaningfulFrameIndex ? environmentName : null
176
}
177
+ showIgnoreList={showIgnoreList}
178
/>
179
))}
180
</div>