@@ -64,6 +64,7 @@ import type {
64
64
ReactAsyncInfo ,
65
65
ReactTimeInfo ,
66
66
ReactStackTrace ,
67
+ ReactCallSite ,
67
68
ReactFunctionLocation ,
68
69
ReactErrorInfo ,
69
70
ReactErrorInfoDev ,
@@ -164,55 +165,73 @@ function defaultFilterStackFrame(
164
165
) ;
165
166
}
166
167
167
- // DEV-only cache of parsed and filtered stack frames.
168
- const stackTraceCache: WeakMap< Error , ReactStackTrace > = __DEV__
169
- ? new WeakMap()
170
- : (null: any);
168
+ function devirtualizeURL(url: string): string {
169
+ if ( url . startsWith ( 'rsc://React/' ) ) {
170
+ // This callsite is a virtual fake callsite that came from another Flight client.
171
+ // We need to reverse it back into the original location by stripping its prefix
172
+ // and suffix. We don't need the environment name because it's available on the
173
+ // parent object that will contain the stack.
174
+ const envIdx = url . indexOf ( '/' , 12 ) ;
175
+ const suffixIdx = url . lastIndexOf ( '?' ) ;
176
+ if ( envIdx > - 1 && suffixIdx > - 1 ) {
177
+ return url . slice ( envIdx + 1 , suffixIdx ) ;
178
+ }
179
+ }
180
+ return url ;
181
+ }
171
182
172
- function filterStackTrace (
183
+ function findCalledFunctionNameFromStackTrace (
173
184
request : Request ,
174
- error: Error,
175
- skipFrames: number,
176
- ): ReactStackTrace {
177
- const existing = stackTraceCache . get ( error ) ;
178
- if ( existing !== undefined ) {
179
- // Return a clone because the Flight protocol isn't yet resilient to deduping
180
- // objects in the debug info. TODO: Support deduping stacks.
181
- const clone = existing . slice ( 0 ) ;
182
- for ( let i = 0 ; i < clone . length ; i ++ ) {
183
- // $FlowFixMe[invalid-tuple-arity]
184
- clone [ i ] = clone [ i ] . slice ( 0 ) ;
185
+ stack : ReactStackTrace ,
186
+ ) : string {
187
+ // Gets the name of the first function called from first party code.
188
+ let bestMatch = '' ;
189
+ const filterStackFrame = request . filterStackFrame ;
190
+ for ( let i = 0 ; i < stack . length ; i ++ ) {
191
+ const callsite = stack [ i ] ;
192
+ const functionName = callsite [ 0 ] ;
193
+ const url = devirtualizeURL ( callsite [ 1 ] ) ;
194
+ if ( filterStackFrame ( url , functionName ) ) {
195
+ if ( bestMatch === '' ) {
196
+ // If we had no good stack frames for internal calls, just use the last
197
+ // first party function name.
198
+ return functionName ;
199
+ }
200
+ return bestMatch ;
201
+ } else if ( functionName === 'new Promise' ) {
202
+ // Ignore Promise constructors.
203
+ } else if ( url === 'node:internal/async_hooks' ) {
204
+ // Ignore the stack frames from the async hooks themselves.
205
+ } else {
206
+ bestMatch = functionName ;
185
207
}
186
- return clone ;
187
208
}
209
+ return '' ;
210
+ }
211
+
212
+ function filterStackTrace (
213
+ request : Request ,
214
+ stack : ReactStackTrace ,
215
+ ) : ReactStackTrace {
188
216
// Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
189
217
// to save bandwidth even in DEV. We'll also replay these stacks on the client so by
190
218
// stripping them early we avoid that overhead. Otherwise we'd normally just rely on
191
219
// the DevTools or framework's ignore lists to filter them out.
192
220
const filterStackFrame = request . filterStackFrame ;
193
- const stack = parseStackTrace(error, skipFrames) ;
221
+ const filteredStack : ReactStackTrace = [ ] ;
194
222
for ( let i = 0 ; i < stack . length ; i ++ ) {
195
223
const callsite = stack [ i ] ;
196
224
const functionName = callsite [ 0 ] ;
197
- let url = callsite [ 1 ] ;
198
- if ( url . startsWith ( 'rsc://React/' ) ) {
199
- // This callsite is a virtual fake callsite that came from another Flight client.
200
- // We need to reverse it back into the original location by stripping its prefix
201
- // and suffix. We don't need the environment name because it's available on the
202
- // parent object that will contain the stack.
203
- const envIdx = url . indexOf ( '/' , 12 ) ;
204
- const suffixIdx = url . lastIndexOf ( '?' ) ;
205
- if ( envIdx > - 1 && suffixIdx > - 1 ) {
206
- url = callsite [ 1 ] = url . slice ( envIdx + 1 , suffixIdx ) ;
207
- }
208
- }
209
- if ( ! filterStackFrame ( url , functionName ) ) {
210
- stack . splice ( i , 1 ) ;
211
- i -- ;
225
+ const url = devirtualizeURL ( callsite [ 1 ] ) ;
226
+ if ( filterStackFrame ( url , functionName ) ) {
227
+ // Use a clone because the Flight protocol isn't yet resilient to deduping
228
+ // objects in the debug info. TODO: Support deduping stacks.
229
+ const clone : ReactCallSite = ( callsite . slice ( 0 ) : any ) ;
230
+ clone [ 1 ] = url ;
231
+ filteredStack . push ( clone ) ;
212
232
}
213
233
}
214
- stackTraceCache . set ( error , stack ) ;
215
- return stack ;
234
+ return filteredStack ;
216
235
}
217
236
218
237
initAsyncDebugInfo ( ) ;
@@ -240,8 +259,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
240
259
// one stack frame but keeping it simple for now and include all frames.
241
260
const stack = filterStackTrace (
242
261
request ,
243
- new Error ( 'react-stack-top-frame' ) ,
244
- 1 ,
262
+ parseStackTrace ( new Error ( 'react-stack-top-frame' ) , 1 ) ,
245
263
) ;
246
264
request . pendingChunks ++ ;
247
265
const owner : null | ReactComponentInfo = resolveOwner ( ) ;
@@ -1078,7 +1096,7 @@ function callWithDebugContextInDEV<A, T>(
1078
1096
componentDebugInfo . stack =
1079
1097
task . debugStack === null
1080
1098
? null
1081
- : filterStackTrace ( request , task . debugStack , 1 ) ;
1099
+ : filterStackTrace ( request , parseStackTrace ( task . debugStack , 1 ) ) ;
1082
1100
// $FlowFixMe[cannot-write]
1083
1101
componentDebugInfo . debugStack = task . debugStack ;
1084
1102
// $FlowFixMe[cannot-write]
@@ -1279,7 +1297,7 @@ function renderFunctionComponent<Props>(
1279
1297
componentDebugInfo . stack =
1280
1298
task . debugStack === null
1281
1299
? null
1282
- : filterStackTrace ( request , task . debugStack , 1 ) ;
1300
+ : filterStackTrace ( request , parseStackTrace ( task . debugStack , 1 ) ) ;
1283
1301
// $FlowFixMe[cannot-write]
1284
1302
componentDebugInfo . props = props ;
1285
1303
// $FlowFixMe[cannot-write]
@@ -1615,7 +1633,7 @@ function renderClientElement(
1615
1633
task . debugOwner ,
1616
1634
task . debugStack === null
1617
1635
? null
1618
- : filterStackTrace ( request , task . debugStack , 1 ) ,
1636
+ : filterStackTrace ( request , parseStackTrace ( task . debugStack , 1 ) ) ,
1619
1637
validated ,
1620
1638
]
1621
1639
: [ REACT_ELEMENT_TYPE , type , key , props ] ;
@@ -1748,7 +1766,7 @@ function renderElement(
1748
1766
stack :
1749
1767
task . debugStack === null
1750
1768
? null
1751
- : filterStackTrace ( request , task . debugStack , 1 ) ,
1769
+ : filterStackTrace ( request , parseStackTrace ( task . debugStack , 1 ) ) ,
1752
1770
props : props ,
1753
1771
debugStack : task . debugStack ,
1754
1772
debugTask : task . debugTask ,
@@ -1877,7 +1895,10 @@ function visitAsyncNode(
1877
1895
// We don't log it yet though. We return it to be logged by the point where it's awaited.
1878
1896
// The ioNode might be another PromiseNode in the case where none of the AwaitNode had
1879
1897
// unfiltered stacks.
1880
- if ( filterStackTrace ( request , node . stack , 1 ) . length === 0 ) {
1898
+ if (
1899
+ filterStackTrace ( request , parseStackTrace ( node . stack , 1 ) ) . length ===
1900
+ 0
1901
+ ) {
1881
1902
// Typically we assume that the outer most Promise that was awaited in user space has the
1882
1903
// most actionable stack trace for the start of the operation. However, if this Promise
1883
1904
// was created inside only third party code, then try to use the inner node instead.
@@ -1898,7 +1919,10 @@ function visitAsyncNode(
1898
1919
if ( awaited !== null ) {
1899
1920
const ioNode = visitAsyncNode ( request , task , awaited , cutOff , visited ) ;
1900
1921
if ( ioNode !== null ) {
1901
- const stack = filterStackTrace ( request , node . stack , 1 ) ;
1922
+ const stack = filterStackTrace (
1923
+ request ,
1924
+ parseStackTrace ( node . stack , 1 ) ,
1925
+ ) ;
1902
1926
if ( stack . length === 0 ) {
1903
1927
// If this await was fully filtered out, then it was inside third party code
1904
1928
// such as in an external library. We return the I/O node and try another await.
@@ -3272,7 +3296,7 @@ function emitPostponeChunk(
3272
3296
try {
3273
3297
// eslint-disable-next-line react-internal/safe-string-coercion
3274
3298
reason = String ( postponeInstance . message ) ;
3275
- stack = filterStackTrace ( request , postponeInstance , 0 ) ;
3299
+ stack = filterStackTrace ( request , parseStackTrace ( postponeInstance , 0 ) ) ;
3276
3300
} catch ( x ) {
3277
3301
stack = [ ] ;
3278
3302
}
@@ -3295,7 +3319,7 @@ function serializeErrorValue(request: Request, error: Error): string {
3295
3319
name = error . name ;
3296
3320
// eslint-disable-next-line react-internal/safe-string-coercion
3297
3321
message = String ( error . message ) ;
3298
- stack = filterStackTrace ( request , error , 0 ) ;
3322
+ stack = filterStackTrace ( request , parseStackTrace ( error , 0 ) ) ;
3299
3323
const errorEnv = ( error : any ) . environmentName ;
3300
3324
if ( typeof errorEnv === 'string' ) {
3301
3325
// This probably came from another FlightClient as a pass through.
@@ -3334,7 +3358,7 @@ function emitErrorChunk(
3334
3358
name = error . name ;
3335
3359
// eslint-disable-next-line react-internal/safe-string-coercion
3336
3360
message = String ( error . message ) ;
3337
- stack = filterStackTrace ( request , error , 0 ) ;
3361
+ stack = filterStackTrace ( request , parseStackTrace ( error , 0 ) ) ;
3338
3362
const errorEnv = ( error : any ) . environmentName ;
3339
3363
if ( typeof errorEnv === 'string' ) {
3340
3364
// This probably came from another FlightClient as a pass through.
@@ -3496,6 +3520,7 @@ function outlineComponentInfo(
3496
3520
function emitIOInfoChunk (
3497
3521
request : Request ,
3498
3522
id : number ,
3523
+ name : string ,
3499
3524
start : number ,
3500
3525
end : number ,
3501
3526
stack : ?ReactStackTrace ,
@@ -3532,6 +3557,7 @@ function emitIOInfoChunk(
3532
3557
const relativeStartTimestamp = start - request . timeOrigin ;
3533
3558
const relativeEndTimestamp = end - request . timeOrigin ;
3534
3559
const debugIOInfo : Omit < ReactIOInfo , 'debugTask' | 'debugStack' > = {
3560
+ name : name ,
3535
3561
start : relativeStartTimestamp ,
3536
3562
end : relativeEndTimestamp ,
3537
3563
stack : stack ,
@@ -3551,7 +3577,14 @@ function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void {
3551
3577
// We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
3552
3578
request . pendingChunks ++ ;
3553
3579
const id = request . nextChunkId ++ ;
3554
- emitIOInfoChunk ( request , id , ioInfo . start , ioInfo . end , ioInfo . stack ) ;
3580
+ emitIOInfoChunk (
3581
+ request ,
3582
+ id ,
3583
+ ioInfo . name ,
3584
+ ioInfo . start ,
3585
+ ioInfo . end ,
3586
+ ioInfo . stack ,
3587
+ ) ;
3555
3588
request . writtenObjects . set ( ioInfo , serializeByValueID ( id ) ) ;
3556
3589
}
3557
3590
@@ -3566,12 +3599,23 @@ function serializeIONode(
3566
3599
}
3567
3600
3568
3601
let stack = null ;
3602
+ let name = '';
3569
3603
if ( ioNode . stack !== null ) {
3570
- stack = filterStackTrace ( request , ioNode . stack , 1 ) ;
3604
+ const fullStack = parseStackTrace ( ioNode . stack , 1 ) ;
3605
+ stack = filterStackTrace ( request , fullStack ) ;
3606
+ name = findCalledFunctionNameFromStackTrace ( request , fullStack ) ;
3607
+ // The name can include the object that this was called on but sometimes that's
3608
+ // just unnecessary context.
3609
+ if ( name . startsWith ( 'Window.' ) ) {
3610
+ name = name . slice ( 7 ) ;
3611
+ } else if ( name . startsWith ( '<anonymous>.' ) ) {
3612
+ name = name . slice ( 7 ) ;
3613
+ }
3571
3614
}
3615
+
3572
3616
request . pendingChunks ++ ;
3573
3617
const id = request . nextChunkId ++ ;
3574
- emitIOInfoChunk ( request , id , ioNode . start , ioNode . end , stack ) ;
3618
+ emitIOInfoChunk ( request , id , name , ioNode . start , ioNode . end , stack ) ;
3575
3619
const ref = serializeByValueID ( id ) ;
3576
3620
request . writtenObjects . set ( ioNode , ref ) ;
3577
3621
return ref ;
@@ -3712,7 +3756,10 @@ function renderConsoleValue(
3712
3756
let debugStack : null | ReactStackTrace = null ;
3713
3757
if ( element . _debugStack != null ) {
3714
3758
// Outline the debug stack so that it doesn't get cut off.
3715
- debugStack = filterStackTrace ( request , element . _debugStack , 1 ) ;
3759
+ debugStack = filterStackTrace (
3760
+ request ,
3761
+ parseStackTrace ( element . _debugStack , 1 ) ,
3762
+ ) ;
3716
3763
doNotLimit . add ( debugStack ) ;
3717
3764
for ( let i = 0 ; i < debugStack . length ; i ++ ) {
3718
3765
doNotLimit . add ( debugStack [ i ] ) ;
0 commit comments