From 582753f312bf4d8e14c00e8ae69516ff67068f27 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 19 Oct 2026 16:00:25 +0200 Subject: [PATCH 1/2] fix(apollo): Do not capture failed requests for empty GraphQL errors arrays Failed-request detection in the Apollo 3, 4, and 5 integrations matched any `errors` array, including an empty one, which produced false-positive events. Only match an `errors` array with at least one entry. Fixes #6194 --- .../apollo3/SentryApollo3HttpInterceptor.kt | 9 ++++++--- .../SentryApollo3InterceptorClientErrors.kt | 15 +++++++++++++++ .../apollo4/SentryApollo4HttpInterceptor.kt | 9 ++++++--- ...tryApollo4BuilderExtensionsClientErrorsTest.kt | 15 +++++++++++++++ .../apollo5/SentryApollo5HttpInterceptor.kt | 9 ++++++--- ...tryApollo5BuilderExtensionsClientErrorsTest.kt | 15 +++++++++++++++ .../apollo5/SentryApollo5HttpInterceptorTest.kt | 2 +- 7 files changed, 64 insertions(+), 10 deletions(-) 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 53e48657393..29a8a89a9bb 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 @@ -59,7 +59,10 @@ constructor( } } - private val regex: Regex by lazy { "(?i)\"errors\"\\s*:\\s*\\[".toRegex() } + // matches a non-empty `errors` array, e.g. `"errors": [{...}]`, but not `"errors": []` + private val nonEmptyErrorsRegex: Regex by lazy { + "(?i)\"errors\"\\s*:\\s*\\[\\s*[^\\]\\s]".toRegex() + } override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse { val activeSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span @@ -358,8 +361,8 @@ constructor( return } - // if there response body does not have the errors field, do not raise an issue - if (body.isEmpty() || !regex.containsMatchIn(body)) { + // if the response body does not have a non-empty errors field, do not raise an issue + if (body.isEmpty() || !nonEmptyErrorsRegex.containsMatchIn(body)) { return } diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt index 315f15a97b3..82890d75743 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt @@ -67,6 +67,13 @@ class SentryApollo3InterceptorClientErrors { ] }""" + val responseBodyEmptyErrors = + """{ + "data": {}, + "errors": [ + ] +}""" + fun getSut( captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS), @@ -551,6 +558,14 @@ class SentryApollo3InterceptorClientErrors { verify(fixture.scopes).captureEvent(any(), any()) } + @Test + fun `does not capture errors if errors array is empty`() { + val sut = fixture.getSut(responseBody = fixture.responseBodyEmptyErrors) + executeQuery(sut) + + verify(fixture.scopes, never()).captureEvent(any(), any()) + } + @Test fun `capture errors swallow any exception during the error transformation`() { val sut = fixture.getSut(responseBody = fixture.responseBodyNotOk) 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 aea5d6691c4..b931ce21d25 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 @@ -58,7 +58,10 @@ constructor( } } - private val regex: Regex by lazy { "(?i)\"errors\"\\s*:\\s*\\[".toRegex() } + // matches a non-empty `errors` array, e.g. `"errors": [{...}]`, but not `"errors": []` + private val nonEmptyErrorsRegex: Regex by lazy { + "(?i)\"errors\"\\s*:\\s*\\[\\s*[^\\]\\s]".toRegex() + } override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse { val activeSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span @@ -357,8 +360,8 @@ constructor( return } - // if the response body does not have the errors field, do not raise an issue - if (body.isEmpty() || !regex.containsMatchIn(body)) { + // if the response body does not have a non-empty errors field, do not raise an issue + if (body.isEmpty() || !nonEmptyErrorsRegex.containsMatchIn(body)) { return } diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsClientErrorsTest.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsClientErrorsTest.kt index 4c13e35a83e..542d1301f3f 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsClientErrorsTest.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsClientErrorsTest.kt @@ -81,6 +81,13 @@ abstract class SentryApollo4BuilderExtensionsClientErrorsTest( ] }""" + val responseBodyEmptyErrors = + """{ + "data": {}, + "errors": [ + ] +}""" + fun getSut( captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS), @@ -561,6 +568,14 @@ abstract class SentryApollo4BuilderExtensionsClientErrorsTest( verify(fixture.scopes).captureEvent(any(), any()) } + @Test + fun `does not capture errors if errors array is empty`() { + val sut = fixture.getSut(responseBody = fixture.responseBodyEmptyErrors) + executeQuery(sut) + + verify(fixture.scopes, never()).captureEvent(any(), any()) + } + @Test fun `capture errors swallow any exception during the error transformation`() { val sut = fixture.getSut(responseBody = fixture.responseBodyNotOk) 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 f74d0fec111..80e63848910 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 @@ -58,7 +58,10 @@ constructor( } } - private val regex: Regex by lazy { "(?i)\"errors\"\\s*:\\s*\\[".toRegex() } + // matches a non-empty `errors` array, e.g. `"errors": [{...}]`, but not `"errors": []` + private val nonEmptyErrorsRegex: Regex by lazy { + "(?i)\"errors\"\\s*:\\s*\\[\\s*[^\\]\\s]".toRegex() + } override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse { val activeSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span @@ -367,8 +370,8 @@ constructor( return } - // if the response body does not have the errors field, do not raise an issue - if (body.isEmpty() || !regex.containsMatchIn(body)) { + // if the response body does not have a non-empty errors field, do not raise an issue + if (body.isEmpty() || !nonEmptyErrorsRegex.containsMatchIn(body)) { return } diff --git a/sentry-apollo-5/src/test/java/io/sentry/apollo5/SentryApollo5BuilderExtensionsClientErrorsTest.kt b/sentry-apollo-5/src/test/java/io/sentry/apollo5/SentryApollo5BuilderExtensionsClientErrorsTest.kt index 1307544eb89..1c0308e496b 100644 --- a/sentry-apollo-5/src/test/java/io/sentry/apollo5/SentryApollo5BuilderExtensionsClientErrorsTest.kt +++ b/sentry-apollo-5/src/test/java/io/sentry/apollo5/SentryApollo5BuilderExtensionsClientErrorsTest.kt @@ -78,6 +78,13 @@ abstract class SentryApollo5BuilderExtensionsClientErrorsTest( ] }""" + val responseBodyEmptyErrors = + """{ + "data": {}, + "errors": [ + ] +}""" + fun getSut( captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS), @@ -550,6 +557,14 @@ abstract class SentryApollo5BuilderExtensionsClientErrorsTest( verify(fixture.scopes).captureEvent(any(), any()) } + @Test + fun `does not capture errors if errors array is empty`() { + val sut = fixture.getSut(responseBody = fixture.responseBodyEmptyErrors) + executeQuery(sut) + + verify(fixture.scopes, never()).captureEvent(any(), any()) + } + @Test fun `capture errors swallow any exception during the error transformation`() { val sut = fixture.getSut(responseBody = fixture.responseBodyNotOk) 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 399667a7647..405f0627ae1 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 @@ -300,7 +300,7 @@ abstract class SentryApollo5HttpInterceptorTest( private suspend fun interceptWithContentType(contentType: String?): Boolean { var bodyRead = false val body = - object : ForwardingSource(Buffer().writeUtf8("{\"errors\":[]}")) { + object : ForwardingSource(Buffer().writeUtf8("{\"errors\":[{\"message\":\"error\"}]}")) { override fun read(sink: Buffer, byteCount: Long): Long { bodyRead = true return super.read(sink, byteCount) From bd0b9f95a2c48ef0c40ef0ed3522d7b5907dc523 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 19 Oct 2026 16:00:44 +0200 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a439b35e5f3..ad0b1d72818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Do not capture failed Apollo 3, 4, and 5 requests when the GraphQL `errors` array is empty ([#6285](https://github.com/getsentry/sentry-java/pull/6285)) + ## 8.61.0 ### Fixes