diff --git a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegate.cpp b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegate.cpp index 81930abb05103d..451f4ec9c4eece 100644 --- a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegate.cpp +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegate.cpp @@ -72,10 +72,9 @@ class HermesRuntimeAgentDelegate::Impl final : public RuntimeAgentDelegate { } bool handleRequest(const cdp::PreparsedRequest& req) override { - // TODO: Change to string::starts_with when we're on C++20. - if (req.method.rfind("Log.", 0) == 0) { - // Since we know Hermes doesn't do anything useful with Log messages, - // but our containing HostAgent will, bail out early. + if (req.method.starts_with("Log.") || req.method.starts_with("Network.")) { + // Since we know Hermes doesn't do anything useful with Log or Network + // messages, but our containing HostAgent will, bail out early. // TODO: We need a way to negotiate this more dynamically with Hermes // through the API. return false; 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..09489561f06565 100644 --- a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp @@ -78,6 +78,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 +224,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 +329,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..1db506d5a0fec4 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 @@ -26,6 +27,7 @@ target_link_libraries(jsinspector react_featureflags runtimeexecutor reactperflogger + react_utils ) target_compile_reactnative_options(jsinspector PRIVATE) if(${CMAKE_BUILD_TYPE} MATCHES Debug OR REACT_NATIVE_DEBUG_OPTIMIZED) diff --git a/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeTargetDelegate.cpp b/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeTargetDelegate.cpp index 0198ba13959aa5..6ea90381de251b 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/HostAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp index 431fdf512c910c..bebb24a3bd7c8c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp @@ -9,6 +9,7 @@ #include "InstanceAgent.h" #ifdef REACT_NATIVE_DEBUGGER_ENABLED +#include "InspectorFlags.h" #include "NetworkIOAgent.h" #include "SessionState.h" #include "TracingAgent.h" @@ -142,6 +143,24 @@ class HostAgent::Impl final { .shouldSendOKResponse = true, }; } + if (InspectorFlags::getInstance().getNetworkInspectionEnabled()) { + if (req.method == "Network.enable") { + sessionState_.isNetworkDomainEnabled = true; + + return { + .isFinishedHandlingRequest = false, + .shouldSendOKResponse = true, + }; + } + if (req.method == "Network.disable") { + sessionState_.isNetworkDomainEnabled = false; + + return { + .isFinishedHandlingRequest = false, + .shouldSendOKResponse = true, + }; + } + } // Methods other than domain enables/disables: handle anything we know how // to handle, and delegate to the InstanceAgent otherwise. (In some special diff --git a/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp index 5e06150595352e..acbae362278bdc 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/NetworkIOAgent.cpp @@ -280,15 +280,15 @@ bool NetworkIOAgent::handleRequest( if (req.method == "Network.enable") { networkHandler.setFrontendChannel(frontendChannel_); networkHandler.enable(); - frontendChannel_(cdp::jsonResult(req.id)); - return true; + // NOTE: Domain enable/disable responses are sent by HostAgent. + return false; } // @cdp Network.disable support is experimental. if (req.method == "Network.disable") { networkHandler.disable(); - frontendChannel_(cdp::jsonResult(req.id)); - return true; + // NOTE: Domain enable/disable responses are sent by HostAgent. + return false; } // @cdp Network.getResponseBody support is experimental. diff --git a/packages/react-native/ReactCommon/jsinspector-modern/React-jsinspector.podspec b/packages/react-native/ReactCommon/jsinspector-modern/React-jsinspector.podspec index 9fd5a0816452a4..92234f46dee464 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,7 @@ Pod::Spec.new do |s| add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing') s.dependency "React-perflogger", version add_dependency(s, "React-oscompat") - + add_dependency(s, "React-utils", :additional_framework_paths => ["react/utils/platform/ios"]) 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..ff3451ff73515f 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,12 @@ class JSINSPECTOR_EXPORT RuntimeTarget */ void emitDebuggerSessionDestroyed(); + /** + * \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/RuntimeTargetConsole.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetConsole.cpp index 6b30b988631412..25a6e694c17edf 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetConsole.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetConsole.cpp @@ -525,30 +525,6 @@ void RuntimeTarget::installConsoleHandler() { auto console = objectCreate(runtime, std::move(consolePrototype)); auto state = std::make_shared(); - /** - * An executor that runs synchronously and provides 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. - */ - auto delegateExecutorSync = - [selfWeak, - selfExecutor](std::invocable auto func) { - if (auto self = selfWeak.lock()) { - // 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->delegate_); - // 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; }); - } - }; - /** * Install a console method with the given name and body. The body receives * the usual JSI host function parameters plus a ConsoleState reference, a @@ -569,20 +545,26 @@ void RuntimeTarget::installConsoleHandler() { forwardToOriginalConsole( originalConsole, methodName, - [body = std::move(body), state, delegateExecutorSync]( + [body = std::move(body), state, selfWeak]( jsi::Runtime& runtime, const jsi::Value& /*thisVal*/, const jsi::Value* args, size_t count) { auto timestampMs = getTimestampMs(); - delegateExecutorSync([&](auto& runtimeTargetDelegate) { - auto stackTrace = runtimeTargetDelegate.captureStackTrace( + tryExecuteSync(selfWeak, [&](auto& self) { + // 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( runtime, /* framesToSkip */ 1); body( runtime, args, count, - runtimeTargetDelegate, + self.delegate_, *state, timestampMs, std::move(stackTrace)); 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..2e2e0f3fd30730 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetNetwork.cpp @@ -0,0 +1,103 @@ +/* + * 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 +#include + +using namespace facebook::jsi; +using namespace std::string_literals; + +namespace facebook::react::jsinspector_modern { + +namespace { + +/** + * JS `Object.create()` + */ +Object objectCreate(Runtime& runtime, 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 + */ +Object objectFreeze(Runtime& runtime, 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() { + if (!InspectorFlags::getInstance().getNetworkInspectionEnabled()) { + return; + } + auto jsiCreateDevToolsRequestId = [selfWeak = weak_from_this()]( + Runtime& runtime, + const Value& /*thisVal*/, + const Value* /*args*/, + size_t /*count*/) -> Value { + std::optional devToolsRequestId; + tryExecuteSync(selfWeak, [&](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(runtime); + // 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(runtime, "React Native Runtime is shutting down"); + } + return String::createFromUtf8(runtime, *devToolsRequestId); + }; + + jsExecutor_([selfWeak = weak_from_this(), + selfExecutor = executorFromThis(), + jsiCreateDevToolsRequestId = + std::move(jsiCreateDevToolsRequestId)](Runtime& runtime) { + auto globalObj = runtime.global(); + auto networkReporterApi = objectCreate(runtime, nullptr); + networkReporterApi.setProperty( + runtime, + "createDevToolsRequestId", + Function::createFromHostFunction( + runtime, + PropNameID::forAscii(runtime, "createDevToolsRequestId"), + 0, + jsiCreateDevToolsRequestId)); + 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/ScopedExecutor.h b/packages/react-native/ReactCommon/jsinspector-modern/ScopedExecutor.h index 2c848fd63a6ea2..3b683cd2b13c1a 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/ScopedExecutor.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/ScopedExecutor.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -94,4 +95,32 @@ class EnableExecutorFromThis : public std::enable_shared_from_this { VoidExecutor baseExecutor_; }; +/** + * Synchronously executes a callback if the given object is still alive, + * and keeps the object alive at least until the callback returns, without + * moving ownership of the object itself across threads. + * + * The caller is responsible for all thread safety concerns outside of the + * lifetime of the object itself (e.g. the safety of calling particular methods + * on the object). + */ +template + requires std::derived_from< + ExecutorEnabledType, + EnableExecutorFromThis> +static void tryExecuteSync( + std::weak_ptr selfWeak, + std::invocable auto func) { + if (auto self = selfWeak.lock()) { + auto selfExecutor = self->executorFromThis(); + OnScopeExit onScopeExit{[self, selfExecutor = std::move(selfExecutor)]() { + // To ensure we never destroy `self` on the wrong thread, send + // our shared_ptr back to the correct executor. + selfExecutor([self = std::move(self)](auto&) { (void)self; }); + }}; + + func(*self); + } +} + } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/SessionState.h b/packages/react-native/ReactCommon/jsinspector-modern/SessionState.h index 9ff27db0a798df..6bb643918b85da 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/SessionState.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/SessionState.h @@ -24,6 +24,7 @@ struct SessionState { bool isLogDomainEnabled{false}; bool isReactNativeApplicationDomainEnabled{false}; bool isRuntimeDomainEnabled{false}; + bool isNetworkDomainEnabled{false}; /** * Whether the Trace Recording was initialized via CDP Tracing.start method 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 906804c8dc9f7c..212f7510d5e098 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/InspectorMocks.h @@ -169,6 +169,11 @@ class MockRuntimeTargetDelegate : public RuntimeTargetDelegate { collectSamplingProfile, (), (override)); + MOCK_METHOD( + std::optional, + serializeStackTrace, + (const StackTrace& stackTrace), + (override)); inline MockRuntimeTargetDelegate() { using namespace testing; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp index 27776a32f3b87f..896802818606b8 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp @@ -44,9 +44,37 @@ class NetworkReporterTest : public JsiIntegrationPortableTestBase< 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) { @@ -455,6 +483,104 @@ TEST_P(NetworkReporterTest, testNetworkEventsWhenDisabled) { 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{