Crashes and ANRs (Application Not Responding) are the clearest signals of app quality. Understanding how to read crash reports and identify ANR root causes lets you fix the right things first.
Crash Reporting with Firebase Crashlytics
Setup
// build.gradle.kts (app)
plugins {
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
}
// dependencies
implementation("com.google.firebase:firebase-crashlytics-ktx")
Enriching Crash Reports
class AppCrashReporter @Inject constructor(
private val crashlytics: FirebaseCrashlytics
) {
fun setUser(userId: String) {
crashlytics.setUserId(userId)
}
fun setContext(key: String, value: String) {
crashlytics.setCustomKey(key, value)
}
fun logBreadcrumb(message: String) {
crashlytics.log(message) // appears in crash report's log section
}
fun recordNonFatal(throwable: Throwable) {
crashlytics.recordException(throwable)
}
}
// In ViewModel
fun loadArticle(id: String) = viewModelScope.launch {
reporter.setContext("article_id", id)
reporter.logBreadcrumb("Loading article $id")
try {
val article = repository.getArticle(id)
reporter.logBreadcrumb("Article loaded: ${article.title}")
_state.value = UiState.Success(article)
} catch (e: Exception) {
reporter.recordNonFatal(e)
_state.value = UiState.Error(e.message ?: "Unknown error")
}
}
Crash-Free Rate Target
Crash-free sessions: > 99.5% (industry standard for high-quality apps)
Crash-free users: > 99.9%
Alert threshold: crash rate > 0.1% → page on-call
Reading a Crash Report
A Crashlytics report contains:
- Exception class + message — the actual error
- Stack trace — where in the code it happened
- Custom keys — your context (user_id, screen, etc.)
- Logs (breadcrumbs) — what the user was doing before the crash
- Device info — OS version, RAM, storage, device model
- Session info — time since app start, foreground/background
ANR Analysis
ANRs happen when the main thread is blocked for > 5 seconds (input dispatch) or > 10 seconds (broadcast receivers).
Common ANR Causes
// BAD: Network call on main thread
fun loadUser(id: String): User {
return api.getUser(id) // BLOCKS main thread → ANR
}
// BAD: Long lock on main thread
val lock = ReentrantLock()
fun onClickButton() {
lock.lock() // if lock held by background thread → deadlock → ANR
try { /* ... */ }
finally { lock.unlock() }
}
// BAD: SharedPreferences.apply() called synchronarily
override fun onStop() {
prefs.edit().putString("key", "value").commit() // commit() = sync disk I/O
}
// GOOD: Use coroutines to move work off main thread
fun loadUser(id: String) = viewModelScope.launch {
val user = withContext(Dispatchers.IO) { api.getUser(id) }
_state.value = UiState.Success(user)
}
Reading the ANR Trace
Play Console → Android Vitals → ANRs shows the main thread stack trace at the time of ANR:
// Typical lock contention ANR:
at java.lang.Object.wait(Native Method)
at java.lang.Thread.parkFor$(Thread.java:1235)
at sun.misc.Unsafe.park(Unsafe.java:299)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:158)
at com.myapp.ArticleCache.get(ArticleCache.kt:42) ← your code
The key line is the first one from your package: com.myapp.ArticleCache.get holding a lock while the main thread waits.
Android Vitals Thresholds
| Metric | Bad | Good |
|---|---|---|
| Crash rate | > 1.09% | < 0.5% |
| ANR rate | > 0.47% | < 0.2% |
| Slow renders | > 50% frames slow | < 20% |
| Frozen renders | > 0.1% frames frozen | < 0.05% |
| Startup time | > 5s cold | < 2s cold |
Custom Traces for Non-Crash Errors
class ArticleApiMonitor(private val crashlytics: FirebaseCrashlytics) {
suspend fun fetchWithMonitoring(id: String): Article {
return try {
api.getArticle(id)
} catch (e: HttpException) {
// Non-fatal: 404 is expected; 5xx is unexpected
if (e.code() >= 500) {
crashlytics.setCustomKey("http_status", e.code())
crashlytics.recordException(e)
}
throw e
}
}
}
Key Takeaways
| Signal | What to watch |
|---|---|
| Crash-free rate | Target > 99.5%; alert if drops below 99% |
| Custom keys | Always set user_id, screen_name, last_action before sensitive operations |
| Breadcrumbs | Log user actions; crucial for reproducing rare crashes |
| ANR root cause | Main thread lock contention; sync I/O; blocking coroutine |
| Non-fatals | Track expected errors (timeouts, 4xx) separately from bugs |
| Android Vitals | Google uses these to surface your app in Play Store; below threshold = featured penalty |