Firebase Crashlytics Cheat Sheet
Overview
Firebase Crashlytics is a lightweight, real-time crash reporting tool that helps mobile developers track, prioritize, and fix stability issues affecting app quality. It automatically groups crashes into manageable issues, highlights the severity based on user impact, and provides detailed stack traces with device context for each crash. Crashlytics integrates deeply with the Firebase ecosystem and is used by millions of apps to maintain stability and improve user experience.
Crashlytics goes beyond basic crash reporting by providing crash-free user and session metrics, velocity alerts for sudden stability regressions, custom logging and key-value pairs for debugging context, non-fatal error tracking, and ANR (Application Not Responding) detection for Android. It supports both iOS (Swift/Objective-C) and Android (Kotlin/Java), works with both native and cross-platform frameworks like React Native and Flutter, and provides BigQuery export for custom analysis and dashboards.
Installation (Android)
// Project-level build.gradle.kts
plugins {
id("com.google.gms.google-services") version "4.4.2" apply false
id("com.google.firebase.crashlytics") version "3.0.2" apply false
}
// App-level build.gradle.kts
plugins {
id("com.android.application")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
}
dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.6.0"))
implementation("com.google.firebase:firebase-crashlytics")
implementation("com.google.firebase:firebase-analytics")
}
# Download google-services.json from Firebase Console
# Place in app/ directory
# Verify setup
./gradlew assembleDebug
# Look for "FirebaseCrashlytics" in logcat
Installation (iOS)
# Podfile
pod 'FirebaseCrashlytics'
pod 'FirebaseAnalytics'
pod install
// AppDelegate.swift or App init
import Firebase
@main
struct MyApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
# Download GoogleService-Info.plist from Firebase Console
# Add to Xcode project (ensure it's in the target)
# Add Run Script build phase for dSYM upload
# Build Phases > + > New Run Script Phase
# Script:
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"
# Or with CocoaPods:
"${PODS_ROOT}/FirebaseCrashlytics/run"
# Input Files:
# ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}
# ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}
# ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist
# $(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist
Basic Usage (Android / Kotlin)
import com.google.firebase.crashlytics.FirebaseCrashlytics
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Optional: disable during development
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
}
}
// Force a test crash
fun forceCrash() {
throw RuntimeException("Test Crashlytics crash")
}
// Log custom keys for debugging context
FirebaseCrashlytics.getInstance().apply {
setCustomKey("user_level", 5)
setCustomKey("screen", "checkout")
setCustomKey("cart_items", 3)
setCustomKey("payment_method", "credit_card")
setCustomKey("app_theme", "dark")
}
// Set user identifier
FirebaseCrashlytics.getInstance().setUserId("user_12345")
// Log custom messages (breadcrumbs)
FirebaseCrashlytics.getInstance().log("User tapped checkout button")
FirebaseCrashlytics.getInstance().log("Payment processing started")
FirebaseCrashlytics.getInstance().log("API response: status 200")
// Record non-fatal exceptions
try {
riskyOperation()
} catch (e: Exception) {
FirebaseCrashlytics.getInstance().recordException(e)
// Handle gracefully
}
// Record non-fatal with custom message
try {
parseResponse(data)
} catch (e: JsonSyntaxException) {
FirebaseCrashlytics.getInstance().apply {
log("Failed to parse API response")
setCustomKey("response_data", data.take(500))
recordException(e)
}
}
Basic Usage (iOS / Swift)
import FirebaseCrashlytics
// Force test crash
func forceCrash() {
fatalError("Test Crashlytics crash")
}
// Custom keys
Crashlytics.crashlytics().setCustomValue(5, forKey: "user_level")
Crashlytics.crashlytics().setCustomValue("checkout", forKey: "screen")
Crashlytics.crashlytics().setCustomValue(3, forKey: "cart_items")
// User identifier
Crashlytics.crashlytics().setUserID("user_12345")
// Log messages
Crashlytics.crashlytics().log("User tapped checkout button")
Crashlytics.crashlytics().log("Payment processing started")
// Record non-fatal errors
do {
try riskyOperation()
} catch {
Crashlytics.crashlytics().record(error: error)
}
// Record with custom NSError
let error = NSError(domain: "com.example.myapp",
code: 1001,
userInfo: [NSLocalizedDescriptionKey: "Payment failed",
"payment_id": "pay_abc123",
"amount": 29.99])
Crashlytics.crashlytics().record(error: error)
// Opt-in/out of collection
Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(true)
Custom Keys Reference
| Key Type | Android | iOS |
|---|---|---|
| String | setCustomKey("key", "value") | setCustomValue("value", forKey: "key") |
| Boolean | setCustomKey("key", true) | setCustomValue(true, forKey: "key") |
| Int | setCustomKey("key", 42) | setCustomValue(42, forKey: "key") |
| Float | setCustomKey("key", 3.14f) | setCustomValue(3.14, forKey: "key") |
| Long | setCustomKey("key", 123L) | setCustomValue(123, forKey: "key") |
| Max keys | 64 per crash | 64 per crash |
| Max key length | 1024 chars | 1024 chars |
| Max value length | 1024 chars | 1024 chars |
ANR Detection (Android)
// ANR detection is automatic on Android
// Crashlytics detects:
// - Main thread blocked for 5+ seconds
// - Input dispatching timeout
// - BroadcastReceiver timeout
// To reduce ANRs:
// 1. Move heavy work off main thread
viewModelScope.launch(Dispatchers.IO) {
val result = heavyComputation()
withContext(Dispatchers.Main) {
updateUI(result)
}
}
// 2. Use WorkManager for background tasks
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build()
WorkManager.getInstance(context).enqueue(request)
React Native Integration
# Install
npm install @react-native-firebase/app @react-native-firebase/crashlytics
cd ios && pod install
import crashlytics from '@react-native-firebase/crashlytics';
// Force crash (for testing)
crashlytics().crash();
// Log message
crashlytics().log('User signed in');
// Set user ID
crashlytics().setUserId('user_12345');
// Custom attributes
await crashlytics().setAttribute('screen', 'checkout');
await crashlytics().setAttributes({
cart_items: '3',
payment_method: 'credit_card'
});
// Record error
try {
await fetchData();
} catch (error) {
crashlytics().recordError(error);
}
// JavaScript error handler
const errorHandler = (error, isFatal) => {
crashlytics().log(`JS Error: ${error.message}`);
crashlytics().recordError(error);
};
// Set global error handler
ErrorUtils.setGlobalHandler(errorHandler);
// Enable/disable
await crashlytics().setCrashlyticsCollectionEnabled(true);
Flutter Integration
# pubspec.yaml
dependencies:
firebase_core: ^3.6.0
firebase_crashlytics: ^4.1.3
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());
}
// Custom keys
FirebaseCrashlytics.instance.setCustomKey('screen', 'checkout');
FirebaseCrashlytics.instance.setUserIdentifier('user_12345');
FirebaseCrashlytics.instance.log('Payment started');
// Record non-fatal
try {
await riskyOperation();
} catch (e, stack) {
FirebaseCrashlytics.instance.recordError(e, stack);
}
Configuration
<!-- AndroidManifest.xml - Disable auto-collection -->
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<!-- iOS Info.plist - Disable auto-collection -->
<key>FirebaseCrashlyticsCollectionEnabled</key>
<false/>
// Enable at runtime (after user consent)
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
BigQuery Export
-- Query crash data in BigQuery
-- Enable in Firebase Console: Crashlytics > BigQuery
-- Top crashes by user impact
SELECT
issue_id,
issue_title,
COUNT(DISTINCT installation_uuid) as affected_users,
COUNT(*) as crash_count,
MIN(event_timestamp) as first_seen,
MAX(event_timestamp) as last_seen
FROM `project.firebase_crashlytics.events_*`
WHERE event_type = 'crash'
AND _TABLE_SUFFIX BETWEEN '20250101' AND '20250131'
GROUP BY issue_id, issue_title
ORDER BY affected_users DESC
LIMIT 20;
-- Crash-free rate by app version
SELECT
application.display_version,
COUNT(DISTINCT CASE WHEN event_type = 'crash' THEN installation_uuid END) as crashed_users,
COUNT(DISTINCT installation_uuid) as total_users,
1 - (COUNT(DISTINCT CASE WHEN event_type = 'crash' THEN installation_uuid END) * 1.0 / COUNT(DISTINCT installation_uuid)) as crash_free_rate
FROM `project.firebase_crashlytics.events_*`
WHERE _TABLE_SUFFIX >= '20250101'
GROUP BY application.display_version
ORDER BY application.display_version DESC;
Advanced Usage
// Structured error handling with Crashlytics
class CrashReporter {
private val crashlytics = FirebaseCrashlytics.getInstance()
fun setUserContext(userId: String, properties: Map<String, String>) {
crashlytics.setUserId(userId)
properties.forEach { (key, value) ->
crashlytics.setCustomKey(key, value)
}
}
fun logBreadcrumb(message: String) {
crashlytics.log("[${System.currentTimeMillis()}] $message")
}
fun recordError(
throwable: Throwable,
context: Map<String, String> = emptyMap()
) {
context.forEach { (key, value) ->
crashlytics.setCustomKey(key, value)
}
crashlytics.recordException(throwable)
}
fun recordNetworkError(
url: String,
statusCode: Int,
error: Throwable
) {
crashlytics.setCustomKey("failed_url", url)
crashlytics.setCustomKey("status_code", statusCode)
crashlytics.log("Network error: $url returned $statusCode")
crashlytics.recordException(error)
}
}
// Coroutine exception handler
val crashlyticsHandler = CoroutineExceptionHandler { _, throwable ->
FirebaseCrashlytics.getInstance().recordException(throwable)
}
viewModelScope.launch(crashlyticsHandler + Dispatchers.IO) {
// This crash will be recorded as non-fatal
fetchData()
}
CLI and dSYM Upload
# Upload dSYMs manually (iOS)
# Using Firebase CLI
firebase crashlytics:symbols:upload --app=APP_ID path/to/dSYMs
# Using upload-symbols script
./Pods/FirebaseCrashlytics/upload-symbols \
-gsp GoogleService-Info.plist \
-p ios \
path/to/MyApp.dSYM
# Android mapping file upload (ProGuard/R8)
# Automatic when using Firebase Crashlytics Gradle plugin
# Manual upload:
firebase crashlytics:mappingfile:upload \
--app=APP_ID \
--resource-file=app/build/outputs/mapping/release/mapping.txt
Troubleshooting
| Issue | Solution |
|---|---|
| Crashes not appearing | Force a test crash; wait 5 minutes; check Firebase Console |
| Symbolication missing | Upload dSYMs (iOS) or mapping file (Android); verify build phase |
| ”Crashlytics not initialized” | Ensure FirebaseApp.configure() called before any Crashlytics API |
| Duplicate crash reports | Check for multiple recordException calls; review error handling flow |
| Crash-free rate incorrect | Wait 24 hours for metric to stabilize; check user sampling |
| ANRs not detected | Only on Android 11+; ensure app targets API 30+ |
| BigQuery export empty | Enable export in Console; data starts flowing after activation |
| ProGuard obfuscating crash logs | Enable Crashlytics Gradle plugin; verify mapping file upload |
| Too many non-fatal events | Limit recordException calls; sample high-frequency errors |
| Debug crashes showing | Disable collection in debug: setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG) |