From 793094fc0f1ba72ae3b80ac4f03d6dc3ce0d3d13 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Wed, 26 Mar 2025 12:25:00 -0700 Subject: [PATCH] Implement core Network CDP events on iOS (#50142) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50142 This is a first pass at integrating `NetworkReporter` in our networking stack on iOS (`RCTNetworking.mm`). **Implemented events** Wires up minimal events sufficient to populate the Chrome DevTools Network panel: - `Network.requestWillBeSent` - `Network.responseReceived` - `Network.loadingFinished` **Other notes** `RCTNetworking` is used (tentatively) as the integration point since it: - Is the default implementation for the network stack on iOS. - Should allow us to pair with originating JS call site down the line. - Intercepts Blob requests (at least `RCTImageLoader`). - Sits outside the user-configurable `RCTNetworkingResponseHandler` and `RCTNetworkingRequestHandler` concepts. - Is where network events are currently sent to JavaScript (`sendEventWithName`). NOTE: Reminder: `NetworkReporter` is currently a no-op without the `fuseboxNetworkInspectionEnabled` experiment set. Changelog: [Internal] Reviewed By: javache Differential Revision: D71470038 fbshipit-source-id: 069d77473c333a98f796b3dffa670a39b3016b2b --- .../Network/RCTInspectorNetworkReporter.h | 30 ++++ .../Network/RCTInspectorNetworkReporter.mm | 63 ++++++++ .../Libraries/Network/RCTNetworking.mm | 13 +- .../Network/React-RCTNetwork.podspec | 2 + .../React/React-RCTFabric.podspec | 1 + .../jsinspector-modern/NetworkIOAgent.cpp | 7 +- .../jsinspector-modern/network/CMakeLists.txt | 1 + .../network/NetworkReporter.cpp | 152 ++++++++++++++++-- .../network/NetworkReporter.h | 52 +++++- .../network/React-jsinspectornetwork.podspec | 2 + packages/rn-tester/Podfile.lock | 10 +- 11 files changed, 315 insertions(+), 18 deletions(-) create mode 100644 packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.h create mode 100644 packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.mm diff --git a/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.h b/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.h new file mode 100644 index 00000000000..7d7fa91aa17 --- /dev/null +++ b/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.h @@ -0,0 +1,30 @@ +/* + * 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 + +#import + +/** + * [Experimental] An interface for reporting network events to the modern + * debugger server and Web Performance APIs. + * + * This is a helper class wrapping + * `facebook::react::jsinspector_modern::NetworkReporter`. + */ +@interface RCTInspectorNetworkReporter : NSObject + ++ (void)reportRequestStart:(NSNumber *)requestId + request:(NSURLRequest *)request + encodedDataLength:(int)encodedDataLength; ++ (void)reportResponseStart:(NSNumber *)requestId + response:(NSURLResponse *)response + statusCode:(int)statusCode + headers:(NSDictionary *)headers; ++ (void)reportResponseEnd:(NSNumber *)requestId encodedDataLength:(int)encodedDataLength; + +@end diff --git a/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.mm b/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.mm new file mode 100644 index 00000000000..5dcec2a5e03 --- /dev/null +++ b/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.mm @@ -0,0 +1,63 @@ +/* + * 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. + */ + +#import "RCTInspectorNetworkReporter.h" + +#import + +namespace { + +using namespace facebook::react::jsinspector_modern; + +Headers convertNSDictionaryToHeaders(const NSDictionary *headers) +{ + Headers responseHeaders; + for (NSString *key in headers) { + responseHeaders[[key UTF8String]] = [headers[key] UTF8String]; + } + return responseHeaders; +} + +} // namespace + +@implementation RCTInspectorNetworkReporter { +} + ++ (void)reportRequestStart:(NSNumber *)requestId + request:(NSURLRequest *)request + encodedDataLength:(int)encodedDataLength +{ + RequestInfo requestInfo; + requestInfo.url = [request.URL absoluteString].UTF8String; + requestInfo.httpMethod = [request.HTTPMethod UTF8String]; + requestInfo.headers = convertNSDictionaryToHeaders(request.allHTTPHeaderFields); + requestInfo.httpBody = std::string((const char *)request.HTTPBody.bytes, request.HTTPBody.length); + + NetworkReporter::getInstance().reportRequestStart( + requestId.stringValue.UTF8String, requestInfo, encodedDataLength, std::nullopt); +} + ++ (void)reportResponseStart:(NSNumber *)requestId + response:(NSURLResponse *)response + statusCode:(int)statusCode + headers:(NSDictionary *)headers +{ + ResponseInfo responseInfo; + responseInfo.url = response.URL.absoluteString.UTF8String; + responseInfo.statusCode = statusCode; + responseInfo.headers = convertNSDictionaryToHeaders(headers); + + NetworkReporter::getInstance().reportResponseStart( + requestId.stringValue.UTF8String, responseInfo, response.expectedContentLength); +} + ++ (void)reportResponseEnd:(NSNumber *)requestId encodedDataLength:(int)encodedDataLength +{ + NetworkReporter::getInstance().reportResponseEnd(requestId.stringValue.UTF8String, encodedDataLength); +} + +@end diff --git a/packages/react-native/Libraries/Network/RCTNetworking.mm b/packages/react-native/Libraries/Network/RCTNetworking.mm index 482a3e1edd3..d4cb986b45d 100644 --- a/packages/react-native/Libraries/Network/RCTNetworking.mm +++ b/packages/react-native/Libraries/Network/RCTNetworking.mm @@ -17,6 +17,7 @@ #import +#import "RCTInspectorNetworkReporter.h" #import "RCTNetworkPlugins.h" static BOOL gEnableNetworkingRequestQueue = NO; @@ -587,16 +588,22 @@ RCT_EXPORT_MODULE() RCTURLRequestResponseBlock responseBlock = ^(NSURLResponse *response) { NSDictionary *headers; NSInteger status; - if ([response isKindOfClass:[NSHTTPURLResponse class]]) { // Might be a local file request + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; headers = httpResponse.allHeaderFields ?: @{}; status = httpResponse.statusCode; } else { + // Other HTTP-like request headers = response.MIMEType ? @{@"Content-Type" : response.MIMEType} : @{}; status = 200; } id responseURL = response.URL ? response.URL.absoluteString : [NSNull null]; NSArray *responseJSON = @[ task.requestID, @(status), headers, responseURL ]; + + [RCTInspectorNetworkReporter reportResponseStart:task.requestID + response:response + statusCode:status + headers:headers]; [weakSelf sendEventWithName:@"didReceiveNetworkResponse" body:responseJSON]; }; @@ -655,6 +662,7 @@ RCT_EXPORT_MODULE() NSArray *responseJSON = @[ task.requestID, RCTNullIfNil(error.localizedDescription), error.code == kCFURLErrorTimedOut ? @YES : @NO ]; + [RCTInspectorNetworkReporter reportResponseEnd:task.requestID encodedDataLength:data.length]; [strongSelf sendEventWithName:@"didCompleteNetworkResponse" body:responseJSON]; [strongSelf->_tasksByRequestID removeObjectForKey:task.requestID]; }; @@ -673,6 +681,9 @@ RCT_EXPORT_MODULE() responseSender(@[ task.requestID ]); } + [RCTInspectorNetworkReporter reportRequestStart:task.requestID + request:request + encodedDataLength:task.response.expectedContentLength]; [task start]; } diff --git a/packages/react-native/Libraries/Network/React-RCTNetwork.podspec b/packages/react-native/Libraries/Network/React-RCTNetwork.podspec index 01d8f63168e..5560831aa5e 100644 --- a/packages/react-native/Libraries/Network/React-RCTNetwork.podspec +++ b/packages/react-native/Libraries/Network/React-RCTNetwork.podspec @@ -46,6 +46,8 @@ Pod::Spec.new do |s| add_dependency(s, "React-RCTFBReactNativeSpec") add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"]) + add_dependency(s, "React-jsinspectorcdp", :framework_name => 'jsinspector_moderncdp') + add_dependency(s, "React-jsinspectornetwork", :framework_name => 'jsinspector_modernnetwork') add_dependency(s, "React-NativeModulesApple", :additional_framework_paths => ["build/generated/ios"]) add_rn_third_party_dependencies(s) diff --git a/packages/react-native/React/React-RCTFabric.podspec b/packages/react-native/React/React-RCTFabric.podspec index 3b52f552ec4..ee49b6f2098 100644 --- a/packages/react-native/React/React-RCTFabric.podspec +++ b/packages/react-native/React/React-RCTFabric.podspec @@ -86,6 +86,7 @@ Pod::Spec.new do |s| add_dependency(s, "React-RCTAnimation", :framework_name => 'RCTAnimation') add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern') add_dependency(s, "React-jsinspectorcdp", :framework_name => 'jsinspector_moderncdp') + add_dependency(s, "React-jsinspectornetwork", :framework_name => 'jsinspector_modernnetwork') add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing') add_dependency(s, "React-renderercss") diff --git a/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp index ffdcf205cec..15315a161d0 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp @@ -271,16 +271,19 @@ bool NetworkIOAgent::handleRequest( } if (InspectorFlags::getInstance().getNetworkInspectionEnabled()) { + auto& networkReporter = NetworkReporter::getInstance(); + // @cdp Network.enable support is experimental. if (req.method == "Network.enable") { - NetworkReporter::getInstance().enableDebugging(); + networkReporter.setFrontendChannel(frontendChannel_); + networkReporter.enableDebugging(); frontendChannel_(cdp::jsonResult(req.id)); return true; } // @cdp Network.disable support is experimental. if (req.method == "Network.disable") { - NetworkReporter::getInstance().disableDebugging(); + networkReporter.disableDebugging(); frontendChannel_(cdp::jsonResult(req.id)); return true; } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/network/CMakeLists.txt b/packages/react-native/ReactCommon/jsinspector-modern/network/CMakeLists.txt index b87fc827803..67017852a2f 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/network/CMakeLists.txt +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/CMakeLists.txt @@ -24,4 +24,5 @@ target_include_directories(jsinspector_network PUBLIC ${REACT_COMMON_DIR}) target_link_libraries(jsinspector_network folly_runtime glog + jsinspector_cdp ) diff --git a/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.cpp b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.cpp index d6579425fa4..99cf5fd613d 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.cpp @@ -8,16 +8,110 @@ #include "NetworkReporter.h" #include +#include #include namespace facebook::react::jsinspector_modern { +namespace { + +/** + * Get the CDP `ResourceType` for a given MIME type. + * + * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ResourceType + */ +std::string mimeTypeToResourceType(const std::string& mimeType) { + if (mimeType.find("image/") == 0) { + return "Image"; + } + + if (mimeType.find("video/") == 0 || mimeType.find("audio/") == 0) { + return "Media"; + } + + if (mimeType == "application/javascript" || mimeType == "text/javascript" || + mimeType == "application/x-javascript") { + return "Script"; + } + + if (mimeType == "application/json" || mimeType.find("application/xml") == 0 || + mimeType == "text/xml") { + // Assume XHR for JSON/XML types + return "XHR"; + } + + return "Other"; +} + +folly::dynamic headersToDynamic(const std::optional& headers) { + folly::dynamic result = folly::dynamic::object; + + if (headers) { + for (const auto& [key, value] : *headers) { + result[key] = value; + } + } + + return result; +} + +folly::dynamic requestToCdpParams(const RequestInfo& request) { + folly::dynamic result = folly::dynamic::object; + result["url"] = request.url; + result["method"] = request.httpMethod; + result["headers"] = headersToDynamic(request.headers); + result["postData"] = request.httpBody.value(); + + return result; +} + +folly::dynamic responseToCdpParams( + const ResponseInfo& response, + int encodedDataLength) { + auto headers = response.headers.value_or(Headers()); + std::string mimeType = "Other"; + + if (headers.find("Content-Type") != headers.end()) { + mimeType = mimeTypeToResourceType(headers.at("Content-Type")); + } + + folly::dynamic result = folly::dynamic::object; + result["url"] = response.url; + result["status"] = response.statusCode; + result["statusText"] = ""; + result["headers"] = headersToDynamic(response.headers); + result["mimeType"] = mimeType; + result["encodedDataLength"] = encodedDataLength; + + return result; +} + +/** + * Get the current Unix timestamp in seconds (µs precision). + */ +double getCurrentUnixTimestampSeconds() { + auto now = std::chrono::system_clock::now().time_since_epoch(); + auto seconds = std::chrono::duration_cast(now).count(); + auto micros = + std::chrono::duration_cast(now).count() % + 1000000; + + return static_cast(seconds) + + (static_cast(micros) / 1000000.0); +} + +} // namespace + NetworkReporter& NetworkReporter::getInstance() { static NetworkReporter tracer; return tracer; } +void NetworkReporter::setFrontendChannel(FrontendChannel frontendChannel) { + frontendChannel_ = std::move(frontendChannel); +} + bool NetworkReporter::enableDebugging() { if (debuggingEnabled_.load(std::memory_order_acquire)) { return false; @@ -38,13 +132,34 @@ bool NetworkReporter::disableDebugging() { return true; } -void NetworkReporter::reportRequestStart(const std::string& /*requestId*/) { +void NetworkReporter::reportRequestStart( + const std::string& requestId, + const RequestInfo& requestInfo, + int encodedDataLength, + const std::optional& redirectResponse) { if (!debuggingEnabled_.load(std::memory_order_relaxed)) { return; } - // TODO(T216933356) - throw std::runtime_error("Not implemented"); + double timestamp = getCurrentUnixTimestampSeconds(); + + folly::dynamic params = folly::dynamic::object; + params["requestId"] = requestId; + params["loaderId"] = ""; + params["documentURL"] = "mobile"; + params["request"] = requestToCdpParams(requestInfo); + // NOTE: timestamp and wallTime share the same time unit and precision, + // except wallTime is from an arbitrary epoch - use the Unix epoch for both. + params["timestamp"] = timestamp; + params["wallTime"] = timestamp; + params["initiator"] = folly::dynamic::object("type", "script"); + params["redirectHasExtraInfo"] = redirectResponse.has_value(); + if (redirectResponse.has_value()) { + params["redirectResponse"] = + responseToCdpParams(redirectResponse.value(), encodedDataLength); + } + + frontendChannel_(cdp::jsonNotification("Network.requestWillBeSent", params)); } void NetworkReporter::reportConnectionTiming(const std::string& /*requestId*/) { @@ -65,13 +180,26 @@ void NetworkReporter::reportRequestFailed(const std::string& /*requestId*/) { throw std::runtime_error("Not implemented"); } -void NetworkReporter::reportResponseStart(const std::string& /*requestId*/) { +void NetworkReporter::reportResponseStart( + const std::string& requestId, + const ResponseInfo& responseInfo, + int encodedDataLength) { if (!debuggingEnabled_.load(std::memory_order_relaxed)) { return; } - // TODO(T216933356) - throw std::runtime_error("Not implemented"); + folly::dynamic responseParams = + responseToCdpParams(responseInfo, encodedDataLength); + + folly::dynamic params = folly::dynamic::object; + params["requestId"] = requestId; + params["loaderId"] = ""; + params["timestamp"] = getCurrentUnixTimestampSeconds(); + params["type"] = responseParams["mimeType"]; + params["response"] = responseParams; + params["hasExtraInfo"] = false; + + frontendChannel_(cdp::jsonNotification("Network.responseReceived", params)); } void NetworkReporter::reportDataReceived(const std::string& /*requestId*/) { @@ -83,13 +211,19 @@ void NetworkReporter::reportDataReceived(const std::string& /*requestId*/) { throw std::runtime_error("Not implemented"); } -void NetworkReporter::reportResponseEnd(const std::string& /*requestId*/) { +void NetworkReporter::reportResponseEnd( + const std::string& requestId, + int encodedDataLength) { if (!debuggingEnabled_.load(std::memory_order_relaxed)) { return; } - // TODO(T216933356) - throw std::runtime_error("Not implemented"); + folly::dynamic params = folly::dynamic::object; + params["requestId"] = requestId; + params["timestamp"] = getCurrentUnixTimestampSeconds(); + params["encodedDataLength"] = encodedDataLength; + + frontendChannel_(cdp::jsonNotification("Network.loadingFinished", params)); } } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.h b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.h index 277883bf502..c2c61ec1dda 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.h @@ -8,10 +8,41 @@ #pragma once #include +#include +#include +#include #include namespace facebook::react::jsinspector_modern { +/** + * A callback that can be used to send debugger messages (method responses and + * events) to the frontend. The message must be a JSON-encoded string. + * The callback may be called from any thread. + */ +using FrontendChannel = std::function; + +using Headers = std::map; + +/** + * Request info from the request caller. + */ +struct RequestInfo { + std::string url; + std::string httpMethod; + std::optional headers; + std::optional httpBody; +}; + +/** + * Response info from the request caller. + */ +struct ResponseInfo { + std::string url; + uint16_t statusCode; + std::optional headers; +}; + /** * [Experimental] An interface for reporting network events to the modern * debugger server and Web Performance APIs. @@ -20,6 +51,12 @@ class NetworkReporter { public: static NetworkReporter& getInstance(); + /** + * Set the channel used to send CDP events to the frontend. This should be + * supplied before calling `enableDebugging`. + */ + void setFrontendChannel(FrontendChannel frontendChannel); + /** * Enable network tracking over CDP. Once enabled, network events will be * sent to the debugger client. Returns `false` if already enabled. @@ -45,7 +82,11 @@ class NetworkReporter { * * https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-requeststart */ - void reportRequestStart(const std::string& requestId); + void reportRequestStart( + const std::string& requestId, + const RequestInfo& requestInfo, + int encodedDataLength, + const std::optional& redirectResponse); /** * Report detailed timing info, such as DNS lookup, when a request has @@ -75,7 +116,10 @@ class NetworkReporter { * * https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-responsestart */ - void reportResponseStart(const std::string& requestId); + void reportResponseStart( + const std::string& requestId, + const ResponseInfo& responseInfo, + int encodedDataLength); /** * Report when additional chunks of the response body have been received. @@ -93,9 +137,11 @@ class NetworkReporter { * * https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-responseend */ - void reportResponseEnd(const std::string& requestId); + void reportResponseEnd(const std::string& requestId, int encodedDataLength); private: + FrontendChannel frontendChannel_; + NetworkReporter() = default; NetworkReporter(const NetworkReporter&) = delete; NetworkReporter& operator=(const NetworkReporter&) = delete; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/network/React-jsinspectornetwork.podspec b/packages/react-native/ReactCommon/jsinspector-modern/network/React-jsinspectornetwork.podspec index e5ed66f9b76..eb2b513439a 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/network/React-jsinspectornetwork.podspec +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/React-jsinspectornetwork.podspec @@ -46,5 +46,7 @@ Pod::Spec.new do |s| s.header_mappings_dir = "../.." end + add_dependency(s, "React-jsinspectorcdp", :framework_name => 'jsinspector_moderncdp') + add_rn_third_party_dependencies(s) end diff --git a/packages/rn-tester/Podfile.lock b/packages/rn-tester/Podfile.lock index 7a7b63c04e3..bfe6d426a62 100644 --- a/packages/rn-tester/Podfile.lock +++ b/packages/rn-tester/Podfile.lock @@ -1706,6 +1706,7 @@ PODS: - glog - RCT-Folly - RCT-Folly/Fabric + - React-jsinspectorcdp - SocketRocket - React-jsinspectortracing (1000.0.0): - boost @@ -1903,6 +1904,7 @@ PODS: - React-jsi - React-jsinspector - React-jsinspectorcdp + - React-jsinspectornetwork - React-jsinspectortracing - React-performancetimeline - React-RCTAnimation @@ -1967,6 +1969,8 @@ PODS: - RCTTypeSafety - React-Core/RCTNetworkHeaders - React-jsi + - React-jsinspectorcdp + - React-jsinspectornetwork - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon @@ -2594,7 +2598,7 @@ SPEC CHECKSUMS: React-jsiexecutor: 569425f7cd2c3e005a17e5211843e541c11d6916 React-jsinspector: 885e8180e898f07e4d7df29e2681a89e69d736d3 React-jsinspectorcdp: 5fb266e5f23d3a2819ba848e9d4d0b6b00f95934 - React-jsinspectornetwork: 79fbc86b7c308aeb29e883177b8573d953f6b5e2 + React-jsinspectornetwork: 207422b56a7918e83c94c207570849f83ab9052a React-jsinspectortracing: 80e9418ac67630c76f15ef06534087037a822330 React-jsitooling: b1af12acbfcb039bf87f8f2a82bde3a79c2ddade React-jsitracing: ce443686f52538d1033ce7db1e7d643e866262f0 @@ -2609,11 +2613,11 @@ SPEC CHECKSUMS: React-RCTAnimation: 263593e66c89bf810604b1ace15dfa382a1ca2df React-RCTAppDelegate: 3d35d7226338009b22d1cf9621eaa827acb8fd1d React-RCTBlob: 7b76230c53fe87d305eeeb250b0aae031bb6cbae - React-RCTFabric: b3746cb91560703c95028002fb6db618aebb3b61 + React-RCTFabric: 78b6aec5985ef438a9c088032c0e89d2bd77b3ba React-RCTFBReactNativeSpec: 503491a0584dc29f03ef9f8ed366794604cd59ef React-RCTImage: de404b6b0ebe53976a97e3a0dee819c83e12977b React-RCTLinking: 06742cfad41c506091403a414370743a4ed75af3 - React-RCTNetwork: 091d9c4c3e24f0800d782d0c2a0077419b478d66 + React-RCTNetwork: f8f7ac330958ac85300827267390bc8decc06ba0 React-RCTPushNotification: ea11178d499696516e0ff9ae335edbe99b06f94b React-RCTRuntime: 07b41aed797e8d950ada851c6363ecf931335663 React-RCTSettings: d3c2dd305ec81f7faf42762ec598d57f07fd43be