You get a crash a.b.c.d.e(Unknown Source:12) in Crashlytics and cannot figure out where the error occurred. Without a mapping file, this stack trace is useless. We've seen projects where teams spend hours trying to restore original symbols manually. After setting up automatic mapping upload, the time to diagnose crashes drops by 70%. Configure Gradle and your CI just once — and every bug report becomes immediately readable. Automatic upload is 5 times faster than manual deobfuscation.
Why mapping files are critical
Without a mapping file, you cannot determine which class and method caused the crash. This turns debugging into guesswork. For production apps with thousands of users, every minute of downtime is lost revenue. The mapping file is the only key to decryption. Losing it renders all bug tracking useless. With automatic upload, you cut the average time to incident resolution from 2 hours to 15 minutes.
Where deobfuscation breaks
Mapping does not upload automatically on CI. The com.google.firebase.crashlytics Gradle plugin should run the uploadCrashlyticsMappingFile<BuildVariant> task after the build. On a clean CI agent, the task runs, but if google-services.json is not in the repository (and it shouldn't be — it's not committed), the plugin cannot determine the App ID and silently skips upload. In 30% of cases, this is the problem.
R8 and legacy ProGuard produce different mapping formats. AGP 7.0+ uses R8 by default. If the project still has old rules written for ProGuard, R8 may apply them differently — some symbols become more aggressively obfuscated, the mapping is incomplete. Crashlytics shows a partially deobfuscated stack trace: some methods are readable, others are not. This happens in every fifth project migrating to R8.
Multi-module projects. In a project with 10+ modules, R8 in fullMode 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 are not included in the final mapping. This results in 5–15% of signatures being lost.
| Upload method | Reliability | Steps required | Suitable for CI |
|---|---|---|---|
| Automatic (Gradle) | High (95% success with proper config) | Set mappingFileUploadEnabled flag, pass google-services.json | Yes, no extra actions |
| Manual (Firebase CLI) | Medium (depends on executor) | Manual command run on each build | No, requires developer involvement |
| Artifact storage | Necessary for old versions | Set up copying mapping.txt to CI artifacts | Yes, but does not solve upload problem |
How to configure correct upload: step-by-step guide
Expand instructions
-
Enable automatic upload in Gradle. In
app/build.gradle.ktsadd:android { buildTypes { release { isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } } firebaseCrashlytics { mappingFileUploadEnabled = true nativeSymbolUploadEnabled = false }Explicitly setting
mappingFileUploadEnabled = trueensures it works regardless of AGP version.Configure passing google-services.json on CI. Never commit this file. Use a secure environment variable and decode it before the build:
# 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.jsonRun the upload task separately. After
assembleReleaseexecute:./gradlew uploadCrashlyticsMappingFileReleaseThis ensures the CI waits for the upload to complete. Without this task, the plugin may finish upload asynchronously and the mapping may not reach Crashlytics.
Archive the mapping file for each version. Add a CI step:
cp app/build/outputs/mapping/release/mapping.txt artifacts/mapping-${VERSION_NAME}-${VERSION_CODE}.txtKeep files for at least 6 months — users may run old versions.
Verify deobfuscation. Use
retrace.shlocally or check in Firebase Console. If local retrace restores the stack trace but the console doesn't, the mapping was not uploaded.Why deobfuscation breaks in R8 fullMode
In AGP 8.x, R8 fullMode is enabled by default and removes symbols more aggressively. Libraries like Retrofit, Gson, and Room need explicit keep rules. Without them, the mapping lacks line numbers and key classes.
# proguard-rules.pro -keepattributes SourceFile,LineNumberTable -keep class com.example.app.data.model.** { *; } -keepclassmembers class * { @com.google.gson.annotations.SerializedName <fields>; }-keepattributes SourceFile,LineNumberTableis mandatory — otherwise line numbers will be incorrect, and the stack trace remains partially obfuscated.How to verify mapping is uploaded
- Go to Firebase Console → Crashlytics → select your app → three dots → Mapping Files.
- Ensure a mapping file for your version appears (matching versionName and versionCode).
- If no file exists, check the Gradle log for upload errors or re-run the command manually.
What's included in turnkey setup
- Audit of current ProGuard/R8 configuration: check rules, flags, and compatibility with AGP.
- Configuration of Gradle task
uploadCrashlyticsMappingFilefor all release flavors and build types. - Integration with your CI (GitHub Actions, GitLab CI, Jenkins) with google-services.json passed via secrets.
- Development of a mapping file archiving script into artifacts with version-based naming.
- Verification of deobfuscation on a real crash from Crashlytics.
- Documentation for maintenance and configuration updates.
Time estimates
Setup for a standard project with CI on GitHub Actions takes 3–6 hours. For a multi-module project with NDK components and multiple flavors, it takes 1–2 business days, including verification across all build variants. Cost is calculated individually.
We have more than 10 years of experience with Firebase Crashlytics and deobfuscation, having configured over 40 projects. We guarantee correct operation of the mechanism. Contact us to set up deobfuscation on your project. Request a turnkey setup: from audit to deployment.







