Skip to content

Commit fbd84d9

Browse files
motiz88facebook-github-bot
authored andcommitted
Add JS API for allocating network request IDs, capturing call stacks (facebook#54051)
Summary: 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] Differential Revision: D83238216
1 parent dbb9249 commit fbd84d9

File tree

7 files changed

+279
-1
lines changed

7 files changed

+279
-1
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ target_merge_so(jsinspector)
1919
target_include_directories(jsinspector PUBLIC ${REACT_COMMON_DIR})
2020

2121
target_link_libraries(jsinspector
22+
boost
2223
folly_runtime
2324
glog
2425
jsinspector_network
2526
jsinspector_tracing
2627
react_featureflags
2728
runtimeexecutor
2829
reactperflogger
30+
react_utils
2931
)
3032
target_compile_reactnative_options(jsinspector PRIVATE)
3133
if(${CMAKE_BUILD_TYPE} MATCHES Debug OR REACT_NATIVE_DEBUG_OPTIMIZED)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Pod::Spec.new do |s|
4646

4747
resolve_use_frameworks(s, module_name: module_name)
4848

49+
add_dependency(s, "boost")
4950
add_dependency(s, "React-oscompat") # Needed for USE_FRAMEWORKS=dynamic
5051
s.dependency "React-featureflags"
5152
add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"])
@@ -55,7 +56,7 @@ Pod::Spec.new do |s|
5556
add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing')
5657
s.dependency "React-perflogger", version
5758
add_dependency(s, "React-oscompat")
58-
59+
add_dependency(s, "React-utils", :additional_framework_paths => ["react/utils/platform/ios"])
5960
if use_hermes()
6061
s.dependency "hermes-engine"
6162
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: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 <boost/uuid/random_generator.hpp>
13+
#include <boost/uuid/uuid.hpp>
14+
#include <boost/uuid/uuid_io.hpp>
15+
16+
using namespace facebook::jsi;
17+
using namespace std::string_literals;
18+
19+
namespace facebook::react::jsinspector_modern {
20+
21+
namespace {
22+
23+
/**
24+
* JS `Object.create()`
25+
*/
26+
Object objectCreate(Runtime& runtime, Value prototype) {
27+
auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object");
28+
auto createFn = objectGlobal.getPropertyAsFunction(runtime, "create");
29+
return createFn.callWithThis(runtime, objectGlobal, prototype)
30+
.getObject(runtime);
31+
}
32+
33+
/**
34+
* JS `Object.freeze
35+
*/
36+
Object objectFreeze(Runtime& runtime, Object object) {
37+
auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object");
38+
auto freezeFn = objectGlobal.getPropertyAsFunction(runtime, "freeze");
39+
return freezeFn.callWithThis(runtime, objectGlobal, object)
40+
.getObject(runtime);
41+
}
42+
43+
} // namespace
44+
45+
void RuntimeTarget::installNetworkReporterAPI() {
46+
if (!InspectorFlags::getInstance().getNetworkInspectionEnabled()) {
47+
return;
48+
}
49+
auto jsiCreateDevToolsRequestId = [selfWeak = weak_from_this()](
50+
Runtime& runtime,
51+
const Value& /*thisVal*/,
52+
const Value* /*args*/,
53+
size_t /*count*/) -> Value {
54+
std::optional<std::string> devToolsRequestId;
55+
tryExecuteSync(selfWeak, [&](RuntimeTarget& self) {
56+
devToolsRequestId = self.createNetworkRequestId();
57+
// Q: Why is it safe to use self.delegate_ here?
58+
// A: Because the caller of InspectorTarget::registerRuntime
59+
// is explicitly required to guarantee that the delegate not
60+
// only outlives the target, but also outlives all JS code
61+
// execution that occurs on the JS thread.
62+
auto stackTrace = self.delegate_.captureStackTrace(runtime);
63+
// TODO(moti): Instead of checking the singleton state,
64+
// directly check whether the current target has a session
65+
// with the Network domain enabled.
66+
if (NetworkHandler::getInstance().isEnabled()) {
67+
auto cdpStackTrace = self.delegate_.serializeStackTrace(*stackTrace);
68+
if (cdpStackTrace) {
69+
NetworkHandler::getInstance().recordRequestInitiatorStack(
70+
*devToolsRequestId, std::move(*cdpStackTrace));
71+
}
72+
}
73+
});
74+
if (!devToolsRequestId) {
75+
throw JSError(runtime, "React Native Runtime is shutting down");
76+
}
77+
return String::createFromUtf8(runtime, *devToolsRequestId);
78+
};
79+
80+
jsExecutor_([selfWeak = weak_from_this(),
81+
selfExecutor = executorFromThis(),
82+
jsiCreateDevToolsRequestId =
83+
std::move(jsiCreateDevToolsRequestId)](Runtime& runtime) {
84+
auto globalObj = runtime.global();
85+
auto networkReporterApi = objectCreate(runtime, nullptr);
86+
networkReporterApi.setProperty(
87+
runtime,
88+
"createDevToolsRequestId",
89+
Function::createFromHostFunction(
90+
runtime,
91+
PropNameID::forAscii(runtime, "createDevToolsRequestId"),
92+
0,
93+
jsiCreateDevToolsRequestId));
94+
networkReporterApi = objectFreeze(runtime, std::move(networkReporterApi));
95+
globalObj.setProperty(runtime, "__NETWORK_REPORTER__", networkReporterApi);
96+
});
97+
}
98+
99+
std::string RuntimeTarget::createNetworkRequestId() {
100+
return boost::uuids::to_string(boost::uuids::random_generator()());
101+
}
102+
103+
} // 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{

0 commit comments

Comments
 (0)