Show component code frame, if available (#48785)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48785

## Overview
This change adds code frames for component stacks if it differs from the call stack frame.

## Background
After adding native stack based stack frames, which we already symbolicate, we had the capability to show a code frame for component stacks as well as the stack frame (if they differ).

However, this usually wasn't that useful, because the native component stack was unlikely to be more useful than the component stack (see the comparisons below for key errors).

## Owner stacks

With owner stacks the component frame is a lot more useful and in many cases are better at showing the location than the call stack frame.

## Example Screens

Before:
{F1974436645}

After (with owner stacks):
{F1974436723}

Changelog:
[General][Added] - Add owner stack code frames to LogBox

Reviewed By: hoxyq

Differential Revision: D68285627

fbshipit-source-id: 541cecbd4786fffd1970d2e85e659757e3f81604
This commit is contained in:
Rick Hanlon
2025-01-20 10:07:00 -08:00
committed by Facebook GitHub Bot
parent 967ef32154
commit 0affa544c3
7 changed files with 1181 additions and 241 deletions
+42 -20
View File
@@ -79,6 +79,7 @@ class LogBoxLog {
count: number;
level: LogLevel;
codeFrame: ?CodeFrame;
componentCodeFrame: ?CodeFrame;
isComponentError: boolean;
extraData: mixed | void;
symbolicated:
@@ -140,8 +141,18 @@ class LogBoxLog {
}
retrySymbolicate(callback?: (status: SymbolicationStatus) => void): void {
let retry = false;
if (this.symbolicated.status !== 'COMPLETE') {
LogBoxSymbolication.deleteStack(this.stack);
retry = true;
}
if (this.symbolicatedComponentStack.status !== 'COMPLETE') {
LogBoxSymbolication.deleteStack(
convertComponentStateToStack(this.componentStack),
);
retry = true;
}
if (retry) {
this.handleSymbolicate(callback);
}
}
@@ -153,7 +164,10 @@ class LogBoxLog {
}
handleSymbolicate(callback?: (status: SymbolicationStatus) => void): void {
if (this.symbolicated.status !== 'PENDING') {
if (
this.symbolicated.status !== 'PENDING' &&
this.symbolicated.status !== 'COMPLETE'
) {
this.updateStatus(null, null, null, callback);
LogBoxSymbolication.symbolicate(this.stack, this.extraData).then(
data => {
@@ -163,25 +177,30 @@ class LogBoxLog {
this.updateStatus(error, null, null, callback);
},
);
if (this.componentStack != null && this.componentStackType === 'stack') {
this.updateComponentStackStatus(null, null, null, callback);
const componentStackFrames = convertComponentStateToStack(
this.componentStack,
);
LogBoxSymbolication.symbolicate(componentStackFrames, []).then(
data => {
this.updateComponentStackStatus(
null,
convertStackToComponentStack(data.stack),
null,
callback,
);
},
error => {
this.updateComponentStackStatus(error, null, null, callback);
},
);
}
}
if (
this.componentStack != null &&
this.componentStackType === 'stack' &&
this.symbolicatedComponentStack.status !== 'PENDING' &&
this.symbolicatedComponentStack.status !== 'COMPLETE'
) {
this.updateComponentStackStatus(null, null, null, callback);
const componentStackFrames = convertComponentStateToStack(
this.componentStack,
);
LogBoxSymbolication.symbolicate(componentStackFrames, []).then(
data => {
this.updateComponentStackStatus(
null,
convertStackToComponentStack(data.stack),
data?.codeFrame,
callback,
);
},
error => {
this.updateComponentStackStatus(error, null, null, callback);
},
);
}
}
@@ -235,6 +254,9 @@ class LogBoxLog {
status: 'FAILED',
};
} else if (componentStack != null) {
if (codeFrame) {
this.componentCodeFrame = codeFrame;
}
this.symbolicatedComponentStack = {
error: null,
componentStack,
File diff suppressed because it is too large Load Diff
@@ -52,7 +52,10 @@ export default function LogBoxInspectorBody(props: {
title={headerTitle}
/>
<ScrollView style={styles.scrollBody}>
<LogBoxInspectorCodeFrame codeFrame={props.log.codeFrame} />
<LogBoxInspectorCodeFrame
codeFrame={props.log.codeFrame}
componentCodeFrame={props.log.componentCodeFrame}
/>
<LogBoxInspectorReactFrames log={props.log} />
<LogBoxInspectorStackFrames log={props.log} onRetry={props.onRetry} />
</ScrollView>
@@ -68,7 +71,10 @@ export default function LogBoxInspectorBody(props: {
level={props.log.level}
title={headerTitle}
/>
<LogBoxInspectorCodeFrame codeFrame={props.log.codeFrame} />
<LogBoxInspectorCodeFrame
codeFrame={props.log.codeFrame}
componentCodeFrame={props.log.componentCodeFrame}
/>
<LogBoxInspectorReactFrames log={props.log} />
<LogBoxInspectorStackFrames log={props.log} onRetry={props.onRetry} />
</ScrollView>
@@ -22,16 +22,13 @@ import LogBoxButton from './LogBoxButton';
import LogBoxInspectorSection from './LogBoxInspectorSection';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
type Props = $ReadOnly<{
componentCodeFrame: ?CodeFrame,
codeFrame: ?CodeFrame,
}>;
function LogBoxInspectorCodeFrame(props: Props): React.Node {
const codeFrame = props.codeFrame;
if (codeFrame == null) {
return null;
}
function CodeFrameDisplay({codeFrame}: {codeFrame: CodeFrame}): React.Node {
function getFileName() {
// $FlowFixMe[incompatible-use]
const matches = /[^/]*$/.exec(codeFrame.fileName);
@@ -56,30 +53,52 @@ function LogBoxInspectorCodeFrame(props: Props): React.Node {
}
return (
<LogBoxInspectorSection heading="Source" action={<AppInfo />}>
<View style={styles.box}>
<View style={styles.frame}>
<ScrollView
horizontal
contentContainerStyle={styles.contentContainer}>
<AnsiHighlight style={styles.content} text={codeFrame.content} />
</ScrollView>
</View>
<LogBoxButton
backgroundColor={{
default: 'transparent',
pressed: LogBoxStyle.getBackgroundDarkColor(1),
}}
style={styles.button}
onPress={() => {
openFileInEditor(codeFrame.fileName, codeFrame.location?.row ?? 0);
}}>
<Text style={styles.fileText}>
{getFileName()}
{getLocation()}
</Text>
</LogBoxButton>
<View style={styles.box}>
<View style={styles.frame}>
<ScrollView horizontal contentContainerStyle={styles.contentContainer}>
<AnsiHighlight style={styles.content} text={codeFrame.content} />
</ScrollView>
</View>
<LogBoxButton
backgroundColor={{
default: 'transparent',
pressed: LogBoxStyle.getBackgroundDarkColor(1),
}}
style={styles.button}
onPress={() => {
openFileInEditor(codeFrame.fileName, codeFrame.location?.row ?? 0);
}}>
<Text style={styles.fileText}>
{getFileName()}
{getLocation()}
</Text>
</LogBoxButton>
</View>
);
}
function LogBoxInspectorCodeFrame(props: Props): React.Node {
const {codeFrame, componentCodeFrame} = props;
let sources = [];
if (codeFrame != null) {
sources.push(codeFrame);
}
if (
componentCodeFrame != null &&
componentCodeFrame?.content !== codeFrame?.content
) {
sources.push(componentCodeFrame);
}
if (sources.length === 0) {
return null;
}
return (
<LogBoxInspectorSection
heading={sources.length > 1 ? 'Sources' : 'Source'}
action={<AppInfo />}>
{sources.map((frame, index) => (
<CodeFrameDisplay key={index} codeFrame={frame} />
))}
</LogBoxInspectorSection>
);
}
@@ -37,7 +37,7 @@ jest.mock('../LogBoxInspectorSection', () => ({
describe('LogBoxInspectorCodeFrame', () => {
it('should render null for no code frame', async () => {
const output = await render.create(
<LogBoxInspectorCodeFrame codeFrame={null} />,
<LogBoxInspectorCodeFrame componentCodeFrame={null} codeFrame={null} />,
);
expect(output).toMatchSnapshot();
@@ -46,6 +46,7 @@ describe('LogBoxInspectorCodeFrame', () => {
it('should render a code frame', async () => {
const output = await render.create(
<LogBoxInspectorCodeFrame
componentCodeFrame={null}
codeFrame={{
fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js',
location: {row: 199, column: 0},
@@ -61,9 +62,72 @@ describe('LogBoxInspectorCodeFrame', () => {
expect(output).toMatchSnapshot();
});
it('should render both a code frame and a component frame', async () => {
const output = await render.create(
<LogBoxInspectorCodeFrame
componentCodeFrame={{
content: ` 89 |
90 | function Child() {
> 91 | return <ConsoleWithThrow />;
| ^
92 | }
93 |
94 |`,
location: {row: 90, column: 10},
fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js',
}}
codeFrame={{
fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js',
location: {row: 64, column: 16},
content: ` 62 |
63 | function ConsoleWithThrow() {
> 64 | console.error('hit');
| ^
65 | throw new Error('test');
66 | }
67 |`,
}}
/>,
);
expect(output).toMatchSnapshot();
});
it('should dedupe if code frames are the same', async () => {
const output = await render.create(
<LogBoxInspectorCodeFrame
componentCodeFrame={{
content: ` 63 | function ConsoleWithThrow() {
64 | console.error('hit');
> 65 | throw new Error('test');
| ^
66 | }
67 |
68 |`,
location: {row: 65, column: 18},
fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js',
}}
codeFrame={{
content: ` 63 | function ConsoleWithThrow() {
64 | console.error('hit');
> 65 | throw new Error('test');
| ^
66 | }
67 |
68 |`,
location: {row: 65, column: 18},
fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js',
}}
/>,
);
expect(output).toMatchSnapshot();
});
it('should render a code frame without a location', async () => {
const output = await render.create(
<LogBoxInspectorCodeFrame
componentCodeFrame={null}
codeFrame={{
fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js',
location: null,
@@ -1,5 +1,94 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LogBoxInspectorCodeFrame should dedupe if code frames are the same 1`] = `
<LogBoxInspectorSection
action={<AppInfo />}
heading="Source"
>
<View
style={
Object {
"backgroundColor": "rgba(51, 51, 51, 1)",
"borderRadius": 3,
"marginLeft": 10,
"marginRight": 10,
"marginTop": 5,
}
}
>
<View
style={
Object {
"borderBottomColor": "rgba(255, 255, 255, 0.1)",
"borderBottomWidth": 1,
"padding": 10,
}
}
>
<ScrollView
contentContainerStyle={
Object {
"minWidth": "100%",
}
}
horizontal={true}
>
<Ansi
style={
Object {
"color": "rgba(255, 255, 255, 1)",
"fontFamily": "Menlo",
"fontSize": 12,
"includeFontPadding": false,
"lineHeight": 20,
}
}
text=" 63 | function ConsoleWithThrow() {
64 | console.error('hit');
> 65 | throw new Error('test');
| ^
66 | }
67 |
68 |"
/>
</ScrollView>
</View>
<LogBoxButton
backgroundColor={
Object {
"default": "transparent",
"pressed": "rgba(34, 34, 34, 1)",
}
}
onPress={[Function]}
style={
Object {
"paddingBottom": 10,
"paddingTop": 10,
}
}
>
<Text
style={
Object {
"color": "rgba(255, 255, 255, 0.5)",
"flex": 1,
"fontFamily": "Menlo",
"fontSize": 12,
"includeFontPadding": false,
"lineHeight": 16,
"textAlign": "center",
}
}
>
CrashReactApp.js
(65:19)
</Text>
</LogBoxButton>
</View>
</LogBoxInspectorSection>
`;
exports[`LogBoxInspectorCodeFrame should render a code frame 1`] = `
<LogBoxInspectorSection
action={<AppInfo />}
@@ -173,4 +262,174 @@ exports[`LogBoxInspectorCodeFrame should render a code frame without a location
</LogBoxInspectorSection>
`;
exports[`LogBoxInspectorCodeFrame should render both a code frame and a component frame 1`] = `
<LogBoxInspectorSection
action={<AppInfo />}
heading="Sources"
>
<View
style={
Object {
"backgroundColor": "rgba(51, 51, 51, 1)",
"borderRadius": 3,
"marginLeft": 10,
"marginRight": 10,
"marginTop": 5,
}
}
>
<View
style={
Object {
"borderBottomColor": "rgba(255, 255, 255, 0.1)",
"borderBottomWidth": 1,
"padding": 10,
}
}
>
<ScrollView
contentContainerStyle={
Object {
"minWidth": "100%",
}
}
horizontal={true}
>
<Ansi
style={
Object {
"color": "rgba(255, 255, 255, 1)",
"fontFamily": "Menlo",
"fontSize": 12,
"includeFontPadding": false,
"lineHeight": 20,
}
}
text=" 62 |
63 | function ConsoleWithThrow() {
> 64 | console.error('hit');
| ^
65 | throw new Error('test');
66 | }
67 |"
/>
</ScrollView>
</View>
<LogBoxButton
backgroundColor={
Object {
"default": "transparent",
"pressed": "rgba(34, 34, 34, 1)",
}
}
onPress={[Function]}
style={
Object {
"paddingBottom": 10,
"paddingTop": 10,
}
}
>
<Text
style={
Object {
"color": "rgba(255, 255, 255, 0.5)",
"flex": 1,
"fontFamily": "Menlo",
"fontSize": 12,
"includeFontPadding": false,
"lineHeight": 16,
"textAlign": "center",
}
}
>
CrashReactApp.js
(64:17)
</Text>
</LogBoxButton>
</View>
<View
style={
Object {
"backgroundColor": "rgba(51, 51, 51, 1)",
"borderRadius": 3,
"marginLeft": 10,
"marginRight": 10,
"marginTop": 5,
}
}
>
<View
style={
Object {
"borderBottomColor": "rgba(255, 255, 255, 0.1)",
"borderBottomWidth": 1,
"padding": 10,
}
}
>
<ScrollView
contentContainerStyle={
Object {
"minWidth": "100%",
}
}
horizontal={true}
>
<Ansi
style={
Object {
"color": "rgba(255, 255, 255, 1)",
"fontFamily": "Menlo",
"fontSize": 12,
"includeFontPadding": false,
"lineHeight": 20,
}
}
text=" 89 |
90 | function Child() {
> 91 | return <ConsoleWithThrow />;
| ^
92 | }
93 |
94 |"
/>
</ScrollView>
</View>
<LogBoxButton
backgroundColor={
Object {
"default": "transparent",
"pressed": "rgba(34, 34, 34, 1)",
}
}
onPress={[Function]}
style={
Object {
"paddingBottom": 10,
"paddingTop": 10,
}
}
>
<Text
style={
Object {
"color": "rgba(255, 255, 255, 0.5)",
"flex": 1,
"fontFamily": "Menlo",
"fontSize": 12,
"includeFontPadding": false,
"lineHeight": 16,
"textAlign": "center",
}
}
>
CrashReactApp.js
(90:11)
</Text>
</LogBoxButton>
</View>
</LogBoxInspectorSection>
`;
exports[`LogBoxInspectorCodeFrame should render null for no code frame 1`] = `null`;
@@ -6064,6 +6064,7 @@ declare class LogBoxLog {
count: number;
level: LogLevel;
codeFrame: ?CodeFrame;
componentCodeFrame: ?CodeFrame;
isComponentError: boolean;
extraData: mixed | void;
symbolicated:
@@ -6257,6 +6258,7 @@ exports[`public API should not change unintentionally Libraries/LogBox/UI/LogBox
exports[`public API should not change unintentionally Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js 1`] = `
"type Props = $ReadOnly<{
componentCodeFrame: ?CodeFrame,
codeFrame: ?CodeFrame,
}>;
declare function LogBoxInspectorCodeFrame(props: Props): React.Node;