Skip to content

Commit 8fd4040

Browse files
bmc08gtclaude
andcommitted
fix(chat): stop reporting an empty transcript to Bugsnag
Opening a chat nobody has written in yet returns GetMessages NOT_FOUND, which InternalChatMessagingRepository hands to ErrorUtils.handleError. Every reporter sees it, so a normal empty conversation opens a Bugsnag error group — 72 events from one user on 2026.8.5. The notifiability tiers had no way to say "expected, don't report": a plain CodeServerError is still sent, just downgraded to INFO. Add UnreportedError as a third tier below that. Errors marked with it are still logged, so they stay in the trace attached to a real report, but ErrorUtils does not hand them to the reporters. GetMessagesError.NotFound is the first user; the sibling NotFound results are candidates but are left alone here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent fb91d09 commit 8fd4040

5 files changed

Lines changed: 74 additions & 6 deletions

File tree

‎libs/logging/src/main/kotlin/com/getcode/utils/ErrorUtils.kt‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,7 @@ object ErrorUtils {
6161
)
6262
}
6363

64-
if (
65-
BuildConfig.NOTIFY_ERRORS &&
66-
ignoredErrors.none { it.isInstance(throwable) } &&
67-
ignoredErrors.none { it.isInstance(throwableCause) }
68-
) {
64+
if (BuildConfig.NOTIFY_ERRORS && shouldReport(throwable, throwableCause)) {
6965
val isNotifiable = when {
7066
throwable is ConditionallyNotifiable -> throwable.isNotifiable
7167
throwableCause is ConditionallyNotifiable -> throwableCause.isNotifiable
@@ -76,6 +72,17 @@ object ErrorUtils {
7672
}
7773
}
7874

75+
/**
76+
* Whether [throwable] is worth handing to the reporters at all. [UnreportedError] marks an
77+
* expected server result the caller already handles, so it stays in the log without opening a
78+
* Bugsnag error group.
79+
*/
80+
internal fun shouldReport(throwable: Throwable, cause: Throwable): Boolean =
81+
throwable !is UnreportedError &&
82+
cause !is UnreportedError &&
83+
ignoredErrors.none { it.isInstance(throwable) } &&
84+
ignoredErrors.none { it.isInstance(cause) }
85+
7986
private fun isNetworkError(throwable: Throwable): Boolean =
8087
throwable is TimeoutException ||
8188
throwable.cause is TimeoutException ||
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.getcode.utils
2+
3+
/**
4+
* Marker interface for errors that are an expected outcome rather than a fault — a server result
5+
* the caller already handles and renders. [ErrorUtils] still logs these, so they stay in the trace
6+
* attached to a real report, but it does not hand them to the reporters and they never open a
7+
* Bugsnag error group.
8+
*
9+
* This is a third tier below [NotifiableError] (reported, WARNING, drives the Slack filter) and a
10+
* plain [CodeServerError] (reported, INFO, recorded for reference).
11+
*/
12+
interface UnreportedError
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.getcode.utils
2+
3+
import java.net.UnknownHostException
4+
import kotlin.test.Test
5+
import kotlin.test.assertFalse
6+
import kotlin.test.assertTrue
7+
8+
class ErrorUtilsTest {
9+
10+
private class Benign : Throwable("benign"), UnreportedError
11+
12+
private class Fault : Throwable("fault")
13+
14+
@Test
15+
fun `a plain throwable is reported`() {
16+
val error = Fault()
17+
assertTrue(ErrorUtils.shouldReport(error, error))
18+
}
19+
20+
@Test
21+
fun `an UnreportedError is not reported`() {
22+
val error = Benign()
23+
assertFalse(ErrorUtils.shouldReport(error, error))
24+
}
25+
26+
@Test
27+
fun `an UnreportedError in the cause position is not reported`() {
28+
assertFalse(ErrorUtils.shouldReport(Fault(), Benign()))
29+
}
30+
31+
@Test
32+
fun `ignored error types stay ignored`() {
33+
val error = UnknownHostException("no dns")
34+
assertFalse(ErrorUtils.shouldReport(error, error))
35+
assertFalse(ErrorUtils.shouldReport(Fault(), error))
36+
}
37+
}

‎services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.flipcash.services.models.chat.BlobRejection
44
import com.getcode.solana.keys.Checksum
55
import com.getcode.utils.CodeServerError
66
import com.getcode.utils.NotifiableError
7+
import com.getcode.utils.UnreportedError
78

89
sealed class LoginError(
910
override val message: String? = null,
@@ -374,7 +375,12 @@ sealed class GetMessagesError(
374375
override val cause: Throwable? = null
375376
): CodeServerError(message, cause) {
376377
class Denied : GetMessagesError("Denied")
377-
class NotFound : GetMessagesError("Not found")
378+
379+
/**
380+
* The chat has no messages to return. Opening a conversation nobody has written in yet is a
381+
* normal outcome, not a fault, so callers render an empty transcript and it is not reported.
382+
*/
383+
class NotFound : GetMessagesError("Not found"), UnreportedError
378384
class Unrecognized : GetMessagesError("Unrecognized"), NotifiableError
379385
data class Other(override val cause: Throwable? = null) : GetMessagesError(message = cause?.message, cause = cause), NotifiableError
380386
}

‎services/flipcash/src/test/kotlin/com/flipcash/services/models/ErrorsTest.kt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.flipcash.services.models
22

33
import com.getcode.utils.CodeServerError
4+
import com.getcode.utils.UnreportedError
45
import kotlin.test.Test
56
import kotlin.test.assertEquals
67
import kotlin.test.assertIs
@@ -147,6 +148,11 @@ class ErrorsTest {
147148
assertIs<CodeServerError>(GetMessagesError.Other())
148149
}
149150

151+
@Test
152+
fun `GetMessagesError NotFound is not reported`() {
153+
assertIs<UnreportedError>(GetMessagesError.NotFound())
154+
}
155+
150156
// -- SendMessageError --
151157

152158
@Test

0 commit comments

Comments
 (0)