Skip to content

Commit

Permalink
Integrate logbox into earlyjs c++ pipeline (#46935)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #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]

Differential Revision: D63769385

Reviewed By: javache
  • Loading branch information
RSNara authored and facebook-github-bot committed Oct 9, 2024
1 parent a119496 commit 35b584d
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 10 deletions.
23 changes: 18 additions & 5 deletions packages/react-native/Libraries/LogBox/LogBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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.
Expand Down
112 changes: 107 additions & 5 deletions packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <cxxreact/ErrorUtils.h>
#include <glog/logging.h>
#include <jserrorhandler/StackTraceParser.h>
#include <react/bridging/Bridging.h>
#include <string>

using namespace facebook;
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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");
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
41 changes: 41 additions & 0 deletions packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <react/runtime/BufferedRuntimeExecutor.h>
#include <react/runtime/JSRuntimeFactory.h>
#include <react/runtime/TimerManager.h>
#include <vector>

namespace facebook::react {

Expand Down

0 comments on commit 35b584d

Please sign in to comment.