diff --git a/libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt b/libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt index 25b93af61..5fa512d29 100644 --- a/libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt +++ b/libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt @@ -93,9 +93,24 @@ object ErrorUtils { throwable is SocketException || throwable.cause is SocketException - private val gmsTransientMessages = setOf("SERVICE_NOT_AVAILABLE", "FIS_AUTH_ERROR", "MISSING_INSTANCEID_SERVICE", "TOO_MANY_REGISTRATIONS") + /** + * Error strings Google Play Services returns for a failed FCM registration. They describe the + * device's GMS state or the FCM backend, not app code, so they stay out of Bugsnag. Firebase + * wraps them as `IOException("FCM Registration failed!")` -> `ExecutionException` -> + * `IOException()`, which is why the cause chain is walked. + */ + private val gmsTransientMessages = setOf( + "SERVICE_NOT_AVAILABLE", + "FIS_AUTH_ERROR", + "MISSING_INSTANCEID_SERVICE", + "TOO_MANY_REGISTRATIONS", + "AUTHENTICATION_FAILED", + "INTERNAL_SERVER_ERROR", + "InternalServerError", + "PHONE_REGISTRATION_ERROR", + ) - private fun isGmsTransientError(throwable: Throwable): Boolean = + internal fun isGmsTransientError(throwable: Throwable): Boolean = generateSequence(throwable) { it.cause } .any { it is java.io.IOException && it.message in gmsTransientMessages } diff --git a/libs/logging/src/test/kotlin/com/getcode/utils/ErrorUtilsTest.kt b/libs/logging/src/test/kotlin/com/getcode/utils/ErrorUtilsTest.kt index 84cc366dc..09efc856c 100644 --- a/libs/logging/src/test/kotlin/com/getcode/utils/ErrorUtilsTest.kt +++ b/libs/logging/src/test/kotlin/com/getcode/utils/ErrorUtilsTest.kt @@ -1,6 +1,8 @@ package com.getcode.utils +import java.io.IOException import java.net.UnknownHostException +import java.util.concurrent.ExecutionException import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -34,4 +36,24 @@ class ErrorUtilsTest { assertFalse(ErrorUtils.shouldReport(error, error)) assertFalse(ErrorUtils.shouldReport(Fault(), error)) } + + @Test + fun `a wrapped FCM registration failure is a transient GMS error`() { + for (code in listOf("AUTHENTICATION_FAILED", "INTERNAL_SERVER_ERROR", "SERVICE_NOT_AVAILABLE")) { + val error = IOException("FCM Registration failed!", ExecutionException(IOException(code))) + assertTrue(ErrorUtils.isGmsTransientError(error), code) + } + } + + @Test + fun `a bare GMS error code is a transient GMS error`() { + assertTrue(ErrorUtils.isGmsTransientError(IOException("InternalServerError"))) + } + + @Test + fun `an unrelated IOException is not a transient GMS error`() { + val error = IOException("FCM Registration failed!", ExecutionException(IOException("disk full"))) + assertFalse(ErrorUtils.isGmsTransientError(error)) + assertFalse(ErrorUtils.isGmsTransientError(Fault())) + } }