Настройка 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 рабочих дня, включая верификацию по всем вариантам сборки.







