Skip to content

Commit bc730ac

Browse files
committed
Encode the transfer of stack traces as a structured form
1 parent 1794201 commit bc730ac

File tree

5 files changed

+148
-65
lines changed

5 files changed

+148
-65
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 73 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
ReactDebugInfo,
1313
ReactComponentInfo,
1414
ReactAsyncInfo,
15+
ReactStackTrace,
1516
} from 'shared/ReactTypes';
1617
import type {LazyComponent} from 'react/src/ReactLazy';
1718

@@ -624,7 +625,7 @@ function createElement(
624625
key: mixed,
625626
props: mixed,
626627
owner: null | ReactComponentInfo, // DEV-only
627-
stack: null | string, // DEV-only
628+
stack: null | ReactStackTrace, // DEV-only
628629
validated: number, // DEV-only
629630
):
630631
| React$Element<any>
@@ -1738,6 +1739,27 @@ function stopStream(
17381739
controller.close(row === '' ? '"$undefined"' : row);
17391740
}
17401741

1742+
function formatV8Stack(
1743+
errorName: string,
1744+
errorMessage: string,
1745+
stack: null | ReactStackTrace,
1746+
): string {
1747+
let v8StyleStack = errorName + ': ' + errorMessage;
1748+
if (stack) {
1749+
for (let i = 0; i < stack.length; i++) {
1750+
const frame = stack[i];
1751+
const [name, filename, line, col] = frame;
1752+
if (!name) {
1753+
v8StyleStack += '\n at ' + filename + ':' + line + ':' + col;
1754+
} else {
1755+
v8StyleStack +=
1756+
'\n at ' + name + ' (' + filename + ':' + line + ':' + col + ')';
1757+
}
1758+
}
1759+
}
1760+
return v8StyleStack;
1761+
}
1762+
17411763
type ErrorWithDigest = Error & {digest?: string};
17421764
function resolveErrorProd(
17431765
response: Response,
@@ -1773,7 +1795,7 @@ function resolveErrorDev(
17731795
id: number,
17741796
digest: string,
17751797
message: string,
1776-
stack: string,
1798+
stack: ReactStackTrace,
17771799
env: string,
17781800
): void {
17791801
if (!__DEV__) {
@@ -1793,7 +1815,8 @@ function resolveErrorDev(
17931815
message ||
17941816
'An error occurred in the Server Components render but no message was provided',
17951817
);
1796-
error.stack = stack;
1818+
// For backwards compat we use the V8 formatting when the flag is off.
1819+
error.stack = formatV8Stack(error.name, error.message, stack);
17971820
} else {
17981821
const callStack = buildFakeCallStack(
17991822
response,
@@ -1853,7 +1876,7 @@ function resolvePostponeDev(
18531876
response: Response,
18541877
id: number,
18551878
reason: string,
1856-
stack: string,
1879+
stack: ReactStackTrace,
18571880
): void {
18581881
if (!__DEV__) {
18591882
// These errors should never make it into a build so we don't need to encode them in codes.json
@@ -1862,11 +1885,34 @@ function resolvePostponeDev(
18621885
'resolvePostponeDev should never be called in production mode. Use resolvePostponeProd instead. This is a bug in React.',
18631886
);
18641887
}
1865-
// eslint-disable-next-line react-internal/prod-error-codes
1866-
const error = new Error(reason || '');
1867-
const postponeInstance: Postpone = (error: any);
1868-
postponeInstance.$$typeof = REACT_POSTPONE_TYPE;
1869-
postponeInstance.stack = stack;
1888+
let postponeInstance: Postpone;
1889+
if (!enableOwnerStacks) {
1890+
// Executing Error within a native stack isn't really limited to owner stacks
1891+
// but we gate it behind the same flag for now while iterating.
1892+
// eslint-disable-next-line react-internal/prod-error-codes
1893+
postponeInstance = (Error(reason || ''): any);
1894+
postponeInstance.$$typeof = REACT_POSTPONE_TYPE;
1895+
// For backwards compat we use the V8 formatting when the flag is off.
1896+
postponeInstance.stack = formatV8Stack(
1897+
postponeInstance.name,
1898+
postponeInstance.message,
1899+
stack,
1900+
);
1901+
} else {
1902+
const callStack = buildFakeCallStack(
1903+
response,
1904+
stack,
1905+
// $FlowFixMe[incompatible-use]
1906+
Error.bind(null, reason || ''),
1907+
);
1908+
const rootTask = response._debugRootTask;
1909+
if (rootTask != null) {
1910+
postponeInstance = rootTask.run(callStack);
1911+
} else {
1912+
postponeInstance = callStack();
1913+
}
1914+
postponeInstance.$$typeof = REACT_POSTPONE_TYPE;
1915+
}
18701916
const chunks = response._chunks;
18711917
const chunk = chunks.get(id);
18721918
if (!chunk) {
@@ -1973,40 +2019,25 @@ function createFakeFunction<T>(
19732019
return fn;
19742020
}
19752021

1976-
// This matches either of these V8 formats.
1977-
// at name (filename:0:0)
1978-
// at filename:0:0
1979-
// at async filename:0:0
1980-
const frameRegExp =
1981-
/^ {3} at (?:(.+) \(([^\)]+):(\d+):(\d+)\)|(?:async )?([^\)]+):(\d+):(\d+))$/;
1982-
19832022
function buildFakeCallStack<T>(
19842023
response: Response,
1985-
stack: string,
2024+
stack: ReactStackTrace,
19862025
innerCall: () => T,
19872026
): () => T {
1988-
const frames = stack.split('\n');
19892027
let callStack = innerCall;
1990-
for (let i = 0; i < frames.length; i++) {
1991-
const frame = frames[i];
1992-
let fn = fakeFunctionCache.get(frame);
2028+
for (let i = 0; i < stack.length; i++) {
2029+
const frame = stack[i];
2030+
const frameKey = frame.join('-');
2031+
let fn = fakeFunctionCache.get(frameKey);
19932032
if (fn === undefined) {
1994-
const parsed = frameRegExp.exec(frame);
1995-
if (!parsed) {
1996-
// We assume the server returns a V8 compatible stack trace.
1997-
continue;
1998-
}
1999-
const name = parsed[1] || '';
2000-
const filename = parsed[2] || parsed[5] || '';
2001-
const line = +(parsed[3] || parsed[6]);
2002-
const col = +(parsed[4] || parsed[7]);
2033+
const [name, filename, line, col] = frame;
20032034
const sourceMap = response._debugFindSourceMapURL
20042035
? response._debugFindSourceMapURL(filename)
20052036
: null;
20062037
fn = createFakeFunction(name, filename, sourceMap, line, col);
20072038
// TODO: This cache should technically live on the response since the _debugFindSourceMapURL
20082039
// function is an input and can vary by response.
2009-
fakeFunctionCache.set(frame, fn);
2040+
fakeFunctionCache.set(frameKey, fn);
20102041
}
20112042
callStack = fn.bind(null, callStack);
20122043
}
@@ -2026,7 +2057,7 @@ function initializeFakeTask(
20262057
return cachedEntry;
20272058
}
20282059

2029-
if (typeof debugInfo.stack !== 'string') {
2060+
if (debugInfo.stack == null) {
20302061
// If this is an error, we should've really already initialized the task.
20312062
// If it's null, we can't initialize a task.
20322063
return null;
@@ -2064,7 +2095,7 @@ function initializeFakeTask(
20642095
const createFakeJSXCallStack = {
20652096
'react-stack-bottom-frame': function (
20662097
response: Response,
2067-
stack: string,
2098+
stack: ReactStackTrace,
20682099
): Error {
20692100
const callStackForError = buildFakeCallStack(
20702101
response,
@@ -2077,7 +2108,7 @@ const createFakeJSXCallStack = {
20772108

20782109
const createFakeJSXCallStackInDEV: (
20792110
response: Response,
2080-
stack: string,
2111+
stack: ReactStackTrace,
20812112
) => Error = __DEV__
20822113
? // We use this technique to trick minifiers to preserve the function name.
20832114
(createFakeJSXCallStack['react-stack-bottom-frame'].bind(
@@ -2100,7 +2131,7 @@ function initializeFakeStack(
21002131
if (cachedEntry !== undefined) {
21012132
return;
21022133
}
2103-
if (typeof debugInfo.stack === 'string') {
2134+
if (debugInfo.stack != null) {
21042135
// $FlowFixMe[cannot-write]
21052136
// $FlowFixMe[prop-missing]
21062137
debugInfo.debugStack = createFakeJSXCallStackInDEV(
@@ -2154,8 +2185,13 @@ function resolveConsoleEntry(
21542185
return;
21552186
}
21562187

2157-
const payload: [string, string, null | ReactComponentInfo, string, mixed] =
2158-
parseModel(response, value);
2188+
const payload: [
2189+
string,
2190+
ReactStackTrace,
2191+
null | ReactComponentInfo,
2192+
string,
2193+
mixed,
2194+
] = parseModel(response, value);
21592195
const methodName = payload[0];
21602196
const stackTrace = payload[1];
21612197
const owner = payload[2];

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,24 @@ function normalizeCodeLocInfo(str) {
2727
);
2828
}
2929

30+
function formatV8Stack(stack) {
31+
let v8StyleStack = '';
32+
if (stack) {
33+
for (let i = 0; i < stack.length; i++) {
34+
const [name] = stack[i];
35+
if (v8StyleStack !== '') {
36+
v8StyleStack += '\n';
37+
}
38+
v8StyleStack += ' in ' + name + ' (at **)';
39+
}
40+
}
41+
return v8StyleStack;
42+
}
43+
3044
function normalizeComponentInfo(debugInfo) {
31-
if (typeof debugInfo.stack === 'string') {
45+
if (Array.isArray(debugInfo.stack)) {
3246
const {debugTask, debugStack, ...copy} = debugInfo;
33-
copy.stack = normalizeCodeLocInfo(debugInfo.stack);
47+
copy.stack = formatV8Stack(debugInfo.stack);
3448
if (debugInfo.owner) {
3549
copy.owner = normalizeComponentInfo(debugInfo.owner);
3650
}

packages/react-server/src/ReactFizzComponentStack.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,17 +165,17 @@ export function getOwnerStackByComponentStackNodeInDev(
165165
// TODO: Should we stash this somewhere for caching purposes?
166166
ownerStack = formatOwnerStack(owner.debugStack);
167167
owner = owner.owner;
168-
} else if (owner.stack != null) {
168+
} else {
169169
// Client Component
170170
const node: ComponentStackNode = (owner: any);
171-
if (typeof owner.stack !== 'string') {
172-
ownerStack = node.stack = formatOwnerStack(owner.stack);
173-
} else {
174-
ownerStack = owner.stack;
171+
if (node.stack != null) {
172+
if (typeof node.stack !== 'string') {
173+
ownerStack = node.stack = formatOwnerStack(node.stack);
174+
} else {
175+
ownerStack = node.stack;
176+
}
175177
}
176178
owner = owner.owner;
177-
} else {
178-
owner = owner.owner;
179179
}
180180
// If we don't actually print the stack if there is no owner of this JSX element.
181181
// In a real app it's typically not useful since the root app is always controlled

0 commit comments

Comments
 (0)