Skip to content

Commit cfd0e35

Browse files
huntiemeta-codesync[bot]
authored andcommitted
Add CDP support for WebSocket events (Android) (#57541)
Summary: Pull Request resolved: #57541 **Context** This stack implements WebSocket event debugging for the Network panel in React Native DevTools. **This diff** Follows the iOS implementation: reports the same six `Network.webSocket*` CDP events through the shared `NetworkReporter` C++ core, gated behind the same `fuseboxWebSocketEventsEnabled` feature flag. **Changes** - `InspectorNetworkReporter` + `JInspectorNetworkReporter`: New JNI-backed `reportWebSocket*` methods bridging to the shared `NetworkReporter` C++ core. Payload conversion costs (base64) are deferred until a debugger is attached. - `WebSocketModule`: Reports connection lifecycle, real handshake headers (from the OkHttp `Request`/`Response`), and sent/received messages, gated behind the same feature flags as iOS. Changelog: [Internal] Reviewed By: GijsWeterings Differential Revision: D111561994 fbshipit-source-id: 735ad0e47fc4e9e943b1092fbc23846ff9baf963
1 parent 020512b commit cfd0e35

7 files changed

Lines changed: 295 additions & 1 deletion

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/InspectorNetworkReporter.kt

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
@file:Suppress("DEPRECATION_ERROR") // Conflicting okhttp versions
9+
810
package com.facebook.react.modules.network
911

1012
import com.facebook.proguard.annotations.DoNotStripAny
1113
import com.facebook.soloader.SoLoader
14+
import okio.ByteString
1215

1316
/**
1417
* [Experimental] An interface for reporting network events to the modern debugger server and Web
@@ -125,4 +128,106 @@ internal object InspectorNetworkReporter {
125128
}
126129

127130
@JvmStatic external fun maybeStoreResponseBodyIncrementalImpl(requestId: String, data: String)
131+
132+
/**
133+
* Report that a WebSocket connection is about to be created.
134+
*
135+
* Corresponds to `Network.webSocketCreated` in CDP.
136+
*/
137+
@JvmStatic external fun reportWebSocketCreated(requestId: String, url: String)
138+
139+
/**
140+
* Report that a WebSocket handshake (HTTP upgrade) request is about to be sent, along with its
141+
* final request headers.
142+
*
143+
* Corresponds to `Network.webSocketWillSendHandshakeRequest` in CDP.
144+
*/
145+
@JvmStatic
146+
external fun reportWebSocketWillSendHandshakeRequest(
147+
requestId: String,
148+
headers: Map<String, String>,
149+
)
150+
151+
/**
152+
* Report that a WebSocket handshake response was received and the connection is established.
153+
*
154+
* Corresponds to `Network.webSocketHandshakeResponseReceived` in CDP.
155+
*/
156+
@JvmStatic
157+
external fun reportWebSocketHandshakeResponseReceived(
158+
requestId: String,
159+
statusCode: Int,
160+
headers: Map<String, String>,
161+
)
162+
163+
/**
164+
* Report a text WebSocket message sent over an open connection.
165+
*
166+
* Corresponds to `Network.webSocketFrameSent` in CDP.
167+
*/
168+
@JvmStatic
169+
fun reportWebSocketMessageSent(requestId: String, message: String) {
170+
if (isDebuggingEnabled()) {
171+
reportWebSocketMessageSentImpl(requestId, message, false)
172+
}
173+
}
174+
175+
/**
176+
* Report a binary WebSocket message sent over an open connection.
177+
*
178+
* Corresponds to `Network.webSocketFrameSent` in CDP.
179+
*/
180+
@JvmStatic
181+
fun reportWebSocketMessageSent(requestId: String, message: ByteString) {
182+
// Guard call to CDP-only reporting method (avoid base64 encoding)
183+
if (isDebuggingEnabled()) {
184+
reportWebSocketMessageSentImpl(requestId, message.base64(), true)
185+
}
186+
}
187+
188+
@JvmStatic
189+
external fun reportWebSocketMessageSentImpl(
190+
requestId: String,
191+
payloadData: String,
192+
isBinary: Boolean,
193+
)
194+
195+
/**
196+
* Report a text WebSocket message received over an open connection.
197+
*
198+
* Corresponds to `Network.webSocketFrameReceived` in CDP.
199+
*/
200+
@JvmStatic
201+
fun reportWebSocketMessageReceived(requestId: String, message: String) {
202+
if (isDebuggingEnabled()) {
203+
reportWebSocketMessageReceivedImpl(requestId, message, false)
204+
}
205+
}
206+
207+
/**
208+
* Report a binary WebSocket message received over an open connection.
209+
*
210+
* Corresponds to `Network.webSocketFrameReceived` in CDP.
211+
*/
212+
@JvmStatic
213+
fun reportWebSocketMessageReceived(requestId: String, message: ByteString) {
214+
// Guard call to CDP-only reporting method (avoid base64 encoding)
215+
if (isDebuggingEnabled()) {
216+
reportWebSocketMessageReceivedImpl(requestId, message.base64(), true)
217+
}
218+
}
219+
220+
@JvmStatic
221+
external fun reportWebSocketMessageReceivedImpl(
222+
requestId: String,
223+
payloadData: String,
224+
isBinary: Boolean,
225+
)
226+
227+
/**
228+
* Report that a WebSocket connection was closed, whether cleanly or due to an error.
229+
*
230+
* Corresponds to `Network.webSocketClosed` in CDP.
231+
*/
232+
@JvmStatic external fun reportWebSocketClosed(requestId: String)
128233
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.kt

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,18 @@ import com.facebook.react.bridge.ReadableType
1919
import com.facebook.react.bridge.WritableMap
2020
import com.facebook.react.bridge.buildReadableMap
2121
import com.facebook.react.common.ReactConstants
22+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
2223
import com.facebook.react.module.annotations.ReactModule
2324
import com.facebook.react.modules.network.CustomClientBuilder
2425
import com.facebook.react.modules.network.ForwardingCookieHandler
26+
import com.facebook.react.modules.network.InspectorNetworkReporter
27+
import com.facebook.react.modules.network.NetworkEventUtil
2528
import com.facebook.react.modules.network.OkHttpClientProvider
2629
import java.io.IOException
2730
import java.net.URI
2831
import java.net.URISyntaxException
2932
import java.util.HashMap
33+
import java.util.UUID
3034
import java.util.concurrent.ConcurrentHashMap
3135
import java.util.concurrent.TimeUnit
3236
import okhttp3.CookieJar
@@ -51,12 +55,31 @@ public class WebSocketModule(context: ReactApplicationContext) :
5155
private val contentHandlers: MutableMap<Int, ContentHandler> = ConcurrentHashMap()
5256
private val cookieHandler: ForwardingCookieHandler = ForwardingCookieHandler()
5357

58+
/** CDP request IDs used to report each connection's events to the modern debugger server. */
59+
private val inspectorRequestIds: MutableMap<Int, String> = ConcurrentHashMap()
60+
5461
override fun invalidate() {
5562
for (socket in webSocketConnections.values) {
5663
socket.close(1_001 /* endpoint is going away */, null)
5764
}
5865
webSocketConnections.clear()
5966
contentHandlers.clear()
67+
inspectorRequestIds.clear()
68+
}
69+
70+
/** Whether WebSocket events should be reported to the modern debugger server. */
71+
private fun isInspectorNetworkReportingEnabled(): Boolean =
72+
ReactNativeFeatureFlags.enableNetworkEventReporting() &&
73+
ReactNativeFeatureFlags.fuseboxWebSocketEventsEnabled()
74+
75+
/**
76+
* Run [block] with the connection's CDP request ID, when WebSocket event reporting to the modern
77+
* debugger server is enabled.
78+
*/
79+
private inline fun reportToInspector(id: Int, block: (requestId: String) -> Unit) {
80+
if (isInspectorNetworkReportingEnabled()) {
81+
inspectorRequestIds[id]?.let(block)
82+
}
6083
}
6184

6285
private fun sendEvent(eventName: String, params: ReadableMap) {
@@ -142,11 +165,30 @@ public class WebSocketModule(context: ReactApplicationContext) :
142165
}
143166
}
144167

168+
val request = builder.build()
169+
170+
if (isInspectorNetworkReportingEnabled()) {
171+
val requestId = UUID.randomUUID().toString()
172+
inspectorRequestIds[id] = requestId
173+
InspectorNetworkReporter.reportWebSocketCreated(requestId, url)
174+
InspectorNetworkReporter.reportWebSocketWillSendHandshakeRequest(
175+
requestId,
176+
NetworkEventUtil.okHttpHeadersToMap(request.headers()),
177+
)
178+
}
179+
145180
client.newWebSocket(
146-
builder.build(),
181+
request,
147182
object : WebSocketListener() {
148183
override fun onOpen(webSocket: WebSocket, response: Response) {
149184
webSocketConnections[id] = webSocket
185+
reportToInspector(id) { requestId ->
186+
InspectorNetworkReporter.reportWebSocketHandshakeResponseReceived(
187+
requestId,
188+
response.code(),
189+
NetworkEventUtil.okHttpHeadersToMap(response.headers()),
190+
)
191+
}
150192
val params = buildReadableMap {
151193
put("id", id)
152194
put("protocol", response.header("Sec-WebSocket-Protocol", ""))
@@ -159,6 +201,10 @@ public class WebSocketModule(context: ReactApplicationContext) :
159201
}
160202

161203
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
204+
reportToInspector(id) { requestId ->
205+
InspectorNetworkReporter.reportWebSocketClosed(requestId)
206+
}
207+
inspectorRequestIds.remove(id)
162208
val params = buildReadableMap {
163209
put("id", id)
164210
put("code", code)
@@ -168,10 +214,17 @@ public class WebSocketModule(context: ReactApplicationContext) :
168214
}
169215

170216
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
217+
reportToInspector(id) { requestId ->
218+
InspectorNetworkReporter.reportWebSocketClosed(requestId)
219+
}
220+
inspectorRequestIds.remove(id)
171221
notifyWebSocketFailed(id, t.message)
172222
}
173223

174224
override fun onMessage(webSocket: WebSocket, text: String) {
225+
reportToInspector(id) { requestId ->
226+
InspectorNetworkReporter.reportWebSocketMessageReceived(requestId, text)
227+
}
175228
val params = Arguments.createMap()
176229
params.putInt("id", id)
177230
params.putString("type", "text")
@@ -186,6 +239,9 @@ public class WebSocketModule(context: ReactApplicationContext) :
186239
}
187240

188241
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
242+
reportToInspector(id) { requestId ->
243+
InspectorNetworkReporter.reportWebSocketMessageReceived(requestId, bytes)
244+
}
189245
val params = Arguments.createMap()
190246
params.putInt("id", id)
191247
params.putString("type", "binary")
@@ -240,9 +296,13 @@ public class WebSocketModule(context: ReactApplicationContext) :
240296
sendEvent("websocketClosed", params)
241297
webSocketConnections.remove(id)
242298
contentHandlers.remove(id)
299+
inspectorRequestIds.remove(id)
243300
return
244301
}
245302
try {
303+
reportToInspector(id) { requestId ->
304+
InspectorNetworkReporter.reportWebSocketMessageSent(requestId, message)
305+
}
246306
client.send(message)
247307
} catch (e: Exception) {
248308
notifyWebSocketFailed(id, e.message)
@@ -267,10 +327,14 @@ public class WebSocketModule(context: ReactApplicationContext) :
267327
sendEvent("websocketClosed", params)
268328
webSocketConnections.remove(id)
269329
contentHandlers.remove(id)
330+
inspectorRequestIds.remove(id)
270331
return
271332
}
272333
try {
273334
val decodedString = checkNotNull(ByteString.decodeBase64(base64String)) { "bytes == null" }
335+
reportToInspector(id) { requestId ->
336+
InspectorNetworkReporter.reportWebSocketMessageSent(requestId, decodedString)
337+
}
274338
client.send(decodedString)
275339
} catch (e: Exception) {
276340
notifyWebSocketFailed(id, e.message)
@@ -294,9 +358,13 @@ public class WebSocketModule(context: ReactApplicationContext) :
294358
sendEvent("websocketClosed", params)
295359
webSocketConnections.remove(id)
296360
contentHandlers.remove(id)
361+
inspectorRequestIds.remove(id)
297362
return
298363
}
299364
try {
365+
reportToInspector(id) { requestId ->
366+
InspectorNetworkReporter.reportWebSocketMessageSent(requestId, byteString)
367+
}
300368
client.send(byteString)
301369
} catch (e: Exception) {
302370
notifyWebSocketFailed(id, e.message)
@@ -321,6 +389,7 @@ public class WebSocketModule(context: ReactApplicationContext) :
321389
sendEvent("websocketClosed", params)
322390
webSocketConnections.remove(id)
323391
contentHandlers.remove(id)
392+
inspectorRequestIds.remove(id)
324393
return
325394
}
326395
try {

packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorNetworkReporter.cpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,60 @@ JInspectorNetworkReporter::maybeStoreResponseBodyIncrementalImpl(
178178
#endif
179179
}
180180

181+
/* static */ void JInspectorNetworkReporter::reportWebSocketCreated(
182+
jni::alias_ref<jclass> /*unused*/,
183+
jni::alias_ref<jstring> requestId,
184+
jni::alias_ref<jstring> url) {
185+
NetworkReporter::getInstance().reportWebSocketCreated(
186+
requestId->toStdString(), url->toStdString());
187+
}
188+
189+
/* static */ void
190+
JInspectorNetworkReporter::reportWebSocketWillSendHandshakeRequest(
191+
jni::alias_ref<jclass> /*unused*/,
192+
jni::alias_ref<jstring> requestId,
193+
jni::alias_ref<jni::JMap<jstring, jstring>> headers) {
194+
NetworkReporter::getInstance().reportWebSocketWillSendHandshakeRequest(
195+
requestId->toStdString(), convertJavaMapToHeaders(headers));
196+
}
197+
198+
/* static */ void
199+
JInspectorNetworkReporter::reportWebSocketHandshakeResponseReceived(
200+
jni::alias_ref<jclass> /*unused*/,
201+
jni::alias_ref<jstring> requestId,
202+
jint statusCode,
203+
jni::alias_ref<jni::JMap<jstring, jstring>> headers) {
204+
NetworkReporter::getInstance().reportWebSocketHandshakeResponseReceived(
205+
requestId->toStdString(),
206+
static_cast<uint16_t>(statusCode),
207+
convertJavaMapToHeaders(headers));
208+
}
209+
210+
/* static */ void JInspectorNetworkReporter::reportWebSocketMessageSentImpl(
211+
jni::alias_ref<jclass> /*unused*/,
212+
jni::alias_ref<jstring> requestId,
213+
jni::alias_ref<jstring> payloadData,
214+
jboolean isBinary) {
215+
NetworkReporter::getInstance().reportWebSocketMessageSent(
216+
requestId->toStdString(), payloadData->toStdString(), isBinary != 0u);
217+
}
218+
219+
/* static */ void JInspectorNetworkReporter::reportWebSocketMessageReceivedImpl(
220+
jni::alias_ref<jclass> /*unused*/,
221+
jni::alias_ref<jstring> requestId,
222+
jni::alias_ref<jstring> payloadData,
223+
jboolean isBinary) {
224+
NetworkReporter::getInstance().reportWebSocketMessageReceived(
225+
requestId->toStdString(), payloadData->toStdString(), isBinary != 0u);
226+
}
227+
228+
/* static */ void JInspectorNetworkReporter::reportWebSocketClosed(
229+
jni::alias_ref<jclass> /*unused*/,
230+
jni::alias_ref<jstring> requestId) {
231+
NetworkReporter::getInstance().reportWebSocketClosed(
232+
requestId->toStdString());
233+
}
234+
181235
/* static */ void JInspectorNetworkReporter::registerNatives() {
182236
javaClassLocal()->registerNatives({
183237
makeNativeMethod(
@@ -204,6 +258,24 @@ JInspectorNetworkReporter::maybeStoreResponseBodyIncrementalImpl(
204258
makeNativeMethod(
205259
"maybeStoreResponseBodyIncrementalImpl",
206260
JInspectorNetworkReporter::maybeStoreResponseBodyIncrementalImpl),
261+
makeNativeMethod(
262+
"reportWebSocketCreated",
263+
JInspectorNetworkReporter::reportWebSocketCreated),
264+
makeNativeMethod(
265+
"reportWebSocketWillSendHandshakeRequest",
266+
JInspectorNetworkReporter::reportWebSocketWillSendHandshakeRequest),
267+
makeNativeMethod(
268+
"reportWebSocketHandshakeResponseReceived",
269+
JInspectorNetworkReporter::reportWebSocketHandshakeResponseReceived),
270+
makeNativeMethod(
271+
"reportWebSocketMessageSentImpl",
272+
JInspectorNetworkReporter::reportWebSocketMessageSentImpl),
273+
makeNativeMethod(
274+
"reportWebSocketMessageReceivedImpl",
275+
JInspectorNetworkReporter::reportWebSocketMessageReceivedImpl),
276+
makeNativeMethod(
277+
"reportWebSocketClosed",
278+
JInspectorNetworkReporter::reportWebSocketClosed),
207279
});
208280
}
209281

0 commit comments

Comments
 (0)