diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/InspectorNetworkReporter.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/InspectorNetworkReporter.kt new file mode 100644 index 000000000000..b730bac715b9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/InspectorNetworkReporter.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.modules.network + +import com.facebook.proguard.annotations.DoNotStripAny + +/** + * [Experimental] An interface for reporting network events to the modern debugger server and Web + * Performance APIs. + * + * In a production (non dev or profiling) build, CDP reporting is disabled. + * + * This is a helper class wrapping `facebook::react::jsinspector_modern::NetworkReporter`. + */ +@DoNotStripAny +internal object InspectorNetworkReporter { + /** + * Report a network request that is about to be sent. + * - Corresponds to `Network.requestWillBeSent` in CDP. + * - Corresponds to `PerformanceResourceTiming.requestStart` (specifically, marking when the + * native request was initiated). + */ + @JvmStatic + external fun reportRequestStart( + requestId: Int, + requestUrl: String, + requestMethod: String, + requestHeaders: Map, + requestBody: String, + encodedDataLength: Long + ) + + /** + * Report detailed timing info, such as DNS lookup, when a request has started. + * - Corresponds to `Network.requestWillBeSentExtraInfo` in CDP. + * - Corresponds to `PerformanceResourceTiming.domainLookupStart`, + * `PerformanceResourceTiming.connectStart`. + */ + @JvmStatic external fun reportConnectionTiming(requestId: Int, headers: Map) + + /** + * Report when HTTP response headers have been received, corresponding to when the first byte of + * the response is available. + * - Corresponds to `Network.responseReceived` in CDP. + * - Corresponds to `PerformanceResourceTiming.responseStart`. + */ + @JvmStatic + external fun reportResponseStart( + requestId: Int, + requestUrl: String, + responseStatus: Int, + responseHeaders: Map, + expectedDataLength: Long + ) + + /** + * Report when a network request is complete and we are no longer receiving response data. + * - Corresponds to `Network.loadingFinished` in CDP. + * - Corresponds to `PerformanceResourceTiming.responseEnd`. + */ + @JvmStatic external fun reportResponseEnd(requestId: Int, encodedDataLength: Long) +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkEventUtil.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkEventUtil.kt new file mode 100644 index 000000000000..fcbc4f1425d5 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkEventUtil.kt @@ -0,0 +1,227 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +@file:Suppress("DEPRECATION_ERROR") // Conflicting okhttp versions + +package com.facebook.react.modules.network + +import android.os.Bundle +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.buildReadableArray +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import java.net.SocketTimeoutException +import okhttp3.Headers +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response + +/** + * Utility class for reporting network lifecycle events to JavaScript and InspectorNetworkReporter. + */ +internal object NetworkEventUtil { + @JvmStatic + fun onCreateRequest(requestId: Int, request: Request) { + if (ReactNativeFeatureFlags.enableNetworkEventReporting()) { + val headersMap = okHttpHeadersToMap(request.headers()) + InspectorNetworkReporter.reportRequestStart( + requestId, + request.url().toString(), + request.method(), + headersMap, + request.body()?.toString().orEmpty(), + request.body()?.contentLength() ?: 0, + ) + InspectorNetworkReporter.reportConnectionTiming(requestId, headersMap) + } + } + + @JvmStatic + fun onDataSend( + reactContext: ReactApplicationContext?, + requestId: Int, + progress: Long, + total: Long + ) { + reactContext?.emitDeviceEvent( + "didSendNetworkData", + buildReadableArray { + add(requestId) + add(progress.toInt()) + add(total.toInt()) + }) + } + + @JvmStatic + fun onIncrementalDataReceived( + reactContext: ReactApplicationContext?, + requestId: Int, + data: String?, + progress: Long, + total: Long + ) { + reactContext?.emitDeviceEvent( + "didReceiveNetworkIncrementalData", + buildReadableArray { + add(requestId) + add(data) + add(progress.toInt()) + add(total.toInt()) + }) + } + + @JvmStatic + fun onDataReceivedProgress( + reactContext: ReactApplicationContext?, + requestId: Int, + progress: Long, + total: Long + ) { + reactContext?.emitDeviceEvent( + "didReceiveNetworkDataProgress", + buildReadableArray { + add(requestId) + add(progress.toInt()) + add(total.toInt()) + }) + } + + @JvmStatic + fun onDataReceived(reactContext: ReactApplicationContext?, requestId: Int, data: String?) { + reactContext?.emitDeviceEvent( + "didReceiveNetworkData", + buildReadableArray { + add(requestId) + add(data) + }) + } + + @JvmStatic + fun onDataReceived(reactContext: ReactApplicationContext?, requestId: Int, data: WritableMap?) { + reactContext?.emitDeviceEvent( + "didReceiveNetworkData", + Arguments.createArray().apply { + pushInt(requestId) + pushMap(data) + }) + } + + @JvmStatic + fun onRequestError( + reactContext: ReactApplicationContext?, + requestId: Int, + error: String?, + e: Throwable? + ) { + reactContext?.emitDeviceEvent( + "didCompleteNetworkResponse", + buildReadableArray { + add(requestId) + add(error) + if (e?.javaClass == SocketTimeoutException::class.java) { + add(true) // last argument is a time out boolean + } + }) + } + + @JvmStatic + fun onRequestSuccess( + reactContext: ReactApplicationContext?, + requestId: Int, + encodedDataLength: Long + ) { + if (ReactNativeFeatureFlags.enableNetworkEventReporting()) { + InspectorNetworkReporter.reportResponseEnd(requestId, encodedDataLength) + } + reactContext?.emitDeviceEvent( + "didCompleteNetworkResponse", + buildReadableArray { + add(requestId) + addNull() + }) + } + + @JvmStatic + fun onResponseReceived( + reactContext: ReactApplicationContext?, + requestId: Int, + requestUrl: String?, + response: Response, + ) { + val headersMap = okHttpHeadersToMap(response.headers()) + val headersBundle = Bundle() + for ((headerName, headerValue) in headersMap) { + headersBundle.putString(headerName, headerValue) + } + + if (ReactNativeFeatureFlags.enableNetworkEventReporting()) { + InspectorNetworkReporter.reportResponseStart( + requestId, + requestUrl.orEmpty(), + response.code(), + headersMap, + response.body()?.contentLength() ?: 0, + ) + } + reactContext?.emitDeviceEvent( + "didReceiveNetworkResponse", + Arguments.createArray().apply { + pushInt(requestId) + pushInt(response.code()) + pushMap(Arguments.fromBundle(headersBundle)) + pushString(requestUrl) + }) + } + + @Deprecated("Compatibility overload") + @JvmStatic + fun onResponseReceived( + reactContext: ReactApplicationContext?, + requestId: Int, + statusCode: Int, + headers: WritableMap?, + url: String? + ) { + val headersBuilder = Headers.Builder() + headers?.let { map -> + val iterator = map.keySetIterator() + while (iterator.hasNextKey()) { + val key = iterator.nextKey() + val value = map.getString(key) + if (value != null) { + headersBuilder.add(key, value) + } + } + } + onResponseReceived( + reactContext, + requestId, + url, + Response.Builder() + .protocol(Protocol.HTTP_1_1) + .request(Request.Builder().url(url.orEmpty()).build()) + .headers(headersBuilder.build()) + .code(statusCode) + .message("") + .build()) + } + + private fun okHttpHeadersToMap(headers: Headers): Map { + val responseHeaders = mutableMapOf() + for (i in 0 until headers.size()) { + val headerName = headers.name(i) + // multiple values for the same header + if (responseHeaders.containsKey(headerName)) { + responseHeaders[headerName] = "${responseHeaders[headerName]}, ${headers.value(i)}" + } else { + responseHeaders[headerName] = headers.value(i) + } + } + return responseHeaders + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.kt index 74cbcea0c24f..35bf4217258c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.kt @@ -11,11 +11,9 @@ package com.facebook.react.modules.network import android.net.Uri -import android.os.Bundle import android.util.Base64 import com.facebook.common.logging.FLog import com.facebook.fbreact.specs.NativeNetworkingAndroidSpec -import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableArray @@ -36,6 +34,7 @@ import okhttp3.JavaNetCookieJar import okhttp3.MediaType import okhttp3.MultipartBody import okhttp3.OkHttpClient +import okhttp3.Protocol import okhttp3.Request import okhttp3.RequestBody import okhttp3.Response @@ -231,7 +230,7 @@ public class NetworkingModule( } catch (th: Throwable) { FLog.e(TAG, "Failed to send url request: $url", th) - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( getReactApplicationContextIfActiveOrWarn(), requestId, th.message, th) } } @@ -256,17 +255,25 @@ public class NetworkingModule( for (handler in uriHandlers) { if (handler.supports(uri, responseType)) { val res = handler.fetch(uri) + val encodedDataLength = res.toString().toByteArray().size // fix: UriHandlers which are not using file:// scheme fail in whatwg-fetch at this line // https://github.com/JakeChampion/fetch/blob/main/fetch.js#L547 - ResponseUtil.onResponseReceived( - reactApplicationContext, requestId, 200, Arguments.createMap(), url) - ResponseUtil.onDataReceived(reactApplicationContext, requestId, res) - ResponseUtil.onRequestSuccess(reactApplicationContext, requestId) + val response = + Response.Builder() + .protocol(Protocol.HTTP_1_1) + .request(Request.Builder().url(url.orEmpty()).build()) + .code(200) + .message("OK") + .build() + NetworkEventUtil.onResponseReceived(reactApplicationContext, requestId, url, response) + NetworkEventUtil.onDataReceived(reactApplicationContext, requestId, res) + NetworkEventUtil.onRequestSuccess( + reactApplicationContext, requestId, encodedDataLength.toLong()) return } } } catch (e: IOException) { - ResponseUtil.onRequestError(reactApplicationContext, requestId, e.message, e) + NetworkEventUtil.onRequestError(reactApplicationContext, requestId, e.message, e) return } @@ -274,7 +281,7 @@ public class NetworkingModule( try { requestBuilder = Request.Builder().url(url.orEmpty()) } catch (e: Exception) { - ResponseUtil.onRequestError(reactApplicationContext, requestId, e.message, null) + NetworkEventUtil.onRequestError(reactApplicationContext, requestId, e.message, e) return } @@ -313,7 +320,7 @@ public class NetworkingModule( // JS below, so no need to do anything here. return } - ResponseUtil.onDataReceivedProgress( + NetworkEventUtil.onDataReceivedProgress( reactApplicationContext, requestId, bytesWritten, contentLength) last = now } @@ -333,7 +340,7 @@ public class NetworkingModule( val requestHeaders = extractHeaders(headers, data) if (requestHeaders == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Unrecognized headers format", null) return } @@ -359,7 +366,7 @@ public class NetworkingModule( handler != null -> requestBody = handler.toRequestBody(data, contentType) data.hasKey(REQUEST_BODY_KEY_STRING) -> { if (contentType == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Payload is set but no content-type header specified", @@ -374,7 +381,7 @@ public class NetworkingModule( requestBody = RequestBodyUtil.createGzip(contentMediaType, body) } if (requestBody == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Failed to gzip request body", null) return } @@ -389,7 +396,7 @@ public class NetworkingModule( checkNotNull(contentMediaType.charset(StandardCharsets.UTF_8)) } if (body == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Received request but body was empty", null) return } @@ -399,7 +406,7 @@ public class NetworkingModule( } data.hasKey(REQUEST_BODY_KEY_BASE64) -> { if (contentType == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Payload is set but no content-type header specified", @@ -411,7 +418,7 @@ public class NetworkingModule( val contentMediaType = MediaType.parse(contentType) if (contentMediaType == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Invalid content type specified: $contentType", @@ -420,7 +427,7 @@ public class NetworkingModule( } val base64DecodedString = ByteString.decodeBase64(base64String) if (base64DecodedString == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Request body base64 string was invalid", null) return } @@ -429,7 +436,7 @@ public class NetworkingModule( } data.hasKey(REQUEST_BODY_KEY_URI) -> { if (contentType == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Payload is set but no content-type header specified", @@ -438,13 +445,13 @@ public class NetworkingModule( } val uri = data.getString(REQUEST_BODY_KEY_URI) if (uri == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Request body URI field was set but null", null) return } val fileInputStream = RequestBodyUtil.getFileInputStream(getReactApplicationContext(), uri) if (fileInputStream == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Could not retrieve file for uri $uri", null) return } @@ -456,7 +463,7 @@ public class NetworkingModule( } val parts = data.getArray(REQUEST_BODY_KEY_FORMDATA) if (parts == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Received request but form data was empty", null) return } @@ -472,8 +479,11 @@ public class NetworkingModule( requestBuilder.method(method, wrapRequestBodyWithProgressEmitter(requestBody, requestId)) addRequest(requestId) + val request = requestBuilder.build() + NetworkEventUtil.onCreateRequest(requestId, request) + client - .newCall(requestBuilder.build()) + .newCall(request) .enqueue( object : Callback { override fun onFailure(call: Call, e: IOException) { @@ -483,7 +493,7 @@ public class NetworkingModule( removeRequest(requestId) val errorMessage = e.message ?: ("Error while executing request: ${e.javaClass.simpleName}") - ResponseUtil.onRequestError(reactApplicationContext, requestId, errorMessage, e) + NetworkEventUtil.onRequestError(reactApplicationContext, requestId, errorMessage, e) } @Throws(IOException::class) @@ -493,12 +503,8 @@ public class NetworkingModule( } removeRequest(requestId) // Before we touch the body send headers to JS - ResponseUtil.onResponseReceived( - reactApplicationContext, - requestId, - response.code(), - translateHeaders(response.headers()), - response.request().url().toString()) + NetworkEventUtil.onResponseReceived( + reactApplicationContext, requestId, url, response) try { // OkHttp implements something called transparent gzip, which mean that it will @@ -517,7 +523,7 @@ public class NetworkingModule( // https://github.com/square/okhttp/blob/5b37cda9e00626f43acf354df145fd452c3031f1/okhttp/src/main/java/okhttp3/internal/http/BridgeInterceptor.java#L76-L111 var responseBody: ResponseBody? = response.body() if (responseBody == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Response body is null", null) return } @@ -538,8 +544,9 @@ public class NetworkingModule( for (responseHandler in responseHandlers) { if (responseHandler.supports(responseType)) { val res = responseHandler.toResponseData(responseBody) - ResponseUtil.onDataReceived(reactApplicationContext, requestId, res) - ResponseUtil.onRequestSuccess(reactApplicationContext, requestId) + NetworkEventUtil.onDataReceived(reactApplicationContext, requestId, res) + NetworkEventUtil.onRequestSuccess( + reactApplicationContext, requestId, responseBody.contentLength()) return } } @@ -549,7 +556,8 @@ public class NetworkingModule( // periodically send response data updates to JS. if (useIncrementalUpdates && responseType == "text") { readWithProgress(requestId, responseBody) - ResponseUtil.onRequestSuccess(reactApplicationContext, requestId) + NetworkEventUtil.onRequestSuccess( + reactApplicationContext, requestId, responseBody.contentLength()) return } @@ -566,17 +574,19 @@ public class NetworkingModule( // Javascript layer. // Introduced to fix issue #7463. } else { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, e.message, e) } } } else if (responseType == "base64") { responseString = Base64.encodeToString(responseBody.bytes(), Base64.NO_WRAP) } - ResponseUtil.onDataReceived(reactApplicationContext, requestId, responseString) - ResponseUtil.onRequestSuccess(reactApplicationContext, requestId) + NetworkEventUtil.onDataReceived( + reactApplicationContext, requestId, responseString) + NetworkEventUtil.onRequestSuccess( + reactApplicationContext, requestId, responseBody.contentLength()) } catch (e: IOException) { - ResponseUtil.onRequestError(reactApplicationContext, requestId, e.message, e) + NetworkEventUtil.onRequestError(reactApplicationContext, requestId, e.message, e) } } }) @@ -598,7 +608,7 @@ public class NetworkingModule( override fun onProgress(bytesWritten: Long, contentLength: Long, done: Boolean) { val now = System.nanoTime() if (done || shouldDispatch(now, last)) { - ResponseUtil.onDataSend( + NetworkEventUtil.onDataSend( reactApplicationContext, requestId, bytesWritten, contentLength) last = now } @@ -633,7 +643,7 @@ public class NetworkingModule( var read: Int val reactApplicationContext = getReactApplicationContextIfActiveOrWarn() while ((inputStream.read(buffer).also { read = it }) != -1) { - ResponseUtil.onIncrementalDataReceived( + NetworkEventUtil.onIncrementalDataReceived( reactApplicationContext, requestId, streamDecoder.decodeNext(buffer, read), @@ -691,7 +701,8 @@ public class NetworkingModule( val multipartBuilder = MultipartBody.Builder() val mediaType = MediaType.parse(contentType) if (mediaType == null) { - ResponseUtil.onRequestError(reactApplicationContext, requestId, "Invalid media type.", null) + NetworkEventUtil.onRequestError( + reactApplicationContext, requestId, "Invalid media type.", null) return null } multipartBuilder.setType(mediaType) @@ -699,7 +710,7 @@ public class NetworkingModule( for (i in 0 until body.size()) { val bodyPart = body.getMap(i) if (bodyPart == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Unrecognized FormData part.", null) return null } @@ -708,7 +719,7 @@ public class NetworkingModule( val headersArray = bodyPart.getArray("headers") var headers = extractHeaders(headersArray, null) if (headers == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Missing or invalid header format for FormData part.", @@ -732,7 +743,7 @@ public class NetworkingModule( } else if (bodyPart.hasKey(REQUEST_BODY_KEY_URI) && bodyPart.getString(REQUEST_BODY_KEY_URI) != null) { if (partContentType == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Binary FormData part needs a content-type header.", @@ -741,14 +752,14 @@ public class NetworkingModule( } val fileContentUriStr = bodyPart.getString(REQUEST_BODY_KEY_URI) if (fileContentUriStr == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Body must have a valid file uri", null) return null } val fileInputStream = RequestBodyUtil.getFileInputStream(getReactApplicationContext(), fileContentUriStr) if (fileInputStream == null) { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Could not retrieve file for uri $fileContentUriStr", @@ -757,7 +768,7 @@ public class NetworkingModule( } multipartBuilder.addPart(headers, RequestBodyUtil.create(partContentType, fileInputStream)) } else { - ResponseUtil.onRequestError( + NetworkEventUtil.onRequestError( reactApplicationContext, requestId, "Unrecognized FormData part.", null) } } @@ -827,20 +838,5 @@ public class NetworkingModule( } private fun shouldDispatch(now: Long, last: Long): Boolean = last + CHUNK_TIMEOUT_NS < now - - private fun translateHeaders(headers: Headers): WritableMap { - val responseHeaders = Bundle() - for (i in 0.. + +#include +#include + +using namespace facebook::jni; +using namespace facebook::react::jsinspector_modern; + +namespace facebook::react { + +namespace { + +Headers convertJavaMapToHeaders( + jni::alias_ref> headers) { + Headers responseHeaders; + + for (auto it : *headers) { + auto key = it.first->toStdString(); + auto value = it.second->toStdString(); + responseHeaders[key] = value; + } + + return responseHeaders; +} + +std::string limitRequestBodySize(std::string requestBody) { + const size_t maxBodySize = 1024 * 1024; // 1MB + auto bodyLength = requestBody.size(); + auto bytesToRead = std::min(bodyLength, maxBodySize); + + requestBody.resize(bytesToRead); + + if (bytesToRead < bodyLength) { + requestBody += "\n... [truncated, showing " + std::to_string(bytesToRead) + + " of " + std::to_string(bodyLength) + " bytes]"; + } + + return requestBody; +} + +} // namespace + +/* static */ void InspectorNetworkReporter::reportRequestStart( + const jni::alias_ref /*unused*/, + jint requestId, + jni::alias_ref requestUrl, + jni::alias_ref requestMethod, + jni::alias_ref> requestHeaders, + jni::alias_ref requestBody, + jlong encodedDataLength) { + RequestInfo requestInfo; + requestInfo.url = requestUrl->toStdString(); + requestInfo.httpMethod = requestMethod->toStdString(); + requestInfo.headers = convertJavaMapToHeaders(requestHeaders); + requestInfo.httpBody = limitRequestBodySize(requestBody->toStdString()); + + NetworkReporter::getInstance().reportRequestStart( + std::to_string(requestId), requestInfo, encodedDataLength, std::nullopt); +} + +/* static */ void InspectorNetworkReporter::reportConnectionTiming( + jni::alias_ref /*unused*/, + jint requestId, + jni::alias_ref> headers) { + NetworkReporter::getInstance().reportConnectionTiming( + std::to_string(requestId), convertJavaMapToHeaders(headers)); +} + +/* static */ void InspectorNetworkReporter::reportResponseStart( + jni::alias_ref /*unused*/, + jint requestId, + jni::alias_ref requestUrl, + jint responseStatus, + jni::alias_ref> responseHeaders, + jlong encodedDataLength) { + ResponseInfo responseInfo; + responseInfo.url = requestUrl->toStdString(); + responseInfo.statusCode = responseStatus; + responseInfo.headers = convertJavaMapToHeaders(responseHeaders); + + NetworkReporter::getInstance().reportResponseStart( + std::to_string(requestId), + responseInfo, + static_cast(encodedDataLength)); +} + +/* static */ void InspectorNetworkReporter::reportResponseEnd( + jni::alias_ref /*unused*/, + jint requestId, + jlong encodedDataLength) { + NetworkReporter::getInstance().reportResponseEnd( + std::to_string(requestId), static_cast(encodedDataLength)); +} + +/* static */ void InspectorNetworkReporter::registerNatives() { + javaClassLocal()->registerNatives({ + makeNativeMethod( + "reportRequestStart", InspectorNetworkReporter::reportRequestStart), + makeNativeMethod( + "reportResponseStart", InspectorNetworkReporter::reportResponseStart), + makeNativeMethod( + "reportResponseEnd", InspectorNetworkReporter::reportResponseEnd), + makeNativeMethod( + "reportConnectionTiming", + InspectorNetworkReporter::reportConnectionTiming), + }); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/InspectorNetworkReporter.h b/packages/react-native/ReactAndroid/src/main/jni/react/jni/InspectorNetworkReporter.h new file mode 100644 index 000000000000..9cdc1cb02fbc --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/InspectorNetworkReporter.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +class InspectorNetworkReporter + : public jni::HybridClass { + public: + static constexpr auto kJavaDescriptor = + "Lcom/facebook/react/modules/network/InspectorNetworkReporter;"; + + static void reportRequestStart( + jni::alias_ref /*unused*/, + jint requestId, + jni::alias_ref requestUrl, + jni::alias_ref requestMethod, + jni::alias_ref> requestHeaders, + jni::alias_ref requestBody, + jlong encodedDataLength); + + static void reportConnectionTiming( + jni::alias_ref /*unused*/, + jint requestId, + jni::alias_ref> headers); + + static void reportResponseStart( + jni::alias_ref /*unused*/, + jint requestId, + jni::alias_ref requestUrl, + jint responseStatus, + jni::alias_ref> responseHeaders, + jlong encodedDataLength); + + static void reportResponseEnd( + jni::alias_ref /*unused*/, + jint requestId, + jlong encodedDataLength); + + static void registerNatives(); + + private: + InspectorNetworkReporter() = delete; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp index 0533498a6858..c9741d9ca43f 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp @@ -5,8 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -#include - #include #include @@ -14,6 +12,7 @@ #include "CatalystInstanceImpl.h" #include "CxxModuleWrapperBase.h" +#include "InspectorNetworkReporter.h" #include "InspectorNetworkRequestListener.h" #include "JInspector.h" #include "JavaScriptExecutorHolder.h" @@ -46,6 +45,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { JInspector::registerNatives(); ReactInstanceManagerInspectorTarget::registerNatives(); InspectorNetworkRequestListener::registerNatives(); + InspectorNetworkReporter::registerNatives(); }); } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/ResponseUtilTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkEventUtilTest.kt similarity index 73% rename from packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/ResponseUtilTest.kt rename to packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkEventUtilTest.kt index db378ec87789..9a98d2d5f14e 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/ResponseUtilTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkEventUtilTest.kt @@ -11,9 +11,17 @@ import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import com.facebook.testutils.shadows.ShadowArguments import java.net.SocketTimeoutException +import okhttp3.Headers +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response import org.assertj.core.api.Assertions.assertThat +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -27,12 +35,23 @@ import org.robolectric.annotation.Config @Config(shadows = [ShadowArguments::class]) @RunWith(RobolectricTestRunner::class) -class ResponseUtilTest { +class NetworkEventUtilTest { private lateinit var reactContext: ReactApplicationContext @Before fun setUp() { reactContext = mock() + + ReactNativeFeatureFlagsForTests.setUp() + ReactNativeFeatureFlags.override( + object : ReactNativeFeatureFlagsDefaults() { + override fun enableNetworkEventReporting(): Boolean = false + }) + } + + @After + fun tearDown() { + ReactNativeFeatureFlags.dangerouslyReset() } @Test @@ -41,7 +60,7 @@ class ResponseUtilTest { val progress = 100L val total = 1000L - ResponseUtil.onDataSend(reactContext, requestId, progress, total) + NetworkEventUtil.onDataSend(reactContext, requestId, progress, total) val eventNameCaptor = ArgumentCaptor.forClass(String::class.java) val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java) @@ -64,7 +83,7 @@ class ResponseUtilTest { val progress = 100L val total = 1000L - ResponseUtil.onIncrementalDataReceived(reactContext, requestId, data, progress, total) + NetworkEventUtil.onIncrementalDataReceived(reactContext, requestId, data, progress, total) val eventNameCaptor = ArgumentCaptor.forClass(String::class.java) val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java) @@ -87,7 +106,7 @@ class ResponseUtilTest { val progress = 500L val total = 1000L - ResponseUtil.onDataReceivedProgress(reactContext, requestId, progress, total) + NetworkEventUtil.onDataReceivedProgress(reactContext, requestId, progress, total) val eventNameCaptor = ArgumentCaptor.forClass(String::class.java) val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java) @@ -108,7 +127,7 @@ class ResponseUtilTest { val requestId = 1 val data = "response data" - ResponseUtil.onDataReceived(reactContext, requestId, data) + NetworkEventUtil.onDataReceived(reactContext, requestId, data) val eventNameCaptor = ArgumentCaptor.forClass(String::class.java) val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java) @@ -128,7 +147,7 @@ class ResponseUtilTest { val requestId = 1 val data: WritableMap = Arguments.createMap().apply { putString("key", "value") } - ResponseUtil.onDataReceived(reactContext, requestId, data) + NetworkEventUtil.onDataReceived(reactContext, requestId, data) val eventNameCaptor = ArgumentCaptor.forClass(String::class.java) val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java) @@ -149,7 +168,7 @@ class ResponseUtilTest { val error = "An error occurred" val e: Throwable? = null - ResponseUtil.onRequestError(reactContext, requestId, error, e) + NetworkEventUtil.onRequestError(reactContext, requestId, error, e) val eventNameCaptor = ArgumentCaptor.forClass(String::class.java) val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java) @@ -170,7 +189,7 @@ class ResponseUtilTest { val error = "Timeout error" val e: Throwable = SocketTimeoutException() - ResponseUtil.onRequestError(reactContext, requestId, error, e) + NetworkEventUtil.onRequestError(reactContext, requestId, error, e) val eventNameCaptor = ArgumentCaptor.forClass(String::class.java) val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java) @@ -190,7 +209,7 @@ class ResponseUtilTest { fun testOnRequestSuccess() { val requestId = 1 - ResponseUtil.onRequestSuccess(reactContext, requestId) + NetworkEventUtil.onRequestSuccess(reactContext, requestId, 128) val eventNameCaptor = ArgumentCaptor.forClass(String::class.java) val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java) @@ -209,11 +228,19 @@ class ResponseUtilTest { fun testOnResponseReceived() { val requestId = 1 val statusCode = 200 - val headers: WritableMap = - Arguments.createMap().apply { putString("Content-Type", "application/json") } + val headers = Headers.Builder().add("Content-Type", "application/json").build() val url = "http://example.com" - ResponseUtil.onResponseReceived(reactContext, requestId, statusCode, headers, url) + val request = Request.Builder().url(url).build() + val response = + Response.Builder() + .protocol(Protocol.HTTP_1_1) + .request(request) + .headers(headers) + .code(statusCode) + .message("OK") + .build() + NetworkEventUtil.onResponseReceived(reactContext, requestId, url, response) val eventNameCaptor = ArgumentCaptor.forClass(String::class.java) val eventArgumentsCaptor = ArgumentCaptor.forClass(WritableArray::class.java) @@ -222,24 +249,38 @@ class ResponseUtilTest { assertThat(eventNameCaptor.value).isEqualTo("didReceiveNetworkResponse") + val expectedHeadersMap: WritableMap = + Arguments.createMap().apply { putString("Content-Type", "application/json") } + val args = eventArgumentsCaptor.value assertThat(args.size()).isEqualTo(4) assertThat(args.getInt(0)).isEqualTo(requestId) assertThat(args.getInt(1)).isEqualTo(statusCode) - assertThat(args.getMap(2)).isEqualTo(headers) + assertThat(args.getMap(2)).isEqualTo(expectedHeadersMap) assertThat(args.getString(3)).isEqualTo(url) } @Test fun testNullReactContext() { - ResponseUtil.onDataSend(null, 1, 100, 1000) - ResponseUtil.onIncrementalDataReceived(null, 1, "data", 100, 1000) - ResponseUtil.onDataReceivedProgress(null, 1, 100, 1000) - ResponseUtil.onDataReceived(null, 1, "data") - ResponseUtil.onDataReceived(null, 1, Arguments.createMap()) - ResponseUtil.onRequestError(null, 1, "error", null) - ResponseUtil.onRequestSuccess(null, 1) - ResponseUtil.onResponseReceived(null, 1, 200, Arguments.createMap(), "http://example.com") + val url = "http://example.com" + val request = Request.Builder().url(url).build() + val response = + Response.Builder() + .protocol(Protocol.HTTP_1_1) + .request(request) + .headers(Headers.Builder().build()) + .code(200) + .message("OK") + .build() + + NetworkEventUtil.onDataSend(null, 1, 100, 1000) + NetworkEventUtil.onIncrementalDataReceived(null, 1, "data", 100, 1000) + NetworkEventUtil.onDataReceivedProgress(null, 1, 100, 1000) + NetworkEventUtil.onDataReceived(null, 1, "data") + NetworkEventUtil.onDataReceived(null, 1, Arguments.createMap()) + NetworkEventUtil.onRequestError(null, 1, "error", null) + NetworkEventUtil.onRequestSuccess(null, 1, 0) + NetworkEventUtil.onResponseReceived(null, 1, url, response) verify(reactContext, never()).emitDeviceEvent(any(), any()) } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt index b6bb1f1d6813..5f4543e0ccff 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt @@ -15,6 +15,9 @@ import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableArray import com.facebook.react.common.network.OkHttpCallUtil +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import com.facebook.testutils.shadows.ShadowArguments import java.io.InputStream import java.nio.charset.StandardCharsets @@ -72,6 +75,12 @@ class NetworkingModuleTest { context = mock() whenever(context.hasActiveReactInstance()).thenReturn(true) + ReactNativeFeatureFlagsForTests.setUp() + ReactNativeFeatureFlags.override( + object : ReactNativeFeatureFlagsDefaults() { + override fun enableNetworkEventReporting(): Boolean = false + }) + networkingModule = NetworkingModule(context, "", httpClient, null) okHttpCallUtil = mockStatic(OkHttpCallUtil::class.java)