diff --git a/ReactCommon/jserrorhandler/BUCK b/ReactCommon/jserrorhandler/BUCK new file mode 100644 index 00000000000..e7ef279bb10 --- /dev/null +++ b/ReactCommon/jserrorhandler/BUCK @@ -0,0 +1,31 @@ +load("//tools/build_defs/oss:rn_defs.bzl", "ANDROID", "APPLE", "CXX", "react_native_xplat_target", "rn_xplat_cxx_library") + +# TODO: Expolre merging this module into venice so we don't to load this library seperately +rn_xplat_cxx_library( + name = "jserrorhandler", + srcs = glob(["*.cpp"]), + header_namespace = "", + exported_headers = {"JsErrorHandler/JsErrorHandler.h": "JsErrorHandler.h"}, + compiler_flags = [ + "-fexceptions", + "-frtti", + ], + labels = [ + "pfh:ReactNative_CommonInfrastructurePlaceholder", + "supermodule:xplat/default/public.react_native.infra", + ], + platforms = (ANDROID, APPLE, CXX), + preprocessor_flags = [ + "-DLOG_TAG=\"ReactNative\"", + "-DWITH_FBSYSTRACE=1", + ], + visibility = [ + "PUBLIC", + ], + deps = [ + "//xplat/folly:dynamic", + "//xplat/folly:json", + "//xplat/jsi:jsi", + react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), + ], +) diff --git a/ReactCommon/jserrorhandler/JsErrorHandler.cpp b/ReactCommon/jserrorhandler/JsErrorHandler.cpp new file mode 100644 index 00000000000..44f5b513d41 --- /dev/null +++ b/ReactCommon/jserrorhandler/JsErrorHandler.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "JsErrorHandler.h" +#include +#include +#include +#include +#include + +namespace facebook { +namespace react { + +static MapBuffer +parseErrorStack(const jsi::JSError &error, bool isFatal, bool isHermes) { + /** + * This parses the different stack traces and puts them into one format + * This borrows heavily from TraceKit (https://github.com/occ/TraceKit) + * This is the same regex from stacktrace-parser.js. + */ + const std::regex REGEX_CHROME( + R"(^\s*at (?:(?:(?:Anonymous function)?|((?:\[object object\])?\S+(?: \[as \S+\])?)) )?\(?((?:file|http|https):.*?):(\d+)(?::(\d+))?\)?\s*$)"); + const std::regex REGEX_GECKO( + R"(^(?:\s*([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$)"); + const std::regex REGEX_NODE( + R"(^\s*at (?:((?:\[object object\])?\S+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$)"); + + // Capture groups for Hermes (from parseHermesStack.js): + // 1. function name + // 2. is this a native stack frame? + // 3. is this a bytecode address or a source location? + // 4. source URL (filename) + // 5. line number (1 based) + // 6. column number (1 based) or virtual offset (0 based) + const std::regex REGEX_HERMES( + R"(^ {4}at (.+?)(?: \((native)\)?| \((address at )?(.*?):(\d+):(\d+)\))$)"); + + std::string line; + std::stringstream strStream(error.getStack()); + + auto errorObj = MapBufferBuilder(); + std::vector frames; + + while (std::getline(strStream, line, '\n')) { + auto frame = MapBufferBuilder(); + auto searchResults = std::smatch{}; + + if (isHermes) { + if (std::regex_search(line, searchResults, REGEX_HERMES)) { + std::string str2 = std::string(searchResults[2]); + if (str2.compare("native")) { + frame.putString(0, std::string(searchResults[4])); + frame.putString(1, std::string(searchResults[1])); + frame.putInt(2, std::stoi(searchResults[5])); + frame.putInt(3, std::stoi(searchResults[6])); + frames.push_back(frame.build()); + } + } + } else { + if (std::regex_search(line, searchResults, REGEX_GECKO)) { + frame.putString(0, std::string(searchResults[3])); + frame.putString(1, std::string(searchResults[1])); + frame.putInt(2, std::stoi(searchResults[4])); + frame.putInt(3, std::stoi(searchResults[5])); + } else if ( + std::regex_search(line, searchResults, REGEX_CHROME) || + std::regex_search(line, searchResults, REGEX_NODE)) { + frame.putString(0, std::string(searchResults[2])); + frame.putString(1, std::string(searchResults[1])); + frame.putInt(2, std::stoi(searchResults[3])); + frame.putInt(3, std::stoi(searchResults[4])); + } else { + continue; + } + frames.push_back(frame.build()); + } + } + errorObj.putMapBufferList(4, std::move(frames)); + errorObj.putString(5, error.getMessage()); + // TODO: If needed, can increment exceptionId by 1 each time + errorObj.putInt(6, 0); + errorObj.putBool(7, isFatal); + return errorObj.build(); +} + +JsErrorHandler::JsErrorHandler( + JsErrorHandler::JsErrorHandlingFunc jsErrorHandlingFunc) { + this->_jsErrorHandlingFunc = jsErrorHandlingFunc; +}; + +JsErrorHandler::~JsErrorHandler() {} + +void JsErrorHandler::handleJsError(const 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. + MapBuffer errorMap = parseErrorStack(error, isFatal, false); + _jsErrorHandlingFunc(std::move(errorMap)); +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/jserrorhandler/JsErrorHandler.h b/ReactCommon/jserrorhandler/JsErrorHandler.h new file mode 100644 index 00000000000..a7b51e9c376 --- /dev/null +++ b/ReactCommon/jserrorhandler/JsErrorHandler.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +namespace facebook { +namespace react { + +const int FILE_KEY_OF_JS_ERROR = 0; +const int METHOD_NAME_KEY_OF_JS_ERROR = 1; +const int LINE_NUMBER_KEY_OF_JS_ERROR = 2; +const int COLUMN_KEY_OF_JS_ERROR = 3; +const int FRAMES_KEY_OF_JS_ERROR = 4; +const int MESSAGE_KEY_OF_JS_ERROR = 5; +const int ID_KEY_OF_JS_ERROR = 6; +const int IS_FATAL_KEY_OF_JS_ERROR = 7; + +class JsErrorHandler { + public: + using JsErrorHandlingFunc = std::function; + + JsErrorHandler(JsErrorHandlingFunc jsErrorHandlingFunc); + ~JsErrorHandler(); + + void handleJsError(const jsi::JSError &error, bool isFatal); + + private: + JsErrorHandlingFunc _jsErrorHandlingFunc; +}; + +} // namespace react +} // namespace facebook