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
This commit is contained in:
Alex Hunt
2025-03-26 12:25:00 -07:00
committed by Facebook GitHub Bot
parent bcce235841
commit 793094fc0f
11 changed files with 315 additions and 18 deletions
@@ -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 <Foundation/Foundation.h>
/**
* [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<NSString *, NSString *> *)headers;
+ (void)reportResponseEnd:(NSNumber *)requestId encodedDataLength:(int)encodedDataLength;
@end
@@ -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 <jsinspector-modern/network/NetworkReporter.h>
namespace {
using namespace facebook::react::jsinspector_modern;
Headers convertNSDictionaryToHeaders(const NSDictionary<NSString *, NSString *> *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<NSString *, NSString *> *)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
@@ -17,6 +17,7 @@
#import <React/RCTHTTPRequestHandler.h>
#import "RCTInspectorNetworkReporter.h"
#import "RCTNetworkPlugins.h"
static BOOL gEnableNetworkingRequestQueue = NO;
@@ -587,16 +588,22 @@ RCT_EXPORT_MODULE()
RCTURLRequestResponseBlock responseBlock = ^(NSURLResponse *response) {
NSDictionary<NSString *, NSString *> *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<id> *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];
}
@@ -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)
@@ -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")
@@ -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;
}
@@ -24,4 +24,5 @@ target_include_directories(jsinspector_network PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(jsinspector_network
folly_runtime
glog
jsinspector_cdp
)
@@ -8,16 +8,110 @@
#include "NetworkReporter.h"
#include <glog/logging.h>
#include <jsinspector-modern/cdp/CdpJson.h>
#include <stdexcept>
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>& 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<std::chrono::seconds>(now).count();
auto micros =
std::chrono::duration_cast<std::chrono::microseconds>(now).count() %
1000000;
return static_cast<double>(seconds) +
(static_cast<double>(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<ResponseInfo>& 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
@@ -8,10 +8,41 @@
#pragma once
#include <atomic>
#include <functional>
#include <map>
#include <optional>
#include <string>
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<void(std::string_view messageJson)>;
using Headers = std::map<std::string, std::string>;
/**
* Request info from the request caller.
*/
struct RequestInfo {
std::string url;
std::string httpMethod;
std::optional<Headers> headers;
std::optional<std::string> httpBody;
};
/**
* Response info from the request caller.
*/
struct ResponseInfo {
std::string url;
uint16_t statusCode;
std::optional<Headers> 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<ResponseInfo>& 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;
@@ -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
+7 -3
View File
@@ -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