diff --git a/README.md b/README.md index 25501f3..57ab44b 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ To tackle this we register a global error handler that could be used to for exam **V2 of this module now supports catching Unhandled Native Exceptions also along with the JS Exceptions ✌🏻🍻** There are **NO** breaking changes. So its safe to upgrade from v1 to v2. So there is no reason not to 😉. +**V2.9** +- Adds support for executing previously set error handlers (now this module can work with other analytics modules) +- Adds an improved approach for overwriting native error handlers. +- Thanks @ [Damien Solimando](https://github.com/dsolimando) + **Example** repo can be found here: *[https://github.com/master-atul/react-native-exception-handler-example](https://github.com/master-atul/react-native-exception-handler-example) * @@ -128,7 +133,7 @@ setJSExceptionHandler((error, isFatal) => { // or hit google analytics to track crashes // or hit a custom api to inform the dev team. }); - +//================================================= // ADVANCED use case: const exceptionhandler = (error, isFatal) => { // your error handler function @@ -156,17 +161,21 @@ setNativeExceptionHandler((exceptionString) => { //NOTE: alert or showing any UI change via JS //WILL NOT WORK in case of NATIVE ERRORS. }); - +//==================================================== // ADVANCED use case: const exceptionhandler = (exceptionString) => { // your exception handler code here } -setNativeExceptionHandler(exceptionhandler,forceAppQuit); +setNativeExceptionHandler(exceptionhandler,forceAppQuit,executeDefaultHandler); // - exceptionhandler is the exception handler function // - forceAppQuit is an optional ANDROID specific parameter that defines -// if the app should be force quit on error. default value is true. -// To see usecase check the common issues section. - +// if the app should be force quit on error. default value is true. +// To see usecase check the common issues section. +// - executeDefaultHandler is an optional boolean (both IOS, ANDROID) +// It executes previous exception handlers if set by some other module. +// It will come handy when you use any other crash analytics module along with this one +// Default value is set to false. Set to true if you are using other analytics modules. + ``` It is recommended you set both the handlers. @@ -201,7 +210,39 @@ In Android and iOS you will see something like

-**Modifying Android Native Exception handler UI** (NATIVE CODE HAS TO BE WRITTEN) *recommended that you do this in android studio* +**Modifying Android Native Exception handler (RECOMMENDED APPROACH)** + +(NATIVE CODE HAS TO BE WRITTEN) *recommended that you do this in android studio* + +- In the `android/app/src/main/java/[...]/MainActivity.java` + +```java +import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerModule; +import com.masteratul.exceptionhandler.NativeExceptionHandlerIfc +... +... +... +public class MainApplication extends Application implements ReactApplication { +... +... + @Override + public void onCreate() { + .... + .... + .... + ReactNativeExceptionHandlerModule.setNativeExceptionHandler(new NativeExceptionHandlerIfc() { + @Override + public void handleNativeException(Thread thread, Throwable throwable, Thread.UncaughtExceptionHandler originalHandler) { + // Put your error handling code here + } + }//This will override the default behaviour of displaying the recover activity. + } + +``` + +**Modifying Android Native Exception handler UI (CUSTOM ACTIVITY APPROACH (OLD APPROACH).. LEAVING FOR BACKWARD COMPATIBILITY)** + +(NATIVE CODE HAS TO BE WRITTEN) *recommended that you do this in android studio* - Create an Empty Activity in the `android/app/src/main/java/[...]/`. For example lets say CustomErrorDialog.java - Customize your activity to look and behave however you need it to be. @@ -431,6 +472,10 @@ This is specifically occuring when you use [wix library](http://wix.github.io/re setNativeExceptionHandler(nativeErrorCallback, false); ``` +### Previously defined exception handlers are not executed anymore + +A lot of frameworks (especially analytics sdk's) implement global exception handlers. In order to keep these frameworks working while using react-native-exception-hanlder, you can pass a boolean value as third argument to `setNativeExceptionHandler(..., ..., true`) what will trigger the execution of the last global handler registered. + ## CONTRIBUTORS - [Atul R](https://github.com/master-atul) @@ -447,6 +492,7 @@ setNativeExceptionHandler(nativeErrorCallback, false); - [TomMahle](https://github.com/TomMahle) - [Sébastien Krafft](https://github.com/skrafft) - [Mark Friedman](https://github.com/mark-friedman) +- [Damien Solimando](https://github.com/dsolimando) ## TESTING NATIVE EXCEPTIONS/ERRORS diff --git a/android/src/main/java/com/masteratul/exceptionhandler/NativeExceptionHandlerIfc.java b/android/src/main/java/com/masteratul/exceptionhandler/NativeExceptionHandlerIfc.java new file mode 100644 index 0000000..bdcd82e --- /dev/null +++ b/android/src/main/java/com/masteratul/exceptionhandler/NativeExceptionHandlerIfc.java @@ -0,0 +1,5 @@ +package com.masteratul.exceptionhandler; + +public interface NativeExceptionHandlerIfc { + void handleNativeException(Thread thread, Throwable throwable, Thread.UncaughtExceptionHandler originalHandler); +} diff --git a/android/src/main/java/com/masteratul/exceptionhandler/ReactNativeExceptionHandlerModule.java b/android/src/main/java/com/masteratul/exceptionhandler/ReactNativeExceptionHandlerModule.java index eba89dc..00352d3 100644 --- a/android/src/main/java/com/masteratul/exceptionhandler/ReactNativeExceptionHandlerModule.java +++ b/android/src/main/java/com/masteratul/exceptionhandler/ReactNativeExceptionHandlerModule.java @@ -1,19 +1,21 @@ package com.masteratul.exceptionhandler; + import android.app.Activity; import android.content.Intent; import android.util.Log; +import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.Callback; public class ReactNativeExceptionHandlerModule extends ReactContextBaseJavaModule { private ReactApplicationContext reactContext; private Activity activity; private static Class errorIntentTargetClass = DefaultErrorScreen.class; + private static NativeExceptionHandlerIfc nativeExceptionHandler; private Callback callbackHolder; private Thread.UncaughtExceptionHandler originalHandler; @@ -29,39 +31,52 @@ public String getName() { @ReactMethod - public void setHandlerforNativeException(final boolean forceToQuit, Callback customHandler){ + public void setHandlerforNativeException( + final boolean executeOriginalUncaughtExceptionHandler, + final boolean forceToQuit, + Callback customHandler) { + callbackHolder = customHandler; originalHandler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + @Override public void uncaughtException(Thread thread, Throwable throwable) { + + String stackTraceString = Log.getStackTraceString(throwable); + callbackHolder.invoke(stackTraceString); + + if (nativeExceptionHandler != null) { + nativeExceptionHandler.handleNativeException(thread, throwable, originalHandler); + } else { activity = getCurrentActivity(); - String stackTraceString = Log.getStackTraceString(throwable); - callbackHolder.invoke(stackTraceString); - Log.d("ERROR",stackTraceString); - Intent i = new Intent(); i.setClass(activity, errorIntentTargetClass); i.putExtra("stack_trace_string",stackTraceString); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - + activity.startActivity(i); activity.finish(); - if (originalHandler != null) { + if (executeOriginalUncaughtExceptionHandler && originalHandler != null) { originalHandler.uncaughtException(thread, throwable); } if (forceToQuit) { - System.exit(0); + System.exit(0); } } + } }); } public static void replaceErrorScreenActivityClass(Class errorScreenActivityClass){ errorIntentTargetClass = errorScreenActivityClass; } + + public static void setNativeExceptionHandler(NativeExceptionHandlerIfc nativeExceptionHandler) { + ReactNativeExceptionHandlerModule.nativeExceptionHandler = nativeExceptionHandler; + } } diff --git a/index.js b/index.js index e917cc0..d1b3200 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,13 @@ +import { NativeModules, Platform } from "react-native"; -import {NativeModules, Platform} from 'react-native'; +const { ReactNativeExceptionHandler } = NativeModules; -const {ReactNativeExceptionHandler} = NativeModules; - -const noop = () => {}; +const noop = () => { }; export const setJSExceptionHandler = (customHandler = noop, allowedInDevMode = false) => { - if ((typeof allowedInDevMode !== 'boolean') || (typeof customHandler !== 'function')) { - console.log('setJSExceptionHandler is called with wrong argument types.. first argument should be callback function and second argument is optional should be a boolean'); - console.log('Not setting the JS handler .. please fix setJSExceptionHandler call'); + if (typeof allowedInDevMode !== "boolean" || typeof customHandler !== "function"){ + console.log("setJSExceptionHandler is called with wrong argument types.. first argument should be callback function and second argument is optional should be a boolean"); + console.log("Not setting the JS handler .. please fix setJSExceptionHandler call"); return; } const allowed = allowedInDevMode ? true : !__DEV__; @@ -16,22 +15,22 @@ export const setJSExceptionHandler = (customHandler = noop, allowedInDevMode = f global.ErrorUtils.setGlobalHandler(customHandler); console.error = (message, error) => global.ErrorUtils.reportError(error); // sending console.error so that it can be caught } else { - console.log('Skipping setJSExceptionHandler: Reason: In DEV mode and allowedInDevMode = false'); + console.log("Skipping setJSExceptionHandler: Reason: In DEV mode and allowedInDevMode = false"); } }; export const getJSExceptionHandler = () => global.ErrorUtils.getGlobalHandler(); -export const setNativeExceptionHandler = (customErrorHandler = noop, forceApplicationToQuit = true) => { - if ((typeof customErrorHandler !== 'function') || (typeof forceApplicationToQuit !== 'boolean')) { - console.log('setNativeExceptionHandler is called with wrong argument types.. first argument should be callback function and second argument is optional should be a boolean'); - console.log('Not setting the native handler .. please fix setNativeExceptionHandler call'); +export const setNativeExceptionHandler = (customErrorHandler = noop, forceApplicationToQuit = true, executeDefaultHandler = false) => { + if (typeof customErrorHandler !== "function" || typeof forceApplicationToQuit !== "boolean") { + console.log("setNativeExceptionHandler is called with wrong argument types.. first argument should be callback function and second argument is optional should be a boolean"); + console.log("Not setting the native handler .. please fix setNativeExceptionHandler call"); return; } - if (Platform.OS === 'ios') { - ReactNativeExceptionHandler.setHandlerforNativeException(customErrorHandler); + if (Platform.OS === "ios") { + ReactNativeExceptionHandler.setHandlerforNativeException(executeDefaultHandler, customErrorHandler); } else { - ReactNativeExceptionHandler.setHandlerforNativeException(forceApplicationToQuit, customErrorHandler); + ReactNativeExceptionHandler.setHandlerforNativeException(executeDefaultHandler, forceApplicationToQuit, customErrorHandler); } }; diff --git a/ios/ReactNativeExceptionHandler.m b/ios/ReactNativeExceptionHandler.m index e8bae91..7cb0a71 100644 --- a/ios/ReactNativeExceptionHandler.m +++ b/ios/ReactNativeExceptionHandler.m @@ -28,24 +28,29 @@ - (dispatch_queue_t)methodQueue //variable to hold the custom error handler passed while customizing native handler void (^nativeErrorCallbackBlock)(NSException *exception, NSString *readeableException); +// variable to hold the previously defined error handler +NSUncaughtExceptionHandler* previousNativeErrorCallbackBlock; + +BOOL callPreviousNativeErrorCallbackBlock = false; + //variable to hold the js error handler when setting up the error handler in RN. void (^jsErrorCallbackBlock)(NSException *exception, NSString *readeableException); //variable that holds the default native error handler void (^defaultNativeErrorCallbackBlock)(NSException *exception, NSString *readeableException) = ^(NSException *exception, NSString *readeableException){ - + UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Unexpected error occured" message:[NSString stringWithFormat:@"%@\n%@", @"Apologies..The app will close now \nPlease restart the app\n", readeableException] preferredStyle:UIAlertControllerStyleAlert]; - + UIApplication* app = [UIApplication sharedApplication]; UIViewController * rootViewController = app.delegate.window.rootViewController; [rootViewController presentViewController:alert animated:YES completion:nil]; - + [NSTimer scheduledTimerWithTimeInterval:5.0 target:[ReactNativeExceptionHandler class] selector:@selector(releaseExceptionHold) @@ -53,7 +58,6 @@ - (dispatch_queue_t)methodQueue repeats:NO]; }; - // ==================================== // REACT NATIVE MODULE EXPOSED METHODS // ==================================== @@ -61,12 +65,15 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_MODULE(); // METHOD TO INITIALIZE THE EXCEPTION HANDLER AND SET THE JS CALLBACK BLOCK -RCT_EXPORT_METHOD(setHandlerforNativeException:(RCTResponseSenderBlock)callback) +RCT_EXPORT_METHOD(setHandlerforNativeException:(BOOL)callPreviouslyDefinedHandler withCallback: (RCTResponseSenderBlock)callback) { jsErrorCallbackBlock = ^(NSException *exception, NSString *readeableException){ callback(@[readeableException]); }; - + + previousNativeErrorCallbackBlock = NSGetUncaughtExceptionHandler(); + callPreviousNativeErrorCallbackBlock = callPreviouslyDefinedHandler; + NSSetUncaughtExceptionHandler(&HandleException); signal(SIGABRT, SignalHandler); signal(SIGILL, SignalHandler); @@ -105,14 +112,19 @@ - (void)handleException:(NSException *)exception [exception reason], [[exception userInfo] objectForKey:RNUncaughtExceptionHandlerAddressesKey]]; dismissApp = false; - + + + if (callPreviousNativeErrorCallbackBlock && previousNativeErrorCallbackBlock) { + previousNativeErrorCallbackBlock(exception); + } + if(nativeErrorCallbackBlock != nil){ nativeErrorCallbackBlock(exception,readeableError); }else{ defaultNativeErrorCallbackBlock(exception,readeableError); } jsErrorCallbackBlock(exception,readeableError); - + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop); while (!dismissApp) @@ -127,9 +139,9 @@ - (void)handleException:(NSException *)exception i++; } } - + CFRelease(allModes); - + NSSetUncaughtExceptionHandler(NULL); signal(SIGABRT, SIG_DFL); signal(SIGILL, SIG_DFL); @@ -137,9 +149,9 @@ - (void)handleException:(NSException *)exception signal(SIGFPE, SIG_DFL); signal(SIGBUS, SIG_DFL); signal(SIGPIPE, SIG_DFL); - + kill(getpid(), [[[exception userInfo] objectForKey:RNUncaughtExceptionHandlerSignalKey] intValue]); - + } @@ -154,14 +166,14 @@ void HandleException(NSException *exception) { return; } - + NSArray *callStack = [ReactNativeExceptionHandler backtrace]; NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]]; [userInfo setObject:callStack forKey:RNUncaughtExceptionHandlerAddressesKey]; - + [[[ReactNativeExceptionHandler alloc] init] performSelectorOnMainThread:@selector(handleException:) withObject: @@ -179,17 +191,17 @@ void SignalHandler(int signal) { return; } - + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:[NSNumber numberWithInt:signal] forKey:RNUncaughtExceptionHandlerSignalKey]; - + NSArray *callStack = [ReactNativeExceptionHandler backtrace]; [userInfo setObject:callStack forKey:RNUncaughtExceptionHandlerAddressesKey]; - + [[[ReactNativeExceptionHandler alloc] init] performSelectorOnMainThread:@selector(handleException:) withObject: @@ -216,7 +228,7 @@ + (NSArray *)backtrace void* callstack[128]; int frames = backtrace(callstack, 128); char **strs = backtrace_symbols(callstack, frames); - + int i; NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames]; for ( @@ -228,8 +240,9 @@ + (NSArray *)backtrace [backtrace addObject:[NSString stringWithUTF8String:strs[i]]]; } free(strs); - + return backtrace; } @end + diff --git a/package.json b/package.json index bdd8e77..ce68a48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-exception-handler", - "version": "2.8.9", + "version": "2.9.0", "description": "A react native module that lets you to register a global error handler that can capture fatal/non fatal uncaught exceptions.", "main": "index.js", "scripts": {