Created
April 10, 2023 00:42
-
-
Save j-roskopf/3e150e7734c906be5cff9bf1b29ebd46 to your computer and use it in GitHub Desktop.
Better Stack Traces with Coroutines
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.lifecycle.ViewModel | |
import androidx.lifecycle.viewModelScope | |
import com.joetr.coroutines.CoroutineScopeExtensions.launchWithBreadcrumbs | |
import com.joetr.coroutines.CoroutinesBreadcrumbExceptionHandler.createBreadcrumbsExceptionHandler | |
import com.joetr.coroutines.FlowExtensions.stateInWithBreadcrumbs | |
import kotlinx.coroutines.* | |
import kotlinx.coroutines.flow.* | |
import java.util.* | |
import kotlin.coroutines.CoroutineContext | |
import kotlin.coroutines.EmptyCoroutineContext | |
class MainViewModel : ViewModel() { | |
val state = flow<String> { | |
throw IllegalArgumentException("oh no") | |
}.stateInWithBreadcrumbs( | |
scope = viewModelScope, | |
started = SharingStarted.Eagerly, | |
initialValue = "", | |
) | |
fun testException() { | |
viewModelScope.launchWithBreadcrumbs { | |
throwException1() | |
} | |
} | |
private suspend fun throwException1() { | |
delay(300) | |
throwException2() | |
} | |
private suspend fun throwException2() { | |
delay(300) | |
throwException3() | |
} | |
private suspend fun throwException3() { | |
delay(300) | |
throw IllegalArgumentException("oh no") | |
} | |
} | |
internal object CoroutinesBreadcrumbExceptionHandler { | |
private fun createBreadcrumbsException() = | |
BreadcrumbException() | |
.apply { | |
val queue: Queue<StackTraceElement> = LinkedList(stackTrace.asList()) | |
val iterator = queue.iterator() | |
while (iterator.hasNext()) { | |
val next = iterator.next() | |
// we need to remove all of the 'helper' classes from the stack trace | |
// as that really just adds noise and isn't overly helpful | |
if (next.className == CoroutinesBreadcrumbExceptionHandler::class.qualifiedName || | |
next.className == FlowExtensions::class.qualifiedName || | |
next.className == CoroutineScopeExtensions::class.qualifiedName | |
) { | |
iterator.remove() | |
} else { | |
break | |
} | |
} | |
stackTrace = queue.toTypedArray() | |
} | |
private fun Throwable.addBreadcrumbs( | |
breadcrumbsException: BreadcrumbException, | |
): Throwable { | |
var lastCause = cause | |
while (lastCause?.cause != null) { | |
lastCause = lastCause.cause | |
} | |
return lastCause?.initCause(breadcrumbsException) ?: initCause(breadcrumbsException) | |
} | |
internal fun createBreadcrumbsExceptionHandler(): CoroutineExceptionHandler { | |
val breadcrumbsException = createBreadcrumbsException() | |
return CoroutineExceptionHandler { _, throwable -> | |
val currentThread = Thread.currentThread() | |
run { | |
currentThread.uncaughtExceptionHandler ?: Thread.getDefaultUncaughtExceptionHandler() | |
}.uncaughtException( | |
currentThread, | |
throwable.addBreadcrumbs(breadcrumbsException), | |
) | |
} | |
} | |
} | |
internal class BreadcrumbException : RuntimeException("More details about the original stack trace:") | |
object CoroutineScopeExtensions { | |
fun CoroutineScope.launchWithBreadcrumbs( | |
context: CoroutineContext = EmptyCoroutineContext, | |
start: CoroutineStart = CoroutineStart.DEFAULT, | |
block: suspend CoroutineScope.() -> Unit, | |
): Job { | |
return this.launch( | |
context = context + createBreadcrumbsExceptionHandler(), | |
start = start, | |
block = block, | |
) | |
} | |
} | |
object FlowExtensions { | |
fun <T> Flow<T>.stateInWithBreadcrumbs( | |
scope: CoroutineScope, | |
started: SharingStarted, | |
initialValue: T, | |
): StateFlow<T> { | |
return stateIn( | |
scope = scope + createBreadcrumbsExceptionHandler(), | |
started = started, | |
initialValue = initialValue, | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment