Skip to content

Commit 793094f

Browse files
huntiefacebook-github-bot
authored andcommitted
Implement core Network CDP events on iOS (facebook#50142)
Summary: Pull Request resolved: facebook#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
1 parent bcce235 commit 793094f

File tree

11 files changed

+315
-18
lines changed

11 files changed

+315
-18
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#import <Foundation/Foundation.h>
11+
12+
/**
13+
* [Experimental] An interface for reporting network events to the modern
14+
* debugger server and Web Performance APIs.
15+
*
16+
* This is a helper class wrapping
17+
* `facebook::react::jsinspector_modern::NetworkReporter`.
18+
*/
19+
@interface RCTInspectorNetworkReporter : NSObject
20+
21+
+ (void)reportRequestStart:(NSNumber *)requestId
22+
request:(NSURLRequest *)request
23+
encodedDataLength:(int)encodedDataLength;
24+
+ (void)reportResponseStart:(NSNumber *)requestId
25+
response:(NSURLResponse *)response
26+
statusCode:(int)statusCode
27+
headers:(NSDictionary<NSString *, NSString *> *)headers;
28+
+ (void)reportResponseEnd:(NSNumber *)requestId encodedDataLength:(int)encodedDataLength;
29+
30+
@end
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTInspectorNetworkReporter.h"
9+
10+
#import <jsinspector-modern/network/NetworkReporter.h>
11+
12+
namespace {
13+
14+
using namespace facebook::react::jsinspector_modern;
15+
16+
Headers convertNSDictionaryToHeaders(const NSDictionary<NSString *, NSString *> *headers)
17+
{
18+
Headers responseHeaders;
19+
for (NSString *key in headers) {
20+
responseHeaders[[key UTF8String]] = [headers[key] UTF8String];
21+
}
22+
return responseHeaders;
23+
}
24+
25+
} // namespace
26+
27+
@implementation RCTInspectorNetworkReporter {
28+
}
29+
30+
+ (void)reportRequestStart:(NSNumber *)requestId
31+
request:(NSURLRequest *)request
32+
encodedDataLength:(int)encodedDataLength
33+
{
34+
RequestInfo requestInfo;
35+
requestInfo.url = [request.URL absoluteString].UTF8String;
36+
requestInfo.httpMethod = [request.HTTPMethod UTF8String];
37+
requestInfo.headers = convertNSDictionaryToHeaders(request.allHTTPHeaderFields);
38+
requestInfo.httpBody = std::string((const char *)request.HTTPBody.bytes, request.HTTPBody.length);
39+
40+
NetworkReporter::getInstance().reportRequestStart(
41+
requestId.stringValue.UTF8String, requestInfo, encodedDataLength, std::nullopt);
42+
}
43+
44+
+ (void)reportResponseStart:(NSNumber *)requestId
45+
response:(NSURLResponse *)response
46+
statusCode:(int)statusCode
47+
headers:(NSDictionary<NSString *, NSString *> *)headers
48+
{
49+
ResponseInfo responseInfo;
50+
responseInfo.url = response.URL.absoluteString.UTF8String;
51+
responseInfo.statusCode = statusCode;
52+
responseInfo.headers = convertNSDictionaryToHeaders(headers);
53+
54+
NetworkReporter::getInstance().reportResponseStart(
55+
requestId.stringValue.UTF8String, responseInfo, response.expectedContentLength);
56+
}
57+
58+
+ (void)reportResponseEnd:(NSNumber *)requestId encodedDataLength:(int)encodedDataLength
59+
{
60+
NetworkReporter::getInstance().reportResponseEnd(requestId.stringValue.UTF8String, encodedDataLength);
61+
}
62+
63+
@end

packages/react-native/Libraries/Network/RCTNetworking.mm

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#import <React/RCTHTTPRequestHandler.h>
1919

20+
#import "RCTInspectorNetworkReporter.h"
2021
#import "RCTNetworkPlugins.h"
2122

2223
static BOOL gEnableNetworkingRequestQueue = NO;
@@ -587,16 +588,22 @@ - (void)sendRequest:(NSURLRequest *)request
587588
RCTURLRequestResponseBlock responseBlock = ^(NSURLResponse *response) {
588589
NSDictionary<NSString *, NSString *> *headers;
589590
NSInteger status;
590-
if ([response isKindOfClass:[NSHTTPURLResponse class]]) { // Might be a local file request
591+
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
591592
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
592593
headers = httpResponse.allHeaderFields ?: @{};
593594
status = httpResponse.statusCode;
594595
} else {
596+
// Other HTTP-like request
595597
headers = response.MIMEType ? @{@"Content-Type" : response.MIMEType} : @{};
596598
status = 200;
597599
}
598600
id responseURL = response.URL ? response.URL.absoluteString : [NSNull null];
599601
NSArray<id> *responseJSON = @[ task.requestID, @(status), headers, responseURL ];
602+
603+
[RCTInspectorNetworkReporter reportResponseStart:task.requestID
604+
response:response
605+
statusCode:status
606+
headers:headers];
600607
[weakSelf sendEventWithName:@"didReceiveNetworkResponse" body:responseJSON];
601608
};
602609

@@ -655,6 +662,7 @@ - (void)sendRequest:(NSURLRequest *)request
655662
NSArray *responseJSON =
656663
@[ task.requestID, RCTNullIfNil(error.localizedDescription), error.code == kCFURLErrorTimedOut ? @YES : @NO ];
657664

665+
[RCTInspectorNetworkReporter reportResponseEnd:task.requestID encodedDataLength:data.length];
658666
[strongSelf sendEventWithName:@"didCompleteNetworkResponse" body:responseJSON];
659667
[strongSelf->_tasksByRequestID removeObjectForKey:task.requestID];
660668
};
@@ -673,6 +681,9 @@ - (void)sendRequest:(NSURLRequest *)request
673681
responseSender(@[ task.requestID ]);
674682
}
675683

684+
[RCTInspectorNetworkReporter reportRequestStart:task.requestID
685+
request:request
686+
encodedDataLength:task.response.expectedContentLength];
676687
[task start];
677688
}
678689

packages/react-native/Libraries/Network/React-RCTNetwork.podspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ Pod::Spec.new do |s|
4646

4747
add_dependency(s, "React-RCTFBReactNativeSpec")
4848
add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"])
49+
add_dependency(s, "React-jsinspectorcdp", :framework_name => 'jsinspector_moderncdp')
50+
add_dependency(s, "React-jsinspectornetwork", :framework_name => 'jsinspector_modernnetwork')
4951
add_dependency(s, "React-NativeModulesApple", :additional_framework_paths => ["build/generated/ios"])
5052

5153
add_rn_third_party_dependencies(s)

packages/react-native/React/React-RCTFabric.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Pod::Spec.new do |s|
8686
add_dependency(s, "React-RCTAnimation", :framework_name => 'RCTAnimation')
8787
add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
8888
add_dependency(s, "React-jsinspectorcdp", :framework_name => 'jsinspector_moderncdp')
89+
add_dependency(s, "React-jsinspectornetwork", :framework_name => 'jsinspector_modernnetwork')
8990
add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing')
9091
add_dependency(s, "React-renderercss")
9192

packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,16 +271,19 @@ bool NetworkIOAgent::handleRequest(
271271
}
272272

273273
if (InspectorFlags::getInstance().getNetworkInspectionEnabled()) {
274+
auto& networkReporter = NetworkReporter::getInstance();
275+
274276
// @cdp Network.enable support is experimental.
275277
if (req.method == "Network.enable") {
276-
NetworkReporter::getInstance().enableDebugging();
278+
networkReporter.setFrontendChannel(frontendChannel_);
279+
networkReporter.enableDebugging();
277280
frontendChannel_(cdp::jsonResult(req.id));
278281
return true;
279282
}
280283

281284
// @cdp Network.disable support is experimental.
282285
if (req.method == "Network.disable") {
283-
NetworkReporter::getInstance().disableDebugging();
286+
networkReporter.disableDebugging();
284287
frontendChannel_(cdp::jsonResult(req.id));
285288
return true;
286289
}

packages/react-native/ReactCommon/jsinspector-modern/network/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ target_include_directories(jsinspector_network PUBLIC ${REACT_COMMON_DIR})
2424
target_link_libraries(jsinspector_network
2525
folly_runtime
2626
glog
27+
jsinspector_cdp
2728
)

packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.cpp

Lines changed: 143 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,110 @@
88
#include "NetworkReporter.h"
99

1010
#include <glog/logging.h>
11+
#include <jsinspector-modern/cdp/CdpJson.h>
1112

1213
#include <stdexcept>
1314

1415
namespace facebook::react::jsinspector_modern {
1516

17+
namespace {
18+
19+
/**
20+
* Get the CDP `ResourceType` for a given MIME type.
21+
*
22+
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ResourceType
23+
*/
24+
std::string mimeTypeToResourceType(const std::string& mimeType) {
25+
if (mimeType.find("image/") == 0) {
26+
return "Image";
27+
}
28+
29+
if (mimeType.find("video/") == 0 || mimeType.find("audio/") == 0) {
30+
return "Media";
31+
}
32+
33+
if (mimeType == "application/javascript" || mimeType == "text/javascript" ||
34+
mimeType == "application/x-javascript") {
35+
return "Script";
36+
}
37+
38+
if (mimeType == "application/json" || mimeType.find("application/xml") == 0 ||
39+
mimeType == "text/xml") {
40+
// Assume XHR for JSON/XML types
41+
return "XHR";
42+
}
43+
44+
return "Other";
45+
}
46+
47+
folly::dynamic headersToDynamic(const std::optional<Headers>& headers) {
48+
folly::dynamic result = folly::dynamic::object;
49+
50+
if (headers) {
51+
for (const auto& [key, value] : *headers) {
52+
result[key] = value;
53+
}
54+
}
55+
56+
return result;
57+
}
58+
59+
folly::dynamic requestToCdpParams(const RequestInfo& request) {
60+
folly::dynamic result = folly::dynamic::object;
61+
result["url"] = request.url;
62+
result["method"] = request.httpMethod;
63+
result["headers"] = headersToDynamic(request.headers);
64+
result["postData"] = request.httpBody.value();
65+
66+
return result;
67+
}
68+
69+
folly::dynamic responseToCdpParams(
70+
const ResponseInfo& response,
71+
int encodedDataLength) {
72+
auto headers = response.headers.value_or(Headers());
73+
std::string mimeType = "Other";
74+
75+
if (headers.find("Content-Type") != headers.end()) {
76+
mimeType = mimeTypeToResourceType(headers.at("Content-Type"));
77+
}
78+
79+
folly::dynamic result = folly::dynamic::object;
80+
result["url"] = response.url;
81+
result["status"] = response.statusCode;
82+
result["statusText"] = "";
83+
result["headers"] = headersToDynamic(response.headers);
84+
result["mimeType"] = mimeType;
85+
result["encodedDataLength"] = encodedDataLength;
86+
87+
return result;
88+
}
89+
90+
/**
91+
* Get the current Unix timestamp in seconds (µs precision).
92+
*/
93+
double getCurrentUnixTimestampSeconds() {
94+
auto now = std::chrono::system_clock::now().time_since_epoch();
95+
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(now).count();
96+
auto micros =
97+
std::chrono::duration_cast<std::chrono::microseconds>(now).count() %
98+
1000000;
99+
100+
return static_cast<double>(seconds) +
101+
(static_cast<double>(micros) / 1000000.0);
102+
}
103+
104+
} // namespace
105+
16106
NetworkReporter& NetworkReporter::getInstance() {
17107
static NetworkReporter tracer;
18108
return tracer;
19109
}
20110

111+
void NetworkReporter::setFrontendChannel(FrontendChannel frontendChannel) {
112+
frontendChannel_ = std::move(frontendChannel);
113+
}
114+
21115
bool NetworkReporter::enableDebugging() {
22116
if (debuggingEnabled_.load(std::memory_order_acquire)) {
23117
return false;
@@ -38,13 +132,34 @@ bool NetworkReporter::disableDebugging() {
38132
return true;
39133
}
40134

41-
void NetworkReporter::reportRequestStart(const std::string& /*requestId*/) {
135+
void NetworkReporter::reportRequestStart(
136+
const std::string& requestId,
137+
const RequestInfo& requestInfo,
138+
int encodedDataLength,
139+
const std::optional<ResponseInfo>& redirectResponse) {
42140
if (!debuggingEnabled_.load(std::memory_order_relaxed)) {
43141
return;
44142
}
45143

46-
// TODO(T216933356)
47-
throw std::runtime_error("Not implemented");
144+
double timestamp = getCurrentUnixTimestampSeconds();
145+
146+
folly::dynamic params = folly::dynamic::object;
147+
params["requestId"] = requestId;
148+
params["loaderId"] = "";
149+
params["documentURL"] = "mobile";
150+
params["request"] = requestToCdpParams(requestInfo);
151+
// NOTE: timestamp and wallTime share the same time unit and precision,
152+
// except wallTime is from an arbitrary epoch - use the Unix epoch for both.
153+
params["timestamp"] = timestamp;
154+
params["wallTime"] = timestamp;
155+
params["initiator"] = folly::dynamic::object("type", "script");
156+
params["redirectHasExtraInfo"] = redirectResponse.has_value();
157+
if (redirectResponse.has_value()) {
158+
params["redirectResponse"] =
159+
responseToCdpParams(redirectResponse.value(), encodedDataLength);
160+
}
161+
162+
frontendChannel_(cdp::jsonNotification("Network.requestWillBeSent", params));
48163
}
49164

50165
void NetworkReporter::reportConnectionTiming(const std::string& /*requestId*/) {
@@ -65,13 +180,26 @@ void NetworkReporter::reportRequestFailed(const std::string& /*requestId*/) {
65180
throw std::runtime_error("Not implemented");
66181
}
67182

68-
void NetworkReporter::reportResponseStart(const std::string& /*requestId*/) {
183+
void NetworkReporter::reportResponseStart(
184+
const std::string& requestId,
185+
const ResponseInfo& responseInfo,
186+
int encodedDataLength) {
69187
if (!debuggingEnabled_.load(std::memory_order_relaxed)) {
70188
return;
71189
}
72190

73-
// TODO(T216933356)
74-
throw std::runtime_error("Not implemented");
191+
folly::dynamic responseParams =
192+
responseToCdpParams(responseInfo, encodedDataLength);
193+
194+
folly::dynamic params = folly::dynamic::object;
195+
params["requestId"] = requestId;
196+
params["loaderId"] = "";
197+
params["timestamp"] = getCurrentUnixTimestampSeconds();
198+
params["type"] = responseParams["mimeType"];
199+
params["response"] = responseParams;
200+
params["hasExtraInfo"] = false;
201+
202+
frontendChannel_(cdp::jsonNotification("Network.responseReceived", params));
75203
}
76204

77205
void NetworkReporter::reportDataReceived(const std::string& /*requestId*/) {
@@ -83,13 +211,19 @@ void NetworkReporter::reportDataReceived(const std::string& /*requestId*/) {
83211
throw std::runtime_error("Not implemented");
84212
}
85213

86-
void NetworkReporter::reportResponseEnd(const std::string& /*requestId*/) {
214+
void NetworkReporter::reportResponseEnd(
215+
const std::string& requestId,
216+
int encodedDataLength) {
87217
if (!debuggingEnabled_.load(std::memory_order_relaxed)) {
88218
return;
89219
}
90220

91-
// TODO(T216933356)
92-
throw std::runtime_error("Not implemented");
221+
folly::dynamic params = folly::dynamic::object;
222+
params["requestId"] = requestId;
223+
params["timestamp"] = getCurrentUnixTimestampSeconds();
224+
params["encodedDataLength"] = encodedDataLength;
225+
226+
frontendChannel_(cdp::jsonNotification("Network.loadingFinished", params));
93227
}
94228

95229
} // namespace facebook::react::jsinspector_modern

0 commit comments

Comments
 (0)