diff --git a/packages/react-native/React/CoreModules/RCTExceptionsManager.h b/packages/react-native/React/CoreModules/RCTExceptionsManager.h index d47ad65cf28042..6e9667979df585 100644 --- a/packages/react-native/React/CoreModules/RCTExceptionsManager.h +++ b/packages/react-native/React/CoreModules/RCTExceptionsManager.h @@ -19,6 +19,9 @@ NS_ASSUME_NONNULL_BEGIN stack:(nullable NSArray *)stack exceptionId:(NSNumber *)exceptionId extraDataAsJSON:(nullable NSString *)extraDataAsJSON; + +@optional +- (NSDictionary *)decorateJSExceptionData:(NSDictionary *)exceptionData; @end @interface RCTExceptionsManager : NSObject diff --git a/packages/react-native/React/CoreModules/RCTExceptionsManager.mm b/packages/react-native/React/CoreModules/RCTExceptionsManager.mm index d7f8f647604a90..82b1d295f256c0 100644 --- a/packages/react-native/React/CoreModules/RCTExceptionsManager.mm +++ b/packages/react-native/React/CoreModules/RCTExceptionsManager.mm @@ -83,6 +83,7 @@ - (void)reportFatal:(NSString *)message } } +// TODO(T205456329): This method is deprecated in favour of reportException. Delete in v0.77 RCT_EXPORT_METHOD(reportSoftException : (NSString *)message stack : (NSArray *)stack exceptionId @@ -91,6 +92,7 @@ - (void)reportFatal:(NSString *)message [self reportSoft:message stack:stack exceptionId:exceptionId extraDataAsJSON:nil]; } +// TODO(T205456329): This method is deprecated in favour of reportException. Delete in v0.77 RCT_EXPORT_METHOD(reportFatalException : (NSString *)message stack : (NSArray *)stack exceptionId @@ -103,15 +105,24 @@ - (void)reportFatal:(NSString *)message RCT_EXPORT_METHOD(reportException : (JS::NativeExceptionsManager::ExceptionData &)data) { - NSString *message = data.message(); - double exceptionId = data.id_(); + NSMutableDictionary *mutableErrorData = [NSMutableDictionary new]; + mutableErrorData[@"message"] = data.message(); + if (data.originalMessage()) { + mutableErrorData[@"originalMessage"] = data.originalMessage(); + } + if (data.name()) { + mutableErrorData[@"name"] = data.name(); + } + if (data.componentStack()) { + mutableErrorData[@"componentStack"] = data.componentStack(); + } // Reserialize data.stack() into an array of untyped dictionaries. // TODO: (moti) T53588496 Replace `(NSArray *)stack` in // reportFatalException etc with a typed interface. - NSMutableArray *stackArray = [NSMutableArray new]; + NSMutableArray *> *stackArray = [NSMutableArray *> new]; for (auto frame : data.stack()) { - NSMutableDictionary *frameDict = [NSMutableDictionary new]; + NSMutableDictionary *frameDict = [NSMutableDictionary new]; if (frame.column().has_value()) { frameDict[@"column"] = @(frame.column().value()); } @@ -126,13 +137,28 @@ - (void)reportFatal:(NSString *)message [stackArray addObject:frameDict]; } - NSDictionary *extraData = (NSDictionary *)data.extraData(); - NSString *extraDataAsJSON = RCTJSONStringify(extraData, NULL); + mutableErrorData[@"stack"] = stackArray; + mutableErrorData[@"id"] = @(data.id_()); + mutableErrorData[@"isFatal"] = @(data.isFatal()); + + if (data.extraData()) { + mutableErrorData[@"extraData"] = data.extraData(); + } + + NSDictionary *errorData = mutableErrorData; + if ([_delegate respondsToSelector:@selector(decorateJSExceptionData:)]) { + errorData = [_delegate decorateJSExceptionData:errorData]; + } + + NSString *extraDataAsJSON = RCTJSONStringify(errorData[@"extraData"], NULL); + NSString *message = errorData[@"message"]; + NSArray *> *stack = errorData[@"stack"]; + double exceptionId = [errorData[@"id"] doubleValue]; - if (data.isFatal()) { - [self reportFatal:message stack:stackArray exceptionId:exceptionId extraDataAsJSON:extraDataAsJSON]; + if (errorData[@"isFatal"]) { + [self reportFatal:message stack:stack exceptionId:exceptionId extraDataAsJSON:extraDataAsJSON]; } else { - [self reportSoft:message stack:stackArray exceptionId:exceptionId extraDataAsJSON:extraDataAsJSON]; + [self reportSoft:message stack:stack exceptionId:exceptionId extraDataAsJSON:extraDataAsJSON]; } } diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 20de33332daf97..64a170c54c3933 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -956,6 +956,7 @@ public class com/facebook/react/bridge/JavaOnlyMap : com/facebook/react/bridge/R public fun putMap (Ljava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V public fun putNull (Ljava/lang/String;)V public fun putString (Ljava/lang/String;Ljava/lang/String;)V + public fun remove (Ljava/lang/String;)V public fun toHashMap ()Ljava/util/HashMap; public fun toString ()Ljava/lang/String; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java index b3f217af8a53b3..afb27217b44cb2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java @@ -246,6 +246,10 @@ public String toString() { return mBackingMap.toString(); } + public void remove(@NonNull String key) { + mBackingMap.remove(key); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp index 2c156e6e43126d..7c19502c334214 100644 --- a/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp +++ b/packages/react-native/ReactCommon/jserrorhandler/JsErrorHandler.cpp @@ -78,6 +78,43 @@ class SetFalseOnDestruct { } }; +void logErrorWhileReporting( + std::string message, + jsi::JSError& error, + jsi::JSError& originalError) { + LOG(ERROR) << "JsErrorHandler::" << message << std::endl + << "Js error message: " << error.getMessage() << std::endl + << "Original js error message: " << originalError.getMessage() + << std::endl; +} + +jsi::Value getBundleMetadata(jsi::Runtime& runtime, jsi::JSError& error) { + auto jsGetBundleMetadataValue = + runtime.global().getProperty(runtime, "__getBundleMetadata"); + + if (!jsGetBundleMetadataValue.isObject() || + !jsGetBundleMetadataValue.asObject(runtime).isFunction(runtime)) { + return jsi::Value::null(); + } + + auto jsGetBundleMetadataValueFn = + jsGetBundleMetadataValue.asObject(runtime).asFunction(runtime); + + try { + auto bundleMetadataValue = jsGetBundleMetadataValueFn.call(runtime); + if (bundleMetadataValue.isObject()) { + return bundleMetadataValue; + } + return bundleMetadataValue; + } catch (jsi::JSError& ex) { + logErrorWhileReporting( + "getBundleMetadata(): Error raised while calling __getBundleMetadata(). Returning null.", + ex, + error); + } + + return jsi::Value::null(); +} } // namespace namespace facebook::react { @@ -199,12 +236,11 @@ void JsErrorHandler::handleError( try { handleJSError(runtime, error, isFatal); return; - } catch (jsi::JSError& e) { - LOG(ERROR) - << "JsErrorHandler: Failed to report js error using js pipeline. Using C++ pipeline instead." - << std::endl - << "Reporting failure: " << e.getMessage() << std::endl - << "Original js error: " << error.getMessage() << std::endl; + } catch (jsi::JSError& ex) { + logErrorWhileReporting( + "handleError(): Error raised while reporting using js pipeline. Using c++ pipeline instead.", + ex, + error); } } @@ -249,8 +285,14 @@ void JsErrorHandler::handleErrorWithCppPipeline( objectAssign(runtime, extraData, extraDataValue.asObject(runtime)); } + auto isDEV = + isTruthy(runtime, runtime.global().getProperty(runtime, "__DEV__")); + extraData.setProperty(runtime, "jsEngine", jsEngineValue); extraData.setProperty(runtime, "rawStack", error.getStack()); + extraData.setProperty(runtime, "__DEV__", isDEV); + extraData.setProperty( + runtime, "bundleMetadata", getBundleMetadata(runtime, error)); auto cause = errorObj.getProperty(runtime, "cause"); if (cause.isObject()) { @@ -324,7 +366,14 @@ void JsErrorHandler::handleErrorWithCppPipeline( data.setProperty(runtime, "preventDefault", preventDefault); for (auto& errorListener : _errorListeners) { - errorListener(runtime, jsi::Value(runtime, data)); + try { + errorListener(runtime, jsi::Value(runtime, data)); + } catch (jsi::JSError& ex) { + logErrorWhileReporting( + "handleErrorWithCppPipeline(): Error raised inside an error listener. Executing next listener.", + ex, + error); + } } if (*shouldPreventDefault) { diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost+Internal.h b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost+Internal.h index e1708784a45340..dd92a4e30f9b51 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost+Internal.h +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost+Internal.h @@ -15,4 +15,6 @@ - (void)setBundleURLProvider:(RCTHostBundleURLProvider)bundleURLProvider; - (void)setContextContainerHandler:(id)contextContainerHandler; +@property (nonatomic, readonly) RCTBundleManager *bundleManager; + @end diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm index 814e8503d9e997..55cd2553f951ee 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm @@ -273,6 +273,11 @@ - (RCTSurfacePresenter *)surfacePresenter return [_instance surfacePresenter]; } +- (RCTBundleManager *)bundleManager +{ + return _bundleManager; +} + - (void)callFunctionOnJSModule:(NSString *)moduleName method:(NSString *)method args:(NSArray *)args { [_instance callFunctionOnJSModule:moduleName method:method args:args];