Log app scope failures instead of crashing outside debug builds

This commit is contained in:
AntsyLich
2026-10-10 22:51:17 +06:00
parent a82d27c3a6
commit f9c5fa9011
4 changed files with 28 additions and 1 deletions
+19 -1
View File
@@ -4,15 +4,20 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import logcat.LogPriority
import mihon.core.metro.AppCoroutineScope
import mihon.core.metro.IsDebugBuild
import mihon.telemetry.TelemetryConfig
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.common.util.system.logcat
@BindingContainer
object AppBindings {
@@ -20,7 +25,20 @@ object AppBindings {
@Provides
@SingleIn(AppScope::class)
@AppCoroutineScope
fun providesAppCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun providesAppCoroutineScope(@IsDebugBuild isDebugBuild: Boolean): CoroutineScope {
// Work here outlives the screen that started it, so a failure would crash the app on whatever screen is open
// by then. Debug builds still crash so the bug gets noticed.
val handler = CoroutineExceptionHandler { _, throwable ->
if (isDebugBuild) {
val thread = Thread.currentThread()
thread.uncaughtExceptionHandler?.uncaughtException(thread, throwable)
} else {
logcat(LogPriority.ERROR, throwable) { "Uncaught exception in the app scope" }
TelemetryConfig.recordException(throwable)
}
}
return CoroutineScope(SupervisorJob() + Dispatchers.IO + handler)
}
@Provides
@SingleIn(AppScope::class)
@@ -9,6 +9,9 @@ import dev.zacsweers.metro.Qualifier
* [kotlinx.coroutines.Dispatchers.IO], so a failure in one child never takes the others down. Use it for work that is
* owned by an application scoped component and must outlive any screen. Work that belongs to a screen, a view model or
* another cancellable lifecycle keeps its own scope.
*
* A child that fails with an uncaught exception is logged and reported instead of crashing the app, except in debug
* builds, since by the time it fails the screen that started it may be long gone.
*/
@Qualifier
@Target(
@@ -59,6 +59,10 @@ object TelemetryConfig {
.getCertificateFingerprints()
.any { it == MIHON_CERTIFICATE_FINGERPRINT }
}
fun recordException(throwable: Throwable) {
crashlytics?.recordException(throwable)
}
}
private val MIHON_PACKAGES = hashSetOf("app.mihon", "app.mihon.debug")
@@ -9,4 +9,6 @@ object TelemetryConfig {
fun setAnalyticsEnabled(enabled: Boolean) = Unit
fun setCrashlyticsEnabled(enabled: Boolean) = Unit
fun recordException(throwable: Throwable) = Unit
}