From c91cafae321c0cf6922260e059fef3ac14619aaa Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Wed, 26 Mar 2025 15:40:42 -0700 Subject: [PATCH] Refactor NetworkReporter internals (#50173) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50173 Refactors the internals of `NetworkReporter` (and the `jsinspector_network` library) to better organise concepts before we scale to more Network CDP events. - Introduces `cdp::network` structs modelling CDP `Network` domain events and data types. - Moves implementation details in converting input data objects to CDP types into `CdpNetwork.cpp` and `HttpUtils.cpp`. Changelog: [Internal] Reviewed By: hoxyq Differential Revision: D71470039 fbshipit-source-id: 0c04ffb40efbbb6d6d9782959f5adb33c9097ccb --- .../jsinspector-modern/network/CdpNetwork.cpp | 143 ++++++++++++++ .../jsinspector-modern/network/CdpNetwork.h | 110 +++++++++++ .../jsinspector-modern/network/HttpUtils.cpp | 22 +++ .../jsinspector-modern/network/HttpUtils.h | 22 +++ .../network/NetworkReporter.cpp | 176 ++++++------------ .../network/NetworkReporter.h | 42 ++--- .../jsinspector-modern/network/NetworkTypes.h | 42 +++++ 7 files changed, 410 insertions(+), 147 deletions(-) create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/network/CdpNetwork.cpp create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/network/CdpNetwork.h create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/network/HttpUtils.cpp create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/network/HttpUtils.h create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/network/NetworkTypes.h diff --git a/packages/react-native/ReactCommon/jsinspector-modern/network/CdpNetwork.cpp b/packages/react-native/ReactCommon/jsinspector-modern/network/CdpNetwork.cpp new file mode 100644 index 00000000000..875e3ac73da --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/CdpNetwork.cpp @@ -0,0 +1,143 @@ +/* + * 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. + */ + +#include "CdpNetwork.h" + +#include "HttpUtils.h" + +namespace facebook::react::jsinspector_modern::cdp::network { + +namespace { + +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; +} + +} // namespace + +/* static */ Request Request::fromInputParams(const RequestInfo& requestInfo) { + return { + .url = requestInfo.url, + .method = requestInfo.httpMethod, + .headers = requestInfo.headers, + .postData = requestInfo.httpBody, + }; +} + +folly::dynamic Request::toDynamic() const { + folly::dynamic result = folly::dynamic::object; + + result["url"] = url; + result["method"] = method; + result["headers"] = headersToDynamic(headers); + result["postData"] = postData.value_or(""); + + return result; +} + +/* static */ Response Response::fromInputParams( + const ResponseInfo& responseInfo, + int encodedDataLength) { + auto headers = responseInfo.headers.value_or(Headers()); + + return { + .url = responseInfo.url, + .status = responseInfo.statusCode, + .statusText = "", + .headers = headers, + .mimeType = mimeTypeFromHeaders(headers), + .encodedDataLength = encodedDataLength, + }; +} + +folly::dynamic Response::toDynamic() const { + folly::dynamic result = folly::dynamic::object; + + result["url"] = url; + result["status"] = status; + result["statusText"] = statusText; + result["headers"] = headersToDynamic(headers); + result["mimeType"] = mimeType; + result["encodedDataLength"] = encodedDataLength; + + return result; +} + +folly::dynamic RequestWillBeSentParams::toDynamic() const { + folly::dynamic params = folly::dynamic::object; + + params["requestId"] = requestId; + params["loaderId"] = loaderId; + params["documentURL"] = documentURL; + params["request"] = request.toDynamic(); + params["timestamp"] = timestamp; + params["wallTime"] = wallTime; + params["initiator"] = initiator; + params["redirectHasExtraInfo"] = redirectResponse.has_value(); + + if (redirectResponse.has_value()) { + params["redirectResponse"] = redirectResponse->toDynamic(); + } + + return params; +} + +folly::dynamic ResponseReceivedParams::toDynamic() const { + folly::dynamic params = folly::dynamic::object; + + params["requestId"] = requestId; + params["loaderId"] = loaderId; + params["timestamp"] = timestamp; + params["type"] = type; + params["response"] = response.toDynamic(); + params["hasExtraInfo"] = hasExtraInfo; + + return params; +} + +folly::dynamic LoadingFinishedParams::toDynamic() const { + folly::dynamic params = folly::dynamic::object; + + params["requestId"] = requestId; + params["timestamp"] = timestamp; + params["encodedDataLength"] = encodedDataLength; + + return params; +} + +std::string resourceTypeFromMimeType(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"; +} + +} // namespace facebook::react::jsinspector_modern::cdp::network diff --git a/packages/react-native/ReactCommon/jsinspector-modern/network/CdpNetwork.h b/packages/react-native/ReactCommon/jsinspector-modern/network/CdpNetwork.h new file mode 100644 index 00000000000..709e2e74922 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/CdpNetwork.h @@ -0,0 +1,110 @@ +/* + * 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 + +#include "NetworkTypes.h" + +#include + +#include + +// Data containers for CDP Network domain types, supporting serialization to +// folly::dynamic objects. + +namespace facebook::react::jsinspector_modern::cdp::network { + +/** + * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Request + */ +struct Request { + std::string url; + std::string method; + std::optional headers; + std::optional postData; + + /** + * Convenience function to construct a `Request` from the generic + * `RequestInfo` input object. + */ + static Request fromInputParams(const RequestInfo& requestInfo); + + folly::dynamic toDynamic() const; +}; + +/** + * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Response + */ +struct Response { + std::string url; + uint16_t status; + std::string statusText; + std::optional headers; + std::string mimeType; + int encodedDataLength; + + /** + * Convenience function to construct a `Response` from the generic + * `ResponseInfo` input object. + */ + static Response fromInputParams( + const ResponseInfo& responseInfo, + int encodedDataLength); + + folly::dynamic toDynamic() const; +}; + +/** + * https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-requestWillBeSent + */ +struct RequestWillBeSentParams { + std::string requestId; + std::string loaderId; + std::string documentURL; + Request request; + double timestamp; + double wallTime; + folly::dynamic initiator; + bool redirectHasExtraInfo; + std::optional redirectResponse; + + folly::dynamic toDynamic() const; +}; + +/** + * https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-responseReceived + */ +struct ResponseReceivedParams { + std::string requestId; + std::string loaderId; + double timestamp; + std::string type; + Response response; + bool hasExtraInfo; + + folly::dynamic toDynamic() const; +}; + +/** + * https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-loadingFinished + */ +struct LoadingFinishedParams { + std::string requestId; + double timestamp; + int encodedDataLength; + + folly::dynamic toDynamic() const; +}; + +/** + * Get the CDP `ResourceType` for a given MIME type. + * + * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ResourceType + */ +std::string resourceTypeFromMimeType(const std::string& mimeType); + +} // namespace facebook::react::jsinspector_modern::cdp::network diff --git a/packages/react-native/ReactCommon/jsinspector-modern/network/HttpUtils.cpp b/packages/react-native/ReactCommon/jsinspector-modern/network/HttpUtils.cpp new file mode 100644 index 00000000000..ebad1b620bc --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/HttpUtils.cpp @@ -0,0 +1,22 @@ +/* + * 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. + */ + +#include "HttpUtils.h" + +namespace facebook::react::jsinspector_modern { + +std::string mimeTypeFromHeaders(const Headers& headers) { + std::string mimeType = "application/octet-stream"; + + if (headers.find("Content-Type") != headers.end()) { + mimeType = headers.at("Content-Type"); + } + + return mimeType; +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/network/HttpUtils.h b/packages/react-native/ReactCommon/jsinspector-modern/network/HttpUtils.h new file mode 100644 index 00000000000..57a0401ec49 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/HttpUtils.h @@ -0,0 +1,22 @@ +/* + * 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 + +#include "NetworkTypes.h" + +#include + +namespace facebook::react::jsinspector_modern { + +/** + * Get the MIME type for a response based on the 'Content-Type' header. If + * the header is not present, returns 'application/octet-stream'. + */ +std::string mimeTypeFromHeaders(const Headers& headers); + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.cpp b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.cpp index 99cf5fd613d..a3e0ee29c5b 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.cpp @@ -7,86 +7,18 @@ #include "NetworkReporter.h" -#include +#include "CdpNetwork.h" + +#include #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). */ @@ -104,8 +36,8 @@ double getCurrentUnixTimestampSeconds() { } // namespace NetworkReporter& NetworkReporter::getInstance() { - static NetworkReporter tracer; - return tracer; + static NetworkReporter instance; + return instance; } void NetworkReporter::setFrontendChannel(FrontendChannel frontendChannel) { @@ -118,7 +50,6 @@ bool NetworkReporter::enableDebugging() { } debuggingEnabled_.store(true, std::memory_order_release); - LOG(INFO) << "Network debugging enabled" << std::endl; return true; } @@ -128,7 +59,6 @@ bool NetworkReporter::disableDebugging() { } debuggingEnabled_.store(false, std::memory_order_release); - LOG(INFO) << "Network debugging disabled" << std::endl; return true; } @@ -136,34 +66,39 @@ void NetworkReporter::reportRequestStart( const std::string& requestId, const RequestInfo& requestInfo, int encodedDataLength, - const std::optional& redirectResponse) { - if (!debuggingEnabled_.load(std::memory_order_relaxed)) { + const std::optional& redirectResponse) const { + if (!isDebuggingEnabledNoSync()) { return; } double timestamp = getCurrentUnixTimestampSeconds(); + auto request = cdp::network::Request::fromInputParams(requestInfo); + auto params = cdp::network::RequestWillBeSentParams{ + .requestId = requestId, + .loaderId = "", + .documentURL = "mobile", + .request = std::move(request), + // NOTE: Both timestamp and wallTime use the same unit, however wallTime + // is relative to an "arbitrary epoch". In our implementation, use the + // Unix epoch for both. + .timestamp = timestamp, + .wallTime = timestamp, + .initiator = folly::dynamic::object("type", "script"), + .redirectHasExtraInfo = redirectResponse.has_value(), + }; - 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); + params.redirectResponse = cdp::network::Response::fromInputParams( + redirectResponse.value(), encodedDataLength); } - frontendChannel_(cdp::jsonNotification("Network.requestWillBeSent", params)); + frontendChannel_( + cdp::jsonNotification("Network.requestWillBeSent", params.toDynamic())); } -void NetworkReporter::reportConnectionTiming(const std::string& /*requestId*/) { - if (!debuggingEnabled_.load(std::memory_order_relaxed)) { +void NetworkReporter::reportConnectionTiming( + const std::string& /*requestId*/) const { + if (!isDebuggingEnabledNoSync()) { return; } @@ -171,8 +106,9 @@ void NetworkReporter::reportConnectionTiming(const std::string& /*requestId*/) { throw std::runtime_error("Not implemented"); } -void NetworkReporter::reportRequestFailed(const std::string& /*requestId*/) { - if (!debuggingEnabled_.load(std::memory_order_relaxed)) { +void NetworkReporter::reportRequestFailed( + const std::string& /*requestId*/) const { + if (!isDebuggingEnabledNoSync()) { return; } @@ -183,27 +119,29 @@ void NetworkReporter::reportRequestFailed(const std::string& /*requestId*/) { void NetworkReporter::reportResponseStart( const std::string& requestId, const ResponseInfo& responseInfo, - int encodedDataLength) { - if (!debuggingEnabled_.load(std::memory_order_relaxed)) { + int encodedDataLength) const { + if (!isDebuggingEnabledNoSync()) { return; } - folly::dynamic responseParams = - responseToCdpParams(responseInfo, encodedDataLength); + auto response = + cdp::network::Response::fromInputParams(responseInfo, encodedDataLength); + auto params = cdp::network::ResponseReceivedParams{ + .requestId = requestId, + .loaderId = "", + .timestamp = getCurrentUnixTimestampSeconds(), + .type = cdp::network::resourceTypeFromMimeType(response.mimeType), + .response = response, + .hasExtraInfo = false, + }; - 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)); + frontendChannel_( + cdp::jsonNotification("Network.responseReceived", params.toDynamic())); } -void NetworkReporter::reportDataReceived(const std::string& /*requestId*/) { - if (!debuggingEnabled_.load(std::memory_order_relaxed)) { +void NetworkReporter::reportDataReceived( + const std::string& /*requestId*/) const { + if (!isDebuggingEnabledNoSync()) { return; } @@ -213,17 +151,19 @@ void NetworkReporter::reportDataReceived(const std::string& /*requestId*/) { void NetworkReporter::reportResponseEnd( const std::string& requestId, - int encodedDataLength) { - if (!debuggingEnabled_.load(std::memory_order_relaxed)) { + int encodedDataLength) const { + if (!isDebuggingEnabledNoSync()) { return; } - folly::dynamic params = folly::dynamic::object; - params["requestId"] = requestId; - params["timestamp"] = getCurrentUnixTimestampSeconds(); - params["encodedDataLength"] = encodedDataLength; + auto params = cdp::network::LoadingFinishedParams{ + .requestId = requestId, + .timestamp = getCurrentUnixTimestampSeconds(), + .encodedDataLength = encodedDataLength, + }; - frontendChannel_(cdp::jsonNotification("Network.loadingFinished", params)); + frontendChannel_( + cdp::jsonNotification("Network.loadingFinished", params.toDynamic())); } } // 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 c2c61ec1dda..fc8c4d6f69c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkReporter.h @@ -7,10 +7,10 @@ #pragma once +#include "NetworkTypes.h" + #include #include -#include -#include #include namespace facebook::react::jsinspector_modern { @@ -22,27 +22,6 @@ namespace facebook::react::jsinspector_modern { */ 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. @@ -86,7 +65,7 @@ class NetworkReporter { const std::string& requestId, const RequestInfo& requestInfo, int encodedDataLength, - const std::optional& redirectResponse); + const std::optional& redirectResponse) const; /** * Report detailed timing info, such as DNS lookup, when a request has @@ -98,14 +77,14 @@ class NetworkReporter { * * https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-connectstart */ - void reportConnectionTiming(const std::string& requestId); + void reportConnectionTiming(const std::string& requestId) const; /** * Report when a network request has failed. * * Corresponds to `Network.loadingFailed` in CDP. */ - void reportRequestFailed(const std::string& requestId); + void reportRequestFailed(const std::string& requestId) const; /** * Report when HTTP response headers have been received, corresponding to @@ -119,14 +98,14 @@ class NetworkReporter { void reportResponseStart( const std::string& requestId, const ResponseInfo& responseInfo, - int encodedDataLength); + int encodedDataLength) const; /** * Report when additional chunks of the response body have been received. * * Corresponds to `Network.dataReceived` in CDP. */ - void reportDataReceived(const std::string& requestId); + void reportDataReceived(const std::string& requestId) const; /** * Report when a network request is complete and we are no longer receiving @@ -137,7 +116,8 @@ class NetworkReporter { * * https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-responseend */ - void reportResponseEnd(const std::string& requestId, int encodedDataLength); + void reportResponseEnd(const std::string& requestId, int encodedDataLength) + const; private: FrontendChannel frontendChannel_; @@ -148,6 +128,10 @@ class NetworkReporter { ~NetworkReporter() = default; std::atomic debuggingEnabled_{false}; + + inline bool isDebuggingEnabledNoSync() const { + return debuggingEnabled_.load(std::memory_order_relaxed); + } }; } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkTypes.h b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkTypes.h new file mode 100644 index 00000000000..f493326a063 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkTypes.h @@ -0,0 +1,42 @@ +/* + * 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 + +#include +#include +#include + +// Defines generic input object types for NetworkReporter. + +namespace facebook::react::jsinspector_modern { + +/** + * A collection of parsed HTTP headers. + */ +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; +}; + +} // namespace facebook::react::jsinspector_modern