@@ -14,6 +14,7 @@ import com.apollographql.apollo.network.http.HttpInterceptorChain
1414import com.google.common.truth.Truth.assertThat
1515import io.sentry.BaggageHeader
1616import io.sentry.Breadcrumb
17+ import io.sentry.DataCategory
1718import io.sentry.Hint
1819import io.sentry.IScopes
1920import io.sentry.ITransaction
@@ -32,6 +33,7 @@ import io.sentry.TransactionContext
3233import io.sentry.W3CTraceparentHeader
3334import io.sentry.apollo5.SentryApollo5HttpInterceptor.BeforeSpanCallback
3435import io.sentry.apollo5.generated.LaunchDetailsQuery
36+ import io.sentry.clientreport.DiscardReason
3537import io.sentry.mockServerRequestTimeoutMillis
3638import io.sentry.protocol.SdkVersion
3739import io.sentry.protocol.SentryTransaction
@@ -40,6 +42,7 @@ import java.util.concurrent.TimeUnit
4042import kotlin.reflect.KSuspendFunction1
4143import kotlin.test.Test
4244import kotlin.test.assertEquals
45+ import kotlin.test.assertFailsWith
4346import kotlin.test.assertNotNull
4447import kotlin.test.assertNull
4548import kotlin.test.assertTrue
@@ -60,6 +63,7 @@ import org.mockito.kotlin.doAnswer
6063import org.mockito.kotlin.mock
6164import org.mockito.kotlin.never
6265import org.mockito.kotlin.verify
66+ import org.mockito.kotlin.verifyNoMoreInteractions
6367import org.mockito.kotlin.whenever
6468
6569class SentryApollo5HttpInterceptorTestWithV5Implementation :
@@ -421,16 +425,83 @@ abstract class SentryApollo5HttpInterceptorTest(
421425 }
422426
423427 @Test
424- fun `when customizer throws, exception is handled` () {
425- executeQuery(fixture.getSut(beforeSpan = { _, _, _ -> throw RuntimeException () }))
428+ fun `reports callback errors only for sampled spans` (): Unit = runBlocking {
429+ for (sampled in listOf (true , false , null )) {
430+ val onDiscard = mock<SentryOptions .OnDiscardCallback >()
431+ fixture.options.onDiscard = onDiscard
432+ val tx = SentryTracer (TransactionContext (" op" , " desc" ), fixture.scopes)
433+ tx.spanContext.sampled = sampled
434+ whenever(fixture.scopes.span).thenReturn(tx)
435+ val sut =
436+ fixture.getSut(
437+ beforeSpan = { span, _, _ ->
438+ span.spanContext.sampled = false
439+ throw IllegalStateException (" callback failed" )
440+ }
441+ )
442+
443+ assertThat(executeQueryImplementation(sut.query(LaunchDetailsQuery (" 83" ))).data).isNotNull()
444+ tx.finish()
445+
446+ if (sampled == true ) {
447+ verify(onDiscard).execute(DiscardReason .CALLBACK_ERROR , DataCategory .Span , 1 )
448+ }
449+ verifyNoMoreInteractions(onDiscard)
450+ }
451+ }
426452
453+ @Test
454+ fun `when beforeSpan throws, drops span and preserves response` (): Unit = runBlocking {
455+ val failure = IllegalStateException (" callback failed" )
456+ val tx =
457+ SentryTracer (TransactionContext (" op" , " desc" , TracesSamplingDecision (true )), fixture.scopes)
458+ whenever(fixture.scopes.span).thenReturn(tx)
459+ val sut =
460+ fixture.getSut(
461+ beforeSpan = { span, _, _ ->
462+ span.description = " partially modified"
463+ throw failure
464+ }
465+ )
466+
467+ val response = executeQueryImplementation(sut.query(LaunchDetailsQuery (" 83" )))
468+ assertThat(response.data).isNotNull()
469+ val span = tx.children.single()
470+ assertThat(span.isSampled).isFalse()
471+ assertThat(span.isFinished).isTrue()
472+ tx.finish()
427473 verify(fixture.scopes)
428474 .captureTransaction(
429- check { assertEquals( 1 , it.spans.size ) },
475+ check { assertThat( it.spans).isEmpty( ) },
430476 anyOrNull<TraceContext >(),
431477 anyOrNull(),
432478 anyOrNull(),
433479 )
480+ verify(fixture.scopes).addBreadcrumb(any<Breadcrumb >(), anyOrNull())
481+ }
482+
483+ @Test
484+ fun `fatal error from beforeSpan is not swallowed` (): Unit = runBlocking {
485+ val failure = OutOfMemoryError (" callback failed" )
486+ val tx =
487+ SentryTracer (TransactionContext (" op" , " desc" , TracesSamplingDecision (true )), fixture.scopes)
488+ whenever(fixture.scopes.span).thenReturn(tx)
489+ val request = HttpRequest .Builder (HttpMethod .Post , " https://example.com/graphql" ).build()
490+ val response = HttpResponse .Builder (200 ).build()
491+ val chain =
492+ object : HttpInterceptorChain {
493+ override suspend fun proceed (request : HttpRequest ): HttpResponse = response
494+ }
495+ val interceptor =
496+ SentryApollo5HttpInterceptor (
497+ fixture.scopes,
498+ beforeSpan = { _, _, _ -> throw failure },
499+ captureFailedRequests = false ,
500+ )
501+
502+ val thrown = assertFailsWith<OutOfMemoryError > { interceptor.intercept(request, chain) }
503+
504+ assertThat(thrown).isSameInstanceAs(failure)
434505 }
435506
436507 @Test
0 commit comments