Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RNMobile] Add error boundary components and exception logging #59221

Merged
merged 33 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
34221bb
Add parse function to send exceptions over the RN bridge
fluiddot Feb 20, 2024
95e9a25
Add RN bridge function to log exceptions to the host app
fluiddot Feb 20, 2024
808d2d9
Add error boundary to blocks
fluiddot Oct 26, 2023
205987e
Add error boundary at editor level
fluiddot Jan 31, 2024
9d57c27
Log exceptions from error boundary components
fluiddot Feb 20, 2024
265b139
Remove `in_app` stack trace parameter
fluiddot Feb 21, 2024
ca76b90
Remove `getPopSize`
fluiddot Feb 22, 2024
5d4be1a
Simplify functions from `parseException`
fluiddot Feb 22, 2024
1c9384e
Rename exception tag to `gutenberg_mobile_version`
fluiddot Feb 22, 2024
53b2f2e
Add `gutenbergDidRequestLogException` bridge function to demo app
fluiddot Feb 22, 2024
bc67ebb
Trigger callback upon sending JS exception
fluiddot Feb 23, 2024
3e30aaf
Format `RNReactNativeGutenbergBridge`
fluiddot Feb 26, 2024
f4bfed0
Update inline comments
fluiddot Feb 26, 2024
37348cb
Merge `reverseEntries` logic into `parseStacktrace`
fluiddot Feb 26, 2024
2a519b4
Set second param of `parseException` (context and tags) as optional
fluiddot Feb 26, 2024
4dece96
Add unit tests of `parseException`
fluiddot Feb 26, 2024
12aff79
Add inline comment to `getReactNativeContext`
fluiddot Feb 26, 2024
5f4e208
Add typing for JavaScript exception in bridge
fluiddot Feb 27, 2024
a746556
Fix param type in `gutenbergDidRequestLogException`
fluiddot Feb 27, 2024
35338be
Update `gutenbergDidRequestLogException` implementation of demo app
fluiddot Feb 27, 2024
5457c0e
Rename `JSException` to avoid disambiguation with Crash Logging service
fluiddot Feb 27, 2024
4a6b38b
Add `actions` prop to `Warning` component
fluiddot Feb 27, 2024
5e1daea
Allow extra styles in `Warning` component
fluiddot Feb 27, 2024
260cb3a
Implement copy buttons in Error boundary component
fluiddot Feb 27, 2024
a598b77
Fix style import path in error boundary component
fluiddot Feb 27, 2024
c017e12
Merge branch 'trunk' into rnmobile/add/error-boundary
fluiddot Feb 28, 2024
7c18727
Change GutenbergJSException `function` param to non-optional
fluiddot Feb 29, 2024
363cb12
Rename JSException param to `message`
fluiddot Feb 29, 2024
294ac99
Rename GutenbergJSException param to `message`
fluiddot Feb 29, 2024
7dadd4a
Merge branch 'trunk' into rnmobile/add/error-boundary
fluiddot Mar 6, 2024
2002dcf
Update `react-native-editor` changelog
fluiddot Mar 6, 2024
cfaf638
Update unit tests of `parseException`
fluiddot Mar 6, 2024
49f32e7
[RNMobile] Add error boundry handling for Android (#59385)
jhnstn Mar 7, 2024
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { logException } from '@wordpress/react-native-bridge';

class BlockCrashBoundary extends Component {
constructor() {
super( ...arguments );

this.state = {
error: null,
};
}

static getDerivedStateFromError( error ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's more information about this static function: https://react.dev/reference/react/Component#static-getderivedstatefromerror

return { error };
}

componentDidCatch( error ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's more information about this component function: https://react.dev/reference/react/Component#componentdidcatch

const { blockName } = this.props;

logException( error, {
context: {
component_stack: error.componentStack,
block_name: blockName,
},
isHandled: true,
handledBy: 'Block-level Error Boundary',
} );
}

render() {
const { error } = this.state;
if ( ! error ) {
return this.props.children;
}

return this.props.fallback;
}
}

export default BlockCrashBoundary;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import Warning from '../warning';

const warning = (
<Warning
message={ __(
'This block has encountered an error and cannot be previewed.'
) }
/>
);

export default () => warning;
21 changes: 14 additions & 7 deletions packages/block-editor/src/components/block-list/block.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import { useLayout } from './layout';
import useScrollUponInsertion from './use-scroll-upon-insertion';
import { useSettings } from '../use-settings';
import { unlock } from '../../lock-unlock';
import BlockCrashBoundary from './block-crash-boundary';
import BlockCrashWarning from './block-crash-warning';

const EMPTY_ARRAY = [];

Expand Down Expand Up @@ -139,14 +141,19 @@ function BlockWrapper( {
isSelected={ isSelected }
name={ name }
/>
<BlockDraggable
clientId={ clientId }
draggingClientId={ draggingClientId }
enabled={ draggingEnabled }
testID="draggable-trigger-content"
<BlockCrashBoundary
blockName={ name }
fallback={ <BlockCrashWarning /> }
>
{ children }
</BlockDraggable>
<BlockDraggable
clientId={ clientId }
draggingClientId={ draggingClientId }
enabled={ draggingEnabled }
testID="draggable-trigger-content"
>
{ children }
</BlockDraggable>
</BlockCrashBoundary>
</Pressable>
);
}
Expand Down
34 changes: 19 additions & 15 deletions packages/block-editor/src/components/warning/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,36 @@ import { normalizeIconObject } from '@wordpress/blocks';
import styles from './style.scss';

function Warning( {
actions,
title,
message,
icon,
iconClass,
preferredColorScheme,
getStylesFromColorScheme,
containerStyle: extraContainerStyle,
titleStyle: extraTitleStyle,
messageStyle: extraMessageStyle,
...viewProps
} ) {
icon = icon && normalizeIconObject( icon );
const internalIconClass = 'warning-icon' + '-' + preferredColorScheme;
const titleStyle = getStylesFromColorScheme(
styles.title,
styles.titleDark
);
const messageStyle = getStylesFromColorScheme(
styles.message,
styles.messageDark
);

const containerStyle = [
getStylesFromColorScheme( styles.container, styles.containerDark ),
extraContainerStyle,
];
const titleStyle = [
getStylesFromColorScheme( styles.title, styles.titleDark ),
extraTitleStyle,
];
const messageStyle = [
getStylesFromColorScheme( styles.message, styles.messageDark ),
extraMessageStyle,
];

return (
<View
style={ getStylesFromColorScheme(
styles.container,
styles.containerDark
) }
{ ...viewProps }
>
<View style={ containerStyle } { ...viewProps }>
{ icon && (
<View style={ styles.icon }>
<Icon
Expand All @@ -53,6 +56,7 @@ function Warning( {
) }
{ title && <Text style={ titleStyle }>{ title }</Text> }
{ message && <Text style={ messageStyle }>{ message }</Text> }
{ actions }
</View>
);
}
Expand Down
10 changes: 8 additions & 2 deletions packages/edit-post/src/editor.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { EditorProvider, store as editorStore } from '@wordpress/editor';
import {
EditorProvider,
ErrorBoundary,
store as editorStore,
} from '@wordpress/editor';
import { parse, serialize } from '@wordpress/blocks';
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
Expand Down Expand Up @@ -143,7 +147,9 @@ class Editor extends Component {
useSubRegistry={ false }
{ ...props }
>
<Layout setTitleRef={ this.setTitleRef } />
<ErrorBoundary>
<Layout setTitleRef={ this.setTitleRef } />
</ErrorBoundary>
</EditorProvider>
</SlotFillProvider>
</GestureHandlerRootView>
Expand Down
122 changes: 122 additions & 0 deletions packages/editor/src/components/error-boundary/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* External dependencies
*/
import { Text, TouchableOpacity, View } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';

/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { select } from '@wordpress/data';
import { Warning } from '@wordpress/block-editor';
import { logException } from '@wordpress/react-native-bridge';
import { usePreferredColorSchemeStyle } from '@wordpress/compose';

/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';
import styles from './style.scss';

function getContent() {
try {
// While `select` in a component is generally discouraged, it is
// used here because it (a) reduces the chance of data loss in the
// case of additional errors by performing a direct retrieval and
// (b) avoids the performance cost associated with unnecessary
// content serialization throughout the lifetime of a non-erroring
// application.
return select( editorStore ).getEditedPostContent();
} catch ( error ) {}
}

function CopyButton( { text, label, accessibilityLabel, accessibilityHint } ) {
const containerStyle = usePreferredColorSchemeStyle(
styles[ 'copy-button__container' ],
styles[ 'copy-button__container--dark' ]
);
const textStyle = usePreferredColorSchemeStyle(
styles[ 'copy-button__text' ],
styles[ 'copy-button__text--dark' ]
);

return (
<TouchableOpacity
activeOpacity={ 0.5 }
accessibilityLabel={ accessibilityLabel }
style={ containerStyle }
accessibilityRole={ 'button' }
accessibilityHint={ accessibilityHint }
onPress={ () => {
Clipboard.setString(
typeof text === 'function' ? text() : text || ''
);
} }
>
<Text style={ textStyle }>{ label }</Text>
</TouchableOpacity>
);
}

class ErrorBoundary extends Component {
constructor() {
super( ...arguments );

this.state = {
error: null,
};
}

componentDidCatch( error ) {
logException( error, {
context: {
component_stack: error.componentStack,
},
isHandled: true,
handledBy: 'Editor-level Error Boundary',
} );
}

static getDerivedStateFromError( error ) {
return { error };
}

render() {
const { error } = this.state;
if ( ! error ) {
return this.props.children;
}

const actions = (
<View style={ styles[ 'error-boundary__actions-container' ] }>
<CopyButton
label={ __( 'Copy Post Text' ) }
accessibilityLabel={ __( 'Button to copy post text' ) }
accessibilityHint={ __( 'Tap here to copy post text' ) }
text={ getContent }
/>
<CopyButton
label={ __( 'Copy Error' ) }
accessibilityLabel={ __( 'Button to copy error' ) }
accessibilityHint={ __( 'Tap here to copy error' ) }
text={ error.stack }
/>
</View>
);

return (
<Warning
actions={ actions }
message={ __(
'The editor has encountered an unexpected error.'
) }
containerStyle={ styles[ 'error-boundary__container' ] }
messageStyle={ styles[ 'error-boundary__message' ] }
/>
);
}
}

export default ErrorBoundary;
39 changes: 39 additions & 0 deletions packages/editor/src/components/error-boundary/style.native.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.error-boundary__container {
flex-grow: 1;
justify-content: flex-start;
}

.error-boundary__message {
font-size: 16;
}

.error-boundary__actions-container {
margin-top: 16px;
justify-content: center;
flex-direction: row;
}

.copy-button__container {
background-color: $light-primary;
border-radius: 3px;
padding: $grid-unit $grid-unit-20;
margin-top: 8px;
margin-left: 8px;
margin-right: 8px;
}

.copy-button__container--dark {
color: $background-dark-secondary;
background-color: $gray-20;
}

.copy-button__text {
text-align: center;
color: $white;
font-size: 16;
font-weight: 400;
}

.copy-button__text--dark {
color: $black;
}
1 change: 1 addition & 0 deletions packages/editor/src/components/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export { default as EditorProvider } from './provider';
// Other Components.
export { default as EditorHelpTopics } from './editor-help';
export { default as OfflineStatus } from './offline-status';
export { default as ErrorBoundary } from './error-boundary';

export * from './deprecated';
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@

import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;

import org.wordpress.mobile.WPAndroidGlue.GutenbergJsException;
import org.wordpress.mobile.WPAndroidGlue.MediaOption;
import org.wordpress.mobile.WPAndroidGlue.RequestExecutor;

import java.util.ArrayList;
import java.util.List;

public interface GutenbergBridgeJS2Parent extends RequestExecutor {

void responseHtml(String title, String html, boolean changed, ReadableMap contentInfo);

void editorDidMount(ReadableArray unsupportedBlockNames);
Expand Down Expand Up @@ -65,6 +64,10 @@ interface ConnectionStatusCallback {
void onRequestConnectionStatus(boolean isConnected);
}

interface LogExceptionCallback {
void onLogException(boolean success);
}

// Ref: https://github.com/facebook/react-native/blob/HEAD/Libraries/polyfills/console.js#L376
enum LogLevel {
TRACE(0),
Expand Down Expand Up @@ -178,4 +181,6 @@ void gutenbergDidRequestUnsupportedBlockFallback(ReplaceUnsupportedBlockCallback
void toggleRedoButton(boolean isDisabled);

void requestConnectionStatus(ConnectionStatusCallback connectionStatusCallback);

void logException(GutenbergJsException exception, LogExceptionCallback logExceptionCallback);
}
Loading
Loading