Skip to content

Commit

Permalink
feat(crashlytics): add configuration to exception handler chaining be…
Browse files Browse the repository at this point in the history
…havior

- enabling crashlytics in debug really enables it in debug now
- add ability to specify in firebase.json if you want to chain to other (maybe native) handlers

Now that javascript stack traces are more useful since the crashlytics backend will convert them
to fatal crashes, developers should prefer using them

However the default global crash handler we chain to in react-ntaive will also log them, just
as pure native crashes.

This allows developers to specify they do not want the continued exception processing, and
the module will attempt to simply exit cleanly after logging the unhandled crash
  • Loading branch information
mikehardy committed Apr 16, 2021
1 parent 60442e4 commit 4c640ff
Show file tree
Hide file tree
Showing 14 changed files with 180 additions and 34 deletions.
3 changes: 3 additions & 0 deletions .spellcheck.dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ invertase
iOS
iOS13
IPs
Javascript
javascript
JS
JSON
launchProperties
Expand Down Expand Up @@ -122,6 +124,7 @@ SHA-256
SIGABRT
SMS
src
stacktrace
SVG
TestLab
TFUA
Expand Down
2 changes: 1 addition & 1 deletion docs/crashlytics/android-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ buildscript {
// ..
dependencies {
// ..
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2'
}
// ..
}
Expand Down
27 changes: 20 additions & 7 deletions docs/crashlytics/usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async function onSignIn(user) {
role: 'admin',
followers: '13',
email: user.email,
username: user.username
username: user.username,
}),
]);
}
Expand Down Expand Up @@ -221,9 +221,9 @@ React Native. You can disable Crashlytics NDK in your `firebase.json` config.
}
```

## Crashlytics additional non-fatal issue generation
## Crashlytics Javascript stacktrace issue generation

React Native Crashlytics module is generating additional non-fatal issues on JavaScript exceptions by default. Sometimes it is not desirable behavior since it might duplicate issues and hide original exceptions logs. You can disable this behavior by setting appropriate option to false:
React Native Crashlytics module by default installs a global javascript exception handler, and it records a crash with a javascript stack trace any time an unhandled javascript exception is thrown. Sometimes it is not desirable behavior since it might duplicate issues in combination with the default mode of javascript global exception handler chaining. We recommend leaving JS crashes enabled and turning off exception handler chaining. However, if you have special crash handling requirements, you may disable this behavior by setting the appropriate option to false:

```json
// <project-root>/firebase.json
Expand All @@ -234,13 +234,26 @@ React Native Crashlytics module is generating additional non-fatal issues on Jav
}
```

## Crashlytics Javascript exception handler chaining

React Native Crashlytics module's global javascript exception handler by default chains to any previously installed global javascript exception handler after logging the crash with the javascript stack trace. In default react-native setups, this means in development you will then see a "red box" and in release mode you will see a second native crash in the Crashlytics console with no javascript stack trace. These duplicate crash reports are probably not desirable, and the one from the chained handler will not have the javascript stack trace. We recommend disabling this once Crashlytics is integrated in testing. It is enabled by default for easier initial integration testing and to be sure introducing the option was not a breaking change. You may disable exception handler chaining by setting the appropriate option to false:

```json
// <project-root>/firebase.json
{
"react-native": {
"crashlytics_javascript_exception_handler_chaining_enabled": false
}
}
```

## Crashlytics non-fatal exceptions native handling

In case you need to log non-fatal (handled) exceptions on the native side (e.g from `try catch` block), you may use the following static methods:
<br />

### Android

```
```java
try {
//...
} catch (Exception e) {
Expand All @@ -251,7 +264,7 @@ try {

### iOS

```
```objectivec
@try {
//...
} @catch (NSException *exception) {
Expand All @@ -266,4 +279,4 @@ try {
[RNFBCrashlyticsNativeHelper recordNativeError:error];
}

```
```
10 changes: 9 additions & 1 deletion packages/app/firebase-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@
"type": "boolean"
},
"crashlytics_debug_enabled": {
"description": "Because you have stack traces readily available while you`re debugging your app, Crashlytics is disabled by default in debug mode. You can set Crashlytics to be enabled regardless of debug mode through the debug_enabled option in your firebase.json.",
"description": "Stack traces are readily available while you`re debugging your app, so Crashlytics is disabled by default in debug mode. You can set Crashlytics to be enabled regardless of debug mode through the debug_enabled option in your firebase.json. This may be useful to test your integration, remembering reports are sent next app start.",
"type": "boolean"
},
"crashlytics_is_error_generation_on_js_crash_enabled": {
"description": "By default React Native Firebase Crashlytics installs a global javascript-level unhandled exception handler that will log unhandled javascript exceptions as fatal crashes (since v11.3.0, non-fatal prior) with javascript stacks. Set to false to disable javascript-level crash handling.",
"type": "boolean"
},
"crashlytics_javascript_exception_handler_chaining_enabled": {
"description": "By default React Native Firebase Crashlytics will preserve existing global javascript-level uhandled exception handlers by reporting to Crashlytics then passing the exception on for further handling. This could lead to duplicate reports, for example a fatal javascript-level report and a fatal native level report for the same crash. Set to false to terminate error handling after logging the javascript-level crash.",
"type": "boolean"
},
"crashlytics_ndk_enabled": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ class Constants {
final static String KEY_CRASHLYTICS_DEBUG_ENABLED = "crashlytics_debug_enabled";
final static String KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED = "crashlytics_auto_collection_enabled";
final static String KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED = "crashlytics_is_error_generation_on_js_crash_enabled";
final static String KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED = "crashlytics_javascript_exception_handler_chaining_enabled";
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,23 @@ static boolean isCrashlyticsCollectionEnabled() {

if (prefs.contains(KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED)) {
enabled = prefs.getBooleanValue(KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED, true);
Log.d(TAG, "isCrashlyticsCollectionEnabled via RNFBPreferences: " + enabled);
} else if (json.contains(KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED)) {
enabled = json.getBooleanValue(KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED, true);
Log.d(TAG, "isCrashlyticsCollectionEnabled via RNFBJSON: " + enabled);
} else {
enabled = meta.getBooleanValue(KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED, true);
Log.d(TAG, "isCrashlyticsCollectionEnabled via RNFBMeta: " + enabled);
}

if (!json.getBooleanValue(KEY_CRASHLYTICS_DEBUG_ENABLED, false) && BuildConfig.DEBUG) {
enabled = false;

if (BuildConfig.DEBUG) {
if (!json.getBooleanValue(KEY_CRASHLYTICS_DEBUG_ENABLED, false)) {
enabled = false;
}
Log.d(TAG, "isCrashlyticsCollectionEnabled after checking " + KEY_CRASHLYTICS_DEBUG_ENABLED + ": " + enabled);
}

Log.d(TAG, "isCrashlyticsCollectionEnabled final value: " + enabled);
return enabled;
}

Expand All @@ -58,12 +65,37 @@ static boolean isErrorGenerationOnJSCrashEnabled() {

if (prefs.contains(KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED)) {
enabled = prefs.getBooleanValue(KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED, true);
Log.d(TAG, "isErrorGenerationOnJSCrashEnabled via RNFBPreferences: " + enabled);
} else if (json.contains(KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED)) {
enabled = json.getBooleanValue(KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED, true);
Log.d(TAG, "isErrorGenerationOnJSCrashEnabled via RNFBJSON: " + enabled);
} else {
enabled = meta.getBooleanValue(KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED, true);
Log.d(TAG, "isErrorGenerationOnJSCrashEnabled via RNFBMeta: " + enabled);
}

Log.d(TAG, "isErrorGenerationOnJSCrashEnabled final value: " + enabled);
return enabled;
}

static boolean isCrashlyticsJavascriptExceptionHandlerChainingEnabled() {
boolean enabled;
ReactNativeFirebaseJSON json = ReactNativeFirebaseJSON.getSharedInstance();
ReactNativeFirebaseMeta meta = ReactNativeFirebaseMeta.getSharedInstance();
ReactNativeFirebasePreferences prefs = ReactNativeFirebasePreferences.getSharedInstance();

if (prefs.contains(KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED)) {
enabled = prefs.getBooleanValue(KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED, true);
Log.d(TAG, "isCrashlyticsJavascriptExceptionHandlerChainingEnabled via RNFBPreferences: " + enabled);
} else if (json.contains(KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED)) {
enabled = json.getBooleanValue(KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED, true);
Log.d(TAG, "isCrashlyticsJavascriptExceptionHandlerChainingEnabled via RNFBJSON: " + enabled);
} else {
enabled = meta.getBooleanValue(KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED, true);
Log.d(TAG, "isCrashlyticsJavascriptExceptionHandlerChainingEnabled via RNFBMeta: " + enabled);
}

Log.d(TAG, "isCrashlyticsJavascriptExceptionHandlerChainingEnabled final value: " + enabled);
return enabled;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import android.os.Handler;
import android.util.Log;

import com.google.firebase.crashlytics.FirebaseCrashlytics;
import com.facebook.react.bridge.*;
Expand Down Expand Up @@ -52,6 +53,19 @@ public void checkForUnsentReports(Promise promise){
});
}

@ReactMethod
public void crashWithStackPromise(ReadableMap jsErrorMap, Promise promise) {
if (ReactNativeFirebaseCrashlyticsInitProvider.isCrashlyticsCollectionEnabled()) {
Exception e = recordJavaScriptError(jsErrorMap);
e.printStackTrace(System.err);
Log.e(TAG, "Crash logged. Terminating app.");
System.exit(0);
} else {
Log.i(TAG, "crashlytics collection is not enabled, not crashing.");
}
promise.resolve(null);
}

@ReactMethod
public void crash() {
if (ReactNativeFirebaseCrashlyticsInitProvider.isCrashlyticsCollectionEnabled()) {
Expand All @@ -62,6 +76,8 @@ public void run() {
throw new RuntimeException("Crash Test");
}
}, 50);
} else {
Log.i(TAG, "crashlytics collection is not enabled, not crashing.");
}
}

Expand Down Expand Up @@ -141,18 +157,22 @@ public void setCrashlyticsCollectionEnabled(Boolean enabled, Promise promise) {
public void recordError(ReadableMap jsErrorMap) {
if (ReactNativeFirebaseCrashlyticsInitProvider.isCrashlyticsCollectionEnabled()) {
recordJavaScriptError(jsErrorMap);
} else {
Log.i(TAG, "crashlytics collection is not enabled, not crashing.");
}
}

@ReactMethod
public void recordErrorPromise(ReadableMap jsErrorMap, Promise promise) {
if (ReactNativeFirebaseCrashlyticsInitProvider.isCrashlyticsCollectionEnabled()) {
recordJavaScriptError(jsErrorMap);
} else {
Log.i(TAG, "crashlytics collection is not enabled, not crashing.");
}
promise.resolve(null);
}

private void recordJavaScriptError(ReadableMap jsErrorMap) {
private Exception recordJavaScriptError(ReadableMap jsErrorMap) {
String message = jsErrorMap.getString("message");
ReadableArray stackFrames = Objects.requireNonNull(jsErrorMap.getArray("frames"));
boolean isUnhandledPromiseRejection = jsErrorMap.getBoolean("isUnhandledRejection");
Expand All @@ -176,6 +196,7 @@ private void recordJavaScriptError(ReadableMap jsErrorMap) {
customException.setStackTrace(stackTraceElements);

FirebaseCrashlytics.getInstance().recordException(customException);
return customException;
}

@Override
Expand All @@ -189,6 +210,10 @@ public Map<String, Object> getConstants() {
"isErrorGenerationOnJSCrashEnabled",
ReactNativeFirebaseCrashlyticsInitProvider.isErrorGenerationOnJSCrashEnabled()
);
constants.put(
"isCrashlyticsJavascriptExceptionHandlerChainingEnabled",
ReactNativeFirebaseCrashlyticsInitProvider.isCrashlyticsJavascriptExceptionHandlerChainingEnabled()
);
return constants;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

+ (BOOL)isErrorGenerationOnJSCrashEnabled;

+ (BOOL)isCrashlyticsJavascriptExceptionHandlerChainingEnabled;

/// Returns one or more FIRComponents that will be registered in
/// FIRApp and participate in dependency resolution and injection.
+ (NSArray<FIRComponent *> *)componentsToRegister;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,36 +26,37 @@
NSString *const KEY_CRASHLYTICS_DEBUG_ENABLED = @"crashlytics_debug_enabled";
NSString *const KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED = @"crashlytics_auto_collection_enabled";
NSString *const KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED = @"crashlytics_is_error_generation_on_js_crash_enabled";
NSString *const KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED = @"crashlytics_javascript_exception_handler_chaining_enabled";

@implementation RNFBCrashlyticsInitProvider

+ (void)load {
[FIRApp registerInternalLibrary:self withName:@"react-native-firebase-crashlytics" withVersion:@"6.0.0"];
[FIRApp registerInternalLibrary:self withName:@"react-native-firebase-crashlytics" withVersion:@"11.3.0"];
}

+ (BOOL)isCrashlyticsCollectionEnabled {
BOOL enabled;

if ([[RNFBPreferences shared] contains:KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED]) {
enabled = [[RNFBPreferences shared] getBooleanValue:KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED defaultValue:YES];
DLog(@"isCrashlyticsCollectionEnabled via RNFBPreferences: %d", enabled);
DLog(@"RNFBCrashlyticsInit isCrashlyticsCollectionEnabled via RNFBPreferences: %d", enabled);
} else if ([[RNFBJSON shared] contains:KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED]) {
enabled = [[RNFBJSON shared] getBooleanValue:KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED defaultValue:YES];
DLog(@"isCrashlyticsCollectionEnabled via RNFBJSON: %d", enabled);
DLog(@"RNFBCrashlyticsInit isCrashlyticsCollectionEnabled via RNFBJSON: %d", enabled);
} else {
// Note that if we're here, and the key is not set on the app's bundle, we default to "YES"
enabled = [RNFBMeta getBooleanValue:KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED defaultValue:YES];
DLog(@"isCrashlyticsCollectionEnabled via RNFBMeta: %d", enabled);
DLog(@"RNFBCrashlyticsInit isCrashlyticsCollectionEnabled via RNFBMeta: %d", enabled);
}

#ifdef DEBUG
if (![[RNFBJSON shared] getBooleanValue:KEY_CRASHLYTICS_DEBUG_ENABLED defaultValue:NO]) {
enabled = NO;
}
DLog(@"isCrashlyticsCollectionEnabled after checking RNFBJSON %@: %d", KEY_CRASHLYTICS_DEBUG_ENABLED, enabled);
DLog(@"RNFBCrashlyticsInit isCrashlyticsCollectionEnabled after checking RNFBJSON %@: %d", KEY_CRASHLYTICS_DEBUG_ENABLED, enabled);
#endif

DLog(@"isCrashlyticsCollectionEnabled: %d", enabled);
DLog(@"RNFBCrashlyticsInit isCrashlyticsCollectionEnabled final value: %d", enabled);

return enabled;
}
Expand All @@ -65,17 +66,37 @@ + (BOOL)isErrorGenerationOnJSCrashEnabled {

if ([[RNFBPreferences shared] contains:KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED]) {
enabled = [[RNFBPreferences shared] getBooleanValue:KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED defaultValue:YES];
DLog(@"isErrorGenerationOnJSCrashEnabled via RNFBPreferences: %d", enabled);
DLog(@"RNFBCrashlyticsInit isErrorGenerationOnJSCrashEnabled via RNFBPreferences: %d", enabled);
} else if ([[RNFBJSON shared] contains:KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED]) {
enabled = [[RNFBJSON shared] getBooleanValue:KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED defaultValue:YES];
DLog(@"isErrorGenerationOnJSCrashEnabled via RNFBJSON: %d", enabled);
DLog(@"RNFBCrashlyticsInit isErrorGenerationOnJSCrashEnabled via RNFBJSON: %d", enabled);
} else {
// Note that if we're here, and the key is not set on the app's bundle, we default to "YES"
enabled = [RNFBMeta getBooleanValue:KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED defaultValue:YES];
DLog(@"isErrorGenerationOnJSCrashEnabled via RNFBMeta: %d", enabled);
DLog(@"RNFBCrashlyticsInit isErrorGenerationOnJSCrashEnabled via RNFBMeta: %d", enabled);
}

DLog(@"isErrorGenerationOnJSCrashEnabled: %d", enabled);
DLog(@"RNFBCrashlyticsInit isErrorGenerationOnJSCrashEnabled final value: %d", enabled);

return enabled;
}

+ (BOOL)isCrashlyticsJavascriptExceptionHandlerChainingEnabled {
BOOL enabled;

if ([[RNFBPreferences shared] contains:KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED]) {
enabled = [[RNFBPreferences shared] getBooleanValue:KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED defaultValue:YES];
DLog(@"RNFBCrashlyticsInit isCrashlyticsJavascriptExceptionHandlerChainingEnabled via RNFBPreferences: %d", enabled);
} else if ([[RNFBJSON shared] contains:KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED]) {
enabled = [[RNFBJSON shared] getBooleanValue:KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED defaultValue:YES];
DLog(@"RNFBCrashlyticsInit isCrashlyticsJavascriptExceptionHandlerChainingEnabled via RNFBJSON: %d", enabled);
} else {
// Note that if we're here, and the key is not set on the app's bundle, we default to "YES"
enabled = [RNFBMeta getBooleanValue:KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED defaultValue:YES];
DLog(@"RNFBCrashlyticsInit isCrashlyticsJavascriptExceptionHandlerChainingEnabled via RNFBMeta: %d", enabled);
}

DLog(@"RNFBCrashlyticsInit isCrashlyticsJavascriptExceptionHandlerChainingEnabled final value: %d", enabled);

return enabled;
}
Expand All @@ -96,7 +117,7 @@ + (void)configureWithApp:(FIRApp *)app {
// This setting is sticky. setCrashlyticsCollectionEnabled persists the setting to disk until it is explicitly set otherwise or the app is deleted.
// Jump to the setCrashlyticsCollectionEnabled definition to see implementation details.
[[FIRCrashlytics crashlytics] setCrashlyticsCollectionEnabled:self.isCrashlyticsCollectionEnabled];
DLog(@"initialization successful");
DLog(@"RNFBCrashlyticsInit initialization successful");
}

@end
Loading

0 comments on commit 4c640ff

Please sign in to comment.