Skip to content

Commit a4be9b2

Browse files
motiz88meta-codesync[bot]
authored andcommitted
Add JS API for allocating network request IDs, capturing call stacks (#54051)
Summary: Pull Request resolved: #54051 Adds the private, experimental `__NETWORK_REPORTER__.createDevToolsRequestId()` JavaScript method behind the Fusebox network inspection feature flag. `createDevToolsRequestId()` returns a unique string ID for a network request, and records the current call stack as the request's [initiator](https://cdpstatus.reactnative.dev/devtools-protocol/tot/Network#type-Initiator). If the native networking layer passes the same request ID into the C++ `NetworkReporter::reportRequestStart` method, the corresponding CDP [`requestWillBeSent`](https://cdpstatus.reactnative.dev/devtools-protocol/tot/Network#event-requestWillBeSent) event will contain the stack trace. Changelog: [Internal] Reviewed By: robhogan, huntie Differential Revision: D83238216 fbshipit-source-id: 5076d887254cd9711f86f72792e791eabc43f297
1 parent d932f28 commit a4be9b2

8 files changed

Lines changed: 276 additions & 2 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ target_link_libraries(jsinspector
2626
react_featureflags
2727
runtimeexecutor
2828
reactperflogger
29+
react_utils
2930
)
3031
target_compile_reactnative_options(jsinspector PRIVATE)
3132
if(${CMAKE_BUILD_TYPE} MATCHES Debug OR REACT_NATIVE_DEBUG_OPTIMIZED)

packages/react-native/ReactCommon/jsinspector-modern/React-jsinspector.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Pod::Spec.new do |s|
5555
add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing')
5656
s.dependency "React-perflogger", version
5757
add_dependency(s, "React-oscompat")
58-
58+
add_dependency(s, "React-utils", :additional_framework_paths => ["react/utils/platform/ios"])
5959
if use_hermes()
6060
s.dependency "hermes-engine"
6161
end

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@ RuntimeTarget::RuntimeTarget(
5454
void RuntimeTarget::installGlobals() {
5555
// NOTE: RuntimeTarget::installConsoleHandler is in RuntimeTargetConsole.cpp
5656
installConsoleHandler();
57+
// NOTE: RuntimeTarget::installDebuggerSessionObserver is in
58+
// RuntimeTargetDebuggerSessionObserver.cpp
5759
installDebuggerSessionObserver();
60+
// NOTE: RuntimeTarget::installNetworkReporterAPI is in
61+
// RuntimeTargetNetwork.cpp
62+
installNetworkReporterAPI();
5863
}
5964

6065
std::shared_ptr<RuntimeAgent> RuntimeTarget::createAgent(

packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,12 @@ class JSINSPECTOR_EXPORT RuntimeTarget
299299
*/
300300
void installDebuggerSessionObserver();
301301

302+
/**
303+
* Installs the private __NETWORK_REPORTER__ object on the Runtime's
304+
* global object.
305+
*/
306+
void installNetworkReporterAPI();
307+
302308
/**
303309
* Propagates the debugger session state change to the JavaScript via calling
304310
* onStatusChange on __DEBUGGER_SESSION_OBSERVER__.
@@ -311,6 +317,12 @@ class JSINSPECTOR_EXPORT RuntimeTarget
311317
*/
312318
void emitDebuggerSessionDestroyed();
313319

320+
/**
321+
* \returns a globally unique ID for a network request.
322+
* May be called from any thread as long as the RuntimeTarget is valid.
323+
*/
324+
std::string createNetworkRequestId();
325+
314326
// Necessary to allow RuntimeAgent to access RuntimeTarget's internals in a
315327
// controlled way (i.e. only RuntimeTargetController gets friend access, while
316328
// RuntimeAgent itself doesn't).
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <jsinspector-modern/InspectorFlags.h>
9+
#include <jsinspector-modern/RuntimeTarget.h>
10+
#include <jsinspector-modern/network/NetworkHandler.h>
11+
12+
#include <react/utils/Uuid.h>
13+
14+
using namespace facebook::jsi;
15+
using namespace std::string_literals;
16+
17+
namespace facebook::react::jsinspector_modern {
18+
19+
namespace {
20+
21+
/**
22+
* JS `Object.create()`
23+
*/
24+
Object objectCreate(Runtime& runtime, Value prototype) {
25+
auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object");
26+
auto createFn = objectGlobal.getPropertyAsFunction(runtime, "create");
27+
return createFn.callWithThis(runtime, objectGlobal, prototype)
28+
.getObject(runtime);
29+
}
30+
31+
/**
32+
* JS `Object.freeze()`
33+
*/
34+
Object objectFreeze(Runtime& runtime, Object object) {
35+
auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object");
36+
auto freezeFn = objectGlobal.getPropertyAsFunction(runtime, "freeze");
37+
return freezeFn.callWithThis(runtime, objectGlobal, object)
38+
.getObject(runtime);
39+
}
40+
41+
} // namespace
42+
43+
void RuntimeTarget::installNetworkReporterAPI() {
44+
if (!InspectorFlags::getInstance().getNetworkInspectionEnabled()) {
45+
return;
46+
}
47+
auto jsiCreateDevToolsRequestId = [selfWeak = weak_from_this()](
48+
Runtime& runtime,
49+
const Value& /*thisVal*/,
50+
const Value* /*args*/,
51+
size_t /*count*/) -> Value {
52+
std::optional<std::string> devToolsRequestId;
53+
tryExecuteSync(selfWeak, [&](RuntimeTarget& self) {
54+
devToolsRequestId = self.createNetworkRequestId();
55+
// TODO(moti): Instead of checking the singleton state,
56+
// directly check whether the current target has a session
57+
// with the Network domain enabled.
58+
if (NetworkHandler::getInstance().isEnabled()) {
59+
// Q: Why is it safe to use self.delegate_ here?
60+
// A: Because the caller of InspectorTarget::registerRuntime
61+
// is explicitly required to guarantee that the delegate not
62+
// only outlives the target, but also outlives all JS code
63+
// execution that occurs on the JS thread.
64+
auto stackTrace = self.delegate_.captureStackTrace(runtime);
65+
auto cdpStackTrace = self.delegate_.serializeStackTrace(*stackTrace);
66+
if (cdpStackTrace) {
67+
NetworkHandler::getInstance().recordRequestInitiatorStack(
68+
*devToolsRequestId, std::move(*cdpStackTrace));
69+
}
70+
}
71+
});
72+
if (!devToolsRequestId) {
73+
throw JSError(runtime, "React Native Runtime is shutting down");
74+
}
75+
return String::createFromUtf8(runtime, *devToolsRequestId);
76+
};
77+
78+
jsExecutor_([selfWeak = weak_from_this(),
79+
selfExecutor = executorFromThis(),
80+
jsiCreateDevToolsRequestId =
81+
std::move(jsiCreateDevToolsRequestId)](Runtime& runtime) {
82+
auto globalObj = runtime.global();
83+
auto networkReporterApi = objectCreate(runtime, nullptr);
84+
networkReporterApi.setProperty(
85+
runtime,
86+
"createDevToolsRequestId",
87+
Function::createFromHostFunction(
88+
runtime,
89+
PropNameID::forAscii(runtime, "createDevToolsRequestId"),
90+
0,
91+
jsiCreateDevToolsRequestId));
92+
networkReporterApi = objectFreeze(runtime, std::move(networkReporterApi));
93+
globalObj.setProperty(runtime, "__NETWORK_REPORTER__", networkReporterApi);
94+
});
95+
}
96+
97+
std::string RuntimeTarget::createNetworkRequestId() {
98+
return generateRandomUuidString();
99+
}
100+
101+
} // namespace facebook::react::jsinspector_modern

packages/react-native/ReactCommon/jsinspector-modern/ScopedExecutor.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#pragma once
99

10+
#include <react/utils/OnScopeExit.h>
1011
#include <cassert>
1112
#include <functional>
1213
#include <memory>
@@ -94,4 +95,32 @@ class EnableExecutorFromThis : public std::enable_shared_from_this<Self> {
9495
VoidExecutor baseExecutor_;
9596
};
9697

98+
/**
99+
* Synchronously executes a callback if the given object is still alive,
100+
* and keeps the object alive at least until the callback returns, without
101+
* moving ownership of the object itself across threads.
102+
*
103+
* The caller is responsible for all thread safety concerns outside of the
104+
* lifetime of the object itself (e.g. the safety of calling particular methods
105+
* on the object).
106+
*/
107+
template <typename ExecutorEnabledType>
108+
requires std::derived_from<
109+
ExecutorEnabledType,
110+
EnableExecutorFromThis<ExecutorEnabledType>>
111+
static void tryExecuteSync(
112+
std::weak_ptr<ExecutorEnabledType> selfWeak,
113+
std::invocable<ExecutorEnabledType&> auto func) {
114+
if (auto self = selfWeak.lock()) {
115+
auto selfExecutor = self->executorFromThis();
116+
OnScopeExit onScopeExit{[self, selfExecutor = std::move(selfExecutor)]() {
117+
// To ensure we never destroy `self` on the wrong thread, send
118+
// our shared_ptr back to the correct executor.
119+
selfExecutor([self = std::move(self)](auto&) { (void)self; });
120+
}};
121+
122+
func(*self);
123+
}
124+
}
125+
97126
} // namespace facebook::react::jsinspector_modern

packages/react-native/ReactCommon/jsinspector-modern/tests/NetworkReporterTest.cpp

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,37 @@ class NetworkReporterTest : public JsiIntegrationPortableTestBase<
4444
void SetUp() override {
4545
JsiIntegrationPortableTestBase::SetUp();
4646
connect();
47+
EXPECT_CALL(
48+
fromPage(),
49+
onMessage(
50+
JsonParsed(AllOf(AtJsonPtr("/method", "Debugger.scriptParsed")))))
51+
.Times(AnyNumber())
52+
.WillRepeatedly(Invoke<>([this](const std::string& message) {
53+
auto params = folly::parseJson(message);
54+
// Store the script ID and URL for later use.
55+
scriptUrlsById_.emplace(
56+
params.at("params").at("scriptId").getString(),
57+
params.at("params").at("url").getString());
58+
}));
59+
}
60+
61+
template <typename InnerMatcher>
62+
Matcher<folly::dynamic> ScriptIdMapsTo(InnerMatcher urlMatcher) {
63+
return ResultOf(
64+
[this](const auto& id) { return getScriptUrlById(id.getString()); },
65+
urlMatcher);
4766
}
4867

4968
private:
69+
std::optional<std::string> getScriptUrlById(const std::string& scriptId) {
70+
auto it = scriptUrlsById_.find(scriptId);
71+
if (it == scriptUrlsById_.end()) {
72+
return std::nullopt;
73+
}
74+
return it->second;
75+
}
76+
77+
std::unordered_map<std::string, std::string> scriptUrlsById_;
5078
};
5179

5280
TEST_P(NetworkReporterTest, testNetworkEnableDisable) {
@@ -455,6 +483,104 @@ TEST_P(NetworkReporterTest, testNetworkEventsWhenDisabled) {
455483
NetworkReporter::getInstance().reportRequestFailed("disabled-request", false);
456484
}
457485

486+
TEST_P(NetworkReporterTest, testRequestWillBeSentWithInitiator) {
487+
InSequence s;
488+
489+
this->expectMessageFromPage(JsonEq(R"({
490+
"id": 0,
491+
"result": {}
492+
})"));
493+
this->toPage_->sendMessage(R"({
494+
"id": 0,
495+
"method": "Debugger.enable"
496+
})");
497+
498+
this->expectMessageFromPage(JsonEq(R"({
499+
"id": 1,
500+
"result": {}
501+
})"));
502+
this->toPage_->sendMessage(R"({
503+
"id": 1,
504+
"method": "Network.enable"
505+
})");
506+
RequestInfo requestInfo;
507+
requestInfo.url = "https://example.com/initiator";
508+
requestInfo.httpMethod = "GET";
509+
510+
auto& runtime = engineAdapter_->getRuntime();
511+
512+
auto requestId = this->eval(R"( // line 0
513+
function inner() { // line 1
514+
return globalThis.__NETWORK_REPORTER__.createDevToolsRequestId(); // line 2
515+
} // line 3
516+
function outer() { // line 4
517+
return inner(); // line 5
518+
} // line 6
519+
outer(); // line 7
520+
521+
//# sourceURL=initiatorTest.js
522+
)")
523+
.asString(runtime)
524+
.utf8(runtime);
525+
526+
this->expectMessageFromPage(JsonParsed(AllOf(
527+
AtJsonPtr("/method", "Network.requestWillBeSent"),
528+
AtJsonPtr("/params/requestId", requestId),
529+
AtJsonPtr("/params/initiator/type", "script"),
530+
AtJsonPtr(
531+
"/params/initiator/stack/callFrames",
532+
AllOf(
533+
Each(AllOf(
534+
AtJsonPtr("/url", "initiatorTest.js"),
535+
AtJsonPtr(
536+
"/scriptId", this->ScriptIdMapsTo("initiatorTest.js")))),
537+
ElementsAre(
538+
AllOf(
539+
AtJsonPtr("/functionName", "inner"),
540+
AtJsonPtr("/lineNumber", 2)),
541+
AllOf(
542+
AtJsonPtr("/functionName", "outer"),
543+
AtJsonPtr("/lineNumber", 5)),
544+
AllOf(
545+
AtJsonPtr("/functionName", "global"),
546+
AtJsonPtr("/lineNumber", 7))))))));
547+
548+
NetworkReporter::getInstance().reportRequestStart(
549+
requestId, requestInfo, 0, std::nullopt);
550+
551+
this->expectMessageFromPage(JsonEq(R"({
552+
"id": 2,
553+
"result": {}
554+
})"));
555+
this->toPage_->sendMessage(R"({
556+
"id": 2,
557+
"method": "Network.disable"
558+
})");
559+
}
560+
561+
TEST_P(NetworkReporterTest, testCreateRequestIdWithoutNetworkDomain) {
562+
InSequence s;
563+
564+
auto& runtime = engineAdapter_->getRuntime();
565+
566+
auto id1 = this->eval(R"(
567+
globalThis.__NETWORK_REPORTER__.createDevToolsRequestId();
568+
)")
569+
.asString(runtime)
570+
.utf8(runtime);
571+
EXPECT_NE(id1, "");
572+
573+
auto id2 = this->eval(R"(
574+
globalThis.__NETWORK_REPORTER__.createDevToolsRequestId();
575+
)")
576+
.asString(runtime)
577+
.utf8(runtime);
578+
579+
EXPECT_NE(id2, "");
580+
581+
EXPECT_NE(id1, id2);
582+
}
583+
458584
static const auto paramValues = testing::Values(
459585
Params{.enableNetworkEventReporting = true},
460586
Params{

packages/react-native/ReactCommon/react/utils/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ target_include_directories(react_utils
3030
target_link_libraries(react_utils
3131
glog
3232
glog_init
33-
jsireact
33+
jsi
3434
react_debug)
3535
target_compile_reactnative_options(react_utils PRIVATE)
3636
target_compile_options(react_utils PRIVATE -Wpedantic)

0 commit comments

Comments
 (0)