Skip to content

Commit cd2afba

Browse files
committed
feat: collect metrics for requests
1 parent 2b56f1b commit cd2afba

17 files changed

+498
-38
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
22
package="com.mattermost.networkclient">
33

4+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
45
</manifest>

android/src/main/java/com/mattermost/networkclient/ApiClientModuleImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ class ApiClientModuleImpl(reactApplicationContext: ReactApplicationContext) {
294294
}
295295

296296
override fun onResponse(call: Call, response: Response) {
297-
promise.resolve(response.toWritableMap())
297+
promise.resolve(response.toWritableMap(null))
298298
client.cleanUpAfter(response)
299299
calls.remove(taskId)
300300
}

android/src/main/java/com/mattermost/networkclient/Extensions.kt

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package com.mattermost.networkclient
22

3-
import com.facebook.react.bridge.*
3+
import com.facebook.react.bridge.Arguments
4+
import com.facebook.react.bridge.ReadableMap
5+
import com.facebook.react.bridge.WritableArray
6+
import com.facebook.react.bridge.WritableMap
7+
import com.mattermost.networkclient.metrics.RequestMetadata
48
import okhttp3.Headers
59
import okhttp3.Request
610
import okhttp3.Response
@@ -40,14 +44,25 @@ fun Response.getRedirectUrls(): WritableArray? {
4044
*
4145
* @return WriteableMap for passing back to App
4246
*/
43-
fun Response.toWritableMap(): WritableMap {
47+
fun Response.toWritableMap(metadata: RequestMetadata?): WritableMap {
4448
val map = Arguments.createMap()
49+
val metrics = Arguments.createMap()
4550
map.putMap("headers", headers.toWritableMap())
4651
map.putInt("code", code)
4752
map.putBoolean("ok", isSuccessful)
4853

49-
if (body !== null) {
50-
val bodyString = body!!.string()
54+
body?.let { responseBody ->
55+
val source = responseBody.source()
56+
source.request(Long.MAX_VALUE)
57+
val buffer = source.buffer.clone()
58+
59+
if (metadata != null) {
60+
val compressedSize = header("X-Compressed-Size")?.toDoubleOrNull() ?: header("Content-Length")?.toDoubleOrNull() ?: 0.0
61+
metrics.putDouble("compressedSize", compressedSize)
62+
metrics.putDouble("size", buffer.size.toDouble())
63+
}
64+
65+
val bodyString = buffer.readUtf8()
5166
try {
5267
when (val json = JSONTokener(bodyString).nextValue()) {
5368
is JSONArray -> {
@@ -74,6 +89,17 @@ fun Response.toWritableMap(): WritableMap {
7489
map.putArray("redirectUrls", redirectUrls)
7590
}
7691

92+
if (metadata != null) {
93+
metrics.putDouble("latency", metadata.getLatency().toDouble())
94+
metrics.putDouble("connectionTime", metadata.getConnectionTime().toDouble())
95+
metrics.putString("httpVersion", metadata.httpVersion)
96+
metrics.putString("tlsVersion", metadata.sslVersion ?: "None")
97+
metrics.putString("tlsCipherSuite", metadata.sslCipher ?: "None")
98+
metrics.putBoolean("isCached", metadata.isCached)
99+
metrics.putString("networkType", metadata.networkType)
100+
map.putMap("metrics", metrics)
101+
}
102+
77103
return map
78104
}
79105

android/src/main/java/com/mattermost/networkclient/NetworkClient.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import com.mattermost.networkclient.helpers.KeyStoreHelper
1717
import com.mattermost.networkclient.helpers.UploadFileRequestBody
1818
import com.mattermost.networkclient.interceptors.*
1919
import com.mattermost.networkclient.interfaces.RetryInterceptor
20+
import com.mattermost.networkclient.metrics.MetricsEventFactory
21+
import com.mattermost.networkclient.metrics.RequestMetadata
22+
import com.mattermost.networkclient.metrics.getNetworkType
2023
import okhttp3.*
2124
import okhttp3.RequestBody.Companion.toRequestBody
2225
import okhttp3.internal.EMPTY_REQUEST
@@ -53,6 +56,8 @@ internal class NetworkClient(private val context: ReactApplicationContext, priva
5356

5457
private var trustSelfSignedServerCertificate = false
5558
private val builder: OkHttpClient.Builder = OkHttpClient().newBuilder()
59+
private var shouldCollectMetrics: Boolean = false
60+
private var metricsEventFactory: MetricsEventFactory? = null
5661

5762
private val baseUrlString = baseUrl.toString().trimTrailingSlashes()
5863
private val baseUrlHash = baseUrlString.sha256()
@@ -76,6 +81,12 @@ internal class NetworkClient(private val context: ReactApplicationContext, priva
7681
}
7782

7883
init {
84+
initCollectMetrics(options)
85+
86+
if (shouldCollectMetrics) {
87+
builder.addNetworkInterceptor(CompressedResponseSizeInterceptor())
88+
}
89+
7990
if (baseUrl == null) {
8091
applyGenericClientBuilderConfiguration()
8192
} else {
@@ -94,9 +105,25 @@ internal class NetworkClient(private val context: ReactApplicationContext, priva
94105
builder.certificatePinner(certificatePinner)
95106
}
96107

108+
if (metricsEventFactory != null) {
109+
builder.eventListenerFactory(metricsEventFactory!!)
110+
}
111+
97112
okHttpClient = builder.build()
98113
}
99114

115+
private fun initCollectMetrics(options: ReadableMap?) {
116+
if (options != null && options.hasKey("sessionConfiguration")) {
117+
val sessionConfiguration = options.getMap("sessionConfiguration")!!
118+
if (sessionConfiguration.hasKey("collectMetrics")) {
119+
shouldCollectMetrics = sessionConfiguration.getBoolean("collectMetrics")
120+
if (shouldCollectMetrics) {
121+
metricsEventFactory = MetricsEventFactory()
122+
}
123+
}
124+
}
125+
}
126+
100127
private fun applyGenericClientBuilderConfiguration() {
101128
builder.followRedirects(true)
102129
builder.followSslRedirects(true)
@@ -205,6 +232,7 @@ internal class NetworkClient(private val context: ReactApplicationContext, priva
205232
val call = okHttpClient.newCall(request)
206233
call.enqueue(object : Callback {
207234
override fun onFailure(call: Call, e: IOException) {
235+
metricsEventFactory?.removeMetadata(call)
208236
if (e is javax.net.ssl.SSLPeerUnverifiedException) {
209237
cancelAllRequests()
210238
val fingerPrintsMap = getCertificatesFingerPrints()
@@ -224,7 +252,13 @@ internal class NetworkClient(private val context: ReactApplicationContext, priva
224252
}
225253

226254
override fun onResponse(call: Call, response: Response) {
227-
promise.resolve(response.toWritableMap())
255+
var metadata: RequestMetadata? = null
256+
if (shouldCollectMetrics) {
257+
metadata = metricsEventFactory?.getMetadata(call)
258+
metadata?.networkType = getNetworkType(context)
259+
metricsEventFactory?.removeMetadata(call)
260+
}
261+
promise.resolve(response.toWritableMap(metadata))
228262
cleanUpAfter(response)
229263
}
230264
})

android/src/main/java/com/mattermost/networkclient/WebSocketClientModuleImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class WebSocketClientModuleImpl(reactApplicationContext: ReactApplicationContext
113113
}
114114

115115
try {
116-
clients[wsUri]!!.webSocket!!.close(1000, "manual")
116+
clients[wsUri]!!.webSocket!!.cancel()
117117
} catch (error: Exception) {
118118
promise.reject(error)
119119
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.mattermost.networkclient.interceptors
2+
3+
import okhttp3.Interceptor
4+
import okhttp3.Response
5+
import okhttp3.ResponseBody.Companion.toResponseBody
6+
7+
class CompressedResponseSizeInterceptor: Interceptor {
8+
override fun intercept(chain: Interceptor.Chain): Response {
9+
val response = chain.proceed(chain.request())
10+
11+
val modifiedResponse = response.newBuilder()
12+
13+
var compressedSize = response.header("Content-Length")?.toLongOrNull() ?: response.header("content-length")?.toLongOrNull()
14+
if (compressedSize == null) {
15+
val rawBytes = response.body?.byteStream()?.readBytes()
16+
compressedSize = rawBytes?.size?.toLong() ?: -1L
17+
modifiedResponse.body((rawBytes ?: ByteArray(0)).toResponseBody(response.body?.contentType()))
18+
}
19+
20+
return modifiedResponse
21+
.header("X-Compressed-Size", compressedSize.toString())
22+
.build()
23+
}
24+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.mattermost.networkclient.metrics
2+
3+
import okhttp3.Call
4+
import okhttp3.EventListener
5+
6+
class MetricsEventFactory : EventListener.Factory {
7+
private val metadataMap = mutableMapOf<Call, RequestMetadata>()
8+
9+
override fun create(call: Call): EventListener {
10+
val metadata = RequestMetadata()
11+
metadataMap[call] = metadata
12+
return MetricsEventListener(metadata, call, this)
13+
}
14+
15+
fun getMetadata(call: Call): RequestMetadata? = metadataMap[call]
16+
17+
fun removeMetadata(call: Call) {
18+
metadataMap.remove(call)
19+
}
20+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.mattermost.networkclient.metrics
2+
3+
import okhttp3.Call
4+
import okhttp3.EventListener
5+
import okhttp3.Handshake
6+
import okhttp3.Protocol
7+
import okhttp3.Response
8+
import java.io.IOException
9+
import java.net.InetSocketAddress
10+
import java.net.Proxy
11+
12+
class MetricsEventListener(
13+
private val requestMetadata: RequestMetadata,
14+
private val call: Call,
15+
private val factory: MetricsEventFactory,
16+
): EventListener() {
17+
18+
override fun callStart(call: Call) {
19+
super.callStart(call)
20+
requestMetadata.callStartNanos = System.nanoTime()
21+
}
22+
23+
override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {
24+
super.connectStart(call, inetSocketAddress, proxy)
25+
requestMetadata.connectStartNanos = System.nanoTime()
26+
}
27+
28+
override fun connectEnd(
29+
call: Call,
30+
inetSocketAddress: InetSocketAddress,
31+
proxy: Proxy,
32+
protocol: Protocol?
33+
) {
34+
super.connectEnd(call, inetSocketAddress, proxy, protocol)
35+
requestMetadata.connectEndNanos = System.nanoTime()
36+
requestMetadata.httpVersion = protocol?.toString() ?: "Unknown"
37+
}
38+
39+
override fun secureConnectEnd(call: Call, handshake: Handshake?) {
40+
super.secureConnectEnd(call, handshake)
41+
if (handshake != null) {
42+
requestMetadata.sslVersion = handshake.tlsVersion.javaName
43+
requestMetadata.sslCipher = handshake.cipherSuite.javaName
44+
}
45+
}
46+
47+
override fun responseHeadersStart(call: Call) {
48+
super.responseHeadersStart(call)
49+
requestMetadata.responseStartNanos = System.nanoTime()
50+
}
51+
52+
override fun responseHeadersEnd(call: Call, response: Response) {
53+
super.responseHeadersEnd(call, response)
54+
requestMetadata.isCached = response.cacheResponse != null
55+
56+
val handshake = response.handshake
57+
if (requestMetadata.sslVersion == null && handshake != null) {
58+
requestMetadata.sslVersion = handshake.tlsVersion?.javaName
59+
requestMetadata.sslCipher = handshake.cipherSuite?.javaName
60+
}
61+
62+
if (requestMetadata.httpVersion == null) {
63+
requestMetadata.httpVersion = response.protocol.toString()
64+
}
65+
}
66+
67+
override fun callFailed(call: Call, ioe: IOException) {
68+
super.callFailed(call, ioe)
69+
}
70+
71+
override fun canceled(call: Call) {
72+
super.canceled(call)
73+
factory.removeMetadata(call)
74+
}
75+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.mattermost.networkclient.metrics
2+
3+
import android.content.Context
4+
import android.net.ConnectivityManager
5+
import android.net.NetworkCapabilities
6+
import android.os.Build
7+
8+
fun getNetworkType(context: Context): String {
9+
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
10+
val network = connectivityManager.activeNetwork ?: return "No Network"
11+
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return "Unknown"
12+
13+
return when {
14+
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "Wi-Fi"
15+
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
16+
when {
17+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) -> "5G"
18+
capabilities.linkDownstreamBandwidthKbps >= 30000 -> "4G"
19+
capabilities.linkDownstreamBandwidthKbps >= 1000 -> "3G"
20+
else -> "2G"
21+
}
22+
}
23+
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "Wired Ethernet"
24+
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> "Bluetooth"
25+
else -> "Other"
26+
}
27+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.mattermost.networkclient.metrics
2+
3+
import java.util.concurrent.TimeUnit
4+
5+
data class RequestMetadata(
6+
var callStartNanos: Long = 0,
7+
var connectStartNanos: Long = 0,
8+
var connectEndNanos: Long = 0,
9+
var responseStartNanos: Long = 0,
10+
var isCached: Boolean = false,
11+
var sslVersion: String? = null,
12+
var sslCipher: String? = null,
13+
var httpVersion: String? = null,
14+
var networkType: String? = null
15+
) {
16+
fun getLatency() = TimeUnit.NANOSECONDS.toMillis(responseStartNanos - callStartNanos)
17+
fun getConnectionTime() = TimeUnit.NANOSECONDS.toMillis(connectEndNanos - connectStartNanos)
18+
}

0 commit comments

Comments
 (0)