Integrate logbox into earlyjs c++ pipeline (#46935)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/46935

Now, whenever the earlyjs c++ pipeline handles an error, it'll invoke callbacks registered via javascript.

## The API

```
if (global.RN$registerExceptionListener != null) {
  global.RN$registerExceptionListener(
    (error: ExtendedExceptionData & {preventDefault: () => mixed}) => {
      error.preventDefault();

      // show logbox
    },
  );
}
```

## The Future API
We want something more aligned with the HTML spec:

```
addEventListener('error', (event) => {
   event.preventDefault();
   // show logbox
});
```

## Fatals vs soft errors
The earlyjs pipeline covers just main bundle execution for now.

So, it displays logbox only if there was a soft error. If there was a fatal error, it'll just continue to display redbox as usual.

Changelog: [Internal]

Reviewed By: javache

Differential Revision: D63769385

fbshipit-source-id: 9c01e1cfe0ec80842af2e5bcfbf0adfb040dbcf3
This commit is contained in:
Ramanpreet Nara
2024-10-10 16:03:02 -07:00
committed by Facebook GitHub Bot
parent 509abff063
commit 9619ba5ce6
6 changed files with 173 additions and 10 deletions
+18 -5
View File
@@ -52,6 +52,17 @@ if (__DEV__) {
isLogBoxInstalled = true;
if (global.RN$registerExceptionListener != null) {
global.RN$registerExceptionListener(
(error: ExtendedExceptionData & {preventDefault: () => mixed}) => {
if (!error.isFatal) {
error.preventDefault();
addException(error);
}
},
);
}
// Trigger lazy initialization of module.
require('../NativeModules/specs/NativeLogBox');
@@ -122,13 +133,15 @@ if (__DEV__) {
}
},
addException(error: ExtendedExceptionData): void {
if (isLogBoxInstalled) {
LogBoxData.addException(error);
}
},
addException,
};
function addException(error: ExtendedExceptionData): void {
if (isLogBoxInstalled) {
LogBoxData.addException(error);
}
}
const isRCTLogAdviceWarning = (...args: Array<mixed>) => {
// RCTLogAdvice is a native logging function designed to show users
// a message in the console, but not show it to them in Logbox.
@@ -8,6 +8,7 @@
#include "JsErrorHandler.h"
#include <cxxreact/ErrorUtils.h>
#include <glog/logging.h>
#include <react/bridging/Bridging.h>
#include <string>
#include "StackTraceParser.h"
@@ -35,10 +36,65 @@ bool isEmptyString(jsi::Runtime& runtime, const jsi::Value& value) {
std::string stringifyToCpp(jsi::Runtime& runtime, const jsi::Value& value) {
return value.toString(runtime).utf8(runtime);
}
bool isTruthy(jsi::Runtime& runtime, const jsi::Value& value) {
auto Boolean = runtime.global().getPropertyAsFunction(runtime, "Boolean");
return Boolean.call(runtime, value).getBool();
}
} // namespace
namespace facebook::react {
template <>
struct Bridging<JsErrorHandler::ParsedError::StackFrame> {
static jsi::Value toJs(
jsi::Runtime& runtime,
const JsErrorHandler::ParsedError::StackFrame& frame) {
auto stackFrame = jsi::Object(runtime);
auto file = bridging::toJs(runtime, frame.file, nullptr);
auto lineNumber = bridging::toJs(runtime, frame.lineNumber, nullptr);
auto column = bridging::toJs(runtime, frame.column, nullptr);
stackFrame.setProperty(runtime, "file", file);
stackFrame.setProperty(runtime, "methodName", frame.methodName);
stackFrame.setProperty(runtime, "lineNumber", lineNumber);
stackFrame.setProperty(runtime, "column", column);
return stackFrame;
}
};
template <>
struct Bridging<JsErrorHandler::ParsedError> {
static jsi::Value toJs(
jsi::Runtime& runtime,
const JsErrorHandler::ParsedError& error) {
auto data = jsi::Object(runtime);
data.setProperty(runtime, "message", error.message);
data.setProperty(
runtime,
"originalMessage",
bridging::toJs(runtime, error.originalMessage, nullptr));
data.setProperty(
runtime, "name", bridging::toJs(runtime, error.name, nullptr));
data.setProperty(
runtime,
"componentStack",
bridging::toJs(runtime, error.componentStack, nullptr));
auto stack = jsi::Array(runtime, error.stack.size());
for (size_t i = 0; i < error.stack.size(); i++) {
auto& frame = error.stack[i];
stack.setValueAtIndex(runtime, i, bridging::toJs(runtime, frame));
}
data.setProperty(runtime, "stack", stack);
data.setProperty(runtime, "id", error.id);
data.setProperty(runtime, "isFatal", error.isFatal);
data.setProperty(runtime, "extraData", error.extraData);
return data;
}
};
std::ostream& operator<<(
std::ostream& os,
const JsErrorHandler::ParsedError::StackFrame& frame) {
@@ -96,12 +152,12 @@ void JsErrorHandler::handleError(
jsi::JSError& error,
bool isFatal) {
// TODO: Current error parsing works and is stable. Can investigate using
// REGEX_HERMES to get additional Hermes data, though it requires JS setup.
if (isFatal) {
_hasHandledFatalError = true;
}
// REGEX_HERMES to get additional Hermes data, though it requires JS setup
if (_isRuntimeReady) {
if (isFatal) {
_hasHandledFatalError = true;
}
try {
handleJSError(runtime, error, isFatal);
return;
@@ -114,6 +170,13 @@ void JsErrorHandler::handleError(
}
}
emitError(runtime, error, isFatal);
}
void JsErrorHandler::emitError(
jsi::Runtime& runtime,
jsi::JSError& error,
bool isFatal) {
auto message = error.getMessage();
auto errorObj = error.value().getObject(runtime);
auto componentStackValue = errorObj.getProperty(runtime, "componentStack");
@@ -182,9 +245,48 @@ void JsErrorHandler::handleError(
.extraData = std::move(extraData),
};
auto data = bridging::toJs(runtime, parsedError).asObject(runtime);
auto isComponentError =
isTruthy(runtime, errorObj.getProperty(runtime, "isComponentError"));
data.setProperty(runtime, "isComponentError", isComponentError);
std::shared_ptr<bool> shouldPreventDefault = std::make_shared<bool>(false);
auto preventDefault = jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "preventDefault"),
0,
[shouldPreventDefault](
jsi::Runtime& /*rt*/,
const jsi::Value& /*thisVal*/,
const jsi::Value* /*args*/,
size_t /*count*/) {
*shouldPreventDefault = true;
return jsi::Value::undefined();
});
data.setProperty(runtime, "preventDefault", preventDefault);
for (auto& errorListener : _errorListeners) {
errorListener(runtime, jsi::Value(runtime, data));
}
if (*shouldPreventDefault) {
return;
}
if (isFatal) {
_hasHandledFatalError = true;
}
_onJsError(runtime, parsedError);
}
void JsErrorHandler::registerErrorListener(
const std::function<void(jsi::Runtime&, jsi::Value)>& errorListener) {
_errorListeners.push_back(errorListener);
}
bool JsErrorHandler::hasHandledFatalError() {
return _hasHandledFatalError;
}
@@ -45,6 +45,8 @@ class JsErrorHandler {
void handleError(jsi::Runtime& runtime, jsi::JSError& error, bool isFatal);
bool hasHandledFatalError();
void registerErrorListener(
const std::function<void(jsi::Runtime&, jsi::Value)>& listener);
void setRuntimeReady();
bool isRuntimeReady();
void notifyOfFatalError();
@@ -60,6 +62,9 @@ class JsErrorHandler {
OnJsError _onJsError;
bool _hasHandledFatalError;
bool _isRuntimeReady{};
std::vector<std::function<void(jsi::Runtime&, jsi::Value)>> _errorListeners;
void emitError(jsi::Runtime& runtime, jsi::JSError& error, bool isFatal);
};
} // namespace facebook::react
@@ -51,6 +51,7 @@ Pod::Spec.new do |s|
s.dependency "React-jsi"
s.dependency "React-cxxreact"
s.dependency "glog"
s.dependency "ReactCommon/turbomodule/bridging"
add_dependency(s, "React-debug")
if ENV['USE_HERMES'] == nil || ENV['USE_HERMES'] == "1"
@@ -429,6 +429,47 @@ void ReactInstance::initializeRuntime(
return jsi::Value(true);
}));
defineReadOnlyGlobal(
runtime,
"RN$registerExceptionListener",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "registerExceptionListener"),
1,
[errorListeners = std::vector<std::shared_ptr<jsi::Function>>(),
jsErrorHandler = jsErrorHandler_](
jsi::Runtime& runtime,
const jsi::Value& /*unused*/,
const jsi::Value* args,
size_t count) mutable {
if (count < 1) {
throw jsi::JSError(
runtime,
"registerExceptionListener: requires 1 argument: fn");
}
if (!args[0].isObject() ||
!args[0].getObject(runtime).isFunction(runtime)) {
throw jsi::JSError(
runtime,
"registerExceptionListener: The first argument must be a function");
}
auto errorListener = std::make_shared<jsi::Function>(
args[0].getObject(runtime).getFunction(runtime));
errorListeners.emplace_back(errorListener);
jsErrorHandler->registerErrorListener(
[weakErrorListener = std::weak_ptr<jsi::Function>(
errorListener)](jsi::Runtime& runtime, jsi::Value data) {
if (auto strongErrorListener = weakErrorListener.lock()) {
strongErrorListener->call(runtime, data);
}
});
return jsi::Value::undefined();
}));
defineReadOnlyGlobal(
runtime,
"RN$registerCallableModule",
@@ -17,6 +17,7 @@
#include <react/runtime/BufferedRuntimeExecutor.h>
#include <react/runtime/JSRuntimeFactory.h>
#include <react/runtime/TimerManager.h>
#include <vector>
namespace facebook::react {