diff --git a/CHANGELOG.md b/CHANGELOG.md index b30ff26bbe0..1cfe1c307ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,10 @@ - `ActivityFramesTracker` is removed, and the `ActivityLifecycleIntegration` constructor no longer takes it - Screenshots attached to errors and user feedback now mask all text and images by default. Turn it off with `options.screenshot.setMaskAllText(false)` and `setMaskAllImages(false)`, or the `io.sentry.screenshot.mask-all-text` and `io.sentry.screenshot.mask-all-images` manifest options. Masking needs `sentry-android-replay`, so apps that depend on `sentry-android-core` alone get no screenshot until they add it or turn masking off ([#6437](https://github.com/getsentry/sentry-java/pull/6437)) +### Fixes + +- Set Apollo 3, 4, and 5 HTTP spans to `CANCELLED` instead of `INTERNAL_ERROR` when the request is cancelled ([#6284](https://github.com/getsentry/sentry-java/pull/6284)) + ### Improvements - Promote check-in APIs from experimental to stable ([#6422](https://github.com/getsentry/sentry-java/pull/6422)) diff --git a/sentry-apollo-3/build.gradle.kts b/sentry-apollo-3/build.gradle.kts index 6467521f328..28030fa16d5 100644 --- a/sentry-apollo-3/build.gradle.kts +++ b/sentry-apollo-3/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { testImplementation(projects.sentryTestSupport) testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(libs.apollo3.kotlin) + testImplementation(libs.google.truth) testImplementation(libs.kotlin.test.junit) testImplementation(libs.google.truth) testImplementation(libs.kotlinx.coroutines) diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt index b9fdd060c3e..f5fb466b21a 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -39,6 +39,7 @@ import io.sentry.util.SpanUtils import io.sentry.util.TracingUtils import io.sentry.util.UrlUtils import io.sentry.vendor.Base64 +import kotlin.coroutines.cancellation.CancellationException import okio.Buffer import org.jetbrains.annotations.ApiStatus @@ -90,6 +91,8 @@ constructor( } catch (e: Throwable) { // https://github.com/apollographql/apollo-kotlin/issues/4711 will change error handling in v4 when (e) { + is CancellationException -> span?.status = SpanStatus.CANCELLED + is ApolloHttpException -> { statusCode = e.statusCode span?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, statusCode) diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt index 1ca41b566de..0921e41fe36 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -1,6 +1,7 @@ package io.sentry.apollo3 import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.api.http.HttpMethod import com.apollographql.apollo3.api.http.HttpRequest import com.apollographql.apollo3.api.http.HttpResponse import com.apollographql.apollo3.exception.ApolloException @@ -36,8 +37,10 @@ import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryTransaction import io.sentry.util.Apollo3PlatformTestManipulator import java.util.concurrent.TimeUnit +import kotlin.coroutines.cancellation.CancellationException import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -214,6 +217,25 @@ class SentryApollo3InterceptorTest { ) } + @Test + fun `sets span status to cancelled and rethrows CancellationException`(): Unit = runBlocking { + val tx = + SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.scopes) + whenever(fixture.scopes.span).thenReturn(tx) + val cancellation = CancellationException("cancelled") + val chain = + object : HttpInterceptorChain { + override suspend fun proceed(request: HttpRequest): HttpResponse = throw cancellation + } + val request = HttpRequest.Builder(HttpMethod.Post, "https://example.com/graphql").build() + val interceptor = SentryApollo3HttpInterceptor(fixture.scopes, captureFailedRequests = false) + + val thrown = assertFailsWith { interceptor.intercept(request, chain) } + + assertThat(thrown).isSameInstanceAs(cancellation) + assertThat(tx.children.single().status).isEqualTo(SpanStatus.CANCELLED) + } + @Test fun `does not add sentry trace header to the request if host is disallowed`() { fixture.options.setTracePropagationTargets(listOf("some-host-that-does-not-exist")) diff --git a/sentry-apollo-4/build.gradle.kts b/sentry-apollo-4/build.gradle.kts index 3bb166ae9a9..d42f652c318 100644 --- a/sentry-apollo-4/build.gradle.kts +++ b/sentry-apollo-4/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { testImplementation(projects.sentryTestSupport) testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(libs.apollo4.kotlin) + testImplementation(libs.google.truth) testImplementation(libs.kotlin.test.junit) testImplementation(libs.google.truth) testImplementation(libs.kotlinx.coroutines) diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt index c39638bac58..83245be52fe 100644 --- a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt @@ -38,6 +38,7 @@ import io.sentry.util.TracingUtils import io.sentry.util.UrlUtils import io.sentry.vendor.Base64 import java.util.Locale +import kotlin.coroutines.cancellation.CancellationException import okio.Buffer import org.jetbrains.annotations.ApiStatus @@ -90,6 +91,8 @@ constructor( // client errors don't throw anymore in v4, but we should still be able to detect all of them // by looking at the status code and/or errors in the response body when (e) { + is CancellationException -> span?.status = SpanStatus.CANCELLED + is ApolloHttpException -> { statusCode = e.statusCode span?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, statusCode) diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt index 0418e55fb3d..f296abf615c 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt @@ -4,6 +4,7 @@ import com.apollographql.apollo.ApolloCall import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.api.http.HttpMethod import com.apollographql.apollo.api.http.HttpRequest import com.apollographql.apollo.api.http.HttpResponse import com.apollographql.apollo.exception.ApolloException @@ -40,9 +41,11 @@ import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryTransaction import io.sentry.util.Apollo4PlatformTestManipulator import java.util.concurrent.TimeUnit +import kotlin.coroutines.cancellation.CancellationException import kotlin.reflect.KSuspendFunction1 import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -228,6 +231,25 @@ abstract class SentryApollo4HttpInterceptorTest( ) } + @Test + fun `sets span status to cancelled and rethrows CancellationException`(): Unit = runBlocking { + val tx = + SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.scopes) + whenever(fixture.scopes.span).thenReturn(tx) + val cancellation = CancellationException("cancelled") + val chain = + object : HttpInterceptorChain { + override suspend fun proceed(request: HttpRequest): HttpResponse = throw cancellation + } + val request = HttpRequest.Builder(HttpMethod.Post, "https://example.com/graphql").build() + val interceptor = SentryApollo4HttpInterceptor(fixture.scopes, captureFailedRequests = false) + + val thrown = assertFailsWith { interceptor.intercept(request, chain) } + + assertThat(thrown).isSameInstanceAs(cancellation) + assertThat(tx.children.single().status).isEqualTo(SpanStatus.CANCELLED) + } + @Test fun `does not add sentry trace header to the request if host is disallowed`() { fixture.options.setTracePropagationTargets(listOf("some-host-that-does-not-exist")) diff --git a/sentry-apollo-5/src/main/java/io/sentry/apollo5/SentryApollo5HttpInterceptor.kt b/sentry-apollo-5/src/main/java/io/sentry/apollo5/SentryApollo5HttpInterceptor.kt index b0c4aadae1d..8bcf97ae162 100644 --- a/sentry-apollo-5/src/main/java/io/sentry/apollo5/SentryApollo5HttpInterceptor.kt +++ b/sentry-apollo-5/src/main/java/io/sentry/apollo5/SentryApollo5HttpInterceptor.kt @@ -38,6 +38,7 @@ import io.sentry.util.SpanUtils import io.sentry.util.TracingUtils import io.sentry.util.UrlUtils import java.util.Locale +import kotlin.coroutines.cancellation.CancellationException import okio.Buffer import org.jetbrains.annotations.ApiStatus @@ -93,6 +94,8 @@ constructor( // client errors don't throw anymore in v5, but we should still be able to detect all of them // by looking at the status code and/or errors in the response body when (e) { + is CancellationException -> span?.status = SpanStatus.CANCELLED + is ApolloHttpException -> { statusCode = e.statusCode span?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, statusCode) diff --git a/sentry-apollo-5/src/test/java/io/sentry/apollo5/SentryApollo5HttpInterceptorTest.kt b/sentry-apollo-5/src/test/java/io/sentry/apollo5/SentryApollo5HttpInterceptorTest.kt index acd27f0ee1b..f0f68e1bb33 100644 --- a/sentry-apollo-5/src/test/java/io/sentry/apollo5/SentryApollo5HttpInterceptorTest.kt +++ b/sentry-apollo-5/src/test/java/io/sentry/apollo5/SentryApollo5HttpInterceptorTest.kt @@ -40,6 +40,7 @@ import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryTransaction import io.sentry.util.Apollo5PlatformTestManipulator import java.util.concurrent.TimeUnit +import kotlin.coroutines.cancellation.CancellationException import kotlin.reflect.KSuspendFunction1 import kotlin.test.Test import kotlin.test.assertEquals @@ -330,6 +331,25 @@ abstract class SentryApollo5HttpInterceptorTest( return bodyRead } + @Test + fun `sets span status to cancelled and rethrows CancellationException`(): Unit = runBlocking { + val tx = + SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.scopes) + whenever(fixture.scopes.span).thenReturn(tx) + val cancellation = CancellationException("cancelled") + val chain = + object : HttpInterceptorChain { + override suspend fun proceed(request: HttpRequest): HttpResponse = throw cancellation + } + val request = HttpRequest.Builder(HttpMethod.Post, "https://example.com/graphql").build() + val interceptor = SentryApollo5HttpInterceptor(fixture.scopes, captureFailedRequests = false) + + val thrown = assertFailsWith { interceptor.intercept(request, chain) } + + assertThat(thrown).isSameInstanceAs(cancellation) + assertThat(tx.children.single().status).isEqualTo(SpanStatus.CANCELLED) + } + @Test fun `does not add sentry trace header to the request if host is disallowed`() { fixture.options.setTracePropagationTargets(listOf("some-host-that-does-not-exist"))