Налаштування завантаження ProGuard/R8 mapping для деобфускації крахів Android
Firebase Crashlytics показує a.b.c.d.e(Unknown Source:12) замість com.example.app.checkout.PaymentViewModel.processPayment(PaymentViewModel.kt:89). ProGuard та R8 перейменовують класи та методи в мініфікованих production-збірках — без mapping-файлу стектрейс нечитаемо. Проблема виникає не тільки у нових проектів: часто mapping перестає завантажуватися після зміни CI або оновлення AGP.
Де ломається деобфускація
Mapping не завантажується автоматично на CI. Плагін com.google.firebase.crashlytics у Gradle має виконати задачу uploadCrashlyticsMappingFile<BuildVariant> після збірки. На чистому CI-агенті задача виконується, але якщо google-services.json не в репозиторії (і це правильно — його не коммітять), плагін не може визначити App ID та мовчки пропускає завантаження.
R8 та legacy ProGuard дають різні mapping-формати. AGP 7.0+ використовує R8 за замовчуванням. Якщо в проекті залишилися старі правила, написані під ProGuard, R8 може застосувати їх інакше — частина символів обфускується агресивніше, mapping неповний. Crashlytics покаже частково деобфусковану стектрейс: одні методи читабельні, інші — ні.
Багатомодульні проекти. У проекті з 10+ модулями R8 у fullMode (увімкнено за замовчуванням в AGP 8.x) працює через весь граф залежностей. Mapping-файл генерується один для всього додатку, але якщо якийсь модуль налаштований з minifyEnabled = false для library variant — його символи не потрапляють у остаточний mapping.
Як налаштувати коректне завантаження
Конфігурація Gradle
// 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 // тільки для NDK-крахів
}
mappingFileUploadEnabled = true — явно вказуємо, не покладаючись на дефолт. Після AGP 8.x дефолт — true для release, але краще задати явно.
Передача google-services.json на CI
google-services.json не має бути в репозиторії. На CI передаємо через змінну окруження:
# 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
Задача uploadCrashlyticsMappingFileRelease запускається окремо — це важливо, тому що при assembleRelease плагін іноді завершує upload асинхронно та CI не чекає результату.
Ручне завантаження через Firebase CLI
Якщо автоматичне завантаження з якоїсь причини не підходить:
firebase crashlytics:mappingfile:upload \
--app=1:123456789:android:abcdef \
app/build/outputs/mapping/release/mapping.txt
Mapping-файл завжди знаходиться в app/build/outputs/mapping/<buildType>/mapping.txt. Зберігайте його як артефакт CI — без нього деобфускація старих крахів неможлива після зміни версії кода.
Зберігання mapping-файлів
Правило: кожний production-релиз → архівуємо mapping.txt з позначкою версії та build number. Через 6 місяців користувачі все ще можуть запускати старі версії додатку, й краші з них приходитимуть без символів, якщо mapping втрачено.
# У CI: зберегти як артефакт
cp app/build/outputs/mapping/release/mapping.txt \
artifacts/mapping-${VERSION_NAME}-${VERSION_CODE}.txt
Перевірка через Retrace
Для локальної верифікації:
# Android SDK tools
retrace.sh \
app/build/outputs/mapping/release/mapping.txt \
obfuscated-stacktrace.txt
Якщо retrace відновлює читабельний стектрейс локально, але Crashlytics все рівно показує обфусковану — mapping не був завантажений. Перевіряємо у Firebase Console: Crashlytics → App → три точки → Mapping Files.
R8 fullMode та збереження потрібних символів
В AGP 8.x R8 fullMode увімкнений за замовчуванням та видаляє символи агресивніше. Для Retrofit, Gson, Room потрібні явні keep-правила:
# proguard-rules.pro
-keepattributes SourceFile,LineNumberTable
-keep class com.example.app.data.model.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
-keepattributes SourceFile,LineNumberTable — без цього mapping є, але номери рядків у стектрейсі будуть неверними.
Орієнтири по строкам
Налаштування для стандартного проекту з CI на GitHub Actions — 3–6 годин. Багатомодульний проект з NDK-компонентами та кількома flavors — 1–2 робочих дні, включаючи верифікацію за всіма варіантами збірки.







