From 50270fc3fb2ee5f749323f7e06de6e24045e65b8 Mon Sep 17 00:00:00 2001 From: Ramanpreet Nara Date: Fri, 25 Oct 2024 12:01:17 -0700 Subject: [PATCH] earlyjs: Integrate c++ pipeline with console.error Summary: ## Changes If the c++ pipeline is active: If someone calls console.error: - The c++ pipeline will report it as a soft error If someone reports an error: - The c++ pipeline will log it via console.error Changelog: [Internal] Differential Revision: D64506069 --- .../Libraries/Core/ExceptionsManager.js | 31 ++++++++----- .../jserrorhandler/JsErrorHandler.cpp | 43 ++++++++++++++++--- .../jserrorhandler/JsErrorHandler.h | 14 +++++- .../react/runtime/ReactInstance.cpp | 28 ++++++++++-- 4 files changed, 96 insertions(+), 20 deletions(-) diff --git a/packages/react-native/Libraries/Core/ExceptionsManager.js b/packages/react-native/Libraries/Core/ExceptionsManager.js index 09c5941f06240f..d10b6245e28682 100644 --- a/packages/react-native/Libraries/Core/ExceptionsManager.js +++ b/packages/react-native/Libraries/Core/ExceptionsManager.js @@ -142,6 +142,7 @@ let inExceptionHandler = false; */ function handleException(e: mixed, isFatal: boolean) { // TODO(T196834299): We should really use a c++ turbomodule for this + const reportToConsole = true; if (!global.RN$handleException || !global.RN$handleException(e, isFatal)) { let error: Error; if (e instanceof Error) { @@ -158,7 +159,7 @@ function handleException(e: mixed, isFatal: boolean) { /* $FlowFixMe[class-object-subtyping] added when improving typing for this * parameters */ // $FlowFixMe[incompatible-call] - reportException(error, isFatal, /*reportToConsole*/ true); + reportException(error, isFatal, reportToConsole); } finally { inExceptionHandler = false; } @@ -173,7 +174,10 @@ function reactConsoleErrorHandler(...args) { if (!console.reportErrorsAsExceptions) { return; } - if (inExceptionHandler) { + if ( + inExceptionHandler || + (global.RN$inExceptionHandler && global.RN$inExceptionHandler()) + ) { // The fundamental trick here is that are multiple entry point to logging errors: // (see D19743075 for more background) // @@ -227,14 +231,21 @@ function reactConsoleErrorHandler(...args) { error.name = 'console.error'; } - reportException( - /* $FlowFixMe[class-object-subtyping] added when improving typing for this - * parameters */ - // $FlowFixMe[incompatible-call] - error, - false, // isFatal - false, // reportToConsole - ); + const isFatal = false; + const reportToConsole = false; + if ( + !global.RN$handleException || + !global.RN$handleException(error, isFatal, reportToConsole) + ) { + reportException( + /* $FlowFixMe[class-object-subtyping] added when improving typing for this + * parameters */ + // $FlowFixMe[incompatible-call] + error, + isFatal, + reportToConsole, + ); + } } /** diff --git a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp index 69652a173caaa7..2c156e6e43126d 100644 --- a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp +++ b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp @@ -62,6 +62,22 @@ jsi::Object wrapInErrorIfNecessary( : Error.callAsConstructor(runtime, value).getObject(runtime); return error; } + +class SetFalseOnDestruct { + std::shared_ptr _value; + + public: + SetFalseOnDestruct(const SetFalseOnDestruct&) = delete; + SetFalseOnDestruct& operator=(const SetFalseOnDestruct&) = delete; + SetFalseOnDestruct(SetFalseOnDestruct&&) = delete; + SetFalseOnDestruct& operator=(SetFalseOnDestruct&&) = delete; + explicit SetFalseOnDestruct(std::shared_ptr value) + : _value(std::move(value)) {} + ~SetFalseOnDestruct() { + *_value = false; + } +}; + } // namespace namespace facebook::react { @@ -162,7 +178,7 @@ std::ostream& operator<<( JsErrorHandler::JsErrorHandler(JsErrorHandler::OnJsError onJsError) : _onJsError(std::move(onJsError)), - _hasHandledFatalError(false){ + _inErrorHandler(std::make_shared(false)){ }; @@ -171,7 +187,8 @@ JsErrorHandler::~JsErrorHandler() {} void JsErrorHandler::handleError( jsi::Runtime& runtime, jsi::JSError& error, - bool isFatal) { + bool isFatal, + bool logToConsole) { // TODO: Current error parsing works and is stable. Can investigate using // REGEX_HERMES to get additional Hermes data, though it requires JS setup if (_isRuntimeReady) { @@ -191,13 +208,17 @@ void JsErrorHandler::handleError( } } - emitError(runtime, error, isFatal); + handleErrorWithCppPipeline(runtime, error, isFatal, logToConsole); } -void JsErrorHandler::emitError( +void JsErrorHandler::handleErrorWithCppPipeline( jsi::Runtime& runtime, jsi::JSError& error, - bool isFatal) { + bool isFatal, + bool logToConsole) { + *_inErrorHandler = true; + SetFalseOnDestruct temp{_inErrorHandler}; + auto message = error.getMessage(); auto errorObj = wrapInErrorIfNecessary(runtime, error.value()); auto componentStackValue = errorObj.getProperty(runtime, "componentStack"); @@ -278,6 +299,14 @@ void JsErrorHandler::emitError( isTruthy(runtime, errorObj.getProperty(runtime, "isComponentError")); data.setProperty(runtime, "isComponentError", isComponentError); + if (logToConsole) { + auto console = runtime.global().getPropertyAsObject(runtime, "console"); + auto errorFn = console.getPropertyAsFunction(runtime, "error"); + auto finalMessage = + jsi::String::createFromUtf8(runtime, parsedError.message); + errorFn.callWithThis(runtime, console, finalMessage); + } + std::shared_ptr shouldPreventDefault = std::make_shared(false); auto preventDefault = jsi::Function::createFromHostFunction( runtime, @@ -330,4 +359,8 @@ void JsErrorHandler::notifyOfFatalError() { _hasHandledFatalError = true; } +bool JsErrorHandler::inErrorHandler() { + return *_inErrorHandler; +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h index b8d784ac63eb97..8d6f2c6467586c 100644 --- a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h +++ b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h @@ -43,13 +43,18 @@ class JsErrorHandler { explicit JsErrorHandler(OnJsError onJsError); ~JsErrorHandler(); - void handleError(jsi::Runtime& runtime, jsi::JSError& error, bool isFatal); + void handleError( + jsi::Runtime& runtime, + jsi::JSError& error, + bool isFatal, + bool logToConsole = true); bool hasHandledFatalError(); void registerErrorListener( const std::function& listener); void setRuntimeReady(); bool isRuntimeReady(); void notifyOfFatalError(); + bool inErrorHandler(); private: /** @@ -62,9 +67,14 @@ class JsErrorHandler { OnJsError _onJsError; bool _hasHandledFatalError; bool _isRuntimeReady{}; + std::shared_ptr _inErrorHandler; std::vector> _errorListeners; - void emitError(jsi::Runtime& runtime, jsi::JSError& error, bool isFatal); + void handleErrorWithCppPipeline( + jsi::Runtime& runtime, + jsi::JSError& error, + bool isFatal, + bool logToConsole); }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp index 15ac4c8465c3a3..20a268761a5dee 100644 --- a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp +++ b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp @@ -409,6 +409,21 @@ void ReactInstance::initializeRuntime( defineReactInstanceFlags(runtime, options); + defineReadOnlyGlobal( + runtime, + "RN$inExceptionHandler", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "inExceptionHandler"), + 0, + [jsErrorHandler = jsErrorHandler_]( + jsi::Runtime& /*runtime*/, + const jsi::Value& /*unused*/, + const jsi::Value* /*args*/, + size_t /*count*/) { + return jsErrorHandler->inErrorHandler(); + })); + // TODO(T196834299): We should really use a C++ turbomodule for this defineReadOnlyGlobal( runtime, @@ -416,7 +431,7 @@ void ReactInstance::initializeRuntime( jsi::Function::createFromHostFunction( runtime, jsi::PropNameID::forAscii(runtime, "handleException"), - 2, + 3, [jsErrorHandler = jsErrorHandler_]( jsi::Runtime& runtime, const jsi::Value& /*unused*/, @@ -425,7 +440,7 @@ void ReactInstance::initializeRuntime( if (count < 2) { throw jsi::JSError( runtime, - "handleException requires 2 arguments: error, isFatal"); + "handleException requires 3 arguments: error, isFatal, logToConsole (optional)"); } auto isFatal = isTruthy(runtime, args[1]); @@ -439,7 +454,14 @@ void ReactInstance::initializeRuntime( auto jsError = jsi::JSError(runtime, jsi::Value(runtime, args[0])); - jsErrorHandler->handleError(runtime, jsError, isFatal); + + if (count == 2) { + jsErrorHandler->handleError(runtime, jsError, isFatal); + } else { + auto logToConsole = isTruthy(runtime, args[2]); + jsErrorHandler->handleError( + runtime, jsError, isFatal, logToConsole); + } return jsi::Value(true); }));