diff --git a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp index 4398431e61b176..546552e646f073 100644 --- a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp @@ -62,6 +62,7 @@ class HermesRuntimeSamplingProfileDelegate { } // namespace #ifdef HERMES_ENABLE_DEBUGGER + class HermesRuntimeTargetDelegate::Impl final : public RuntimeTargetDelegate { using HermesStackTrace = debugger::StackTrace; @@ -78,6 +79,14 @@ class HermesRuntimeTargetDelegate::Impl final : public RuntimeTargetDelegate { return &hermesStackTrace_; } + const HermesStackTrace& operator*() const { + return hermesStackTrace_; + } + + const HermesStackTrace* operator->() const { + return &hermesStackTrace_; + } + private: HermesStackTrace hermesStackTrace_; }; @@ -216,6 +225,16 @@ class HermesRuntimeTargetDelegate::Impl final : public RuntimeTargetDelegate { return samplingProfileDelegate_->collectSamplingProfile(); } + std::optional serializeStackTrace( + const StackTrace& stackTrace) override { + if (auto* hermesStackTraceWrapper = + dynamic_cast(&stackTrace)) { + return folly::parseJson(cdpDebugAPI_->serializeStackTraceToJsonStr( + **hermesStackTraceWrapper)); + } + return std::nullopt; + } + private: HermesRuntimeTargetDelegate& delegate_; std::shared_ptr runtime_; @@ -311,6 +330,11 @@ HermesRuntimeTargetDelegate::collectSamplingProfile() { return impl_->collectSamplingProfile(); } +std::optional HermesRuntimeTargetDelegate::serializeStackTrace( + const StackTrace& stackTrace) { + return impl_->serializeStackTrace(stackTrace); +} + #ifdef HERMES_ENABLE_DEBUGGER CDPDebugAPI& HermesRuntimeTargetDelegate::getCDPDebugAPI() { return impl_->getCDPDebugAPI(); diff --git a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h index 94612ba28c219c..63662d9df270af 100644 --- a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h @@ -60,6 +60,9 @@ class HermesRuntimeTargetDelegate : public RuntimeTargetDelegate { tracing::RuntimeSamplingProfile collectSamplingProfile() override; + std::optional serializeStackTrace( + const StackTrace& stackTrace) override; + private: // We use the private implementation idiom to ensure this class has the same // layout regardless of whether HERMES_ENABLE_DEBUGGER is defined. The net diff --git a/packages/react-native/ReactCommon/jsinspector-modern/CMakeLists.txt b/packages/react-native/ReactCommon/jsinspector-modern/CMakeLists.txt index e06c951fd91fb1..6405ffa6512db0 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/CMakeLists.txt +++ b/packages/react-native/ReactCommon/jsinspector-modern/CMakeLists.txt @@ -19,6 +19,7 @@ target_merge_so(jsinspector) target_include_directories(jsinspector PUBLIC ${REACT_COMMON_DIR}) target_link_libraries(jsinspector + boost folly_runtime glog jsinspector_network diff --git a/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeTargetDelegate.cpp b/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeTargetDelegate.cpp index 0198ba13959aa5..6a45d0d7c9790f 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeTargetDelegate.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeTargetDelegate.cpp @@ -58,4 +58,10 @@ FallbackRuntimeTargetDelegate::collectSamplingProfile() { "Sampling Profiler capabilities are not supported for Runtime fallback"); } +std::optional +FallbackRuntimeTargetDelegate::serializeStackTrace( + const StackTrace& stackTrace) { + return std::nullopt; +} + } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeTargetDelegate.h b/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeTargetDelegate.h index 9c2a6a43ae3066..9507eb2379d4ba 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeTargetDelegate.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeTargetDelegate.h @@ -46,6 +46,9 @@ class FallbackRuntimeTargetDelegate : public RuntimeTargetDelegate { tracing::RuntimeSamplingProfile collectSamplingProfile() override; + std::optional serializeStackTrace( + const StackTrace& stackTrace) override; + private: std::string engineDescription_; }; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/React-jsinspector.podspec b/packages/react-native/ReactCommon/jsinspector-modern/React-jsinspector.podspec index 9fd5a0816452a4..20d64cf70ce00d 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/React-jsinspector.podspec +++ b/packages/react-native/ReactCommon/jsinspector-modern/React-jsinspector.podspec @@ -46,6 +46,7 @@ Pod::Spec.new do |s| resolve_use_frameworks(s, module_name: module_name) + add_dependency(s, "boost") add_dependency(s, "React-oscompat") # Needed for USE_FRAMEWORKS=dynamic s.dependency "React-featureflags" add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"]) @@ -55,7 +56,6 @@ Pod::Spec.new do |s| add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing') s.dependency "React-perflogger", version add_dependency(s, "React-oscompat") - if use_hermes() s.dependency "hermes-engine" end diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp index 50562676704a82..75cf4f0fe9d368 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp @@ -54,7 +54,12 @@ RuntimeTarget::RuntimeTarget( void RuntimeTarget::installGlobals() { // NOTE: RuntimeTarget::installConsoleHandler is in RuntimeTargetConsole.cpp installConsoleHandler(); + // NOTE: RuntimeTarget::installDebuggerSessionObserver is in + // RuntimeTargetDebuggerSessionObserver.cpp installDebuggerSessionObserver(); + // NOTE: RuntimeTarget::installNetworkReporterAPI is in + // RuntimeTargetNetwork.cpp + installNetworkReporterAPI(); } std::shared_ptr RuntimeTarget::createAgent( diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h index 1dd9b8ef398485..882f0bd4c68b21 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h @@ -108,6 +108,14 @@ class RuntimeTargetDelegate { * Return recorded sampling profile for the previous sampling session. */ virtual tracing::RuntimeSamplingProfile collectSamplingProfile() = 0; + + /** + * \returns a JSON representation of the given stack trace, conforming to the + * @cdp Runtime.StackTrace type, if the runtime supports it. Otherwise, + * returns std::nullopt. + */ + virtual std::optional serializeStackTrace( + const StackTrace& stackTrace) = 0; }; /** @@ -291,6 +299,12 @@ class JSINSPECTOR_EXPORT RuntimeTarget */ void installDebuggerSessionObserver(); + /** + * Installs the private __NETWORK_REPORTER__ object on the Runtime's + * global object. + */ + void installNetworkReporterAPI(); + /** * Propagates the debugger session state change to the JavaScript via calling * onStatusChange on __DEBUGGER_SESSION_OBSERVER__. @@ -303,6 +317,50 @@ class JSINSPECTOR_EXPORT RuntimeTarget */ void emitDebuggerSessionDestroyed(); + /** + * Run the callback \p func synchronously while on the JS thread and pass it a + * safe reference to our RuntimeTargetDelegate for use on the JS thread. \see + * RuntimeTargetDelegate for information on which methods are safe to call on + * the JS thread. \warning The callback will not run if the RuntimeTarget has + * been destroyed. + * The \p runtime parameter is required to signify that the function is being + * called on the JS thread. + */ + template Fn> + static void tryRunWithSelfSync( + std::weak_ptr selfWeak, + jsi::Runtime& /*unused*/, + Fn func) { + if (auto self = selfWeak.lock()) { + auto selfExecutor = self->executorFromThis(); + // Q: Why is it safe to use self->delegate_ here? + // A: Because the caller of InspectorTarget::registerRuntime + // is explicitly required to guarantee that the delegate not + // only outlives the target, but also outlives all JS code + // execution that occurs on the JS thread. + func(*self); + // To ensure we never destroy `self` on the JS thread, send + // our shared_ptr back to the inspector thread. + selfExecutor([self = std::move(self)](auto&) { (void)self; }); + } + } + + /** + * \returns an opaque representation of the current stack trace if the + * RuntimeTarget is valid, or std::nullopt otherwise. + * \see RuntimeTargetDelegate::captureStackTrace + */ + static std::optional> tryCaptureStackTrace( + std::weak_ptr selfWeak, + jsi::Runtime& runtime, + size_t framesToSkip = 0); + + /** + * \returns a globally unique ID for a network request. + * May be called from any thread as long as the RuntimeTarget is valid. + */ + std::string createNetworkRequestId(); + // Necessary to allow RuntimeAgent to access RuntimeTarget's internals in a // controlled way (i.e. only RuntimeTargetController gets friend access, while // RuntimeAgent itself doesn't). diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetNetwork.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetNetwork.cpp new file mode 100644 index 00000000000000..018f63ecd5ebef --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetNetwork.cpp @@ -0,0 +1,98 @@ +/* + * 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 +#include + +#include +#include +#include + +using namespace facebook::jsi; +using namespace std::string_literals; + +namespace facebook::react::jsinspector_modern { + +namespace { + +/** + * JS `Object.create()` + */ +jsi::Object objectCreate(jsi::Runtime& runtime, jsi::Value prototype) { + auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object"); + auto createFn = objectGlobal.getPropertyAsFunction(runtime, "create"); + return createFn.callWithThis(runtime, objectGlobal, prototype) + .getObject(runtime); +} + +/** + * JS `Object.freeze + */ +jsi::Object objectFreeze(jsi::Runtime& runtime, jsi::Object object) { + auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object"); + auto freezeFn = objectGlobal.getPropertyAsFunction(runtime, "freeze"); + return freezeFn.callWithThis(runtime, objectGlobal, object) + .getObject(runtime); +} + +} // namespace + +void RuntimeTarget::installNetworkReporterAPI() { + jsExecutor_([selfWeak = weak_from_this(), + selfExecutor = executorFromThis()](jsi::Runtime& runtime) { + auto globalObj = runtime.global(); + auto networkReporterApi = objectCreate(runtime, nullptr); + networkReporterApi.setProperty( + runtime, + "createDevToolsRequestId", + Function::createFromHostFunction( + runtime, + PropNameID::forAscii(runtime, "createDevToolsRequestId"), + 0, + [selfWeak]( + Runtime& rt, + const Value& thisVal, + const Value* args, + size_t count) -> Value { + std::optional devToolsRequestId; + RuntimeTarget::tryRunWithSelfSync( + selfWeak, rt, [&](RuntimeTarget& self) { + devToolsRequestId = self.createNetworkRequestId(); + // Q: Why is it safe to use self.delegate_ here? + // A: Because the caller of InspectorTarget::registerRuntime + // is explicitly required to guarantee that the delegate not + // only outlives the target, but also outlives all JS code + // execution that occurs on the JS thread. + auto stackTrace = self.delegate_.captureStackTrace(rt); + // TODO(moti): Instead of checking the singleton state, + // directly check whether the current target has a session + // with the Network domain enabled. + if (NetworkHandler::getInstance().isEnabled()) { + auto cdpStackTrace = + self.delegate_.serializeStackTrace(*stackTrace); + if (cdpStackTrace) { + NetworkHandler::getInstance() + .recordRequestInitiatorStack( + *devToolsRequestId, std::move(*cdpStackTrace)); + } + } + }); + if (!devToolsRequestId) { + throw JSError(rt, "React Native Runtime is shutting down"); + } + return String::createFromUtf8(rt, *devToolsRequestId); + })); + networkReporterApi = objectFreeze(runtime, std::move(networkReporterApi)); + globalObj.setProperty(runtime, "__NETWORK_REPORTER__", networkReporterApi); + }); +} + +std::string RuntimeTarget::createNetworkRequestId() { + return boost::uuids::to_string(boost::uuids::random_generator()()); +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkHandler.cpp b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkHandler.cpp index 5e7c1b558a2ef3..6fb59b3ea98f72 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkHandler.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkHandler.cpp @@ -69,6 +69,8 @@ void NetworkHandler::onRequestWillBeSent( } double timestamp = getCurrentUnixTimestampSeconds(); + std::optional initiator; + initiator = consumeStoredRequestInitiator(requestId); auto params = cdp::network::RequestWillBeSentParams{ .requestId = requestId, .loaderId = "", @@ -79,7 +81,9 @@ void NetworkHandler::onRequestWillBeSent( // Unix epoch for both. .timestamp = timestamp, .wallTime = timestamp, - .initiator = folly::dynamic::object("type", "script"), + .initiator = initiator.has_value() + ? std::move(initiator.value()) + : folly::dynamic::object("type", "script"), .redirectHasExtraInfo = redirectResponse.has_value(), .redirectResponse = redirectResponse, }; @@ -114,7 +118,7 @@ void NetworkHandler::onResponseReceived( auto resourceType = cdp::network::resourceTypeFromMimeType(response.mimeType); { - std::lock_guard lock(resourceTypeMapMutex_); + std::lock_guard lock(requestMetadataMutex_); resourceTypeMap_.emplace(requestId, resourceType); } @@ -175,7 +179,7 @@ void NetworkHandler::onLoadingFailed( } { - std::lock_guard lock(resourceTypeMapMutex_); + std::lock_guard lock(requestMetadataMutex_); auto params = cdp::network::LoadingFailedParams{ .requestId = requestId, .timestamp = getCurrentUnixTimestampSeconds(), @@ -212,4 +216,30 @@ std::optional> NetworkHandler::getResponseBody( responseBody->data, responseBody->base64Encoded); } +void NetworkHandler::recordRequestInitiatorStack( + const std::string& requestId, + folly::dynamic stackTrace) { + if (!isEnabledNoSync()) { + return; + } + + std::lock_guard lock(requestMetadataMutex_); + requestInitiatorById_.emplace( + requestId, + folly::dynamic::object("type", "script")("stack", std::move(stackTrace))); +} + +std::optional NetworkHandler::consumeStoredRequestInitiator( + const std::string& requestId) { + std::lock_guard lock(requestMetadataMutex_); + auto it = requestInitiatorById_.find(requestId); + if (it == requestInitiatorById_.end()) { + return std::nullopt; + } + // Remove and return + auto result = std::move(it->second); + requestInitiatorById_.erase(it); + return result; +} + } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkHandler.h b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkHandler.h index b3c51b83ce4ca9..3ae55fc2fc169d 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkHandler.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/network/NetworkHandler.h @@ -124,6 +124,13 @@ class NetworkHandler { std::optional> getResponseBody( const std::string& requestId); + /** + * Associate the given stack trace with the given request ID. + */ + void recordRequestInitiatorStack( + const std::string& requestId, + folly::dynamic stackTrace); + private: NetworkHandler() = default; NetworkHandler(const NetworkHandler&) = delete; @@ -136,10 +143,14 @@ class NetworkHandler { return enabled_.load(std::memory_order_relaxed); } + std::optional consumeStoredRequestInitiator( + const std::string& requestId); + FrontendChannel frontendChannel_; std::map resourceTypeMap_{}; - std::mutex resourceTypeMapMutex_{}; + std::map requestInitiatorById_{}; + std::mutex requestMetadataMutex_{}; BoundedRequestBuffer responseBodyBuffer_{}; std::mutex requestBodyMutex_; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h b/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h index 8a7bd893fcf39c..212f7510d5e098 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h @@ -97,6 +97,7 @@ class MockInspectorPackagerConnectionDelegate executor_.add(callback); } })); + EXPECT_CALL(*this, scheduleCallback(_, _)).Times(AnyNumber()); } // InspectorPackagerConnectionDelegate methods @@ -168,6 +169,20 @@ class MockRuntimeTargetDelegate : public RuntimeTargetDelegate { collectSamplingProfile, (), (override)); + MOCK_METHOD( + std::optional, + serializeStackTrace, + (const StackTrace& stackTrace), + (override)); + + inline MockRuntimeTargetDelegate() { + using namespace testing; + + // Silence "uninteresting mock function call" warnings for methods that + // don't have side effects. + + EXPECT_CALL(*this, supportsConsole()).Times(AnyNumber()); + } }; class MockRuntimeAgentDelegate : public RuntimeAgentDelegate { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorPackagerConnectionTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorPackagerConnectionTest.cpp index f61eca78251c40..ca73058026cc60 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorPackagerConnectionTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorPackagerConnectionTest.cpp @@ -49,6 +49,8 @@ class InspectorPackagerConnectionTestBase : public testing::Test { socket->getDelegate().didOpen(); return std::move(socket); }); + EXPECT_CALL(*packagerConnectionDelegate(), connectWebSocket(_, _)) + .Times(AnyNumber()); } void TearDown() override { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.h b/packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.h index 2a3db0b78b3934..af32eedb896dd0 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/JsiIntegrationTest.h @@ -47,9 +47,8 @@ class JsiIntegrationPortableTestBase : public ::testing::Test, protected: Executor executor_; - JsiIntegrationPortableTestBase() - : inspectorFlagsGuard_{EngineAdapter::getInspectorFlagOverrides()}, - engineAdapter_{executor_} {} + JsiIntegrationPortableTestBase(InspectorFlagOverrides overrides = {}) + : inspectorFlagsGuard_(overrides), engineAdapter_{executor_} {} void SetUp() override { // NOTE: Using SetUp() so we can call virtual methods like diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp new file mode 100644 index 00000000000000..896802818606b8 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp @@ -0,0 +1,592 @@ +/* + * 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 "JsiIntegrationTest.h" +#include "engines/JsiIntegrationTestHermesEngineAdapter.h" + +#include +#include +#include +#include + +using namespace ::testing; + +namespace facebook::react::jsinspector_modern { + +namespace { + +struct Params { + bool enableNetworkEventReporting; +}; + +} // namespace + +/** + * A test fixture for the way the internal NetworkReporter API interacts with + * the CDP Network domain. + */ +class NetworkReporterTest : public JsiIntegrationPortableTestBase< + JsiIntegrationTestHermesEngineAdapter, + folly::QueuedImmediateExecutor>, + public WithParamInterface { + protected: + NetworkReporterTest() + : JsiIntegrationPortableTestBase({ + .networkInspectionEnabled = true, + .enableNetworkEventReporting = + GetParam().enableNetworkEventReporting, + }) {} + + void SetUp() override { + JsiIntegrationPortableTestBase::SetUp(); + connect(); + EXPECT_CALL( + fromPage(), + onMessage( + JsonParsed(AllOf(AtJsonPtr("/method", "Debugger.scriptParsed"))))) + .Times(AnyNumber()) + .WillRepeatedly(Invoke<>([this](const std::string& message) { + auto params = folly::parseJson(message); + // Store the script ID and URL for later use. + scriptUrlsById_.emplace( + params.at("params").at("scriptId").getString(), + params.at("params").at("url").getString()); + })); + } + + template + Matcher ScriptIdMapsTo(InnerMatcher urlMatcher) { + return ResultOf( + [this](const auto& id) { return getScriptUrlById(id.getString()); }, + urlMatcher); + } + + private: + std::optional getScriptUrlById(const std::string& scriptId) { + auto it = scriptUrlsById_.find(scriptId); + if (it == scriptUrlsById_.end()) { + return std::nullopt; + } + return it->second; + } + + std::unordered_map scriptUrlsById_; +}; + +TEST_P(NetworkReporterTest, testNetworkEnableDisable) { + InSequence s; + + EXPECT_FALSE(NetworkReporter::getInstance().isDebuggingEnabled()); + this->expectMessageFromPage(JsonEq(R"({ + "id": 1, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 1, + "method": "Network.enable" + })"); + + EXPECT_TRUE(NetworkReporter::getInstance().isDebuggingEnabled()); + + this->expectMessageFromPage(JsonEq(R"({ + "id": 2, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 2, + "method": "Network.disable" + })"); + + EXPECT_FALSE(NetworkReporter::getInstance().isDebuggingEnabled()); +} + +TEST_P(NetworkReporterTest, testGetMissingResponseBody) { + InSequence s; + this->expectMessageFromPage(JsonEq(R"({ + "id": 1, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 1, + "method": "Network.enable" + })"); + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/error/code", (int)cdp::ErrorCode::InternalError), + AtJsonPtr("/id", 2)))); + this->toPage_->sendMessage(R"({ + "id": 2, + "method": "Network.getResponseBody", + "params": { + "requestId": "1234567890-no-such-request" + } + })"); + this->expectMessageFromPage(JsonEq(R"({ + "id": 3, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 3, + "method": "Network.disable" + })"); +} + +TEST_P(NetworkReporterTest, testRequestWillBeSentWithRedirect) { + InSequence s; + this->expectMessageFromPage(JsonEq(R"({ + "id": 1, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 1, + "method": "Network.enable" + })"); + + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/method", "Network.requestWillBeSent"), + AtJsonPtr("/params/requestId", "test-request-1"), + AtJsonPtr("/params/loaderId", ""), + AtJsonPtr("/params/documentURL", "mobile"), + AtJsonPtr("/params/request/url", "https://example.com/redirected"), + AtJsonPtr("/params/request/method", "POST"), + AtJsonPtr("/params/request/headers/Content-Type", "application/json"), + AtJsonPtr("/params/timestamp", Gt(0)), + AtJsonPtr("/params/wallTime", Gt(0)), + AtJsonPtr("/params/initiator/type", "script"), + AtJsonPtr("/params/redirectHasExtraInfo", true), + AtJsonPtr("/params/redirectResponse", Not(IsEmpty())), + AtJsonPtr("/params/redirectResponse/url", "https://example.com/original"), + AtJsonPtr("/params/redirectResponse/status", 302), + AtJsonPtr( + "/params/redirectResponse/headers/Location", + "https://example.com/redirected")))); + + RequestInfo requestInfo; + requestInfo.url = "https://example.com/redirected"; + requestInfo.httpMethod = "POST"; + requestInfo.headers = Headers{{"Content-Type", "application/json"}}; + + ResponseInfo redirectResponse; + redirectResponse.url = "https://example.com/original"; + redirectResponse.statusCode = 302; + redirectResponse.headers = + Headers{{"Location", "https://example.com/redirected"}}; + + NetworkReporter::getInstance().reportRequestStart( + "test-request-1", requestInfo, 1024, redirectResponse); + + this->expectMessageFromPage(JsonEq(R"({ + "id": 2, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 2, + "method": "Network.disable" + })"); +} + +TEST_P(NetworkReporterTest, testRequestWillBeSentExtraInfoParameters) { + InSequence s; + this->expectMessageFromPage(JsonEq(R"({ + "id": 1, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 1, + "method": "Network.enable" + })"); + + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/method", "Network.requestWillBeSentExtraInfo"), + AtJsonPtr("/params/requestId", "test-extra-info"), + AtJsonPtr("/params/headers/User-Agent", "TestAgent"), + AtJsonPtr("/params/headers/Accept-Language", "en-US"), + AtJsonPtr("/params/connectTiming/requestTime", Gt(0))))); + + Headers extraHeaders = { + {"User-Agent", "TestAgent"}, {"Accept-Language", "en-US"}}; + + NetworkReporter::getInstance().reportConnectionTiming( + "test-extra-info", extraHeaders); + + this->expectMessageFromPage(JsonEq(R"({ + "id": 2, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 2, + "method": "Network.disable" + })"); +} + +TEST_P(NetworkReporterTest, testLoadingFailedCancelled) { + InSequence s; + this->expectMessageFromPage(JsonEq(R"({ + "id": 1, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 1, + "method": "Network.enable" + })"); + + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/method", "Network.loadingFailed"), + AtJsonPtr("/params/requestId", "test-request-1"), + AtJsonPtr("/params/timestamp", Gt(0)), + AtJsonPtr("/params/type", "Other"), + AtJsonPtr("/params/errorText", "net::ERR_ABORTED"), + AtJsonPtr("/params/canceled", true)))); + + NetworkReporter::getInstance().reportRequestFailed("test-request-1", true); + + this->expectMessageFromPage(JsonEq(R"({ + "id": 2, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 2, + "method": "Network.disable" + })"); +} + +TEST_P(NetworkReporterTest, testLoadingFailedError) { + InSequence s; + this->expectMessageFromPage(JsonEq(R"({ + "id": 1, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 1, + "method": "Network.enable" + })"); + + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/method", "Network.loadingFailed"), + AtJsonPtr("/params/requestId", "test-request-1"), + AtJsonPtr("/params/timestamp", Gt(0)), + AtJsonPtr("/params/type", "Other"), + AtJsonPtr("/params/errorText", "net::ERR_FAILED"), + AtJsonPtr("/params/canceled", false)))); + + NetworkReporter::getInstance().reportRequestFailed("test-request-1", false); + + this->expectMessageFromPage(JsonEq(R"({ + "id": 2, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 2, + "method": "Network.disable" + })"); +} + +TEST_P(NetworkReporterTest, testCompleteNetworkFlow) { + InSequence s; + this->expectMessageFromPage(JsonEq(R"({ + "id": 1, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 1, + "method": "Network.enable" + })"); + + const std::string requestId = "complete-flow-request"; + + // Step 1: Request will be sent + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/method", "Network.requestWillBeSent"), + AtJsonPtr("/params/requestId", requestId), + AtJsonPtr("/params/loaderId", ""), + AtJsonPtr("/params/documentURL", "mobile"), + AtJsonPtr("/params/request/url", "https://api.example.com/users"), + AtJsonPtr("/params/request/method", "GET"), + AtJsonPtr("/params/request/headers/Accept", "application/json"), + AtJsonPtr("/params/timestamp", Gt(0)), + AtJsonPtr("/params/wallTime", Gt(0)), + AtJsonPtr("/params/initiator/type", "script"), + AtJsonPtr("/params/redirectHasExtraInfo", false)))); + + RequestInfo requestInfo; + requestInfo.url = "https://api.example.com/users"; + requestInfo.httpMethod = "GET"; + requestInfo.headers = Headers{{"Accept", "application/json"}}; + + NetworkReporter::getInstance().reportRequestStart( + requestId, requestInfo, 0, std::nullopt); + + // Step 2: Connection timing + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/method", "Network.requestWillBeSentExtraInfo"), + AtJsonPtr("/params/requestId", requestId), + AtJsonPtr("/params/headers/Accept", "application/json"), + AtJsonPtr("/params/connectTiming/requestTime", Gt(0))))); + + NetworkReporter::getInstance().reportConnectionTiming( + requestId, requestInfo.headers); + + // Step 3: Response received + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/method", "Network.responseReceived"), + AtJsonPtr("/params/requestId", requestId), + AtJsonPtr("/params/loaderId", ""), + AtJsonPtr("/params/timestamp", Gt(0)), + AtJsonPtr("/params/type", "XHR"), + AtJsonPtr("/params/response/url", "https://api.example.com/users"), + AtJsonPtr("/params/response/status", 200), + AtJsonPtr("/params/response/statusText", "OK"), + AtJsonPtr("/params/response/headers/Content-Type", "application/json"), + AtJsonPtr("/params/response/headers/Content-Length", "1024"), + AtJsonPtr("/params/response/mimeType", "application/json"), + AtJsonPtr("/params/response/encodedDataLength", 1024), + AtJsonPtr("/params/hasExtraInfo", false)))); + + ResponseInfo responseInfo; + responseInfo.url = "https://api.example.com/users"; + responseInfo.statusCode = 200; + responseInfo.headers = + Headers{{"Content-Type", "application/json"}, {"Content-Length", "1024"}}; + + NetworkReporter::getInstance().reportResponseStart( + requestId, responseInfo, 1024); + + // Step 4: Data received (multiple chunks) + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/method", "Network.dataReceived"), + AtJsonPtr("/params/requestId", requestId), + AtJsonPtr("/params/timestamp", Gt(0)), + AtJsonPtr("/params/dataLength", 512), + AtJsonPtr("/params/encodedDataLength", 512)))); + + NetworkReporter::getInstance().reportDataReceived(requestId, 512, 512); + + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/method", "Network.dataReceived"), + AtJsonPtr("/params/requestId", requestId), + AtJsonPtr("/params/timestamp", Gt(0)), + AtJsonPtr("/params/dataLength", 512), + AtJsonPtr("/params/encodedDataLength", 512)))); + + NetworkReporter::getInstance().reportDataReceived(requestId, 512, 512); + + // Step 5: Loading finished + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/method", "Network.loadingFinished"), + AtJsonPtr("/params/requestId", requestId), + AtJsonPtr("/params/timestamp", Gt(0)), + AtJsonPtr("/params/encodedDataLength", 1024)))); + + NetworkReporter::getInstance().reportResponseEnd(requestId, 1024); + + // Store and retrieve response body + NetworkReporter::getInstance().storeResponseBody( + requestId, R"({"users": [{"id": 1, "name": "John"}]})", false); + + this->expectMessageFromPage(JsonEq(R"({ + "id": 2, + "result": { + "body": "{\"users\": [{\"id\": 1, \"name\": \"John\"}]}", + "base64Encoded": false + } + })")); + this->toPage_->sendMessage(fmt::format( + R"({{ + "id": 2, + "method": "Network.getResponseBody", + "params": {{ + "requestId": {0} + }} + }})", + folly::toJson(requestId))); + + this->expectMessageFromPage(JsonEq(R"({ + "id": 3, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 3, + "method": "Network.disable" + })"); +} + +TEST_P(NetworkReporterTest, testGetResponseBodyWithBase64) { + InSequence s; + this->expectMessageFromPage(JsonEq(R"({ + "id": 1, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 1, + "method": "Network.enable" + })"); + + const std::string requestId = "base64-response-test"; + + // Store base64-encoded response body + NetworkReporter::getInstance().storeResponseBody( + requestId, "SGVsbG8gV29ybGQ=", true); + + this->expectMessageFromPage(JsonEq(R"({ + "id": 2, + "result": { + "body": "SGVsbG8gV29ybGQ=", + "base64Encoded": true + } + })")); + this->toPage_->sendMessage(fmt::format( + R"({{ + "id": 2, + "method": "Network.getResponseBody", + "params": {{ + "requestId": {0} + }} + }})", + folly::toJson(requestId))); + + this->expectMessageFromPage(JsonEq(R"({ + "id": 3, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 3, + "method": "Network.disable" + })"); +} + +TEST_P(NetworkReporterTest, testNetworkEventsWhenDisabled) { + EXPECT_FALSE(NetworkReporter::getInstance().isDebuggingEnabled()); + + // NOTE: The test will automatically fail if any unexpected CDP messages are + // received as a result of the following calls. + + RequestInfo requestInfo; + requestInfo.url = "https://example.com/disabled"; + requestInfo.httpMethod = "GET"; + + NetworkReporter::getInstance().reportRequestStart( + "disabled-request", requestInfo, 0, std::nullopt); + + ResponseInfo responseInfo; + responseInfo.url = "https://example.com/disabled"; + responseInfo.statusCode = 200; + + NetworkReporter::getInstance().reportConnectionTiming("disabled-request", {}); + NetworkReporter::getInstance().reportResponseStart( + "disabled-request", responseInfo, 1024); + NetworkReporter::getInstance().reportDataReceived( + "disabled-request", 512, 512); + NetworkReporter::getInstance().reportResponseEnd("disabled-request", 1024); + NetworkReporter::getInstance().reportRequestFailed("disabled-request", false); +} + +TEST_P(NetworkReporterTest, testRequestWillBeSentWithInitiator) { + InSequence s; + + this->expectMessageFromPage(JsonEq(R"({ + "id": 0, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 0, + "method": "Debugger.enable" + })"); + + this->expectMessageFromPage(JsonEq(R"({ + "id": 1, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 1, + "method": "Network.enable" + })"); + RequestInfo requestInfo; + requestInfo.url = "https://example.com/initiator"; + requestInfo.httpMethod = "GET"; + + auto& runtime = engineAdapter_->getRuntime(); + + auto requestId = this->eval(R"( // line 0 + function inner() { // line 1 + return globalThis.__NETWORK_REPORTER__.createDevToolsRequestId(); // line 2 + } // line 3 + function outer() { // line 4 + return inner(); // line 5 + } // line 6 + outer(); // line 7 + + //# sourceURL=initiatorTest.js + )") + .asString(runtime) + .utf8(runtime); + + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/method", "Network.requestWillBeSent"), + AtJsonPtr("/params/requestId", requestId), + AtJsonPtr("/params/initiator/type", "script"), + AtJsonPtr( + "/params/initiator/stack/callFrames", + AllOf( + Each(AllOf( + AtJsonPtr("/url", "initiatorTest.js"), + AtJsonPtr( + "/scriptId", this->ScriptIdMapsTo("initiatorTest.js")))), + ElementsAre( + AllOf( + AtJsonPtr("/functionName", "inner"), + AtJsonPtr("/lineNumber", 2)), + AllOf( + AtJsonPtr("/functionName", "outer"), + AtJsonPtr("/lineNumber", 5)), + AllOf( + AtJsonPtr("/functionName", "global"), + AtJsonPtr("/lineNumber", 7)))))))); + + NetworkReporter::getInstance().reportRequestStart( + requestId, requestInfo, 0, std::nullopt); + + this->expectMessageFromPage(JsonEq(R"({ + "id": 2, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 2, + "method": "Network.disable" + })"); +} + +TEST_P(NetworkReporterTest, testCreateRequestIdWithoutNetworkDomain) { + InSequence s; + + auto& runtime = engineAdapter_->getRuntime(); + + auto id1 = this->eval(R"( + globalThis.__NETWORK_REPORTER__.createDevToolsRequestId(); + )") + .asString(runtime) + .utf8(runtime); + EXPECT_NE(id1, ""); + + auto id2 = this->eval(R"( + globalThis.__NETWORK_REPORTER__.createDevToolsRequestId(); + )") + .asString(runtime) + .utf8(runtime); + + EXPECT_NE(id2, ""); + + EXPECT_NE(id1, id2); +} + +static const auto paramValues = testing::Values( + Params{.enableNetworkEventReporting = true}, + Params{ + .enableNetworkEventReporting = false, + }); + +INSTANTIATE_TEST_SUITE_P(NetworkReporterTest, NetworkReporterTest, paramValues); + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/ReactInstanceIntegrationTest.h b/packages/react-native/ReactCommon/jsinspector-modern/tests/ReactInstanceIntegrationTest.h index c2e482d88e9392..0448d2aaef38e4 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/ReactInstanceIntegrationTest.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/ReactInstanceIntegrationTest.h @@ -50,7 +50,7 @@ class ReactInstanceIntegrationTest std::shared_ptr messageQueueThread; std::shared_ptr errorHandler; - MockRemoteConnection& getRemoteConnection() { + NiceMock& getRemoteConnection() { EXPECT_EQ(mockRemoteConnections_.objectsVended(), 1); auto rawPtr = mockRemoteConnections_[0]; assert(rawPtr); @@ -64,7 +64,7 @@ class ReactInstanceIntegrationTest size_t id_ = 1; bool verbose_ = false; std::optional pageId_; - UniquePtrFactory mockRemoteConnections_; + UniquePtrFactory> mockRemoteConnections_; std::unique_ptr clientToVM_; folly::QueuedImmediateExecutor immediateExecutor_; MockHostTargetDelegate hostTargetDelegate_; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/engines/JsiIntegrationTestGenericEngineAdapter.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/engines/JsiIntegrationTestGenericEngineAdapter.cpp index ea6208d23c4855..fc8474eeeca74b 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/engines/JsiIntegrationTestGenericEngineAdapter.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/engines/JsiIntegrationTestGenericEngineAdapter.cpp @@ -19,11 +19,6 @@ JsiIntegrationTestGenericEngineAdapter::JsiIntegrationTestGenericEngineAdapter( runtimeTargetDelegate_{ "Generic engine (" + runtime_->description() + ")"} {} -/* static */ InspectorFlagOverrides -JsiIntegrationTestGenericEngineAdapter::getInspectorFlagOverrides() noexcept { - return {}; -} - RuntimeTargetDelegate& JsiIntegrationTestGenericEngineAdapter::getRuntimeTargetDelegate() { return runtimeTargetDelegate_; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/engines/JsiIntegrationTestHermesEngineAdapter.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/engines/JsiIntegrationTestHermesEngineAdapter.cpp index ca6d48684bde1e..ad2b3e663bfc5d 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/engines/JsiIntegrationTestHermesEngineAdapter.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/engines/JsiIntegrationTestHermesEngineAdapter.cpp @@ -19,11 +19,6 @@ JsiIntegrationTestHermesEngineAdapter::JsiIntegrationTestHermesEngineAdapter( jsExecutor_{jsExecutor}, runtimeTargetDelegate_{runtime_} {} -/* static */ InspectorFlagOverrides -JsiIntegrationTestHermesEngineAdapter::getInspectorFlagOverrides() noexcept { - return {}; -} - RuntimeTargetDelegate& JsiIntegrationTestHermesEngineAdapter::getRuntimeTargetDelegate() { return runtimeTargetDelegate_; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.cpp index 76aaf2ead4f27a..837301c934a0c5 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.cpp @@ -36,6 +36,18 @@ class ReactNativeFeatureFlagsOverrides ReactNativeFeatureFlagsDefaults::fuseboxNetworkInspectionEnabled()); } + bool enableBridgelessArchitecture() override { + // NOTE: Network support is gated by (enableBridgelessArchitecture && + // fuseboxNetworkInspectionEnabled). + return overrides_.networkInspectionEnabled.value_or( + ReactNativeFeatureFlagsDefaults::enableBridgelessArchitecture()); + } + + bool enableNetworkEventReporting() override { + return overrides_.enableNetworkEventReporting.value_or( + ReactNativeFeatureFlagsDefaults::enableNetworkEventReporting()); + } + private: InspectorFlagOverrides overrides_; }; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.h b/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.h index b711ffdf0897d6..d86201c15c70db 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/utils/InspectorFlagOverridesGuard.h @@ -21,6 +21,7 @@ struct InspectorFlagOverrides { // the implementation file. std::optional fuseboxEnabledRelease; std::optional networkInspectionEnabled; + std::optional enableNetworkEventReporting; }; /**