diff --git a/packages/react-native/Libraries/WebSocket/WebSocket.js b/packages/react-native/Libraries/WebSocket/WebSocket.js index 637264bfc6b6..6fb094160928 100644 --- a/packages/react-native/Libraries/WebSocket/WebSocket.js +++ b/packages/react-native/Libraries/WebSocket/WebSocket.js @@ -143,8 +143,10 @@ class WebSocket extends EventTarget { ); this._socketId = nextWebSocketId++; this._registerEvents(); + // Skip this constructor's own frame so that the request initiator stack + // starts at the caller of `new WebSocket()`. const devToolsRequestId = - global.__NETWORK_REPORTER__?.createDevToolsRequestId(); + global.__NETWORK_REPORTER__?.createDevToolsRequestId(1); NativeWebSocketModule.connect( url, protocolList, diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetNetwork.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetNetwork.cpp index 506b0f9f74d1..267525b81fa9 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetNetwork.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetNetwork.cpp @@ -11,6 +11,8 @@ #include +#include + using namespace facebook::jsi; using namespace std::string_literals; @@ -44,8 +46,16 @@ void RuntimeTarget::installNetworkReporterAPI() { auto jsiCreateDevToolsRequestId = [selfWeak = weak_from_this()]( Runtime& runtime, const Value& /*thisVal*/, - const Value* /*args*/, - size_t /*count*/) -> Value { + const Value* args, + size_t count) -> Value { + // Optional `framesToSkip` argument: the number of leading JS call frames + // that belong to the calling API's own implementation (e.g. the `WebSocket` + // constructor), which are omitted from the recorded initiator stack so + // that its top frame is the code that invoked the API. + size_t framesToSkip = 0; + if (count > 0 && args[0].isNumber()) { + framesToSkip = static_cast(std::max(0.0, args[0].asNumber())); + } std::optional devToolsRequestId; tryExecuteSync(selfWeak, [&](RuntimeTarget& self) { devToolsRequestId = self.createNetworkRequestId(); @@ -58,6 +68,15 @@ void RuntimeTarget::installNetworkReporterAPI() { auto stackTrace = self.delegate_.captureStackTrace(runtime); auto cdpStackTrace = self.delegate_.serializeStackTrace(*stackTrace); if (cdpStackTrace) { + if (framesToSkip > 0) { + if (auto* callFrames = cdpStackTrace->get_ptr("callFrames"); + callFrames != nullptr && callFrames->isArray()) { + callFrames->erase( + callFrames->begin(), + callFrames->begin() + + std::min(framesToSkip, callFrames->size())); + } + } NetworkHandler::getInstance().recordRequestInitiatorStack( *devToolsRequestId, std::move(*cdpStackTrace)); } @@ -81,7 +100,7 @@ void RuntimeTarget::installNetworkReporterAPI() { Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "createDevToolsRequestId"), - 0, + 1, jsiCreateDevToolsRequestId)); networkReporterApi = objectFreeze(runtime, std::move(networkReporterApi)); globalObj.setProperty(runtime, "__NETWORK_REPORTER__", networkReporterApi); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp index 5313ba29a2bd..bed7200087fa 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp @@ -625,6 +625,75 @@ TEST_P(NetworkReporterTest, testWebSocketCreatedWithInitiator) { })"); } +TEST_P(NetworkReporterTest, testWebSocketCreatedWithInitiatorFramesToSkip) { + 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" + })"); + + auto& runtime = engineAdapter_->getRuntime(); + + // `apiShim` stands in for an API implementation (such as the `WebSocket` + // constructor) that asks for its own frame to be omitted. + auto requestId = this->eval(R"( // line 0 + function apiShim() { // line 1 + return globalThis.__NETWORK_REPORTER__.createDevToolsRequestId(1); // line 2 + } // line 3 + function openSocket() { // line 4 + return apiShim(); // line 5 + } // line 6 + openSocket(); // line 7 + + //# sourceURL=webSocketInitiatorFramesToSkipTest.js + )") + .asString(runtime) + .utf8(runtime); + + this->expectMessageFromPage(JsonParsed(AllOf( + AtJsonPtr("/method", "Network.webSocketCreated"), + AtJsonPtr("/params/requestId", requestId), + AtJsonPtr("/params/url", "wss://example.com/frames-to-skip"), + AtJsonPtr("/params/initiator/type", "script"), + AtJsonPtr( + "/params/initiator/stack/callFrames", + AllOf( + Each(AtJsonPtr("/url", "webSocketInitiatorFramesToSkipTest.js")), + ElementsAre( + AllOf( + AtJsonPtr("/functionName", "openSocket"), + AtJsonPtr("/lineNumber", 5)), + AllOf( + AtJsonPtr("/functionName", "global"), + AtJsonPtr("/lineNumber", 7)))))))); + + NetworkReporter::getInstance().reportWebSocketCreated( + requestId, "wss://example.com/frames-to-skip"); + + this->expectMessageFromPage(JsonEq(R"({ + "id": 2, + "result": {} + })")); + this->toPage_->sendMessage(R"({ + "id": 2, + "method": "Network.disable" + })"); +} + TEST_P(NetworkReporterTest, testNetworkEventsWhenDisabled) { EXPECT_FALSE(NetworkReporter::getInstance().isDebuggingEnabled());