Setting Up ProGuard/R8 Mapping Upload for Android Crash Deobfuscation
Firebase Crashlytics shows a.b.c.d.e(Unknown Source:12) instead of com.example.app.checkout.PaymentViewModel.processPayment(PaymentViewModel.kt:89). ProGuard and R8 rename classes and methods in minified production builds — without the mapping file, the stack trace is unreadable. The problem occurs not just in new projects: often mapping stops uploading after switching CI or updating AGP.
Where Deobfuscation Breaks
Mapping doesn't upload automatically on CI. The com.google.firebase.crashlytics plugin in Gradle should execute the uploadCrashlyticsMappingFile<BuildVariant> task after build. On a clean CI agent, the task runs, but if google-services.json isn't in the repository (and it shouldn't be — it's never committed), the plugin can't determine the App ID and silently skips the upload.
R8 and legacy ProGuard produce different mapping formats. AGP 7.0+ uses R8 by default. If the project has old rules written for ProGuard, R8 might apply them differently — some symbols get obfuscated more aggressively, mapping is incomplete. Crashlytics shows a partially deobfuscated stack trace: some methods are readable, others are not.
Multi-module projects. In a project with 10+ modules, R8 in fullMode (default in AGP 8.x) works across the entire dependency graph. A single mapping file is generated for the whole app, but if a module is configured with minifyEnabled = false for the library variant — its symbols don't make it to the final mapping.
How to Configure Proper Upload
Gradle Configuration
// app/build.gradle.kts
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
// Crashlytics mapping upload
firebaseCrashlytics {
mappingFileUploadEnabled = true
nativeSymbolUploadEnabled = false // only for NDK crashes
}
mappingFileUploadEnabled = true — explicitly set this, don't rely on defaults. Post-AGP 8.x, the default is true for release, but it's better to be explicit.
Passing google-services.json to CI
google-services.json should not be in the repository. On CI, pass it via environment variable:
# GitHub Actions
- name: Decode google-services.json
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: echo "$GOOGLE_SERVICES_JSON" | base64 --decode > app/google-services.json
- name: Build and upload mapping
run: ./gradlew assembleRelease uploadCrashlyticsMappingFileRelease
The uploadCrashlyticsMappingFileRelease task runs separately — this is important because on assembleRelease the plugin sometimes completes the upload asynchronously and CI doesn't wait for it.
Manual Upload via Firebase CLI
If automatic upload doesn't work for some reason:
firebase crashlytics:mappingfile:upload \
--app=1:123456789:android:abcdef \
app/build/outputs/mapping/release/mapping.txt
The mapping file is always located at app/build/outputs/mapping/<buildType>/mapping.txt. Save it as a CI artifact — without it, deobfuscating old crashes is impossible after code version changes.
Storing Mapping Files
Rule: each production release → archive mapping.txt with version and build number. In 6 months, users might still be running old versions, and crashes from them will come undeobfuscated if the mapping is lost.
# In CI: save as artifact
cp app/build/outputs/mapping/release/mapping.txt \
artifacts/mapping-${VERSION_NAME}-${VERSION_CODE}.txt
Verification via Retrace
For local verification:
# Android SDK tools
retrace.sh \
app/build/outputs/mapping/release/mapping.txt \
obfuscated-stacktrace.txt
If retrace recovers a readable stack trace locally, but Crashlytics still shows obfuscated — the mapping wasn't uploaded. Check in Firebase Console: Crashlytics → App → three dots → Mapping Files.
R8 fullMode and Preserving Required Symbols
In AGP 8.x R8 fullMode is on by default and removes symbols more aggressively. For Retrofit, Gson, Room, you need explicit keep rules:
# proguard-rules.pro
-keepattributes SourceFile,LineNumberTable
-keep class com.example.app.data.model.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
-keepattributes SourceFile,LineNumberTable — without this, mapping exists, but line numbers in the stack trace will be incorrect.
Timeline Estimates
Setup for a standard project with CI on GitHub Actions — 3–6 hours. Multi-module project with NDK components and multiple flavors — 1–2 business days, including verification across all build variants.







