When a crash lands in your error tracker, the stack trace can look like this:
at com.example.a.b.c(Unknown Source:42)
at com.example.d.e.f(Unknown Source:108)
at android.app.ActivityThread.main(ActivityThread.java:7231)
Those a.b.c and d.e.f names are obfuscated. ProGuard and R8 strip original class and method names to shrink your APK—good for users, painful for debugging. If your mapping file didn't upload to your error tracking service, you'll need to manually deobfuscate crash stack traces using ProGuard Retrace. This guide walks you through how to recover those original names and track down the real root cause in your code.
Why Obfuscated Stack Traces Matter
Android build tools like ProGuard and R8 serve a critical purpose: they shrink your APK by removing unused code and renaming classes/methods to single letters. This saves bytes users must download. But the cost is readability. When a crash happens in production, your stack trace becomes a cipher. The line number points to bytecode, not your source. Without the mapping file—the key that translates a.b.c back to com.example.app.UserManager—you're debugging blind.
When automatic source maps upload to your service fails—network issues, CI/CD misconfiguration, or accidental exclusion—you're left with two choices: re-sign and re-upload your build, or manually retrace the obfuscated crash. For urgent production bugs, the latter is faster.
Prerequisites: Locating Your Mapping File
Before you retrace, you need the exact mapping file that corresponds to the build that crashed. This is critical—a mapping file from a different build will give you garbage output.
ProGuard and R8 generate this file during your Gradle build. For R8 (the default in modern Android projects), it lives here:
app/build/outputs/mapping/release/mapping.txt
For ProGuard explicitly configured:
app/build/outputs/proguard/release/mapping.txt
Keep mapping files forever. Industry best practice is to archive a copy with each release—uploaded to artifact storage, committed to a secure branch, or stored in your release-management system. If you don't have it now, you'll need to rebuild that release from source.
The mapping file is a plain text file mapping obfuscated names to originals:
com.example.app.UserManager -> com.example.a.b:
3:4:void onUserCreate() -> c
5:12:java.lang.String getName() -> d
com.example.app.UserManager$Listener mListener -> e
Setting Up ProGuard Retrace
ProGuard Retrace comes bundled with the ProGuard/R8 tools. You don't need to install anything new—it's already on your machine if you've built Android apps.
Locate the retrace script:
On macOS/Linux:
~/Android/sdk/tools/proguard/bin/retrace.sh
On Windows:
%ANDROID_SDK%\tools\proguard\bin\retrace.bat
If that path doesn't exist, check your SDK manager. Open Android Studio → SDK Manager → SDK Tools, and ensure "ProGuard" or "Android SDK Command-line Tools" is installed.
Modern projects using R8 should use the retrace.jar directly. It's bundled in your Gradle cache:
./gradlew --version # to find your Gradle installation
# Then locate it in ~/.gradle/caches/modules-2/files-2.1/com.android.tools/r8/Using the R8 version ensures compatibility with your exact obfuscation rules.
Step-by-Step Deobfuscation Walkthrough
Let's walk through a real example. Assume your crash log is:
java.lang.NullPointerException
at com.example.a.b.c(Unknown Source:42)
at com.example.a.d.e(Unknown Source:108)
at android.app.ActivityThread.main(ActivityThread.java:7231)
Step 1: Save your obfuscated stack trace to a file.
Create obfuscated_crash.txt:
java.lang.NullPointerException
at com.example.a.b.c(Unknown Source:42)
at com.example.a.d.e(Unknown Source:108)
Step 2: Run retrace.
# Using retrace.sh on macOS/Linux
~/Android/sdk/tools/proguard/bin/retrace.sh -verbose \
path/to/your/mapping.txt \
obfuscated_crash.txt > deobfuscated_crash.txt
# Using R8's retrace.jar
java -jar ~/.gradle/caches/modules-2/files-2.1/com.android.tools/r8/VERSION/r8-VERSION.jar retrace \
-verbose \
path/to/your/mapping.txt \
obfuscated_crash.txt > deobfuscated_crash.txt
The -verbose flag includes intermediate de-obfuscation steps, helping you spot mismatches.
Step 3: Read the output.
Your deobfuscated_crash.txt now shows:
java.lang.NullPointerException
at com.example.app.PaymentManager.processTransaction(PaymentManager.java:42)
at com.example.app.CheckoutActivity.onConfirm(CheckoutActivity.java:108)
Now you can see the real method names and file references. Open your source, jump to those lines, and debug.
Handling Partial Obfuscation
Sometimes not everything is obfuscated. Your crash might include both original and obfuscated names:
at com.example.app.MainActivity.onCreate(MainActivity.java:15)
at com.example.a.b.c(Unknown Source:42)
Retrace handles this seamlessly—it deobfuscates what it recognizes and leaves the rest as-is.
If retrace produces garbled output or mismatches, your mapping file is wrong. Double-check that it's from the exact build version in production.
Integrating with Error Tracking
Once you've manually retraced a crash, you should upload that mapping file to your error tracker so future crashes from that release are automatically deobfuscated. Most error trackers—including LightTrace—support automatic mapping file upload via Gradle plugins or API.
For Android projects using LightTrace, configure your build.gradle:
plugins {
id "io.sentry.android.gradle" version "4.3.0"
}
sentry {
org = "your-org"
projectName = "your-project"
authToken = System.getenv("SENTRY_AUTH_TOKEN")
dsn = "https://<key>@light-trace.robomiri.com/1"
}
Replace the DSN with your LightTrace project DSN. On each release build, this uploads your mapping file automatically. Future crashes are deobfuscated server-side without manual work.
Never commit or expose your mapping files or auth tokens. Use environment variables or secure build-time secrets. Keep mapping files in your CI/CD artifact storage tied to each release version.
Troubleshooting Common Issues
"No line number match" or "Unknown Source": Your mapping file doesn't match the APK. Verify the version and build variant (debug vs. release). ProGuard settings can differ between variants.
Retrace produces garbage names: The mapping file is corrupted or from a different build. Re-download it from your artifact storage and verify its checksum.
You can't find the mapping file: If it's truly lost, rebuild that release version from source (same git tag, same build configuration). Only then will the mapping file match.
Stack traces from different architectures:
If your app ships multiple ABIs (arm64-v8a, armeabi-v7a), use the mapping file from the corresponding release build. The mapping file is the same across ABIs—only the compiled .so files differ.
Automate It Next Time
Manual deobfuscation is a one-time fix. To prevent this friction:
- Upload mapping files on every release. CI/CD should push them to your error tracker or artifact storage.
- Verify upload. Check your error tracker's dashboard that the mapping file for version X is present.
- Monitor obfuscated crashes. If you ever see "Unknown Source" in production, it's a sign your upload failed.
For Android developers using LightTrace, the Sentry SDK handles this automatically if configured. For other services, check their documentation on mapping-file upload—it usually takes minutes to set up.
Interested in how other platforms handle deobfuscation? iOS uses dSYM bundles, JavaScript uses source maps, and native Android uses dSYM-like symbol files. The principle is the same: the build tool generates a key that maps compiled code back to source, and your error tracker uses it to show you readable stacks.
Start tracking errors in minutes
Next time a crash lands in your error tracker, you'll want automatic deobfuscation. LightTrace automatically detects and applies mapping files for every Android release, so your stacks are readable without manual retrace work. Start free—no credit card required.