Introduce NativeExceptionsManager.reportException API

Summary:
@public

`reportException` is a new method on `NativeExceptionsManager` that is designed to allow more structured and flexible JS error reporting. `reportFatalException` and `reportSoftException` are now deprecated.

In addition to all the usual exception fields, `reportException` also accepts an `extraData` property which the JS exception handler can populate with arbitrary JSON-serialisable data (here: the raw stack trace, the current JS engine, and the number of frames popped off the call stack by the exception handler). The contents of `extraData` get attached as JSON to the `JavascriptException` instance (or just logged, in the case of `console.error`).

This change is backwards compatible in two senses:
1. We have a JS fallback that uses `reportFatalException` and `reportSoftException` if the new native method is unavailable.
2. We have a Java fallback that implements `reportFatalException` and `reportSoftException` in terms of `reportException`.

Naturally, both fallbacks mentioned above discard `extraData`.

NOTE: The current implementation is Android-only; for the time being, iOS will continue to use the JS fallback.

While we're in `ExceptionsManager.js`, this also changes `dismissRedbox()` to be optional (which it is, since it's Android-only); existing call sites already guard it with a null check so this requires no other changes.

Reviewed By: mmmulani

Differential Revision: D16133080

fbshipit-source-id: d0b209d58da40b736df63155bbea232e94ce635c
This commit is contained in:
Moti Zilberman
2019-07-16 09:38:03 -07:00
committed by Facebook Github Bot
parent 6fb1690de1
commit 3a825c0360
7 changed files with 222 additions and 27 deletions
@@ -8,12 +8,15 @@ package com.facebook.react.modules.core;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.JavascriptException;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.util.ExceptionDataHelper;
import com.facebook.react.util.JSStackTrace;
@ReactModule(name = ExceptionsManagerModule.NAME)
@@ -33,24 +36,44 @@ public class ExceptionsManagerModule extends BaseJavaModule {
}
@ReactMethod
public void reportFatalException(String title, ReadableArray details, int exceptionId) {
showOrThrowError(title, details, exceptionId);
public void reportFatalException(String message, ReadableArray stack, int id) {
JavaOnlyMap data = new JavaOnlyMap();
data.putString("message", message);
data.putArray("stack", stack);
data.putInt("id", id);
data.putBoolean("isFatal", true);
reportException(data);
}
@ReactMethod
public void reportSoftException(String title, ReadableArray details, int exceptionId) {
if (mDevSupportManager.getDevSupportEnabled()) {
mDevSupportManager.showNewJSError(title, details, exceptionId);
} else {
FLog.e(ReactConstants.TAG, JSStackTrace.format(title, details));
}
public void reportSoftException(String message, ReadableArray stack, int id) {
JavaOnlyMap data = new JavaOnlyMap();
data.putString("message", message);
data.putArray("stack", stack);
data.putInt("id", id);
data.putBoolean("isFatal", false);
reportException(data);
}
private void showOrThrowError(String title, ReadableArray details, int exceptionId) {
@ReactMethod
public void reportException(ReadableMap data) {
String message = data.getString("message");
ReadableArray stack = data.getArray("stack");
int id = data.getInt("id");
boolean isFatal = data.getBoolean("isFatal");
if (mDevSupportManager.getDevSupportEnabled()) {
mDevSupportManager.showNewJSError(title, details, exceptionId);
mDevSupportManager.showNewJSError(message, stack, id);
} else {
throw new JavascriptException(JSStackTrace.format(title, details));
String extraDataAsJson = ExceptionDataHelper.getExtraDataAsJson(data);
if (isFatal) {
throw new JavascriptException(JSStackTrace.format(message, stack))
.setExtraDataAsJson(extraDataAsJson);
} else {
FLog.e(ReactConstants.TAG, JSStackTrace.format(message, stack));
if (extraDataAsJson != null) {
FLog.d(ReactConstants.TAG, "extraData: %s", extraDataAsJson);
}
}
}
}
@@ -1,4 +1,4 @@
load("//tools/build_defs/oss:rn_defs.bzl", "react_native_target", "rn_android_library")
load("//tools/build_defs/oss:rn_defs.bzl", "react_native_dep", "react_native_target", "rn_android_library")
rn_android_library(
name = "util",
@@ -7,6 +7,7 @@ rn_android_library(
"PUBLIC",
],
deps = [
react_native_dep("third-party/java/jsr-305:jsr-305"),
react_native_target("java/com/facebook/react/bridge:bridge"),
],
)
@@ -0,0 +1,36 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.facebook.react.util;
import android.util.JsonWriter;
import com.facebook.react.bridge.JsonWriterHelper;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import javax.annotation.Nullable;
public class ExceptionDataHelper {
static final String EXTRA_DATA_FIELD = "extraData";
public static String getExtraDataAsJson(@Nullable ReadableMap metadata) {
if (metadata == null || metadata.getType(EXTRA_DATA_FIELD) == ReadableType.Null) {
return null;
}
try {
Writer extraDataWriter = new StringWriter();
JsonWriter json = new JsonWriter(extraDataWriter);
JsonWriterHelper.value(json, metadata.getDynamic(EXTRA_DATA_FIELD));
json.close();
extraDataWriter.close();
return extraDataWriter.toString();
} catch (IOException ex) {
}
return null;
}
}