Skip to content

Commit

Permalink
easy: Align earlyjs c++ native data structures with js
Browse files Browse the repository at this point in the history
Summary:
For the js error handling pipeline, the javascript data structure looks like [this](https://www.internalfb.com/code/fbsource/[6181b57f4ba3619f58056bcec65382650d6ff59a]/xplat/js/react-native-github/packages/react-native/src/private/specs/modules/NativeExceptionsManager.js?lines=17-35):

```
export type StackFrame = {|
  column: ?number,
  file: ?string,
  lineNumber: ?number,
  methodName: string,
  collapse?: boolean,
|};
export type ExceptionData = {
  message: string,
  originalMessage: ?string,
  name: ?string,
  componentStack: ?string,
  stack: Array<StackFrame>,
  id: number,
  isFatal: boolean,
  // flowlint-next-line unclear-type:off
  extraData?: Object,
  ...
};
```

So, I made the c++ data structure look similar
```
  struct ParsedError {
    struct StackFrame {
      std::optional<std::string> file;
      std::string methodName;
      std::optional<int> lineNumber;
      std::optional<int> column;
    };

    std::string message;
    std::optional<std::string> originalMessage;
    std::optional<std::string> name;
    std::optional<std::string> componentStack;
    std::vector<StackFrame> stack;
    int id;
    bool isFatal;
    jsi::Object extraData;
  };
```

Notes:
* [parseErrorStack](https://fburl.com/code/e27q9gkc) doesn't actually generate a collapse property on the error object. So, I omitted it from the c++.
* ExceptionsManager [always provides an extraData field](https://fburl.com/code/2bvcsxac). So, I made it required.
* In C++, I just stored extraData as a jsi::Object. I wanted the freedom to store arbitrary key/value pairs. But, I also didn't want to use folly::dynamic.

Changelog: [Internal]

Reviewed By: alanleedev

Differential Revision: D63929580
  • Loading branch information
RSNara authored and facebook-github-bot committed Oct 8, 2024
1 parent cbc0978 commit edfcac9
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,12 @@ - (void)hostDidStart:(RCTHost *)host
- (void)host:(RCTHost *)host
didReceiveJSErrorStack:(NSArray<NSDictionary<NSString *, id> *> *)stack
message:(NSString *)message
exceptionId:(NSUInteger)exceptionId
originalMessage:(NSString *)originalMessage
name:(NSString *)name
componentStack:(NSString *)componentStack
id:(NSUInteger)exceptionId
isFatal:(BOOL)isFatal
extraData:(NSDictionary<NSString *, id> *)extraData
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ public class StackTraceHelper {
public static final String METHOD_NAME_KEY = "methodName";

public static final String MESSAGE_KEY = "message";
public static final String ORIGINAL_MESSAGE_KEY = "originalMessage";
public static final String NAME_KEY = "name";
public static final String COMPONENT_STACK_KEY = "componentStack";
public static final String STACK_KEY = "stack";
public static final String ID_KEY = "id";
public static final String IS_FATAL_KEY = "isFatal";
public static final String EXTRA_DATA_KEY = "extraData";

private static final Pattern STACK_FRAME_PATTERN1 =
Pattern.compile("^(?:(.*?)@)?(.*?)\\:([0-9]+)\\:([0-9]+)$");
Expand Down Expand Up @@ -260,22 +264,32 @@ public static String formatStackTrace(String title, StackFrame[] stack) {
}

public static JavaOnlyMap convertParsedError(ParsedError error) {
List<ParsedError.StackFrame> frames = error.getFrames();
List<ParsedError.StackFrame> frames = error.getStack();
List<ReadableMap> readableMapList = new ArrayList<>();
for (ParsedError.StackFrame frame : frames) {
JavaOnlyMap map = new JavaOnlyMap();
map.putDouble(COLUMN_KEY, frame.getColumnNumber());
map.putDouble(COLUMN_KEY, frame.getColumn());
map.putDouble(LINE_NUMBER_KEY, frame.getLineNumber());
map.putString(FILE_KEY, (String) frame.getFileName());
map.putString(FILE_KEY, (String) frame.getFile());
map.putString(METHOD_NAME_KEY, (String) frame.getMethodName());
readableMapList.add(map);
}

JavaOnlyMap data = new JavaOnlyMap();
data.putString(MESSAGE_KEY, error.getMessage());
if (error.getOriginalMessage() != null) {
data.putString(ORIGINAL_MESSAGE_KEY, error.getOriginalMessage());
}
if (error.getName() != null) {
data.putString(NAME_KEY, error.getName());
}
if (error.getComponentStack() != null) {
data.putString(COMPONENT_STACK_KEY, error.getComponentStack());
}
data.putArray(STACK_KEY, JavaOnlyArray.from(readableMapList));
data.putInt(ID_KEY, error.getExceptionId());
data.putInt(ID_KEY, error.getId());
data.putBoolean(IS_FATAL_KEY, error.isFatal());
data.putMap(EXTRA_DATA_KEY, error.getExtraData());

return data;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
package com.facebook.react.interfaces.exceptionmanager

import com.facebook.proguard.annotations.DoNotStripAny
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableNativeMap
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import java.util.ArrayList

Expand All @@ -18,32 +20,40 @@ public fun interface ReactJsExceptionHandler {
public interface ParsedError {
@DoNotStripAny
public interface StackFrame {
public val fileName: String
public val file: String?
public val methodName: String
public val lineNumber: Int
public val columnNumber: Int
public val lineNumber: Int?
public val column: Int?
}

public val frames: List<StackFrame>
public val message: String
public val exceptionId: Int
public val originalMessage: String?
public val name: String?
public val componentStack: String?
public val stack: List<StackFrame>
public val id: Int
public val isFatal: Boolean
public val extraData: ReadableMap
}

@DoNotStripAny
private data class ParsedStackFrameImpl(
override val fileName: String,
override val file: String?,
override val methodName: String,
override val lineNumber: Int,
override val columnNumber: Int,
override val lineNumber: Int?,
override val column: Int?,
) : ParsedError.StackFrame

@DoNotStripAny
private data class ParsedErrorImpl(
override val frames: ArrayList<ParsedStackFrameImpl>,
override val message: String,
override val exceptionId: Int,
override val originalMessage: String?,
override val name: String?,
override val componentStack: String?,
override val stack: ArrayList<ParsedStackFrameImpl>,
override val id: Int,
override val isFatal: Boolean,
override val extraData: ReadableNativeMap,
) : ParsedError

public fun reportJsException(errorMap: ParsedError)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
#include <fbjni/fbjni.h>
#include <glog/logging.h>
#include <jni.h>
#include <jsi/JSIDynamic.h>
#include <jsi/jsi.h>
#include <react/jni/ReadableNativeMap.h>

namespace facebook::react {

Expand All @@ -28,7 +31,10 @@ class ParsedStackFrameImpl
static facebook::jni::local_ref<ParsedStackFrameImpl> create(
const JsErrorHandler::ParsedError::StackFrame& frame) {
return newInstance(
frame.fileName, frame.methodName, frame.lineNumber, frame.columnNumber);
frame.file ? jni::make_jstring(*frame.file) : nullptr,
frame.methodName,
frame.lineNumber ? jni::JInteger::valueOf(*frame.lineNumber) : nullptr,
frame.column ? jni::JInteger::valueOf(*frame.column) : nullptr);
}
};

Expand All @@ -39,27 +45,42 @@ class ParsedErrorImpl
"Lcom/facebook/react/interfaces/exceptionmanager/ReactJsExceptionHandler$ParsedErrorImpl;";

static facebook::jni::local_ref<ParsedErrorImpl> create(
jsi::Runtime& runtime,
const JsErrorHandler::ParsedError& error) {
auto stackFrames =
facebook::jni::JArrayList<ParsedStackFrameImpl>::create();
for (const auto& frame : error.frames) {
stackFrames->add(ParsedStackFrameImpl::create(frame));
auto stack = facebook::jni::JArrayList<ParsedStackFrameImpl>::create();
for (const auto& frame : error.stack) {
stack->add(ParsedStackFrameImpl::create(frame));
}

auto extraDataDynamic =
jsi::dynamicFromValue(runtime, jsi::Value(runtime, error.extraData));

auto extraData =
ReadableNativeMap::createWithContents(std::move(extraDataDynamic));

return newInstance(
stackFrames, error.message, error.exceptionId, error.isFatal);
error.message,
error.originalMessage ? jni::make_jstring(*error.originalMessage)
: nullptr,
error.name ? jni::make_jstring(*error.name) : nullptr,
error.componentStack ? jni::make_jstring(*error.componentStack)
: nullptr,
stack,
error.id,
error.isFatal,
extraData);
}
};

} // namespace

void JReactExceptionManager::reportJsException(
jsi::Runtime& runtime,
const JsErrorHandler::ParsedError& error) {
static const auto method =
javaClassStatic()->getMethod<void(jni::alias_ref<ParsedError>)>(
"reportJsException");
if (self() != nullptr) {
method(self(), ParsedErrorImpl::create(error));
method(self(), ParsedErrorImpl::create(runtime, error));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ class JReactExceptionManager
static auto constexpr kJavaDescriptor =
"Lcom/facebook/react/interfaces/exceptionmanager/ReactJsExceptionHandler;";

void reportJsException(const JsErrorHandler::ParsedError& error);
void reportJsException(
jsi::Runtime& runtime,
const JsErrorHandler::ParsedError& error);
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ JReactInstance::JReactInstance(
jReactExceptionManager_ = jni::make_global(jReactExceptionManager);
auto onJsError =
[weakJReactExceptionManager = jni::make_weak(jReactExceptionManager)](
jsi::Runtime& runtime,
const JsErrorHandler::ParsedError& error) mutable noexcept {
if (auto jReactExceptionManager =
weakJReactExceptionManager.lockLocal()) {
jReactExceptionManager->reportJsException(error);
jReactExceptionManager->reportJsException(runtime, error);
}
};

Expand Down
81 changes: 69 additions & 12 deletions packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,64 @@
#include <string>
#include <vector>

namespace {
std::string quote(const std::string& view) {
return "\"" + view + "\"";
}
} // namespace

namespace facebook::react {

std::ostream& operator<<(
std::ostream& os,
const JsErrorHandler::ParsedError::StackFrame& frame) {
auto file = frame.file ? quote(*frame.file) : "nil";
auto methodName = quote(frame.methodName);
auto lineNumber =
frame.lineNumber ? std::to_string(*frame.lineNumber) : "nil";
auto column = frame.column ? std::to_string(*frame.column) : "nil";

os << "StackFrame { .file = " << file << ", .methodName = " << methodName
<< ", .lineNumber = " << lineNumber << ", .column = " << column << " }";
return os;
}
std::ostream& operator<<(
std::ostream& os,
const JsErrorHandler::ParsedError& error) {
auto message = quote(error.message);
auto originalMessage =
error.originalMessage ? quote(*error.originalMessage) : "nil";
auto name = error.name ? quote(*error.name) : "nil";
auto componentStack =
error.componentStack ? quote(*error.componentStack) : "nil";
auto id = std::to_string(error.id);
auto isFatal = std::to_string(static_cast<int>(error.isFatal));
auto extraData = "jsi::Object{ <omitted> } ";

os << "ParsedError {\n"
<< " .message = " << message << "\n"
<< " .originalMessage = " << originalMessage << "\n"
<< " .name = " << name << "\n"
<< " .componentStack = " << componentStack << "\n"
<< " .stack = [\n";

for (const auto& frame : error.stack) {
os << " " << frame << ", \n";
}
os << " ]\n"
<< " .id = " << id << "\n"
<< " .isFatal " << isFatal << "\n"
<< " .extraData = " << extraData << "\n"
<< "}";
return os;
}

// TODO(T198763073): Migrate away from std::regex in this function
static JsErrorHandler::ParsedError
parseErrorStack(const jsi::JSError& error, bool isFatal, bool isHermes) {
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)
Expand Down Expand Up @@ -58,32 +111,32 @@ parseErrorStack(const jsi::JSError& error, bool isFatal, bool isHermes) {
std::string str2 = std::string(searchResults[2]);
if (str2.compare("native")) {
frames.push_back({
.fileName = std::string(searchResults[4]),
.file = std::string(searchResults[4]),
.methodName = std::string(searchResults[1]),
.lineNumber = std::stoi(searchResults[5]),
.columnNumber = std::stoi(searchResults[6]),
.column = std::stoi(searchResults[6]),
});
}
}
} else {
// @lint-ignore CLANGTIDY facebook-hte-StdRegexIsAwful
if (std::regex_search(line, searchResults, REGEX_GECKO)) {
frames.push_back({
.fileName = std::string(searchResults[3]),
.file = std::string(searchResults[3]),
.methodName = std::string(searchResults[1]),
.lineNumber = std::stoi(searchResults[4]),
.columnNumber = std::stoi(searchResults[5]),
.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({
.fileName = std::string(searchResults[2]),
.file = std::string(searchResults[2]),
.methodName = std::string(searchResults[1]),
.lineNumber = std::stoi(searchResults[3]),
.columnNumber = std::stoi(searchResults[4]),
.column = std::stoi(searchResults[4]),
});
} else {
continue;
Expand All @@ -92,10 +145,14 @@ parseErrorStack(const jsi::JSError& error, bool isFatal, bool isHermes) {
}

return {
.frames = std::move(frames),
.message = "EarlyJsError: " + error.getMessage(),
.exceptionId = 0,
.originalMessage = std::nullopt,
.name = std::nullopt,
.componentStack = std::nullopt,
.stack = std::move(frames),
.id = 0,
.isFatal = isFatal,
.extraData = jsi::Object(runtime),
};
}

Expand Down Expand Up @@ -127,8 +184,8 @@ void JsErrorHandler::handleFatalError(
}
}
// This is a hacky way to get Hermes stack trace.
ParsedError parsedError = parseErrorStack(error, true, false);
_onJsError(parsedError);
ParsedError parsedError = parseErrorStack(runtime, error, true, false);
_onJsError(runtime, parsedError);
}

bool JsErrorHandler::hasHandledFatalError() {
Expand Down
23 changes: 17 additions & 6 deletions packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,37 @@
#pragma once

#include <jsi/jsi.h>
#include <iostream>
#include <optional>

namespace facebook::react {

class JsErrorHandler {
public:
struct ParsedError {
struct StackFrame {
std::string fileName;
std::optional<std::string> file;
std::string methodName;
int lineNumber;
int columnNumber;
std::optional<int> lineNumber;
std::optional<int> column;
friend std::ostream& operator<<(
std::ostream& os,
const StackFrame& frame);
};

std::vector<StackFrame> frames;
std::string message;
int exceptionId;
std::optional<std::string> originalMessage;
std::optional<std::string> name;
std::optional<std::string> componentStack;
std::vector<StackFrame> stack;
int id;
bool isFatal;
jsi::Object extraData;
friend std::ostream& operator<<(std::ostream& os, const ParsedError& frame);
};

using OnJsError = std::function<void(const ParsedError& error)>;
using OnJsError =
std::function<void(jsi::Runtime& runtime, const ParsedError& error)>;

explicit JsErrorHandler(OnJsError onJsError);
~JsErrorHandler();
Expand Down
Loading

0 comments on commit edfcac9

Please sign in to comment.