Skip to content

Commit 3ca1e35

Browse files
committed
feat(logging): add NotifiableError marker for Slack-visible Bugsnag alerts
Unexpected (non-user-caused) errors now carry Bugsnag metadata (alert.slack_notify) that the Slack integration can filter on, giving the team visibility into backend/state failures without noise from user-caused errors like Denied or RateLimited. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 6ecbe25 commit 3ca1e35

9 files changed

Lines changed: 139 additions & 101 deletions

File tree

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,31 @@ object ErrorUtils {
6464
) {
6565
FirebaseCrashlytics.getInstance().recordException(throwable)
6666
if (Bugsnag.isStarted()) {
67-
Bugsnag.notify(throwable)
67+
Bugsnag.notify(throwable) { event ->
68+
val isNotifiable = throwable is NotifiableError
69+
|| throwableCause is NotifiableError
70+
|| throwableCause !is CodeServerError
71+
if (isNotifiable) {
72+
event.addMetadata("alert", "slack_notify", true)
73+
event.addMetadata("alert", "error_type", throwableCause.javaClass.simpleName)
74+
event.addMetadata("alert", "error_family", throwableCause.javaClass.enclosingClass?.simpleName ?: "Unknown")
75+
}
76+
true
77+
}
78+
}
79+
}
80+
}
81+
82+
fun notifyUnexpected(throwable: Throwable, context: String? = null) {
83+
if (!BuildConfig.NOTIFY_ERRORS) return
84+
Timber.e(throwable)
85+
FirebaseCrashlytics.getInstance().recordException(throwable)
86+
if (Bugsnag.isStarted()) {
87+
Bugsnag.notify(throwable) { event ->
88+
event.addMetadata("alert", "slack_notify", true)
89+
event.addMetadata("alert", "error_type", throwable.javaClass.simpleName)
90+
context?.let { event.addMetadata("alert", "context", it) }
91+
true
6892
}
6993
}
7094
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.getcode.utils
2+
3+
/**
4+
* Marker interface for errors representing unexpected failures (not user-caused).
5+
* Errors implementing this are tagged in Bugsnag with metadata that triggers Slack notifications.
6+
*/
7+
interface NotifiableError
Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,73 @@
11
package com.flipcash.services.models
22

33
import com.getcode.utils.CodeServerError
4+
import com.getcode.utils.NotifiableError
45

56
sealed class LoginError(
67
override val message: String? = null,
78
override val cause: Throwable? = null
89
) : CodeServerError(message, cause) {
9-
class InvalidTimestamp : LoginError("Invalid timestamp")
10+
class InvalidTimestamp : LoginError("Invalid timestamp"), NotifiableError
1011
class Denied : LoginError("Denied")
11-
class Unrecognized : LoginError("Unrecognized")
12-
data class Other(override val cause: Throwable? = null) : LoginError(message = cause?.message, cause = cause)
12+
class Unrecognized : LoginError("Unrecognized"), NotifiableError
13+
data class Other(override val cause: Throwable? = null) : LoginError(message = cause?.message, cause = cause), NotifiableError
1314
}
1415

1516
sealed class RegisterError(
1617
override val message: String? = null,
1718
override val cause: Throwable? = null
1819
) : CodeServerError(message, cause) {
19-
class InvalidSignature : RegisterError("Invalid signature")
20+
class InvalidSignature : RegisterError("Invalid signature"), NotifiableError
2021
class Denied: RegisterError("Denied")
21-
class Unrecognized : RegisterError("Unrecognized")
22-
data class Other(override val cause: Throwable? = null) : RegisterError(message = cause?.message, cause = cause)
22+
class Unrecognized : RegisterError("Unrecognized"), NotifiableError
23+
data class Other(override val cause: Throwable? = null) : RegisterError(message = cause?.message, cause = cause), NotifiableError
2324
}
2425

2526
sealed class GetUserFlagsError(
2627
override val message: String? = null,
2728
override val cause: Throwable? = null
2829
) : CodeServerError(message, cause) {
29-
class Unrecognized : GetUserFlagsError("Unrecognized")
30+
class Unrecognized : GetUserFlagsError("Unrecognized"), NotifiableError
3031
class Denied : GetUserFlagsError("Denied")
31-
data class Other(override val cause: Throwable? = null) : GetUserFlagsError(message = cause?.message, cause = cause)
32+
data class Other(override val cause: Throwable? = null) : GetUserFlagsError(message = cause?.message, cause = cause), NotifiableError
3233
}
3334

3435
sealed class PurchaseAckError(
3536
override val message: String? = null,
3637
override val cause: Throwable? = null
3738
) : CodeServerError(message, cause) {
38-
class Unrecognized : PurchaseAckError("Unrecognized")
39+
class Unrecognized : PurchaseAckError("Unrecognized"), NotifiableError
3940
class Denied : PurchaseAckError("Denied")
4041
class InvalidReceipt: PurchaseAckError("Invalid receipt")
4142
class InvalidMetadata: PurchaseAckError("Invalid metadata")
42-
data class Other(override val cause: Throwable? = null) : PurchaseAckError(message = cause?.message, cause = cause)
43+
data class Other(override val cause: Throwable? = null) : PurchaseAckError(message = cause?.message, cause = cause), NotifiableError
4344
}
4445

4546
sealed class AddTokenError(
4647
override val message: String? = null,
4748
override val cause: Throwable? = null
4849
) : CodeServerError(message, cause) {
4950
class InvalidPushToken : AddTokenError("Invalid push token")
50-
class Unrecognized : AddTokenError("Unrecognized")
51-
data class Other(override val cause: Throwable? = null) : AddTokenError(message = cause?.message, cause = cause)
51+
class Unrecognized : AddTokenError("Unrecognized"), NotifiableError
52+
data class Other(override val cause: Throwable? = null) : AddTokenError(message = cause?.message, cause = cause), NotifiableError
5253
}
5354

5455
sealed class DeleteTokenError(
5556
override val message: String? = null,
5657
override val cause: Throwable? = null
5758
) : CodeServerError(message, cause) {
58-
class Unrecognized : DeleteTokenError("Unrecognized")
59-
data class Other(override val cause: Throwable? = null) : DeleteTokenError(message = cause?.message, cause = cause)
59+
class Unrecognized : DeleteTokenError("Unrecognized"), NotifiableError
60+
data class Other(override val cause: Throwable? = null) : DeleteTokenError(message = cause?.message, cause = cause), NotifiableError
6061
}
6162

6263
sealed class GetActivityFeedMessagesError(
6364
override val message: String? = null,
6465
override val cause: Throwable? = null
6566
) : CodeServerError(message, cause) {
6667
class Denied : GetActivityFeedMessagesError("Denied")
67-
class Unrecognized : GetActivityFeedMessagesError("Unrecognized")
68+
class Unrecognized : GetActivityFeedMessagesError("Unrecognized"), NotifiableError
6869
class NotFound: GetActivityFeedMessagesError("Not found")
69-
data class Other(override val cause: Throwable? = null) : GetActivityFeedMessagesError(message = cause?.message, cause = cause)
70+
data class Other(override val cause: Throwable? = null) : GetActivityFeedMessagesError(message = cause?.message, cause = cause), NotifiableError
7071
}
7172

7273
sealed class CreatePoolError(
@@ -76,25 +77,25 @@ sealed class CreatePoolError(
7677
class RendezvousExists: CreatePoolError("Rendezvous exists")
7778
class FundingDestinationExists: CreatePoolError("Funding destination exists")
7879
class Denied: CreatePoolError("Denied")
79-
class Unrecognized : CreatePoolError("Unrecognized")
80-
data class Other(override val cause: Throwable? = null) : CreatePoolError(message = cause?.message, cause = cause)
80+
class Unrecognized : CreatePoolError("Unrecognized"), NotifiableError
81+
data class Other(override val cause: Throwable? = null) : CreatePoolError(message = cause?.message, cause = cause), NotifiableError
8182
}
8283

8384
sealed class GetPoolError(
8485
override val message: String? = null,
8586
override val cause: Throwable? = null
8687
): CodeServerError(message, cause) {
8788
class NotFound: GetPoolError("Not found")
88-
class Unrecognized : GetPoolError("Unrecognized")
89-
data class Other(override val cause: Throwable? = null) : GetPoolError(message = cause?.message, cause = cause)
89+
class Unrecognized : GetPoolError("Unrecognized"), NotifiableError
90+
data class Other(override val cause: Throwable? = null) : GetPoolError(message = cause?.message, cause = cause), NotifiableError
9091
}
9192

9293
sealed class GetPoolPageError(
9394
override val message: String? = null,
9495
override val cause: Throwable? = null
9596
): CodeServerError(message, cause) {
9697
class NotFound: GetPoolPageError("Not found")
97-
data class Other(override val cause: Throwable? = null) : GetPoolPageError(message = cause?.message, cause = cause)
98+
data class Other(override val cause: Throwable? = null) : GetPoolPageError(message = cause?.message, cause = cause), NotifiableError
9899
}
99100

100101
sealed class PlacePoolBetError(
@@ -106,8 +107,8 @@ sealed class PlacePoolBetError(
106107
class BetAlreadyMade: PlacePoolBetError("Bet already made")
107108
class MaxBetsReceived: PlacePoolBetError("Max bets received")
108109
class BetOutcomeSolidified: PlacePoolBetError("Bet outcome solidified")
109-
class Unrecognized : PlacePoolBetError("Unrecognized")
110-
data class Other(override val cause: Throwable? = null) : PlacePoolBetError(message = cause?.message, cause = cause)
110+
class Unrecognized : PlacePoolBetError("Unrecognized"), NotifiableError
111+
data class Other(override val cause: Throwable? = null) : PlacePoolBetError(message = cause?.message, cause = cause), NotifiableError
111112
}
112113

113114
sealed class ResolvePoolOutcomeError(
@@ -118,8 +119,8 @@ sealed class ResolvePoolOutcomeError(
118119
class Denied: ResolvePoolOutcomeError("Denied")
119120
class PoolOpen: ResolvePoolOutcomeError("Pool still open")
120121
class AlreadyDeclared: ResolvePoolOutcomeError("Different outcome already declared")
121-
class Unrecognized : ResolvePoolOutcomeError("Unrecognized")
122-
data class Other(override val cause: Throwable? = null) : ResolvePoolOutcomeError(message = cause?.message, cause = cause)
122+
class Unrecognized : ResolvePoolOutcomeError("Unrecognized"), NotifiableError
123+
data class Other(override val cause: Throwable? = null) : ResolvePoolOutcomeError(message = cause?.message, cause = cause), NotifiableError
123124
}
124125

125126
sealed class ClosePoolError(
@@ -128,8 +129,8 @@ sealed class ClosePoolError(
128129
): CodeServerError(message, cause) {
129130
class NotFound: ClosePoolError("Not found")
130131
class Denied: ClosePoolError("Denied")
131-
class Unrecognized : ClosePoolError("Unrecognized")
132-
data class Other(override val cause: Throwable? = null) : ClosePoolError(message = cause?.message, cause = cause)
132+
class Unrecognized : ClosePoolError("Unrecognized"), NotifiableError
133+
data class Other(override val cause: Throwable? = null) : ClosePoolError(message = cause?.message, cause = cause), NotifiableError
133134
}
134135

135136
sealed class GetJwtError(
@@ -141,8 +142,8 @@ sealed class GetJwtError(
141142
class InvalidApiKey: GetJwtError("Invalid api key")
142143
class PhoneVerificationRequired: GetJwtError("Phone verification required")
143144
class EmailVerificationRequired: GetJwtError("Email verification required")
144-
class Unrecognized : GetJwtError("Unrecognized")
145-
data class Other(override val cause: Throwable? = null) : GetJwtError(message = cause?.message, cause = cause)
145+
class Unrecognized : GetJwtError("Unrecognized"), NotifiableError
146+
data class Other(override val cause: Throwable? = null) : GetJwtError(message = cause?.message, cause = cause), NotifiableError
146147
}
147148

148149
sealed class EmailVerificationError(
@@ -154,8 +155,8 @@ sealed class EmailVerificationError(
154155
class InvalidEmailAddress: EmailVerificationError("Invalid email address")
155156
class InvalidVerificationCode: EmailVerificationError("Invalid verification code")
156157
class NoVerification: EmailVerificationError("No verification")
157-
class Unrecognized : EmailVerificationError("Unrecognized")
158-
data class Other(override val cause: Throwable? = null) : EmailVerificationError(message = cause?.message, cause = cause)
158+
class Unrecognized : EmailVerificationError("Unrecognized"), NotifiableError
159+
data class Other(override val cause: Throwable? = null) : EmailVerificationError(message = cause?.message, cause = cause), NotifiableError
159160
}
160161

161162

@@ -169,17 +170,17 @@ sealed class PhoneVerificationError(
169170
class UnsupportedPhoneType: PhoneVerificationError("Unsupported phone type")
170171
class InvalidVerificationCode: PhoneVerificationError("Invalid verification code")
171172
class NoVerification: PhoneVerificationError("No verification")
172-
class Unrecognized : PhoneVerificationError("Unrecognized")
173-
data class Other(override val cause: Throwable? = null) : PhoneVerificationError(message = cause?.message, cause = cause)
173+
class Unrecognized : PhoneVerificationError("Unrecognized"), NotifiableError
174+
data class Other(override val cause: Throwable? = null) : PhoneVerificationError(message = cause?.message, cause = cause), NotifiableError
174175
}
175176

176177
sealed class GetUserProfileError(
177178
override val message: String? = null,
178179
override val cause: Throwable? = null
179180
): CodeServerError(message, cause) {
180181
class NotFound: GetUserProfileError("Not found")
181-
class Unrecognized : GetUserProfileError("Unrecognized")
182-
data class Other(override val cause: Throwable? = null) : GetUserProfileError(message = cause?.message, cause = cause)
182+
class Unrecognized : GetUserProfileError("Unrecognized"), NotifiableError
183+
data class Other(override val cause: Throwable? = null) : GetUserProfileError(message = cause?.message, cause = cause), NotifiableError
183184
}
184185

185186
sealed class SetDisplayNameError(
@@ -188,8 +189,8 @@ sealed class SetDisplayNameError(
188189
): CodeServerError(message, cause) {
189190
class InvalidDisplayName: SetDisplayNameError("Invalid display name")
190191
class Denied: SetDisplayNameError("Denied")
191-
class Unrecognized : SetDisplayNameError("Unrecognized")
192-
data class Other(override val cause: Throwable? = null) : SetDisplayNameError(message = cause?.message, cause = cause)
192+
class Unrecognized : SetDisplayNameError("Unrecognized"), NotifiableError
193+
data class Other(override val cause: Throwable? = null) : SetDisplayNameError(message = cause?.message, cause = cause), NotifiableError
193194
}
194195

195196
sealed class LinkSocialAccountError(
@@ -199,27 +200,27 @@ sealed class LinkSocialAccountError(
199200
class InvalidLinkingToken: LinkSocialAccountError("Invalid linking token")
200201
class ExistingLink: LinkSocialAccountError("Existing link")
201202
class Denied: LinkSocialAccountError("Denied")
202-
class Unrecognized : LinkSocialAccountError("Unrecognized")
203-
data class Other(override val cause: Throwable? = null) : LinkSocialAccountError(message = cause?.message, cause = cause)
203+
class Unrecognized : LinkSocialAccountError("Unrecognized"), NotifiableError
204+
data class Other(override val cause: Throwable? = null) : LinkSocialAccountError(message = cause?.message, cause = cause), NotifiableError
204205
}
205206

206207
sealed class UnlinkSocialAccountError(
207208
override val message: String? = null,
208209
override val cause: Throwable? = null
209210
): CodeServerError(message, cause) {
210211
class Denied: UnlinkSocialAccountError("Denied")
211-
class Unrecognized : UnlinkSocialAccountError("Unrecognized")
212-
data class Other(override val cause: Throwable? = null) : UnlinkSocialAccountError(message = cause?.message, cause = cause)
212+
class Unrecognized : UnlinkSocialAccountError("Unrecognized"), NotifiableError
213+
data class Other(override val cause: Throwable? = null) : UnlinkSocialAccountError(message = cause?.message, cause = cause), NotifiableError
213214
}
214215

215216
sealed class UpdateSettingsError(
216217
override val message: String? = null,
217218
override val cause: Throwable? = null
218219
): CodeServerError(message, cause) {
219220
class Denied : UpdateSettingsError("Denied")
220-
class Unrecognized : UpdateSettingsError("Unrecognized")
221+
class Unrecognized : UpdateSettingsError("Unrecognized"), NotifiableError
221222
class InvalidLocale : UpdateSettingsError("Invalid locale")
222223
class InvalidRegion : UpdateSettingsError("Invalid region")
223-
data class Other(override val cause: Throwable? = null) : UpdateSettingsError(message = cause?.message, cause = cause)
224+
data class Other(override val cause: Throwable? = null) : UpdateSettingsError(message = cause?.message, cause = cause), NotifiableError
224225

225226
}

services/opencode/src/main/kotlin/com/getcode/opencode/controllers/TransactionController.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.getcode.opencode.internal.solana.model.SwapId
1212
import com.getcode.opencode.model.accounts.AccountCluster
1313
import com.getcode.opencode.model.accounts.GiftCardAccount
1414
import com.getcode.opencode.model.core.errors.GetIntentMetadataError
15+
import com.getcode.opencode.model.core.errors.GetLimitsError
1516
import com.getcode.opencode.model.core.errors.SwapError
1617
import com.getcode.opencode.model.financial.Distribution
1718
import com.getcode.opencode.model.financial.Fee

services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GiveBillTransactor.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.getcode.opencode.utils.nonce
1818
import com.getcode.solana.keys.Mint
1919
import com.getcode.solana.keys.PublicKey
2020
import com.getcode.utils.CodeServerError
21+
import com.getcode.utils.NotifiableError
2122
import com.getcode.utils.TraceType
2223
import com.getcode.utils.trace
2324
import kotlinx.coroutines.CoroutineScope
@@ -257,12 +258,12 @@ internal class GiveBillTransactor(
257258
override val message: String? = null,
258259
override val cause: Throwable? = null
259260
) : CodeServerError(message, cause) {
260-
class DuplicateTransferException : GiveTransactorError(message = "Duplicate Transfer")
261-
class DestinationSignatureInvalidException : GiveTransactorError(message = "Destination signature invalid")
262-
class ExchangeRateExpiredException : GiveTransactorError(message = "Exchange rate expired")
261+
class DuplicateTransferException : GiveTransactorError(message = "Duplicate Transfer"), NotifiableError
262+
class DestinationSignatureInvalidException : GiveTransactorError(message = "Destination signature invalid"), NotifiableError
263+
class ExchangeRateExpiredException : GiveTransactorError(message = "Exchange rate expired"), NotifiableError
263264
data class Other(
264265
override val message: String? = null,
265266
override val cause: Throwable? = null
266-
) : GiveTransactorError(message, cause)
267+
) : GiveTransactorError(message, cause), NotifiableError
267268
}
268269
}

services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/GrabBillTransactor.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.getcode.opencode.model.transactions.TransactionMetadata
1212
import com.getcode.opencode.providers.TokenMetadataProvider
1313
import com.getcode.utils.CodeServerError
1414
import com.getcode.utils.ErrorUtils
15+
import com.getcode.utils.NotifiableError
1516
import kotlinx.coroutines.CoroutineScope
1617
import kotlinx.coroutines.cancel
1718

@@ -154,5 +155,5 @@ sealed class GrabTransactorError(
154155
data class Other(
155156
override val message: String? = null,
156157
override val cause: Throwable? = null
157-
) : GrabTransactorError(message, cause)
158+
) : GrabTransactorError(message, cause), NotifiableError
158159
}

services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactor.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.getcode.opencode.model.financial.Token
1616
import com.getcode.opencode.providers.TokenMetadataProvider
1717
import com.getcode.utils.CodeServerError
1818
import com.getcode.utils.ErrorUtils
19+
import com.getcode.utils.NotifiableError
1920
import com.getcode.utils.timedTraceSuspend
2021

2122
internal class ReceiveGiftCardTransactor(
@@ -171,13 +172,13 @@ sealed class ReceiveGiftTransactorError(
171172
class FailedToQuery(
172173
override val message: String? = null,
173174
override val cause: Throwable? = null
174-
) : GrabTransactorError(message = message?.let { "Failed to query account - $it" } ?: "Failed to query account")
175+
) : GrabTransactorError(message = message?.let { "Failed to query account - $it" } ?: "Failed to query account"), NotifiableError
175176
class AlreadyClaimed : GrabTransactorError(message = "Already claimed")
176177
class UsersGiftCard : GrabTransactorError(message = "User is gift card issuer")
177178
class Expired : GrabTransactorError(message = "Expired")
178179

179180
data class Other(
180181
override val message: String? = null,
181182
override val cause: Throwable? = null
182-
) : GrabTransactorError(message, cause)
183+
) : GrabTransactorError(message, cause), NotifiableError
183184
}

0 commit comments

Comments
 (0)