Firebase Crashlytics: Mobile App Crash Reporting and QA Testing Guide
Mobile apps crash in ways that web applications don't. The device environment is fragmented — hundreds of device models, multiple OS versions, varied memory configurations, inconsistent network conditions. A crash that affects Samsung Galaxy S21 devices running Android 12 with 6GB RAM during low-memory conditions might never occur in your test lab, but could affect thousands of users in production.
Firebase Crashlytics is the standard crash reporting tool for iOS and Android applications. It's free, integrates with the Firebase ecosystem, and provides the device-level context needed to reproduce mobile-specific crashes. This guide covers setup, advanced configuration, and how to integrate Crashlytics into a mobile QA workflow.
What Crashlytics Captures
When a crash occurs, Crashlytics captures:
Stack trace: Full native stack trace for the thread that crashed, including symbol names (if dSYMs/mapping files are uploaded).
Device information: Device model, OS version, available memory, available disk space, rooted/jailbroken status, device orientation.
App metadata: App version, build number, app foreground/background state at time of crash.
Custom logs: Log messages you've written in your code leading up to the crash — a breadcrumb trail.
User ID: If you've set a user identifier.
Keys: Custom key-value pairs you've added to provide application state context.
Non-fatal exceptions: Caught exceptions and errors you report explicitly — important for monitoring errors that don't crash the app but indicate problems.
Android Setup
Add Crashlytics to your Android project:
// app/build.gradle.kts
plugins {
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
}
dependencies {
implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
implementation("com.google.firebase:firebase-crashlytics")
implementation("com.google.firebase:firebase-analytics")
}// project/build.gradle.kts
plugins {
id("com.google.gms.google-services") version "4.4.0" apply false
id("com.google.firebase.crashlytics") version "2.9.9" apply false
}Crashlytics initializes automatically when your app starts — no initialization call required. Crashes are detected, logged to disk, and sent to Crashlytics on the next app launch (not during the crash itself, to avoid interfering with system crash handling).
Setting user identity:
import com.google.firebase.crashlytics.FirebaseCrashlytics
val crashlytics = FirebaseCrashlytics.getInstance()
// After user logs in
crashlytics.setUserId(user.id)Custom keys for application state:
crashlytics.setCustomKey("selected_tab", "checkout")
crashlytics.setCustomKey("cart_item_count", cartItems.size)
crashlytics.setCustomKey("payment_method", "credit_card")
crashlytics.setCustomKey("network_type", networkType)
crashlytics.setCustomKey("last_api_call", "/api/payments")Custom log messages (breadcrumbs):
// These appear in the log section of each crash report
crashlytics.log("User initiated checkout")
crashlytics.log("Cart validated: ${cartItems.size} items")
crashlytics.log("Payment form submitted")
crashlytics.log("Waiting for payment gateway response...")
// If the crash occurs here, you see all preceding logsReporting non-fatal exceptions:
try {
parseApiResponse(response)
} catch (e: JsonParseException) {
// App can recover, but this is worth tracking
crashlytics.recordException(e)
showFallbackContent()
}Non-fatal exceptions don't appear in the Crashes section — they have their own Non-fatals section in the Firebase console. This separation lets you track errors that indicate data problems or edge cases without conflating them with actual crashes.
iOS Setup
// Podfile
pod 'Firebase/Crashlytics'
pod 'Firebase/Analytics'// AppDelegate.swift
import Firebase
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
FirebaseApp.configure()
return true
}
}Setting user and custom keys:
import FirebaseCrashlytics
// After login
Crashlytics.crashlytics().setUserID(user.id)
// Custom keys
Crashlytics.crashlytics().setCustomValue("checkout", forKey: "current_flow")
Crashlytics.crashlytics().setCustomValue(cartItems.count, forKey: "cart_item_count")
// Custom logs
Crashlytics.crashlytics().log("Payment form validated")
Crashlytics.crashlytics().log("Submitting payment request")
// Non-fatal error reporting
do {
try parseServerResponse(data)
} catch {
Crashlytics.crashlytics().record(error: error)
}React Native (using @react-native-firebase/crashlytics):
import crashlytics from '@react-native-firebase/crashlytics';
// In your error boundary or global error handler
async function onSignIn(user) {
await crashlytics().setUserId(user.uid);
await crashlytics().setAttributes({
plan: user.plan,
account_type: user.accountType,
});
}
// Log breadcrumbs
crashlytics().log('User navigated to checkout');
// Non-fatal reporting
try {
await processPayment(paymentDetails);
} catch (error) {
crashlytics().recordError(error);
showErrorState();
}
// Forcing a test crash (use only in development)
// crashlytics().crash();Flutter:
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// Catch Flutter framework errors
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
// Catch async errors
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
runApp(MyApp());
}
// In your code
FirebaseCrashlytics.instance.setUserIdentifier(user.id);
FirebaseCrashlytics.instance.log('Checkout initiated');
FirebaseCrashlytics.instance.setCustomKey('cart_total', cartTotal);Uploading dSYMs and ProGuard Mappings
Symbolication converts raw memory addresses in crash traces to readable function names and line numbers. Without it, stack traces are unreadable.
iOS dSYMs: The com.google.firebase.crashlytics Gradle-equivalent for iOS is handled by a build phase script. Add the Crashlytics upload script to your Xcode target's Build Phases:
"${PODS_ROOT}/FirebaseCrashlytics/run"And add your app's binary and dSYM paths to the script's input/output files. Or upload manually:
./Pods/FirebaseCrashlytics/upload-symbols \
-gsp GoogleService-Info.plist \
-p ios \
MyApp.app.dSYMAndroid ProGuard/R8 mappings: Add the Crashlytics plugin (shown above) and it handles mapping file uploads automatically as part of your release build. For manual upload:
./gradlew uploadCrashlyticsMappingFileReleaseWithout symbolicated stack traces, crash reports show memory addresses instead of function names. For any crash investigation to be productive, symbolication must be working.
Release Versions and Version Management
Crashlytics filters crash data by app version. When investigating a crash, you need to know whether it's a new regression or a pre-existing issue:
// Android - verify versionName and versionCode are set in build.gradle
android {
defaultConfig {
versionCode = 42
versionName = "1.4.2"
}
}In the Firebase console, you can filter crash data by version range. If a crash was introduced in version 1.4.0, filtering to 1.4.0+ isolates the affected versions. Comparing crash rates between consecutive versions shows whether your fixes are working.
Velocity Alerts
Crashlytics Velocity Alerts notify you when a crash affects a threshold percentage of your active users within a specific time window. This is the primary alerting mechanism for detecting new high-impact crashes quickly after a release.
Configure velocity alerts in the Firebase console:
- Enable "Velocity alerts" in your project settings
- Set the threshold (e.g., alert when a crash affects >1% of active users in 1 hour)
- Set notification channels (email, PagerDuty, Slack via integrations)
For release monitoring, enable velocity alerts before deploying to the Play Store or App Store. If a new crash immediately affects a significant percentage of users, you'll know within an hour rather than waiting for user reviews.
Big Query Export
For teams that need raw crash data for analysis in their own data infrastructure, Crashlytics supports BigQuery export. This enables:
- Custom analytics on crash data
- Correlation with your own business metrics
- Long-term retention beyond what the Firebase console provides
- SQL-based queries across crash, user, and event data
Enable BigQuery linking in the Firebase console under Project Settings > Integrations.
Testing Your Integration
Verify crash reporting works:
// Android - add a test button in debug builds only
if (BuildConfig.DEBUG) {
debugButton.setOnClickListener {
throw RuntimeException("Test crash from debug build")
}
}// iOS - debug-only test crash
#if DEBUG
Button("Test crash") {
let array: [Int] = []
_ = array[1] // Index out of range - forces crash
}
#endifAfter triggering a test crash, close and reopen the app (crashes upload on next launch). Verify in the Firebase console that the crash appears with:
- Correct stack trace
- Your custom keys and logs
- User ID (if set)
- Correct device and OS version
Verify non-fatal error reporting:
// Should appear in Non-fatals section, not Crashes
FirebaseCrashlytics.getInstance().recordException(
Exception("Test non-fatal exception")
)Non-fatals upload immediately (not on next launch), so they should appear in the console within a few minutes.
QA Workflow for Mobile Release Validation
Pre-release:
- Verify Crashlytics integration is active in the release build
- Confirm dSYMs/mapping files will be uploaded during the build process
- Test crash reporting with a forced crash in a development build
- Ensure version number is incremented (for version-based filtering)
Post-release (first 24-48 hours):
- Monitor the Crashlytics dashboard for new crash groups introduced by this release
- Check crash-free user rate — a healthy release maintains or improves this metric
- Verify no velocity alert fires
- Review non-fatal error rates for any significant increases
Crash investigation:
- Open crash group in Firebase console
- Verify stack trace is symbolicated (readable function names)
- Review custom logs for the breadcrumb trail leading to the crash
- Check custom keys to understand application state at crash time
- Filter by device model and OS version — some crashes are device-specific
- Check whether the crash existed in previous versions or is new to this release
The device model and OS version filtering is particularly useful for mobile QA. A crash that only occurs on Android 10 on specific manufacturers might be a memory management issue specific to that OS version's garbage collection behavior, rather than a logic bug in your code.
Crashlytics vs. Third-Party Mobile Error Trackers
Crashlytics is free and deeply integrated with Firebase. If you're already using Firebase (Analytics, FCM, Remote Config, Authentication), Crashlytics adds negligible overhead.
Paid alternatives like Bugsnag, Sentry, and Rollbar provide cross-platform consistency (same SDK for web and mobile), richer breadcrumb capture, and more flexible alerting. For teams with mixed web/mobile applications that want unified dashboards and workflows, these alternatives are worth evaluating.
For teams building mobile-only or mobile-primary applications, Crashlytics provides substantial value with zero additional cost.