64
ReactAsyncInfo,
65
ReactTimeInfo,
66
ReactStackTrace,
67
+ ReactCallSite,
68
ReactFunctionLocation,
69
ReactErrorInfo,
70
ReactErrorInfoDev,
165
);
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
+}
182
172
-function filterStackTrace(
183
+function findCalledFunctionNameFromStackTrace(
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;
207
}
186
- return clone;
208
}
209
+ return '';
210
+}
211
+
212
+function filterStackTrace(
213
+ request: Request,
214
+ stack: ReactStackTrace,
215
+): ReactStackTrace {
216
// Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
217
// to save bandwidth even in DEV. We'll also replay these stacks on the client so by
218
// stripping them early we avoid that overhead. Otherwise we'd normally just rely on
219
// the DevTools or framework's ignore lists to filter them out.
220
const filterStackFrame = request.filterStackFrame;
193
- const stack = parseStackTrace(error, skipFrames);
221
+ const filteredStack: ReactStackTrace = [];
222
for (let i = 0; i < stack.length; i++) {
223
const callsite = stack[i];
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);
232
}
233
}
214
- stackTraceCache.set(error, stack);
215
- return stack;
234
+ return filteredStack;
235
}
236
237
initAsyncDebugInfo();
259
// one stack frame but keeping it simple for now and include all frames.
260
const stack = filterStackTrace(
261
request,
243
- new Error('react-stack-top-frame'),
244
- 1,
262
+ parseStackTrace(new Error('react-stack-top-frame'), 1),
263
);
264
request.pendingChunks++;
265
const owner: null | ReactComponentInfo = resolveOwner();
1096
componentDebugInfo.stack =
1097
task.debugStack === null
1098
? null
1081
- : filterStackTrace(request, task.debugStack, 1);
1099
+ : filterStackTrace(request, parseStackTrace(task.debugStack, 1));
1100
// $FlowFixMe[cannot-write]
1101
componentDebugInfo.debugStack = task.debugStack;
1102
// $FlowFixMe[cannot-write]
1297
componentDebugInfo.stack =
1298
task.debugStack === null
1299
? null
1282
- : filterStackTrace(request, task.debugStack, 1);
1300
+ : filterStackTrace(request, parseStackTrace(task.debugStack, 1));
1301
// $FlowFixMe[cannot-write]
1302
componentDebugInfo.props = props;
1303
// $FlowFixMe[cannot-write]
1633
task.debugOwner,
1634
task.debugStack === null
1635
? null
1618
- : filterStackTrace(request, task.debugStack, 1),
1636
+ : filterStackTrace(request, parseStackTrace(task.debugStack, 1)),
1637
validated,
1638
]
1639
: [REACT_ELEMENT_TYPE, type, key, props];
1766
stack:
1767
task.debugStack === null
1768
? null
1751
- : filterStackTrace(request, task.debugStack, 1),
1769
+ : filterStackTrace(request, parseStackTrace(task.debugStack, 1)),
1770
props: props,
1771
debugStack: task.debugStack,
1772
debugTask: task.debugTask,
1895
// We don't log it yet though. We return it to be logged by the point where it's awaited.
1896
// The ioNode might be another PromiseNode in the case where none of the AwaitNode had
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
+ ) {
1902
// Typically we assume that the outer most Promise that was awaited in user space has the
1903
// most actionable stack trace for the start of the operation. However, if this Promise
1904
// was created inside only third party code, then try to use the inner node instead.
1919
if (awaited !== null) {
1920
const ioNode = visitAsyncNode(request, task, awaited, cutOff, visited);
1921
if (ioNode !== null) {
1901
- const stack = filterStackTrace(request, node.stack, 1);
1922
+ const stack = filterStackTrace(
1923
+ request,
1924
+ parseStackTrace(node.stack, 1),
1925
+ );
1926
if (stack.length === 0) {
1927
// If this await was fully filtered out, then it was inside third party code
1928
// such as in an external library. We return the I/O node and try another await.
3296
try {
3297
// eslint-disable-next-line react-internal/safe-string-coercion
3298
reason = String(postponeInstance.message);
3275
- stack = filterStackTrace(request, postponeInstance, 0);
3299
+ stack = filterStackTrace(request, parseStackTrace(postponeInstance, 0));
3300
} catch (x) {
3301
stack = [];
3302
}
3319
name = error.name;
3320
// eslint-disable-next-line react-internal/safe-string-coercion
3321
message = String(error.message);
3298
- stack = filterStackTrace(request, error, 0);
3322
+ stack = filterStackTrace(request, parseStackTrace(error, 0));
3323
const errorEnv = (error: any).environmentName;
3324
if (typeof errorEnv === 'string') {
3325
// This probably came from another FlightClient as a pass through.
3358
name = error.name;
3359
// eslint-disable-next-line react-internal/safe-string-coercion
3360
message = String(error.message);
3337
- stack = filterStackTrace(request, error, 0);
3361
+ stack = filterStackTrace(request, parseStackTrace(error, 0));
3362
const errorEnv = (error: any).environmentName;
3363
if (typeof errorEnv === 'string') {
3364
// This probably came from another FlightClient as a pass through.
3520
function emitIOInfoChunk(
3521
request: Request,
3522
id: number,
3523
+ name: string,
3524
start: number,
3525
end: number,
3526
stack: ?ReactStackTrace,
3557
const relativeStartTimestamp = start - request.timeOrigin;
3558
const relativeEndTimestamp = end - request.timeOrigin;
3559
const debugIOInfo: Omit<ReactIOInfo, 'debugTask' | 'debugStack'> = {
3560
+ name: name,
3561
start: relativeStartTimestamp,
3562
end: relativeEndTimestamp,
3563
stack: stack,
3577
// We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
3578
request.pendingChunks++;
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
+ );
3588
request.writtenObjects.set(ioInfo, serializeByValueID(id));
3589
}
3590
3599
}
3600
3601
let stack = null;
3602
+ let name = '';
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
+ }
3614
}
3615
+
3616
request.pendingChunks++;
3617
const id = request.nextChunkId++;
3574
- emitIOInfoChunk(request, id, ioNode.start, ioNode.end, stack);
3618
+ emitIOInfoChunk(request, id, name, ioNode.start, ioNode.end, stack);
3619
const ref = serializeByValueID(id);
3620
request.writtenObjects.set(ioNode, ref);
3621
return ref;
3756
let debugStack: null | ReactStackTrace = null;
3757
if (element._debugStack != null) {
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
+ );
3763
doNotLimit.add(debugStack);
3764
for (let i = 0; i < debugStack.length; i++) {
3765
doNotLimit.add(debugStack[i]);