From d8ce9580b1d30a09bc9053621d13d77f2817396f Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 11 May 2026 10:09:24 -0400 Subject: [PATCH 1/2] fix(bugsnag): suppress expected StaleState errors from reporting Known stale state reasons (gift card claimed/expired, pool distributed, race condition, intent already exists) are expected and no longer sent to Bugsnag. Unknown stale state reasons remain notifiable. Adds ConditionallyNotifiable interface for errors where notifiability depends on runtime state. Signed-off-by: Brandon McAnsh --- .../getcode/utils/ConditionallyNotifiable.kt | 9 +++++++++ .../kotlin/com/getcode/utils/ErrorUtils.kt | 10 +++++++--- .../opencode/model/core/errors/Errors.kt | 19 ++++++++++++++++++- 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 libs/logging/src/main/kotlin/com/getcode/utils/ConditionallyNotifiable.kt diff --git a/libs/logging/src/main/kotlin/com/getcode/utils/ConditionallyNotifiable.kt b/libs/logging/src/main/kotlin/com/getcode/utils/ConditionallyNotifiable.kt new file mode 100644 index 000000000..6c80f79a3 --- /dev/null +++ b/libs/logging/src/main/kotlin/com/getcode/utils/ConditionallyNotifiable.kt @@ -0,0 +1,9 @@ +package com.getcode.utils + +/** + * Interface for errors where notifiability depends on runtime state. + * [ErrorUtils] checks [isNotifiable] to decide whether to report to Bugsnag. + */ +interface ConditionallyNotifiable { + val isNotifiable: Boolean +} 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 86bb2c0a8..6b937781a 100644 --- a/libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt +++ b/libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt @@ -64,9 +64,13 @@ object ErrorUtils { ignoredErrors.none { it.isInstance(throwable) } && ignoredErrors.none { it.isInstance(throwableCause) } ) { - val isNotifiable = throwable is NotifiableError - || throwableCause is NotifiableError - || throwableCause !is CodeServerError + val isNotifiable = when { + throwable is ConditionallyNotifiable -> throwable.isNotifiable + throwableCause is ConditionallyNotifiable -> throwableCause.isNotifiable + else -> throwable is NotifiableError + || throwableCause is NotifiableError + || throwableCause !is CodeServerError + } reporters.forEach { it.report(throwable, throwableCause, isNotifiable) } } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt index 9aca5a08a..c355e312b 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/core/errors/Errors.kt @@ -9,6 +9,7 @@ import com.getcode.opencode.model.core.errors.SubmitIntentError.Signature import com.getcode.opencode.model.core.errors.SubmitIntentError.StaleState import com.getcode.opencode.model.core.errors.SubmitIntentError.Unrecognized import com.getcode.utils.CodeServerError +import com.getcode.utils.ConditionallyNotifiable import com.getcode.utils.NotifiableError sealed class CodeAccountCheckError( @@ -122,11 +123,27 @@ sealed class SubmitIntentError( if (details.isNotEmpty()) append(": ${details.joinToString()}") }), NotifiableError data class StaleState(private val reasons: List) : - SubmitIntentError(message = reasons.joinToString()), NotifiableError { + SubmitIntentError(message = reasons.joinToString()), ConditionallyNotifiable { val isRaceCondition: Boolean get() = reasons.any { it.startsWith("race detected:") } val isGiftCardAlreadyClaimed: Boolean get() = reasons.any { it.contains("gift card balance has already been claimed") } + val isGiftCardExpired: Boolean + get() = reasons.any { it.contains("gift card is expired") } + val isPoolAlreadyDistributed: Boolean + get() = reasons.any { it.contains("pool balance has already been distributed") } + val isIntentAlreadyExists: Boolean + get() = reasons.any { it.contains("intent already exists") } + + val isExpected: Boolean + get() = isRaceCondition + || isGiftCardAlreadyClaimed + || isGiftCardExpired + || isPoolAlreadyDistributed + || isIntentAlreadyExists + + override val isNotifiable: Boolean + get() = !isExpected } data class Denied(private val reasons: List) : From d1c359161b9e3adae6635d704aecb779c0b49768 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 11 May 2026 10:10:44 -0400 Subject: [PATCH 2/2] refactor: make NotifiableError extend ConditionallyNotifiable Simplifies ErrorUtils by unifying the notifiability check into a single ConditionallyNotifiable interface. --- libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt | 4 +--- .../src/main/kotlin/com/getcode/utils/NotifiableError.kt | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) 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 6b937781a..11e4af35f 100644 --- a/libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt +++ b/libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt @@ -67,9 +67,7 @@ object ErrorUtils { val isNotifiable = when { throwable is ConditionallyNotifiable -> throwable.isNotifiable throwableCause is ConditionallyNotifiable -> throwableCause.isNotifiable - else -> throwable is NotifiableError - || throwableCause is NotifiableError - || throwableCause !is CodeServerError + else -> throwableCause !is CodeServerError } reporters.forEach { it.report(throwable, throwableCause, isNotifiable) } diff --git a/libs/logging/src/main/kotlin/com/getcode/utils/NotifiableError.kt b/libs/logging/src/main/kotlin/com/getcode/utils/NotifiableError.kt index 577a7c7ff..db9e74538 100644 --- a/libs/logging/src/main/kotlin/com/getcode/utils/NotifiableError.kt +++ b/libs/logging/src/main/kotlin/com/getcode/utils/NotifiableError.kt @@ -4,4 +4,6 @@ package com.getcode.utils * Marker interface for errors representing unexpected failures (not user-caused). * Errors implementing this are tagged in Bugsnag with metadata that triggers Slack notifications. */ -interface NotifiableError +interface NotifiableError : ConditionallyNotifiable { + override val isNotifiable: Boolean get() = true +}