From 2ae97e8274cc77609e9c3a7b16a6e81a521bbbe6 Mon Sep 17 00:00:00 2001 From: YYChen01988 Date: Mon, 30 Jun 2025 16:50:41 +0100 Subject: [PATCH] feat(okhttp) improve okhttp plugin eventlistener --- .../api/bugsnag-plugin-android-okhttp.api | 38 ++- .../detekt-baseline.xml | 1 + .../android/okhttp/BugsnagOkHttpPlugin.kt | 25 +- .../okhttp/util/DelegateEventListener.kt | 145 ++++++++++++ .../android/DelegateEventListenerTest.kt | 218 ++++++++++++++++++ 5 files changed, 420 insertions(+), 7 deletions(-) create mode 100644 bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/util/DelegateEventListener.kt create mode 100644 bugsnag-plugin-android-okhttp/src/test/java/com/bugsnag/android/DelegateEventListenerTest.kt diff --git a/bugsnag-plugin-android-okhttp/api/bugsnag-plugin-android-okhttp.api b/bugsnag-plugin-android-okhttp/api/bugsnag-plugin-android-okhttp.api index 3301f0d212..95e3883256 100644 --- a/bugsnag-plugin-android-okhttp/api/bugsnag-plugin-android-okhttp.api +++ b/bugsnag-plugin-android-okhttp/api/bugsnag-plugin-android-okhttp.api @@ -1,7 +1,8 @@ -public final class com/bugsnag/android/okhttp/BugsnagOkHttpPlugin : okhttp3/EventListener, com/bugsnag/android/Plugin { +public final class com/bugsnag/android/okhttp/BugsnagOkHttpPlugin : com/bugsnag/android/okhttp/util/DelegateEventListener, com/bugsnag/android/Plugin { public fun ()V - public fun (Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lokhttp3/EventListener;)V + public fun (Lokhttp3/EventListener;Lkotlin/jvm/functions/Function0;)V + public synthetic fun (Lokhttp3/EventListener;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun callEnd (Lokhttp3/Call;)V public fun callFailed (Lokhttp3/Call;Ljava/io/IOException;)V public fun callStart (Lokhttp3/Call;)V @@ -22,3 +23,34 @@ public final class com/bugsnag/android/okhttp/OkHttpDelivery : com/bugsnag/andro public fun deliver (Lcom/bugsnag/android/Session;Lcom/bugsnag/android/DeliveryParams;)Lcom/bugsnag/android/DeliveryStatus; } +public class com/bugsnag/android/okhttp/util/DelegateEventListener : okhttp3/EventListener { + public fun (Lokhttp3/EventListener;)V + public fun cacheConditionalHit (Lokhttp3/Call;Lokhttp3/Response;)V + public fun cacheHit (Lokhttp3/Call;Lokhttp3/Response;)V + public fun cacheMiss (Lokhttp3/Call;)V + public fun callEnd (Lokhttp3/Call;)V + public fun callFailed (Lokhttp3/Call;Ljava/io/IOException;)V + public fun callStart (Lokhttp3/Call;)V + public fun canceled (Lokhttp3/Call;)V + public fun connectEnd (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;)V + public fun connectFailed (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;Ljava/io/IOException;)V + public fun connectStart (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;)V + public fun connectionAcquired (Lokhttp3/Call;Lokhttp3/Connection;)V + public fun connectionReleased (Lokhttp3/Call;Lokhttp3/Connection;)V + public fun dnsEnd (Lokhttp3/Call;Ljava/lang/String;Ljava/util/List;)V + public fun dnsStart (Lokhttp3/Call;Ljava/lang/String;)V + protected final fun getDelegateEventListener ()Lokhttp3/EventListener; + public fun proxySelectEnd (Lokhttp3/Call;Lokhttp3/HttpUrl;Ljava/util/List;)V + public fun proxySelectStart (Lokhttp3/Call;Lokhttp3/HttpUrl;)V + public fun requestBodyStart (Lokhttp3/Call;)V + public fun requestFailed (Lokhttp3/Call;Ljava/io/IOException;)V + public fun requestHeadersEnd (Lokhttp3/Call;Lokhttp3/Request;)V + public fun requestHeadersStart (Lokhttp3/Call;)V + public fun responseBodyStart (Lokhttp3/Call;)V + public fun responseFailed (Lokhttp3/Call;Ljava/io/IOException;)V + public fun responseHeadersStart (Lokhttp3/Call;)V + public fun satisfactionFailure (Lokhttp3/Call;Lokhttp3/Response;)V + public fun secureConnectEnd (Lokhttp3/Call;Lokhttp3/Handshake;)V + public fun secureConnectStart (Lokhttp3/Call;)V +} + diff --git a/bugsnag-plugin-android-okhttp/detekt-baseline.xml b/bugsnag-plugin-android-okhttp/detekt-baseline.xml index bf3b02f7b3..98fe83ae93 100644 --- a/bugsnag-plugin-android-okhttp/detekt-baseline.xml +++ b/bugsnag-plugin-android-okhttp/detekt-baseline.xml @@ -6,5 +6,6 @@ MagicNumber:BugsnagOkHttpPlugin.kt$399 MagicNumber:BugsnagOkHttpPlugin.kt$400 MagicNumber:BugsnagOkHttpPlugin.kt$599 + TooManyFunctions:DelegateEventListener.kt$DelegateEventListener : EventListener diff --git a/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/BugsnagOkHttpPlugin.kt b/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/BugsnagOkHttpPlugin.kt index 281968fd74..b0f27d1443 100644 --- a/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/BugsnagOkHttpPlugin.kt +++ b/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/BugsnagOkHttpPlugin.kt @@ -4,6 +4,7 @@ import androidx.annotation.VisibleForTesting import com.bugsnag.android.BreadcrumbType import com.bugsnag.android.Client import com.bugsnag.android.Plugin +import com.bugsnag.android.okhttp.util.DelegateEventListener import com.bugsnag.android.shouldDiscardNetworkBreadcrumb import okhttp3.Call import okhttp3.EventListener @@ -25,8 +26,9 @@ import java.util.concurrent.ConcurrentHashMap * https://square.github.io/okhttp/4.x/okhttp/okhttp3/-response-body/#the-response-body-must-be-closed */ class BugsnagOkHttpPlugin @JvmOverloads constructor( + delegateEventListener: EventListener? = null, internal val timeProvider: () -> Long = { System.currentTimeMillis() } -) : Plugin, EventListener() { +) : Plugin, DelegateEventListener(delegateEventListener) { internal val requestMap = ConcurrentHashMap() private var client: Client? = null @@ -40,24 +42,39 @@ class BugsnagOkHttpPlugin @JvmOverloads constructor( } override fun callStart(call: Call) { + super.callStart(call) requestMap[call] = NetworkRequestMetadata(timeProvider()) } override fun requestBodyEnd(call: Call, byteCount: Long) { + super.requestBodyEnd(call, byteCount) requestMap[call]?.requestBodyCount = byteCount } override fun responseBodyEnd(call: Call, byteCount: Long) { + super.responseBodyEnd(call, byteCount) requestMap[call]?.responseBodyCount = byteCount } override fun responseHeadersEnd(call: Call, response: Response) { + super.responseHeadersEnd(call, response) requestMap[call]?.status = response.code } - override fun callEnd(call: Call) = captureNetworkBreadcrumb(call) - override fun callFailed(call: Call, ioe: IOException) = captureNetworkBreadcrumb(call) - override fun canceled(call: Call) = captureNetworkBreadcrumb(call) + override fun callEnd(call: Call) { + super.callEnd(call) + captureNetworkBreadcrumb(call) + } + + override fun callFailed(call: Call, ioe: IOException) { + super.callFailed(call, ioe) + captureNetworkBreadcrumb(call) + } + + override fun canceled(call: Call) { + super.canceled(call) + captureNetworkBreadcrumb(call) + } private fun captureNetworkBreadcrumb(call: Call) { client?.apply { diff --git a/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/util/DelegateEventListener.kt b/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/util/DelegateEventListener.kt new file mode 100644 index 0000000000..adb32a1493 --- /dev/null +++ b/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/util/DelegateEventListener.kt @@ -0,0 +1,145 @@ +package com.bugsnag.android.okhttp.util + +import okhttp3.Call +import okhttp3.Connection +import okhttp3.EventListener +import okhttp3.Handshake +import okhttp3.HttpUrl +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Proxy + +open class DelegateEventListener( + protected val delegateEventListener: EventListener? +) : EventListener() { + override fun callStart(call: Call) { + delegateEventListener?.callStart(call) + } + + override fun callEnd(call: Call) { + delegateEventListener?.callEnd(call) + } + + override fun callFailed(call: Call, ioe: IOException) { + delegateEventListener?.callFailed(call, ioe) + } + + override fun canceled(call: Call) { + delegateEventListener?.canceled(call) + } + + override fun cacheConditionalHit(call: Call, cachedResponse: Response) { + delegateEventListener?.cacheConditionalHit(call, cachedResponse) + } + + override fun cacheHit(call: Call, response: Response) { + delegateEventListener?.cacheHit(call, response) + } + + override fun cacheMiss(call: Call) { + delegateEventListener?.cacheMiss(call) + } + + override fun connectEnd( + call: Call, + inetSocketAddress: InetSocketAddress, + proxy: Proxy, + protocol: Protocol? + ) { + delegateEventListener?.connectEnd(call, inetSocketAddress, proxy, protocol) + } + + override fun connectFailed( + call: Call, + inetSocketAddress: InetSocketAddress, + proxy: Proxy, + protocol: Protocol?, + ioe: IOException + ) { + delegateEventListener?.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) + } + + override fun connectStart( + call: Call, + inetSocketAddress: InetSocketAddress, + proxy: Proxy + ) { + delegateEventListener?.connectStart(call, inetSocketAddress, proxy) + } + + override fun connectionAcquired(call: Call, connection: Connection) { + delegateEventListener?.connectionAcquired(call, connection) + } + + override fun connectionReleased(call: Call, connection: Connection) { + delegateEventListener?.connectionReleased(call, connection) + } + + override fun dnsEnd( + call: Call, + domainName: String, + inetAddressList: List<@JvmSuppressWildcards InetAddress> + ) { + delegateEventListener?.dnsEnd(call, domainName, inetAddressList) + } + + override fun dnsStart(call: Call, domainName: String) { + delegateEventListener?.dnsStart(call, domainName) + } + + override fun proxySelectEnd( + call: Call, + url: HttpUrl, + proxies: List<@JvmSuppressWildcards Proxy> + ) { + delegateEventListener?.proxySelectEnd(call, url, proxies) + } + + override fun proxySelectStart(call: Call, url: HttpUrl) { + delegateEventListener?.proxySelectStart(call, url) + } + + override fun requestBodyStart(call: Call) { + delegateEventListener?.requestBodyStart(call) + } + + override fun requestFailed(call: Call, ioe: IOException) { + delegateEventListener?.requestFailed(call, ioe) + } + + override fun requestHeadersEnd(call: Call, request: Request) { + delegateEventListener?.requestHeadersEnd(call, request) + } + + override fun requestHeadersStart(call: Call) { + delegateEventListener?.requestHeadersStart(call) + } + + override fun responseBodyStart(call: Call) { + delegateEventListener?.responseBodyStart(call) + } + + override fun responseFailed(call: Call, ioe: IOException) { + delegateEventListener?.responseFailed(call, ioe) + } + + override fun responseHeadersStart(call: Call) { + delegateEventListener?.responseHeadersStart(call) + } + + override fun satisfactionFailure(call: Call, response: Response) { + delegateEventListener?.satisfactionFailure(call, response) + } + + override fun secureConnectEnd(call: Call, handshake: Handshake?) { + delegateEventListener?.secureConnectEnd(call, handshake) + } + + override fun secureConnectStart(call: Call) { + delegateEventListener?.secureConnectStart(call) + } +} diff --git a/bugsnag-plugin-android-okhttp/src/test/java/com/bugsnag/android/DelegateEventListenerTest.kt b/bugsnag-plugin-android-okhttp/src/test/java/com/bugsnag/android/DelegateEventListenerTest.kt new file mode 100644 index 0000000000..1abc04282a --- /dev/null +++ b/bugsnag-plugin-android-okhttp/src/test/java/com/bugsnag/android/DelegateEventListenerTest.kt @@ -0,0 +1,218 @@ +package com.bugsnag.android + +import com.bugsnag.android.okhttp.BugsnagOkHttpPlugin +import com.bugsnag.android.okhttp.util.DelegateEventListener +import okhttp3.Call +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnitRunner +import java.io.IOException + +@RunWith(MockitoJUnitRunner::class) +class DelegateEventListenerTest { + + @Mock + lateinit var client: Client + + @Mock + lateinit var call: Call + + @Mock + private lateinit var delegateEventListener: DelegateEventListener + + private lateinit var plugin: BugsnagOkHttpPlugin + + private val response = mock(okhttp3.Response::class.java) + private val inetSocketAddress = mock(java.net.InetSocketAddress::class.java) + private val proxy = mock(java.net.Proxy::class.java) + private val protocol = okhttp3.Protocol.HTTP_1_1 + private val ioe = IOException("Exception for testing") + private val connection = mock(okhttp3.Connection::class.java) + private val request = mock(okhttp3.Request::class.java) + + @Before + fun setUp() { + plugin = BugsnagOkHttpPlugin(delegateEventListener) + plugin.load(client) + } + + @Test + fun callStart() { + plugin.callStart(call) + verify(delegateEventListener, times(1)).callStart(call) + } + + @Test + fun callEnd() { + plugin.callEnd(call) + verify(delegateEventListener, times(1)).callEnd(call) + } + + @Test + fun callFailed() { + plugin.callFailed(call, ioe) + verify(delegateEventListener, times(1)).callFailed(call, ioe) + } + + @Test + fun canceled() { + plugin.canceled(call) + verify(delegateEventListener, times(1)).canceled(call) + } + + @Test + fun cacheConditionalHit() { + plugin.cacheConditionalHit(call, response) + verify(delegateEventListener, times(1)).cacheConditionalHit(call, response) + } + + @Test + fun cacheHit() { + plugin.cacheHit(call, response) + verify(delegateEventListener, times(1)).cacheHit(call, response) + } + + @Test + fun cacheMiss() { + plugin.cacheMiss(call) + verify(delegateEventListener, times(1)).cacheMiss(call) + } + + @Test + fun connectEnd() { + plugin.connectEnd(call, inetSocketAddress, proxy, protocol) + verify(delegateEventListener, times(1)).connectEnd( + call, + inetSocketAddress, + proxy, + protocol + ) + } + + @Test + fun connectFailed() { + plugin.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) + verify(delegateEventListener, times(1)).connectFailed( + call, + inetSocketAddress, + proxy, + protocol, + ioe + ) + } + + @Test + fun connectStart() { + plugin.connectStart(call, inetSocketAddress, proxy) + verify(delegateEventListener, times(1)).connectStart(call, inetSocketAddress, proxy) + } + + @Test + fun connectionAcquired() { + plugin.connectionAcquired(call, connection) + verify(delegateEventListener, times(1)).connectionAcquired(call, connection) + } + + @Test + fun connectionReleased() { + plugin.connectionReleased(call, connection) + verify(delegateEventListener, times(1)).connectionReleased(call, connection) + } + + @Test + fun dnsEnd() { + plugin.dnsEnd(call, "example.com", listOf()) + verify(delegateEventListener, times(1)).dnsEnd(call, "example.com", listOf()) + } + + @Test + fun dnsStart() { + plugin.dnsStart(call, "example.com") + verify(delegateEventListener, times(1)).dnsStart(call, "example.com") + } + + @Test + fun proxySelectEnd() { + plugin.proxySelectEnd(call, "https://example.com".toHttpUrl(), listOf(proxy)) + verify(delegateEventListener, times(1)).proxySelectEnd( + call, + "https://example.com".toHttpUrl(), + listOf(proxy) + ) + } + + @Test + fun proxySelectStart() { + plugin.proxySelectStart(call, "https://example.com".toHttpUrl()) + verify(delegateEventListener, times(1)).proxySelectStart( + call, + "https://example.com".toHttpUrl() + ) + } + + @Test + fun requestBodyStart() { + plugin.requestBodyStart(call) + verify(delegateEventListener, times(1)).requestBodyStart(call) + } + + @Test + fun requestFailed() { + plugin.requestFailed(call, ioe) + verify(delegateEventListener, times(1)).requestFailed(call, ioe) + } + + @Test + fun requestHeadersEnd() { + plugin.requestHeadersEnd(call, request) + verify(delegateEventListener, times(1)).requestHeadersEnd(call, request) + } + + @Test + fun requestHeadersStart() { + plugin.requestHeadersStart(call) + verify(delegateEventListener, times(1)).requestHeadersStart(call) + } + + @Test + fun responseBodyStart() { + plugin.responseBodyStart(call) + verify(delegateEventListener, times(1)).responseBodyStart(call) + } + + @Test + fun responseFailed() { + plugin.responseFailed(call, ioe) + verify(delegateEventListener, times(1)).responseFailed(call, ioe) + } + + @Test + fun responseHeadersStart() { + plugin.responseHeadersStart(call) + verify(delegateEventListener, times(1)).responseHeadersStart(call) + } + + @Test + fun satisfactionFailure() { + plugin.satisfactionFailure(call, response) + verify(delegateEventListener, times(1)).satisfactionFailure(call, response) + } + + @Test + fun secureConnectEnd() { + plugin.secureConnectEnd(call, null) + verify(delegateEventListener, times(1)).secureConnectEnd(call, null) + } + + @Test + fun secureConnectStart() { + plugin.secureConnectStart(call) + verify(delegateEventListener, times(1)).secureConnectStart(call) + } +}