diff --git a/CHANGELOG.md b/CHANGELOG.md index 67c683f0ff0..609c6051557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - Disable URL caching when reading `META-INF/MANIFEST.MF` files during version detection so that the SDK no longer keeps jar file handles open for the life of the process ([#6124](https://github.com/getsentry/sentry-java/pull/6124) +- Keep the `EventListener` wrapped by `SentryOkHttpEventListener` per `Call` ([#6003](https://github.com/getsentry/sentry-java/pull/6003)) ## 8.57.0 diff --git a/sentry-okhttp/build.gradle.kts b/sentry-okhttp/build.gradle.kts index 47b8bfe5b15..94575f54984 100644 --- a/sentry-okhttp/build.gradle.kts +++ b/sentry-okhttp/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { testImplementation(libs.mockito.inline) testImplementation(libs.okhttp) testImplementation(libs.okhttp.mockwebserver) + testImplementation(libs.google.truth) } buildConfig { diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt index 8ef67b3bb17..b43fcfdfb66 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt @@ -39,13 +39,45 @@ import okhttp3.Response * .addInterceptor(SentryOkHttpInterceptor()) * .build() * ``` + * + * ## Delivery of [canceled] + * + * A wrapped [okhttp3.EventListener] is not guaranteed to receive every [canceled] callback. + * + * OkHttp creates its listener in the [okhttp3.Call] constructor and keeps it for the whole lifetime + * of that call, so it may report [canceled] before [callStart] and after [callEnd]/[callFailed]. A + * single [SentryOkHttpEventListener] instead serves every call of the client, so it can only bind a + * wrapped listener to a call between [callStart] and the terminal [callEnd]/[callFailed] event. + * + * What that means for the wrapped listener: + * - A listener passed as a single [okhttp3.EventListener] instance receives every [canceled] + * callback. Such a listener is shared by all calls anyway, the same as OkHttp's own + * `EventListener.asFactory()`, so it exists independently of that window. + * - A listener created by an [okhttp3.EventListener.Factory] receives only the [canceled] callbacks + * that fall between [callStart] and the terminal event. A cancellation reported outside that + * window is not forwarded, because no listener is bound to the call, and creating one would give + * that call a second listener and break the [okhttp3.EventListener.Factory] contract of one + * listener per call. + * + * Cancellation is still observable in the common case: a call that is canceled before it runs fails + * with `IOException("Canceled")`, which is reported through [callFailed]. A call canceled after it + * has completed reports nothing, which matches [okhttp3.Call.cancel] being documented as a no-op + * for a request that is already complete. + * + * Use `OkHttpClient.Builder.eventListenerFactory` with your own listener if you need every + * cancellation, including the ones outside that window. */ @Suppress("TooManyFunctions") public open class SentryOkHttpEventListener( private val scopes: IScopes = ScopesAdapter.getInstance(), private val originalEventListenerCreator: ((call: Call) -> EventListener)? = null, ) : EventListener() { - private var originalEventListener: EventListener? = null + private val originalEventListenerMap: MutableMap = ConcurrentHashMap() + + // Set only by the constructors that wrap a single EventListener instance. Such a listener is + // shared by every Call anyway, exactly like OkHttp's own EventListener.asFactory(), so it + // exists independently of the callStart()..callEnd() window and can always be delegated to. + private var fixedOriginalEventListener: EventListener? = null public companion object { internal const val PROXY_SELECT_EVENT = "http.client.proxy_select_ms" @@ -65,7 +97,9 @@ public open class SentryOkHttpEventListener( public constructor( originalEventListener: EventListener - ) : this(ScopesAdapter.getInstance(), originalEventListenerCreator = { originalEventListener }) + ) : this(ScopesAdapter.getInstance(), originalEventListenerCreator = { originalEventListener }) { + fixedOriginalEventListener = originalEventListener + } public constructor( originalEventListenerFactory: Factory @@ -77,7 +111,9 @@ public open class SentryOkHttpEventListener( public constructor( scopes: IScopes = ScopesAdapter.getInstance(), originalEventListener: EventListener, - ) : this(scopes, originalEventListenerCreator = { originalEventListener }) + ) : this(scopes, originalEventListenerCreator = { originalEventListener }) { + fixedOriginalEventListener = originalEventListener + } public constructor( scopes: IScopes = ScopesAdapter.getInstance(), @@ -85,18 +121,25 @@ public open class SentryOkHttpEventListener( ) : this(scopes, originalEventListenerCreator = { originalEventListenerFactory.create(it) }) override fun callStart(call: Call) { - originalEventListener = originalEventListenerCreator?.invoke(call) + // The EventListener.Factory contract binds a listener to a single call, so the wrapped + // listener is kept per call instead of in a field shared by all concurrent calls. callStart() + // is invoked only once per Call, so this creates exactly one listener per Call. + val originalEventListener = originalEventListenerCreator?.invoke(call) + if (originalEventListener != null) { + originalEventListenerMap[call] = originalEventListener + } originalEventListener?.callStart(call) // If the wrapped EventListener is ours, we can just delegate the calls, // without creating other events that would create duplicates - if (canCreateEventSpan()) { + if (canCreateEventSpan(originalEventListener)) { eventMap[call] = SentryOkHttpEvent(scopes, call.request()) } } override fun proxySelectStart(call: Call, url: HttpUrl) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.proxySelectStart(call, url) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -104,8 +147,9 @@ public open class SentryOkHttpEventListener( } override fun proxySelectEnd(call: Call, url: HttpUrl, proxies: List) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.proxySelectEnd(call, url, proxies) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -117,8 +161,9 @@ public open class SentryOkHttpEventListener( } override fun dnsStart(call: Call, domainName: String) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.dnsStart(call, domainName) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -126,8 +171,9 @@ public open class SentryOkHttpEventListener( } override fun dnsEnd(call: Call, domainName: String, inetAddressList: List) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.dnsEnd(call, domainName, inetAddressList) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -140,8 +186,9 @@ public open class SentryOkHttpEventListener( } override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.connectStart(call, inetSocketAddress, proxy) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -149,8 +196,9 @@ public open class SentryOkHttpEventListener( } override fun secureConnectStart(call: Call) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.secureConnectStart(call) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -158,8 +206,9 @@ public open class SentryOkHttpEventListener( } override fun secureConnectEnd(call: Call, handshake: Handshake?) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.secureConnectEnd(call, handshake) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -172,8 +221,9 @@ public open class SentryOkHttpEventListener( proxy: Proxy, protocol: Protocol?, ) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.connectEnd(call, inetSocketAddress, proxy, protocol) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -188,8 +238,9 @@ public open class SentryOkHttpEventListener( protocol: Protocol?, ioe: IOException, ) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -202,8 +253,9 @@ public open class SentryOkHttpEventListener( } override fun connectionAcquired(call: Call, connection: Connection) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.connectionAcquired(call, connection) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -211,8 +263,9 @@ public open class SentryOkHttpEventListener( } override fun connectionReleased(call: Call, connection: Connection) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.connectionReleased(call, connection) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -220,8 +273,9 @@ public open class SentryOkHttpEventListener( } override fun requestHeadersStart(call: Call) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.requestHeadersStart(call) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -229,8 +283,9 @@ public open class SentryOkHttpEventListener( } override fun requestHeadersEnd(call: Call, request: Request) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.requestHeadersEnd(call, request) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -238,8 +293,9 @@ public open class SentryOkHttpEventListener( } override fun requestBodyStart(call: Call) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.requestBodyStart(call) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -247,8 +303,9 @@ public open class SentryOkHttpEventListener( } override fun requestBodyEnd(call: Call, byteCount: Long) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.requestBodyEnd(call, byteCount) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -261,8 +318,9 @@ public open class SentryOkHttpEventListener( } override fun requestFailed(call: Call, ioe: IOException) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.requestFailed(call, ioe) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -282,8 +340,9 @@ public open class SentryOkHttpEventListener( } override fun responseHeadersStart(call: Call) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.responseHeadersStart(call) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -291,8 +350,9 @@ public open class SentryOkHttpEventListener( } override fun responseHeadersEnd(call: Call, response: Response) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.responseHeadersEnd(call, response) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -307,8 +367,9 @@ public open class SentryOkHttpEventListener( } override fun responseBodyStart(call: Call) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.responseBodyStart(call) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -316,8 +377,9 @@ public open class SentryOkHttpEventListener( } override fun responseBodyEnd(call: Call, byteCount: Long) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.responseBodyEnd(call, byteCount) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -330,8 +392,9 @@ public open class SentryOkHttpEventListener( } override fun responseFailed(call: Call, ioe: IOException) { + val originalEventListener = originalEventListenerMap[call] originalEventListener?.responseFailed(call, ioe) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return @@ -351,14 +414,15 @@ public open class SentryOkHttpEventListener( } override fun callEnd(call: Call) { - originalEventListener?.callEnd(call) + originalEventListenerMap.remove(call)?.callEnd(call) val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return okHttpEvent.finish() } override fun callFailed(call: Call, ioe: IOException) { + val originalEventListener = originalEventListenerMap.remove(call) originalEventListener?.callFailed(call, ioe) - if (!canCreateEventSpan()) { + if (!canCreateEventSpan(originalEventListener)) { return } val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return @@ -370,26 +434,45 @@ public open class SentryOkHttpEventListener( } override fun canceled(call: Call) { + // canceled() is the only callback that is not part of the call window. OkHttp creates the + // listener in the Call constructor and keeps it for the whole lifetime of the Call, so it may + // deliver canceled() before callStart() and after callEnd()/callFailed(). We only keep the + // listener between callStart() and the terminal event, so out of that window there is nothing + // in originalEventListenerMap. + // + // We deliberately do not create a listener for such a cancel: + // - After the terminal event the call is over. Call.cancel() is documented as a no-op for a + // request that is already complete, so there is nothing to report. + // - Before callStart() there may never be a call at all. A Call that is canceled and then + // never executed gets no callEnd()/callFailed(), so an entry added here could never be + // removed again and would leak the Call. If the Call is executed after all, OkHttp fails it + // with IOException("Canceled") and the listener still learns about it through callFailed(). + // + // Creating one would also hand the EventListener.Factory contract a second listener for a + // single Call. A listener kept in a field instead is shared by all calls by definition, the + // same as OkHttp's own EventListener.asFactory(), so it exists outside the window and always + // gets the event. + val originalEventListener = originalEventListenerMap[call] ?: fixedOriginalEventListener originalEventListener?.canceled(call) } override fun satisfactionFailure(call: Call, response: Response) { - originalEventListener?.satisfactionFailure(call, response) + originalEventListenerMap[call]?.satisfactionFailure(call, response) } override fun cacheHit(call: Call, response: Response) { - originalEventListener?.cacheHit(call, response) + originalEventListenerMap[call]?.cacheHit(call, response) } override fun cacheMiss(call: Call) { - originalEventListener?.cacheMiss(call) + originalEventListenerMap[call]?.cacheMiss(call) } override fun cacheConditionalHit(call: Call, cachedResponse: Response) { - originalEventListener?.cacheConditionalHit(call, cachedResponse) + originalEventListenerMap[call]?.cacheConditionalHit(call, cachedResponse) } - private fun canCreateEventSpan(): Boolean { + private fun canCreateEventSpan(originalEventListener: EventListener?): Boolean { // If the wrapped EventListener is ours, we shouldn't create spans, as the originalEventListener // already did it // In case SentryOkHttpEventListener from sentry-android-okhttp is used, the is check won't work diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerDelegationTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerDelegationTest.kt new file mode 100644 index 00000000000..0d85e9294d6 --- /dev/null +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerDelegationTest.kt @@ -0,0 +1,232 @@ +package io.sentry.okhttp + +import com.google.common.truth.Truth.assertThat +import io.sentry.IScopes +import io.sentry.SentryOptions +import java.io.IOException +import kotlin.test.Test +import okhttp3.Call +import okhttp3.EventListener +import okhttp3.OkHttpClient +import okhttp3.Request +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class SentryOkHttpEventListenerDelegationTest { + class RecordingListener(val ownCall: Call) : EventListener() { + val received = mutableListOf>() + + override fun callStart(call: Call) { + received += "callStart" to call + } + + override fun dnsStart(call: Call, domainName: String) { + received += "dnsStart" to call + } + + override fun callEnd(call: Call) { + received += "callEnd" to call + } + + override fun callFailed(call: Call, ioe: IOException) { + received += "callFailed" to call + } + + override fun canceled(call: Call) { + received += "canceled" to call + } + + fun mismatches(): List> = received.filter { it.second !== ownCall } + } + + class Fixture { + val scopes = mock() + val client = OkHttpClient() + val listeners = mutableListOf() + + fun getSut(): SentryOkHttpEventListener { + whenever(scopes.options).thenReturn(SentryOptions()) + return SentryOkHttpEventListener( + scopes, + EventListener.Factory { call -> RecordingListener(call).also { listeners.add(it) } }, + ) + } + + fun newCall(path: String): Call = + client.newCall(Request.Builder().url("http://localhost/$path").build()) + } + + private val fixture = Fixture() + + @Test + fun `each call is delegated to the listener created for it`() { + val sut = fixture.getSut() + val call1 = fixture.newCall("1") + val call2 = fixture.newCall("2") + + sut.callStart(call1) + sut.callStart(call2) + sut.dnsStart(call1, "sentry.io") + sut.dnsStart(call2, "sentry.io") + sut.callEnd(call1) + sut.callEnd(call2) + + val (listener1, listener2) = fixture.listeners + assertThat(listener1.mismatches()).isEmpty() + assertThat(listener2.mismatches()).isEmpty() + assertThat(listener1.received.map { it.first }) + .containsExactly("callStart", "dnsStart", "callEnd") + .inOrder() + assertThat(listener2.received.map { it.first }) + .containsExactly("callStart", "dnsStart", "callEnd") + .inOrder() + } + + @Test + fun `callbacks without a preceding callStart are not delegated`() { + val sut = fixture.getSut() + val call1 = fixture.newCall("1") + val call2 = fixture.newCall("2") + + sut.callStart(call1) + sut.dnsStart(call2, "sentry.io") + sut.callEnd(call2) + + assertThat(fixture.listeners).hasSize(1) + assertThat(fixture.listeners.single().received.map { it.first }).containsExactly("callStart") + } + + @Test + fun `a finished call is no longer delegated to`() { + val sut = fixture.getSut() + val call = fixture.newCall("1") + + sut.callStart(call) + sut.callEnd(call) + sut.dnsStart(call, "sentry.io") + + assertThat(fixture.listeners.single().received.map { it.first }) + .containsExactly("callStart", "callEnd") + .inOrder() + } + + @Test + fun `a failed call is no longer delegated to`() { + val sut = fixture.getSut() + val call = fixture.newCall("1") + + sut.callStart(call) + sut.callFailed(call, IOException()) + sut.dnsStart(call, "sentry.io") + + assertThat(fixture.listeners.single().received.map { it.first }) + .containsExactly("callStart", "callFailed") + .inOrder() + } + + @Test + fun `cancel during a call is delegated to the listener of that call`() { + val sut = fixture.getSut() + val call = fixture.newCall("1") + + sut.callStart(call) + sut.canceled(call) + sut.callFailed(call, IOException()) + + assertThat(fixture.listeners.single().received.map { it.first }) + .containsExactly("callStart", "canceled", "callFailed") + .inOrder() + } + + /** + * Deliberate deviation from the [okhttp3.EventListener.Factory] contract: OkHttp would give the + * listener of this call the cancellation, we do not. + * + * A cancel can arrive before [okhttp3.EventListener.callStart] and after the terminal event, + * while we can only bind a wrapped listener to a call between the two. Creating a listener here + * would either give the call a second listener, or leave an entry that no terminal event can ever + * remove, because a call that is canceled and never executed gets no terminal event at all. See + * the KDoc of [SentryOkHttpEventListener] for the full reasoning. + */ + @Test + fun `cancel before callStart is not delegated and creates no listener`() { + val sut = fixture.getSut() + val call = fixture.newCall("1") + + sut.canceled(call) + + assertThat(fixture.listeners).isEmpty() + } + + /** The deviation costs only the cancel itself: the call still gets exactly one listener. */ + @Test + fun `a call canceled before callStart still gets a single listener when it starts`() { + val sut = fixture.getSut() + val call = fixture.newCall("1") + + sut.canceled(call) + sut.callStart(call) + sut.callFailed(call, IOException("Canceled")) + + assertThat(fixture.listeners).hasSize(1) + assertThat(fixture.listeners.single().received.map { it.first }) + .containsExactly("callStart", "callFailed") + .inOrder() + } + + /** + * Same deviation, on the other edge. [okhttp3.Call.cancel] is a documented no-op for a request + * that is already complete, so there is nothing to report. + */ + @Test + fun `cancel after the terminal event is ignored`() { + val sut = fixture.getSut() + val call = fixture.newCall("1") + + sut.callStart(call) + sut.callEnd(call) + sut.canceled(call) + + assertThat(fixture.listeners).hasSize(1) + assertThat(fixture.listeners.single().received.map { it.first }) + .containsExactly("callStart", "callEnd") + .inOrder() + } + + /** As above, with [okhttp3.EventListener.callFailed] as the terminal event. */ + @Test + fun `cancel after a failed call is ignored`() { + val sut = fixture.getSut() + val call = fixture.newCall("1") + + sut.callStart(call) + sut.callFailed(call, IOException()) + sut.canceled(call) + + assertThat(fixture.listeners).hasSize(1) + assertThat(fixture.listeners.single().received.map { it.first }) + .containsExactly("callStart", "callFailed") + .inOrder() + } + + /** + * No deviation for a single wrapped instance: it is shared by every call anyway, the same as + * OkHttp's own `EventListener.asFactory()`, so it outlives the window and gets every cancel. + */ + @Test + fun `a single wrapped listener receives cancels outside of the call window`() { + whenever(fixture.scopes.options).thenReturn(SentryOptions()) + val call = fixture.newCall("1") + val listener = RecordingListener(call) + val sut = SentryOkHttpEventListener(fixture.scopes, listener) + + sut.canceled(call) + sut.callStart(call) + sut.callEnd(call) + sut.canceled(call) + + assertThat(listener.received.map { it.first }) + .containsExactly("canceled", "callStart", "callEnd", "canceled") + .inOrder() + } +} diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt index 3c4ebe0dae6..e4772145879 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt @@ -227,7 +227,7 @@ class SentryOkHttpEventListenerTest { val call = sut.newCall(request) val response = mock() whenever(response.protocol).thenReturn(Protocol.HTTP_1_1) - verifyDelegation(listener, fixture.mockEventListener, call, response) + verifyDelegation(listener, fixture.mockEventListener, call, sut.newCall(request), response) } @Test @@ -238,7 +238,7 @@ class SentryOkHttpEventListenerTest { val call = sut.newCall(request) val response = mock() whenever(response.protocol).thenReturn(Protocol.HTTP_1_1) - verifyDelegation(listener, fixture.mockEventListener, call, response) + verifyDelegation(listener, fixture.mockEventListener, call, sut.newCall(request), response) } @Test @@ -250,7 +250,7 @@ class SentryOkHttpEventListenerTest { val call = sut.newCall(request) val response = mock() whenever(response.protocol).thenReturn(Protocol.HTTP_1_1) - verifyDelegation(listener, originalListener, call, response) + verifyDelegation(listener, originalListener, call, sut.newCall(request), response) } @Test @@ -262,7 +262,7 @@ class SentryOkHttpEventListenerTest { val call = sut.newCall(request) val response = mock() whenever(response.protocol).thenReturn(Protocol.HTTP_1_1) - verifyDelegation(listener, originalListener, call, response) + verifyDelegation(listener, originalListener, call, sut.newCall(request), response) } @Test @@ -304,6 +304,7 @@ class SentryOkHttpEventListenerTest { listener: SentryOkHttpEventListener, originalListener: EventListener, call: Call, + failedCall: Call, response: Response, ) { listener.callStart(call) @@ -352,7 +353,10 @@ class SentryOkHttpEventListenerTest { verify(originalListener).responseFailed(eq(call), any()) listener.callEnd(call) verify(originalListener).callEnd(eq(call)) - listener.callFailed(call, mock()) - verify(originalListener).callFailed(eq(call), any()) + + // callEnd and callFailed are both terminal, so a failing call is a separate one + listener.callStart(failedCall) + listener.callFailed(failedCall, mock()) + verify(originalListener).callFailed(eq(failedCall), any()) } }