Skip to content

Commit

Permalink
#1297 - Handle errors during render and other scenarios where it was…
Browse files Browse the repository at this point in the history
… not being reported to user. Provide copy error option and get full stack trace in all scenarios.
  • Loading branch information
petmongrels committed Mar 4, 2024
1 parent 88e4e7e commit dda3712
Show file tree
Hide file tree
Showing 18 changed files with 381 additions and 158 deletions.
36 changes: 36 additions & 0 deletions packages/openchs-android/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/openchs-android/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@
"rules-config": "github:openchs/rules-config#982bb007c4f759639063159196edee386b48cac3",
"stacktrace-js": "2.0.2",
"transducers-js": "0.4.174",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"error-stack-parser": "2.1.4",
"react-native-exception-handler": "2.10.10"
},
"devDependencies": {
"@babel/core": "^7.20.0",
Expand Down
49 changes: 17 additions & 32 deletions packages/openchs-android/src/App.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Alert, Clipboard, NativeModules, Text, View, BackHandler, Image, FlatList} from "react-native";
import {Alert, BackHandler, Image, NativeModules, Text, View, FlatList} from "react-native";
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import PathRegistry from './framework/routing/PathRegistry';
Expand All @@ -8,17 +8,17 @@ import {RegisterAndScheduleJobs} from "./AvniBackgroundJob";
import ErrorHandler from "./utility/ErrorHandler";
import FileSystem from "./model/FileSystem";
import GlobalContext from "./GlobalContext";
import RNRestart from 'react-native-restart';
import AppStore from "./store/AppStore";
import RealmFactory from "./framework/db/RealmFactory";
import General from "./utility/General";
import EnvironmentConfig from "./framework/EnvironmentConfig";
import Config from './framework/Config';
import JailMonkey from 'jail-monkey';

const {TamperCheckModule} = NativeModules;
import KeepAwake from 'react-native-keep-awake';
import moment from "moment";
import AvniErrorBoundary from "./framework/errorHandling/AvniErrorBoundary";
import UnhandledErrorView from "./framework/errorHandling/UnhandledErrorView";

const {TamperCheckModule} = NativeModules;

class App extends Component {
static childContextTypes = {
Expand All @@ -33,12 +33,12 @@ class App extends Component {
this.getBean = this.getBean.bind(this);
this.handleError = this.handleError.bind(this);
ErrorHandler.set(this.handleError);
this.state = {error: '', isInitialisationDone: false, isDeviceRooted: false};
this.state = {avniError: null, isInitialisationDone: false, isDeviceRooted: false};
}

handleError(error, stacktrace) {
handleError(avniError) {
//It is possible for App to not be available during this time, so check if state is available before setting to it
this.setState && this.setState({error, stacktrace});
this.setState && this.setState({avniError: avniError});
}

getChildContext = () => ({
Expand All @@ -49,27 +49,6 @@ class App extends Component {
getStore: () => GlobalContext.getInstance().reduxStore,
});

renderError() {
const clipboardString = `${this.state.error.message}\nStacktrace:${this.state.stacktrace}`;
General.logError("App", `renderError: ${clipboardString}`);

if (EnvironmentConfig.inNonDevMode() && !Config.allowServerURLConfig) {
Alert.alert("App will restart now", this.state.error.message,
[
{
text: "Copy error and Restart",
onPress: () => {
Clipboard.setString(clipboardString);
RNRestart.Restart();
}
}
],
{cancelable: false}
);
}
return <View/>;
}

renderRootedDeviceErrorMessageAndExitApplication() {
const clipboardString = `This is a Rooted Device. Exiting Avni application due to security considerations.`;
General.logError("App", `renderError: ${clipboardString}`);
Expand Down Expand Up @@ -119,12 +98,12 @@ class App extends Component {
}
}

render() {
renderApp() {
if (this.state.isDeviceRooted) {
return this.renderRootedDeviceErrorMessageAndExitApplication();
}
if (this.state.error) {
return this.renderError();
if (this.state.avniError) {
return <UnhandledErrorView avniError={this.state.avniError}/>;
}
if (!_.isNil(GlobalContext.getInstance().routes) && this.state.isInitialisationDone) {
return GlobalContext.getInstance().routes
Expand Down Expand Up @@ -155,6 +134,12 @@ class App extends Component {
/>
</View>);
}

render() {
return <AvniErrorBoundary>
{this.renderApp()}
</AvniErrorBoundary>;
}
}

export default App;
4 changes: 2 additions & 2 deletions packages/openchs-android/src/action/LoginActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { IDP_PROVIDERS } from "../model/IdpProviders";

function restoreDump(context, action, source, successCb) {
const restoreService = context.get(BackupRestoreRealmService);
restoreService.restore((percentProgress, message, failed = false, failureMessage) => {
if (failed) action.checkForRetry(failureMessage, source);
restoreService.restore((percentProgress, message, failed = false, error) => {
if (failed) action.checkForRetry(error, source);
else if (percentProgress === 100) successCb(source);
else action.onLoginProgress(percentProgress, message);
});
Expand Down
23 changes: 23 additions & 0 deletions packages/openchs-android/src/framework/errorHandling/AvniError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import _ from 'lodash';

class AvniError {
userMessage;
reportingText;

static create(userMessage, reportingText) {
const avniError = new AvniError();
avniError.userMessage = userMessage;
avniError.reportingText = reportingText;
return avniError;
}

static createFromUserMessageAndStackTrace(userMessage, stackTraceString) {
return AvniError.create(userMessage, `${userMessage}\n${stackTraceString}`);
}

getDisplayMessage() {
return _.truncate(this.userMessage, {length: 80});
}
}

export default AvniError;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, {Component} from 'react';
import UnhandledErrorView from "./UnhandledErrorView";
import ErrorUtil from "./ErrorUtil";

export default class AvniErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {avniError: null};
}

static getDerivedStateFromError(error) {
const avniError = ErrorUtil.getAvniErrorSync(error);
return {avniError: avniError};
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.log("AvniErrorBoundary", "componentDidCatch");
}

render() {
console.log("AvniErrorBoundary", "render");
if (this.state.avniError) {
return <UnhandledErrorView avniError={this.state.avniError}/>;
}
return this.props.children;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Config from "../Config";
import {Alert, Clipboard, ToastAndroid, View} from "react-native";
import React from 'react';
import RNRestart from "react-native-restart";
import {JSONStringify} from "../../utility/JsonStringify";

export function ErrorDisplay({avniError}) {
console.log("ErrorDisplay", "render", Config.allowServerURLConfig);
if (!Config.allowServerURLConfig) {
Alert.alert("App will restart now", avniError.getDisplayMessage(),
[
{
text: "copyErrorAndRestart",
onPress: () => {
console.log("ErrorDisplay", JSONStringify(avniError));
Clipboard.setString(avniError.reportingText);
RNRestart.Restart();
}
},
{
text: "restart",
onPress: () => RNRestart.Restart()
}
],
{cancelable: true}
);
}
return <View/>;
}
31 changes: 31 additions & 0 deletions packages/openchs-android/src/framework/errorHandling/ErrorUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import StackTrace from "stacktrace-js";
import AvniError from "./AvniError";
import ErrorStackParser from "error-stack-parser";

function createNavigableStackTrace(stackFrames) {
return stackFrames.map((sf) => {
return `at ${sf.toString()}`;
}).join('\n');
}

class ErrorUtil {
//Errors can potentially un-streamed
static createBugsnagStackFrames(error) {
return StackTrace.fromError(error, {offline: true})
.then((x) => {
return x.map((row) => Object.defineProperty(row, 'fileName', {
value: `${row.fileName}:${row.lineNumber || 0}:${row.columnNumber || 0}`
}));
});
}

static getAvniErrorSync(error) {
return AvniError.createFromUserMessageAndStackTrace(error.message, createNavigableStackTrace(ErrorStackParser.parse(error)));
}

static getNavigableStackTraceSync(error) {
return createNavigableStackTrace(ErrorStackParser.parse(error))
}
}

export default ErrorUtil;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, {Component} from 'react';
import {Image, View} from "react-native";
import PropTypes from "prop-types";
import {ErrorDisplay} from "./ErrorDisplay";
import {JSONStringify} from "../../utility/JsonStringify";

export default class UnhandledErrorView extends Component {
static propTypes = {
avniError: PropTypes.object.isRequired
};

render() {
console.log("UnhandledErrorView", JSONStringify(this.props.avniError));
return <View style={{flex: 1}}>
<Image source={{uri: `asset:/logo.png`}}
style={{height: 120, width: 120, alignSelf: 'center'}} resizeMode={'center'}/>
<ErrorDisplay avniError={this.props.avniError}/>
</View>
}
}
5 changes: 1 addition & 4 deletions packages/openchs-android/src/framework/http/requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ const fetchFactory = (endpoint, method = "GET", params, fetchWithoutTimeout) =>
General.logError("requests", response);
return Promise.reject(new AuthenticationError('Http ' + responseCode, response));
}
if (responseCode === 400) {
return Promise.reject(response);
}
return Promise.reject(new ServerError(`Http ${response.status}`, response));
return Promise.reject(new ServerError(response));
};
const requestInit = {"method": method, ...params};
const doFetch = getXSRFPromise(endpoint).then((xsrfToken) => {
Expand Down
1 change: 0 additions & 1 deletion packages/openchs-android/src/framework/routing/Router.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {Navigator} from 'react-native-deprecated-custom-components';
import General from "../../utility/General";

export default class Router extends Component {

static propTypes = {
initialRoute: PropTypes.object.isRequired,
};
Expand Down
8 changes: 2 additions & 6 deletions packages/openchs-android/src/service/BackupRestoreRealm.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,19 +170,15 @@ export default class BackupRestoreRealmService extends BaseService {
})
.catch((error) => {
General.logErrorAsInfo("BackupRestoreRealm", error);
cb(100, "restoreFailed", true, error.message);
cb(100, "restoreFailed", true, error);
});
} else {
cb(100, "restoreNoDump");
}
})
.catch((error) => {
General.logErrorAsInfo("BackupRestoreRealm", error);
if (error.errorText) {
error.errorText.then(message => cb(100, "restoreFailed", true, message));
} else {
cb(100, "restoreFailed", true, error.message);
}
cb(100, "restoreFailed", true, error);
});
}

Expand Down
Loading

0 comments on commit dda3712

Please sign in to comment.