Skip to content

Commit b18cd58

Browse files
huntiefacebook-github-bot
authored andcommitted
Implement core Network CDP events on Android (#52485)
Summary: Pull Request resolved: #52485 Begins integrating `NetworkReporter` (jsinspector-modern) on Android, to enable the Network panel in React Native DevTools. Since the larger lift of initial setup and the C++ subsystem has been done for iOS, this will be a lighter stack of changes solely setting up necessary integration points in the Android Networking stack. NOTE: As with iOS, all changes are gated behind the `enableNetworkEventReporting` and `fuseboxNetworkInspectionEnabled` feature flags. **This diff** Initially integrates the `NetworkReporter` methods corresponding to the `Network.requestWillBeSent`, `Network.requestWillBeSentExtraInfo`, `Network.responseReceived`, `Network.loadingFinished` CDP events, which are sufficient for populating a minimally rendered Network request list. - Create JNI `InspectorNetworkReporter` helper class (may also become the later public API for 3P reporting into the `Network` domain). - Renames `ResponseUtil.kt` as `NetworkEventUtil.kt`. Changelog: [Internal] Reviewed By: cortinico Differential Revision: D71897099 fbshipit-source-id: 90972a5bfa34a095252b7e745e5f4afeb53b0ebe
1 parent cf45aa9 commit b18cd58

File tree

10 files changed

+598
-217
lines changed

10 files changed

+598
-217
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
package com.facebook.react.modules.network
9+
10+
import com.facebook.proguard.annotations.DoNotStripAny
11+
12+
/**
13+
* [Experimental] An interface for reporting network events to the modern debugger server and Web
14+
* Performance APIs.
15+
*
16+
* In a production (non dev or profiling) build, CDP reporting is disabled.
17+
*
18+
* This is a helper class wrapping `facebook::react::jsinspector_modern::NetworkReporter`.
19+
*/
20+
@DoNotStripAny
21+
internal object InspectorNetworkReporter {
22+
/**
23+
* Report a network request that is about to be sent.
24+
* - Corresponds to `Network.requestWillBeSent` in CDP.
25+
* - Corresponds to `PerformanceResourceTiming.requestStart` (specifically, marking when the
26+
* native request was initiated).
27+
*/
28+
@JvmStatic
29+
external fun reportRequestStart(
30+
requestId: Int,
31+
requestUrl: String,
32+
requestMethod: String,
33+
requestHeaders: Map<String, String>,
34+
requestBody: String,
35+
encodedDataLength: Long
36+
)
37+
38+
/**
39+
* Report detailed timing info, such as DNS lookup, when a request has started.
40+
* - Corresponds to `Network.requestWillBeSentExtraInfo` in CDP.
41+
* - Corresponds to `PerformanceResourceTiming.domainLookupStart`,
42+
* `PerformanceResourceTiming.connectStart`.
43+
*/
44+
@JvmStatic external fun reportConnectionTiming(requestId: Int, headers: Map<String, String>)
45+
46+
/**
47+
* Report when HTTP response headers have been received, corresponding to when the first byte of
48+
* the response is available.
49+
* - Corresponds to `Network.responseReceived` in CDP.
50+
* - Corresponds to `PerformanceResourceTiming.responseStart`.
51+
*/
52+
@JvmStatic
53+
external fun reportResponseStart(
54+
requestId: Int,
55+
requestUrl: String,
56+
responseStatus: Int,
57+
responseHeaders: Map<String, String>,
58+
expectedDataLength: Long
59+
)
60+
61+
/**
62+
* Report when a network request is complete and we are no longer receiving response data.
63+
* - Corresponds to `Network.loadingFinished` in CDP.
64+
* - Corresponds to `PerformanceResourceTiming.responseEnd`.
65+
*/
66+
@JvmStatic external fun reportResponseEnd(requestId: Int, encodedDataLength: Long)
67+
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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+
@file:Suppress("DEPRECATION_ERROR") // Conflicting okhttp versions
9+
10+
package com.facebook.react.modules.network
11+
12+
import android.os.Bundle
13+
import com.facebook.react.bridge.Arguments
14+
import com.facebook.react.bridge.ReactApplicationContext
15+
import com.facebook.react.bridge.WritableMap
16+
import com.facebook.react.bridge.buildReadableArray
17+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
18+
import java.net.SocketTimeoutException
19+
import okhttp3.Headers
20+
import okhttp3.Protocol
21+
import okhttp3.Request
22+
import okhttp3.Response
23+
24+
/**
25+
* Utility class for reporting network lifecycle events to JavaScript and InspectorNetworkReporter.
26+
*/
27+
internal object NetworkEventUtil {
28+
@JvmStatic
29+
fun onCreateRequest(requestId: Int, request: Request) {
30+
if (ReactNativeFeatureFlags.enableNetworkEventReporting()) {
31+
val headersMap = okHttpHeadersToMap(request.headers())
32+
InspectorNetworkReporter.reportRequestStart(
33+
requestId,
34+
request.url().toString(),
35+
request.method(),
36+
headersMap,
37+
request.body()?.toString().orEmpty(),
38+
request.body()?.contentLength() ?: 0,
39+
)
40+
InspectorNetworkReporter.reportConnectionTiming(requestId, headersMap)
41+
}
42+
}
43+
44+
@JvmStatic
45+
fun onDataSend(
46+
reactContext: ReactApplicationContext?,
47+
requestId: Int,
48+
progress: Long,
49+
total: Long
50+
) {
51+
reactContext?.emitDeviceEvent(
52+
"didSendNetworkData",
53+
buildReadableArray {
54+
add(requestId)
55+
add(progress.toInt())
56+
add(total.toInt())
57+
})
58+
}
59+
60+
@JvmStatic
61+
fun onIncrementalDataReceived(
62+
reactContext: ReactApplicationContext?,
63+
requestId: Int,
64+
data: String?,
65+
progress: Long,
66+
total: Long
67+
) {
68+
reactContext?.emitDeviceEvent(
69+
"didReceiveNetworkIncrementalData",
70+
buildReadableArray {
71+
add(requestId)
72+
add(data)
73+
add(progress.toInt())
74+
add(total.toInt())
75+
})
76+
}
77+
78+
@JvmStatic
79+
fun onDataReceivedProgress(
80+
reactContext: ReactApplicationContext?,
81+
requestId: Int,
82+
progress: Long,
83+
total: Long
84+
) {
85+
reactContext?.emitDeviceEvent(
86+
"didReceiveNetworkDataProgress",
87+
buildReadableArray {
88+
add(requestId)
89+
add(progress.toInt())
90+
add(total.toInt())
91+
})
92+
}
93+
94+
@JvmStatic
95+
fun onDataReceived(reactContext: ReactApplicationContext?, requestId: Int, data: String?) {
96+
reactContext?.emitDeviceEvent(
97+
"didReceiveNetworkData",
98+
buildReadableArray {
99+
add(requestId)
100+
add(data)
101+
})
102+
}
103+
104+
@JvmStatic
105+
fun onDataReceived(reactContext: ReactApplicationContext?, requestId: Int, data: WritableMap?) {
106+
reactContext?.emitDeviceEvent(
107+
"didReceiveNetworkData",
108+
Arguments.createArray().apply {
109+
pushInt(requestId)
110+
pushMap(data)
111+
})
112+
}
113+
114+
@JvmStatic
115+
fun onRequestError(
116+
reactContext: ReactApplicationContext?,
117+
requestId: Int,
118+
error: String?,
119+
e: Throwable?
120+
) {
121+
reactContext?.emitDeviceEvent(
122+
"didCompleteNetworkResponse",
123+
buildReadableArray {
124+
add(requestId)
125+
add(error)
126+
if (e?.javaClass == SocketTimeoutException::class.java) {
127+
add(true) // last argument is a time out boolean
128+
}
129+
})
130+
}
131+
132+
@JvmStatic
133+
fun onRequestSuccess(
134+
reactContext: ReactApplicationContext?,
135+
requestId: Int,
136+
encodedDataLength: Long
137+
) {
138+
if (ReactNativeFeatureFlags.enableNetworkEventReporting()) {
139+
InspectorNetworkReporter.reportResponseEnd(requestId, encodedDataLength)
140+
}
141+
reactContext?.emitDeviceEvent(
142+
"didCompleteNetworkResponse",
143+
buildReadableArray {
144+
add(requestId)
145+
addNull()
146+
})
147+
}
148+
149+
@JvmStatic
150+
fun onResponseReceived(
151+
reactContext: ReactApplicationContext?,
152+
requestId: Int,
153+
requestUrl: String?,
154+
response: Response,
155+
) {
156+
val headersMap = okHttpHeadersToMap(response.headers())
157+
val headersBundle = Bundle()
158+
for ((headerName, headerValue) in headersMap) {
159+
headersBundle.putString(headerName, headerValue)
160+
}
161+
162+
if (ReactNativeFeatureFlags.enableNetworkEventReporting()) {
163+
InspectorNetworkReporter.reportResponseStart(
164+
requestId,
165+
requestUrl.orEmpty(),
166+
response.code(),
167+
headersMap,
168+
response.body()?.contentLength() ?: 0,
169+
)
170+
}
171+
reactContext?.emitDeviceEvent(
172+
"didReceiveNetworkResponse",
173+
Arguments.createArray().apply {
174+
pushInt(requestId)
175+
pushInt(response.code())
176+
pushMap(Arguments.fromBundle(headersBundle))
177+
pushString(requestUrl)
178+
})
179+
}
180+
181+
@Deprecated("Compatibility overload")
182+
@JvmStatic
183+
fun onResponseReceived(
184+
reactContext: ReactApplicationContext?,
185+
requestId: Int,
186+
statusCode: Int,
187+
headers: WritableMap?,
188+
url: String?
189+
) {
190+
val headersBuilder = Headers.Builder()
191+
headers?.let { map ->
192+
val iterator = map.keySetIterator()
193+
while (iterator.hasNextKey()) {
194+
val key = iterator.nextKey()
195+
val value = map.getString(key)
196+
if (value != null) {
197+
headersBuilder.add(key, value)
198+
}
199+
}
200+
}
201+
onResponseReceived(
202+
reactContext,
203+
requestId,
204+
url,
205+
Response.Builder()
206+
.protocol(Protocol.HTTP_1_1)
207+
.request(Request.Builder().url(url.orEmpty()).build())
208+
.headers(headersBuilder.build())
209+
.code(statusCode)
210+
.message("")
211+
.build())
212+
}
213+
214+
private fun okHttpHeadersToMap(headers: Headers): Map<String, String> {
215+
val responseHeaders = mutableMapOf<String, String>()
216+
for (i in 0 until headers.size()) {
217+
val headerName = headers.name(i)
218+
// multiple values for the same header
219+
if (responseHeaders.containsKey(headerName)) {
220+
responseHeaders[headerName] = "${responseHeaders[headerName]}, ${headers.value(i)}"
221+
} else {
222+
responseHeaders[headerName] = headers.value(i)
223+
}
224+
}
225+
return responseHeaders
226+
}
227+
}

0 commit comments

Comments
 (0)