Skip to content
All posts
March 19, 20264 min read

Crash Reporting and Production Monitoring for Android Apps

Releasing an app without crash reporting is flying blind. Here's how to set up Firebase Crashlytics, interpret crash data, prioritize fixes, and build a monitoring workflow that catches real problems fast.

AndroidMonitoringFirebaseProduction
Share:

Your app is in the hands of users now. Devices you've never tested on, OS versions you don't own, usage patterns you didn't anticipate. Without crash reporting, you find out about problems when your rating drops.

With crash reporting, you find out in minutes.


Firebase Crashlytics: The Setup

Add Crashlytics to your Android project:

kotlin
// project-level build.gradle.kts
plugins {
    id("com.google.gms.google-services") version "4.4.1" apply false
    id("com.google.firebase.crashlytics") version "3.0.1" apply false
}

// app/build.gradle.kts
plugins {
    id("com.google.gms.google-services")
    id("com.google.firebase.crashlytics")
}

dependencies {
    implementation(platform("com.google.firebase:firebase-bom:33.0.0"))
    implementation("com.google.firebase:firebase-crashlytics")
    implementation("com.google.firebase:firebase-analytics") // Required for Crashlytics
}

Download

code
google-services.json
from Firebase Console and place it in the
code
app/
directory.

Crashlytics captures crashes automatically after this setup. No code changes needed for basic functionality.


Enriching Crash Reports

Default crash reports show the stack trace. Custom keys and logs provide context:

kotlin
// Add user context (without PII — no emails, no names)
FirebaseCrashlytics.getInstance().apply {
    setUserId(anonymizedUserId)  // Non-identifying ID
    setCustomKey("subscription_tier", user.tier)
    setCustomKey("app_state", currentScreen)
    setCustomKey("task_count", repository.getTaskCount())
}

Custom log messages before a crash:

kotlin
class TaskSyncService {
    fun syncTasks() {
        FirebaseCrashlytics.getInstance().log("Starting task sync, count: ${tasks.size}")
        
        try {
            performSync()
        } catch (e: Exception) {
            FirebaseCrashlytics.getInstance().log("Sync failed at step: $currentStep")
            throw e // Re-throw so Crashlytics captures it
        }
    }
}

These logs appear in the Crashlytics dashboard alongside the stack trace.


Non-Fatal Exceptions

Not every exception should crash the app. But they should still be tracked:

kotlin
try {
    val result = analyticsService.trackEvent(event)
    if (!result.isSuccess) {
        FirebaseCrashlytics.getInstance().recordException(
            AnalyticsException("Event tracking failed: ${result.errorCode}")
        )
    }
} catch (e: Exception) {
    // Don't crash the app for analytics failure
    FirebaseCrashlytics.getInstance().recordException(e)
    Timber.e(e, "Analytics tracking failed")
}

Non-fatal exceptions appear separately in Crashlytics. They don't affect your crash-free rate but reveal problems in code paths that don't crash.


Android Vitals: Google's View of Your Stability

Android Vitals (Play Console → Android Vitals) provides crash and ANR rates from Google's perspective. The thresholds that trigger "poor app health" badges:

  • Crash rate: > 1.09% of sessions
  • ANR rate: > 0.47% of sessions

These thresholds vary by category but these are common baselines. Exceeding them can reduce your Play Store visibility.

ANRs don't appear in Crashlytics automatically. You must enable ANR reporting:

kotlin
FirebaseCrashlytics.getInstance().apply {
    // Enabled by default in recent versions
    // To explicitly enable:
    setCrashlyticsCollectionEnabled(true)
}

Structuring Your Crash Response Process

Daily monitoring (5 min):

  • Check Crashlytics for new crash types introduced in the last 24 hours
  • Check crash-free rate trend — is it stable?
  • Check if any existing crash spike is resolving

Weekly triage:

  • Review top 10 crash types by affected user count
  • Prioritize based on: user impact × severity × reproduction rate
  • Assign crashes to the next sprint

Post-release (within 4 hours of rollout):

  • Watch crash-free rate in real time
  • Compare crash volume to previous release baseline
  • If crash rate increases > 0.5%, prepare rollback

Crash Prioritization Matrix

User ImpactSeverityAction
High (> 1% users)App unusableEmergency hotfix within hours
HighDegraded experienceNext minor release
Low (< 0.1% users)App unusableFix in next sprint
LowDegraded experienceBacklog

"High user impact" is relative to your total user count. A crash affecting 1,000 users on an app with 10,000 total users is more urgent than one affecting 1,000 on an app with 1 million.


Identifying and Fixing Crashes

For each new crash type, the investigation process:

  1. Read the stack trace — where does it point?
  2. Check the custom keys — what was the app state?
  3. Check the breadcrumb logs — what happened before the crash?
  4. Check OS version and device distribution — is it device-specific?
  5. Reproduce locally — what inputs trigger it?
kotlin
// Example crash: NullPointerException in TaskAdapter
// Stack trace shows: TaskAdapter.onBindViewHolder line 47

// Root cause found: task.dueDate was null but accessed without null check
class TaskAdapter {
    override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
        val task = tasks[position]
        holder.dueDateText.text = task.dueDate?.let { formatDate(it) } ?: "No due date" // Fixed
    }
}

// Add regression test immediately
@Test
fun `task without due date displays fallback text`() {
    val task = Task(id = "1", title = "No date", dueDate = null)
    val adapter = TaskAdapter(listOf(task))
    // ... verify "No due date" is displayed
}

Crash-Free Rate Targets

App TypeAcceptable Crash-Free Rate
Production app> 99.5%
High-quality apps> 99.7%
Mission-critical> 99.9%

A 99% crash-free rate means 1 in 100 sessions crashes. If you have 10,000 daily active users, that's 100 crashed sessions per day. Not acceptable.


Takeaways

  • Firebase Crashlytics is free and essential — there's no excuse not to have it
  • Add custom keys for app state — stack traces alone rarely tell the full story
  • Non-fatal exceptions are as important as crashes — they reveal problems before they become crashes
  • Monitor crash-free rate immediately after every release
  • The investigation process: stack trace → state → breadcrumbs → reproduce → fix → regression test
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this