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..11e4af35f 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,11 @@ 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 -> 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 +} 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) :