Ramanpreet Nara
2024-10-10 16:03:02 -07:00
committed by Facebook GitHub Bot
parent e4645d033a
commit 934af0c59e
7 changed files with 1643 additions and 102 deletions
@@ -8,7 +8,7 @@ set(CMAKE_VERBOSE_MAKEFILE on)
add_compile_options(-std=c++20)
file(GLOB_RECURSE js_error_handler_SRC CONFIGURE_DEPENDS *.cpp)
file(GLOB js_error_handler_SRC CONFIGURE_DEPENDS *.cpp)
add_library(
jserrorhandler
OBJECT
@@ -8,15 +8,33 @@
#include "JsErrorHandler.h"
#include <cxxreact/ErrorUtils.h>
#include <glog/logging.h>
#include <regex>
#include <sstream>
#include <string>
#include <vector>
#include "StackTraceParser.h"
using namespace facebook;
namespace {
std::string quote(const std::string& view) {
return "\"" + view + "\"";
}
int nextExceptionId() {
static int exceptionId = 0;
return exceptionId++;
}
bool isLooselyNull(const jsi::Value& value) {
return value.isNull() || value.isUndefined();
}
bool isEmptyString(jsi::Runtime& runtime, const jsi::Value& value) {
return jsi::Value::strictEquals(
runtime, value, jsi::String::createFromUtf8(runtime, ""));
}
std::string stringifyToCpp(jsi::Runtime& runtime, const jsi::Value& value) {
return value.toString(runtime).utf8(runtime);
}
} // namespace
namespace facebook::react {
@@ -65,97 +83,6 @@ std::ostream& operator<<(
return os;
}
// TODO(T198763073): Migrate away from std::regex in this function
static JsErrorHandler::ParsedError parseErrorStack(
jsi::Runtime& runtime,
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.
*/
// @lint-ignore CLANGTIDY facebook-hte-StdRegexIsAwful
const std::regex REGEX_CHROME(
R"(^\s*at (?:(?:(?:Anonymous function)?|((?:\[object object\])?\S+(?: \[as \S+\])?)) )?\(?((?:file|http|https):.*?):(\d+)(?::(\d+))?\)?\s*$)");
// @lint-ignore CLANGTIDY facebook-hte-StdRegexIsAwful
const std::regex REGEX_GECKO(
R"(^(?:\s*([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$)");
// @lint-ignore CLANGTIDY facebook-hte-StdRegexIsAwful
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)
// @lint-ignore CLANGTIDY facebook-hte-StdRegexIsAwful
const std::regex REGEX_HERMES(
R"(^ {4}at (.+?)(?: \((native)\)?| \((address at )?(.*?):(\d+):(\d+)\))$)");
std::string line;
std::stringstream strStream(error.getStack());
std::vector<JsErrorHandler::ParsedError::StackFrame> frames;
while (std::getline(strStream, line, '\n')) {
auto searchResults = std::smatch{};
if (isHermes) {
// @lint-ignore CLANGTIDY facebook-hte-StdRegexIsAwful
if (std::regex_search(line, searchResults, REGEX_HERMES)) {
std::string str2 = std::string(searchResults[2]);
if (str2.compare("native")) {
frames.push_back({
.file = std::string(searchResults[4]),
.methodName = std::string(searchResults[1]),
.lineNumber = std::stoi(searchResults[5]),
.column = std::stoi(searchResults[6]),
});
}
}
} else {
// @lint-ignore CLANGTIDY facebook-hte-StdRegexIsAwful
if (std::regex_search(line, searchResults, REGEX_GECKO)) {
frames.push_back({
.file = std::string(searchResults[3]),
.methodName = std::string(searchResults[1]),
.lineNumber = std::stoi(searchResults[4]),
.column = std::stoi(searchResults[5]),
});
} else if (
// @lint-ignore CLANGTIDY facebook-hte-StdRegexIsAwful
std::regex_search(line, searchResults, REGEX_CHROME) ||
// @lint-ignore CLANGTIDY facebook-hte-StdRegexIsAwful
std::regex_search(line, searchResults, REGEX_NODE)) {
frames.push_back({
.file = std::string(searchResults[2]),
.methodName = std::string(searchResults[1]),
.lineNumber = std::stoi(searchResults[3]),
.column = std::stoi(searchResults[4]),
});
} else {
continue;
}
}
}
return {
.message = "EarlyJsError: " + error.getMessage(),
.originalMessage = std::nullopt,
.name = std::nullopt,
.componentStack = std::nullopt,
.stack = std::move(frames),
.id = 0,
.isFatal = isFatal,
.extraData = jsi::Object(runtime),
};
}
JsErrorHandler::JsErrorHandler(JsErrorHandler::OnJsError onJsError)
: _onJsError(std::move(onJsError)),
_hasHandledFatalError(false){
@@ -183,8 +110,75 @@ void JsErrorHandler::handleFatalError(
<< "Original js error: " << error.getMessage() << std::endl;
}
}
// This is a hacky way to get Hermes stack trace.
ParsedError parsedError = parseErrorStack(runtime, error, true, false);
auto message = error.getMessage();
auto errorObj = error.value().getObject(runtime);
auto componentStackValue = errorObj.getProperty(runtime, "componentStack");
if (!isLooselyNull(componentStackValue)) {
message += "\n" + stringifyToCpp(runtime, componentStackValue);
}
auto nameValue = errorObj.getProperty(runtime, "name");
auto name = (isLooselyNull(nameValue) || isEmptyString(runtime, nameValue))
? std::nullopt
: std::optional(stringifyToCpp(runtime, nameValue));
if (name && !message.starts_with(*name + ": ")) {
message = *name + ": " + message;
}
auto jsEngineValue = errorObj.getProperty(runtime, "jsEngine");
if (!isLooselyNull(jsEngineValue)) {
message += ", js engine: " + stringifyToCpp(runtime, jsEngineValue);
}
// TODO: What about spreading in decoratedExtraDataKey?
auto extraData = jsi::Object(runtime);
extraData.setProperty(runtime, "jsEngine", jsEngineValue);
extraData.setProperty(runtime, "rawStack", error.getStack());
auto cause = errorObj.getProperty(runtime, "cause");
if (cause.isObject()) {
auto causeObj = cause.asObject(runtime);
// TODO: Consider just forwarding all properties. For now, just forward the
// stack properties to maintain symmetry with js pipeline
auto stackSymbols = causeObj.getProperty(runtime, "stackSymbols");
extraData.setProperty(runtime, "stackSymbols", stackSymbols);
auto stackReturnAddresses =
causeObj.getProperty(runtime, "stackReturnAddresses");
extraData.setProperty(
runtime, "stackReturnAddresses", stackReturnAddresses);
auto stackElements = causeObj.getProperty(runtime, "stackElements");
extraData.setProperty(runtime, "stackElements", stackElements);
}
auto originalMessage = message == error.getMessage()
? std::nullopt
: std::optional(error.getMessage());
auto componentStack = !componentStackValue.isString()
? std::nullopt
: std::optional(componentStackValue.asString(runtime).utf8(runtime));
auto isHermes = runtime.global().hasProperty(runtime, "HermesInternal");
auto stackFrames = StackTraceParser::parse(isHermes, error.getStack());
auto id = nextExceptionId();
ParsedError parsedError = {
.message = "EarlyJsError: " + message,
.originalMessage = originalMessage,
.name = name,
.componentStack = componentStack,
.stack = stackFrames,
.id = id,
.isFatal = true,
.extraData = std::move(extraData),
};
_onJsError(runtime, parsedError);
}
@@ -22,7 +22,7 @@ folly_version = folly_config[:version]
folly_dep_name = folly_config[:dep_name]
boost_config = get_boost_config()
boost_compiler_flags = boost_config[:compiler_flags]
boost_compiler_flags = boost_config[:compiler_flags]
react_native_path = ".."
Pod::Spec.new do |s|
@@ -35,7 +35,7 @@ Pod::Spec.new do |s|
s.platforms = min_supported_versions
s.source = source
s.header_dir = "jserrorhandler"
s.source_files = "JsErrorHandler.{cpp,h}"
s.source_files = "JsErrorHandler.{cpp,h}", "StackTraceParser.{cpp,h}"
s.pod_target_xcconfig = {
"USE_HEADERMAP" => "YES",
"CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard()
@@ -52,7 +52,7 @@ Pod::Spec.new do |s|
s.dependency "React-cxxreact"
s.dependency "glog"
add_dependency(s, "React-debug")
if ENV['USE_HERMES'] == nil || ENV['USE_HERMES'] == "1"
s.dependency 'hermes-engine'
end
@@ -0,0 +1,317 @@
/*
* 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 "StackTraceParser.h"
#include <glog/logging.h>
#include <charconv>
#include <optional>
#include <regex>
#include <sstream>
#include <string>
using namespace facebook::react;
const std::string UNKNOWN_FUNCTION = "<unknown>";
// TODO(T198763073): Migrate away from std::regex in this file
// @lint-ignore-every CLANGTIDY facebook-hte-StdRegexIsAwful
/**
* Stack trace parsing for other jsvms:
* Port of https://github.com/errwischt/stacktrace-parser
*/
namespace {
std::optional<int> toInt(std::string_view input) {
int out;
const std::from_chars_result result =
std::from_chars(input.data(), input.data() + input.size(), out);
if (result.ec == std::errc::invalid_argument ||
result.ec == std::errc::result_out_of_range) {
return std::nullopt;
}
return out;
}
JsErrorHandler::ParsedError::StackFrame parseStackFrame(
std::string_view file,
std::string_view methodName,
std::string_view lineStr,
std::string_view columnStr) {
JsErrorHandler::ParsedError::StackFrame frame;
frame.file = file.empty() ? std::nullopt : std::optional(file);
frame.methodName = !methodName.empty() ? methodName : UNKNOWN_FUNCTION;
frame.lineNumber = !lineStr.empty() ? toInt(lineStr) : std::nullopt;
auto columnOpt = !columnStr.empty() ? toInt(columnStr) : std::nullopt;
frame.column = columnOpt ? std::optional(*columnOpt - 1) : std::nullopt;
return frame;
}
std::optional<JsErrorHandler::ParsedError::StackFrame> parseChrome(
const std::string& line) {
static const std::regex chromeRe(
R"(^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\/|[a-z]:\\|\\\\).*?)(?::(\d+))?(?::(\d+))?\)?\s*$)",
std::regex::icase);
static const std::regex chromeEvalRe(R"(\((\S*)(?::(\d+))(?::(\d+))\))");
std::smatch match;
if (!std::regex_match(line, match, chromeRe)) {
return std::nullopt;
}
std::string methodName = match[1].str();
std::string file = match[2].str();
std::string lineStr = match[3].str();
std::string columnStr = match[4].str();
bool isNative = std::regex_search(file, std::regex("^native"));
bool isEval = std::regex_search(file, std::regex("^eval"));
std::string evalFile;
std::string evalLine;
std::string evalColumn;
if (isEval && std::regex_search(file, match, chromeEvalRe)) {
evalFile = match[1].str();
evalLine = match[2].str();
evalColumn = match[3].str();
file = evalFile;
lineStr = evalLine;
columnStr = evalColumn;
}
std::string actualFile = !isNative ? file : "";
return parseStackFrame(actualFile, methodName, lineStr, columnStr);
}
std::optional<JsErrorHandler::ParsedError::StackFrame> parseWinjs(
const std::string& line) {
static const std::regex winjsRe(
R"(^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$)",
std::regex::icase);
std::smatch match;
if (!std::regex_match(line, match, winjsRe)) {
return std::nullopt;
}
std::string methodName = match[1].str();
std::string file = match[2].str();
std::string lineStr = match[3].str();
std::string columnStr = match[4].str();
return parseStackFrame(file, methodName, lineStr, columnStr);
}
std::optional<JsErrorHandler::ParsedError::StackFrame> parseGecko(
const std::string& line) {
static const std::regex geckoRe(
R"(^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$)",
std::regex::icase);
static const std::regex geckoEvalRe(
R"((\S+) line (\d+)(?: > eval line \d+)* > eval)", std::regex::icase);
std::smatch match;
if (!std::regex_match(line, match, geckoRe)) {
return std::nullopt;
}
std::string methodName = match[1].str();
std::string tmpStr = match[2].str();
std::string file = match[3].str();
std::string lineStr = match[4].str();
std::string columnStr = match[5].str();
bool isEval = std::regex_search(file, std::regex(" > eval"));
std::string evalFile;
std::string evalLine;
if (isEval && std::regex_search(file, match, geckoEvalRe)) {
evalFile = match[1].str();
evalLine = match[2].str();
file = evalFile;
lineStr = evalLine;
columnStr = ""; // No column number in eval
}
return parseStackFrame(file, methodName, lineStr, columnStr);
}
std::optional<JsErrorHandler::ParsedError::StackFrame> parseJSC(
const std::string& line) {
static const std::regex javaScriptCoreRe(
R"(^\s*(?:([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$)",
std::regex::icase);
std::smatch match;
if (!std::regex_match(line, match, javaScriptCoreRe)) {
return std::nullopt;
}
std::string methodName = match[1].str();
std::string tmpStr =
match[2].str(); // This captures any string within parentheses if present
std::string file = match[3].str();
std::string lineStr = match[4].str();
std::string columnStr = match[5].str();
return parseStackFrame(file, methodName, lineStr, columnStr);
}
std::optional<JsErrorHandler::ParsedError::StackFrame> parseNode(
const std::string& line) {
static const std::regex nodeRe(
R"(^\s*at (?:((?:\[object object\])?[^\\/]+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$)",
std::regex::icase);
std::smatch match;
if (!std::regex_match(line, match, nodeRe)) {
return std::nullopt;
}
std::string methodName = match[1].str();
std::string file = match[2].str();
std::string lineStr = match[3].str();
std::string columnStr = match[4].str();
return parseStackFrame(file, methodName, lineStr, columnStr);
}
std::vector<JsErrorHandler::ParsedError::StackFrame> parseOthers(
const std::string& stackString) {
std::vector<JsErrorHandler::ParsedError::StackFrame> stack;
std::istringstream iss(stackString);
std::string line;
while (std::getline(iss, line)) {
std::optional<JsErrorHandler::ParsedError::StackFrame> frame =
parseChrome(line);
if (!frame) {
frame = parseWinjs(line);
}
if (!frame) {
frame = parseGecko(line);
}
if (!frame) {
frame = parseNode(line);
}
if (!frame) {
frame = parseJSC(line);
}
if (frame) {
stack.push_back(*frame);
}
}
return stack;
}
} // namespace
/**
* Hermes stack trace parsing logic
*/
namespace {
struct HermesStackLocation {
std::string type;
std::string sourceUrl;
int line1Based{};
int column1Based{};
int virtualOffset0Based{};
};
struct HermesStackEntry {
std::string type;
std::string functionName;
HermesStackLocation location;
int count{};
};
bool isInternalBytecodeSourceUrl(const std::string& sourceUrl) {
return sourceUrl == "InternalBytecode.js";
}
std::vector<JsErrorHandler::ParsedError::StackFrame> convertHermesStack(
const std::vector<HermesStackEntry>& stack) {
std::vector<JsErrorHandler::ParsedError::StackFrame> frames;
for (const auto& entry : stack) {
if (entry.type != "FRAME") {
continue;
}
if (entry.location.type == "NATIVE" ||
entry.location.type == "INTERNAL_BYTECODE") {
continue;
}
JsErrorHandler::ParsedError::StackFrame frame;
frame.methodName = entry.functionName;
frame.file = entry.location.sourceUrl;
frame.lineNumber = entry.location.line1Based;
if (entry.location.type == "SOURCE") {
frame.column = entry.location.column1Based - 1;
} else {
frame.column = entry.location.virtualOffset0Based;
}
frames.push_back(frame);
}
return frames;
}
HermesStackEntry parseLine(const std::string& line) {
static const std::regex RE_FRAME(
R"(^ {4}at (.+?)(?: \((native)\)?| \((address at )?(.*?):(\d+):(\d+)\))$)");
static const std::regex RE_SKIPPED(R"(^ {4}... skipping (\d+) frames$)");
HermesStackEntry entry;
std::smatch match;
if (std::regex_match(line, match, RE_FRAME)) {
entry.type = "FRAME";
entry.functionName = match[1].str();
std::string type = match[2].str();
std::string addressAt = match[3].str();
std::string sourceUrl = match[4].str();
if (type == "native") {
entry.location.type = "NATIVE";
} else {
int line1Based = std::stoi(match[5].str());
int columnOrOffset = std::stoi(match[6].str());
if (addressAt == "address at ") {
if (isInternalBytecodeSourceUrl(sourceUrl)) {
entry.location = {
"INTERNAL_BYTECODE", sourceUrl, line1Based, 0, columnOrOffset};
} else {
entry.location = {
"BYTECODE", sourceUrl, line1Based, 0, columnOrOffset};
}
} else {
entry.location = {"SOURCE", sourceUrl, line1Based, columnOrOffset, 0};
}
}
return entry;
}
if (std::regex_match(line, match, RE_SKIPPED)) {
entry.type = "SKIPPED";
entry.count = std::stoi(match[1].str());
}
return entry;
}
std::vector<JsErrorHandler::ParsedError::StackFrame> parseHermes(
const std::string& stack) {
static const std::regex RE_COMPONENT_NO_STACK(R"(^ {4}at .*?$)");
std::istringstream stream(stack);
std::string line;
std::vector<HermesStackEntry> entries;
std::smatch match;
while (std::getline(stream, line)) {
if (line.empty()) {
continue;
}
HermesStackEntry entry = parseLine(line);
if (!entry.type.empty()) {
entries.push_back(entry);
continue;
}
if (std::regex_match(line, match, RE_COMPONENT_NO_STACK)) {
continue;
}
entries.clear();
}
return convertHermesStack(entries);
}
} // namespace
std::vector<JsErrorHandler::ParsedError::StackFrame> StackTraceParser::parse(
const bool isHermes,
const std::string& stackString) {
std::vector<JsErrorHandler::ParsedError::StackFrame> stackFrames =
isHermes ? parseHermes(stackString) : parseOthers(stackString);
return stackFrames;
}
@@ -0,0 +1,23 @@
/*
* 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.
*/
#pragma once
#include <string>
#include <vector>
#include "JsErrorHandler.h"
namespace facebook::react {
class StackTraceParser {
public:
static std::vector<JsErrorHandler::ParsedError::StackFrame> parse(
bool isHermes,
const std::string& stackString);
};
} // namespace facebook::react
File diff suppressed because it is too large Load Diff
@@ -365,6 +365,17 @@ bool isTruthy(jsi::Runtime& runtime, const jsi::Value& value) {
return Boolean.call(runtime, value).getBool();
}
jsi::Value wrapInErrorIfNecessary(
jsi::Runtime& runtime,
const jsi::Value& value) {
auto Error = runtime.global().getPropertyAsFunction(runtime, "Error");
auto isError =
value.isObject() && value.asObject(runtime).instanceOf(runtime, Error);
auto error = isError ? value.getObject(runtime)
: Error.callAsConstructor(runtime, value);
return jsi::Value(runtime, error);
}
} // namespace
void ReactInstance::initializeRuntime(
@@ -412,8 +423,8 @@ void ReactInstance::initializeRuntime(
}
if (isFatal) {
auto jsError =
jsi::JSError(runtime, jsi::Value(runtime, args[0]));
auto jsError = jsi::JSError(
runtime, wrapInErrorIfNecessary(runtime, args[0]));
jsErrorHandler->handleFatalError(runtime, jsError);
return jsi::Value(true);
}