diff --git a/CHANGELOG.md b/CHANGELOG.md index a439b35e5f..ad0b1d7281 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 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 53e4865739..29a8a89a9b 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 315f15a97b..82890d7574 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 aea5d6691c..b931ce21d2 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 4c13e35a83..542d1301f3 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 f74d0fec11..80e6384891 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 1307544eb8..1c0308e496 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 399667a764..405f0627ae 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)