Привет! Меня зовут Даниил Климчук, я работаю в команде, занимающейся SDK для авторизации через сервисы экосистемы VK. В него входит несколько компонентов, а именно авторизация по протоколу OAuth 2.1, кнопка One Tap для авторизации в один клик, шторка с описанием сценария авторизации и поддержка авторизации через Mail и OK.

Мы усиленно работаем над качеством нашего SDK, и одним из важных аспектов стал контроль работы SDK на устройствах пользователей. Было решено собирать краши и перформанс-метрики. Для этого отлично подходит новый инструмент AppTracer Lite SDK, разработанный в VK. Сейчас он доступен только внутри VK, но если вас заинтересовало решение и вы хотите внедрить его к себе, напишите в телеграм-чат: t.me/tracer_feedback — обсудим такую возможность.
В статье я расскажу о самом инструменте, о том, для чего его можно использовать, и поделюсь подводными камнями, с которыми я столкнулся при разработке. Подход, описанный тут, универсален, при рассказе я буду говорить вещи, применимые к любой библиотеке.
Зачем следить за качеством библиотеки
В Android-сообществе, как и в большинстве областей IT, развита культура использования сторонних библиотек. Список зависимостей среднестатистического Android-проекта растёт не по дням, а по часам. Каждая из зависимостей может принести баги и проблемы с производительностью, поэтому в крупных компаниях следят за качеством библиотек, которые они используют. Чтобы соответствовать даже самым высоким требованиям, нужно исследовать качество кода, в том числе и во время выполнения в проде. Некоторые проблемы воспроизводятся только при определённых условиях, и их можно пропустить при тестировании. Баг-репорты занимают время, а чем раньше вы получите информацию об ошибке, тем меньше ваших клиентов она затронет.
Что такое AppTracer SDK

Основной SDK AppTracer от VK — российский аналог Firebase Crashlytics, который уже поддерживает всю важную функциональность. Для подавляющего большинства Android-разработчиков Firebase Crashlytics — знакомое решение проблемы сбора крашей. Однако для авторов библиотек это решение не подходит: оно инициализируется в самом приложении, и встроить два клиента (один для библиотеки, а второй для основного приложения) не получится. AppTracer Lite SDK решает эту проблему, позволяя инициализировать несколько SDK, в том числе и параллельно с Crashlytics. В Lite SDK немного урезанный набор фич. Например, сбор крашей возможен только в виде ручной отправки non-fatal. Crashlytics и основной AppTracer SDK регистрируют обработчик. Кроме этого, есть сбор перформанс-метрик и запись логов.
Как трекать качество библиотеки
Для контроля качества библиотеки я использовал несколько инструментов, ниже расскажу, какие именно и как.
Собираем краши
Итак, понятно, что собирать краши автоматически не получится, теперь нужно понять, что с этим делать. Обычно, если нужно перехватить исключение, используют конструкцию try-catch, с помощью него и будем перехватывать исключения в нашем коде. Нужно обернуть все точки входа в код библиотеки, а после этого залогировать исключения в AppTracer. Это намного более удобный подход, чем логировать то же самое, например, в Sentry, потому что у AppTracer есть удобный интерфейс для просмотра и фильтрации крашей.
В базовом варианте точками входа в библиотеку будут её методы, но если у вас есть UI, то надо не забыть обернуть функции, которые вызывает система, например YourActivity#onCreate. Также, если в библиотеке есть @Composable, придётся потрудиться, чтобы собирать из них краши. @Composable нельзя обернуть в try-catch, поэтому нужно оборачивать сам код, который вызывается из них. Из-за того, что Compose может запустить любую @Composable функцию отдельно после обновления параметров, конструкция try-catch не будет вызвана. К сожалению, это ограничение Compose и с ним ничего поделать нельзя.
Для логирования крашей заведём простой класс:
private val crashReporter: TracerCrashReportLite
public fun <T> runReportingCrashes(
errorValueProvider: (error: Throwable) -> T,
action: () -> T
): T {
try {
return action()
} catch (t: Throwable) {
crashReporter.report(t)
return errorValueProvider(t)
}
}
Как мы видим, ошибки в коде перехватываются и логируются. При исключении возвращается дефолтное значение, чтобы дать время Tracer отправить исключение. Разработчики обещают реализовать сохранение non-fatal, чтобы можно было пробрасывать исключения в ближайшем будущем. Если используются корутины, то правильнее игнорировать возникающие CancellationException, поскольку это механизм завершения корутины, а не ошибка.
Далее все методы библиотеки запускаются через CrashReporter:
private val crashReporter: CrashReporter
fun libraryMethod() {
crashReporter.runReportingCrashes({ defaultValue}) {
...
}
}
После этого в интерфейсе Tracer можно проанализировать отчёт, увидев частоту воспроизводимости ошибки, распределение по параметрам девайса, полный стектрейс и другие данные:

Логируем аналитику
Наш SDK собирает аналитику для того, чтобы мы понимали, как ведут себя пользователи, и делали выводы на основе реальных данных. Оказывается, эти же данные полезны для анализа потенциальных крашей. Если залогировать аналитику перед отправкой non-fatal, можно будет понять, какие действия пользователя привели к ошибке. Это даёт полезную информацию, например, чтобы понять состояние системы в момент отправки non-fatal:

Собираем метрики производительности
Помимо крашей в SDK, качество библиотеки определяется её производительностью. Чтобы оценивать скорость работы и видеть проседания в работе в зависимости от девайса или OS, мы собираем перформанс-метрики. В базовом варианте было достаточно добавить замер вычисления методов SDK. Более сложный подход — мерить критичные участки кода, например, в нашем SDK измеряется время загрузки WebView. Реализация выглядит вот так:
internal class TracerPerformanceTracker {
private val reporter: TracerPerformanceMetricsLite = null!!/*...*/
private val startTimes = ConcurrentHashMap<String, Long>()
@Synchronized
fun startTracking(key: String) {
startTimes[key] = SystemClock.elapsedRealtimeNanos()
}
@Synchronized
fun endTracking(key: String) {
val startTime = startTimes.remove(key) ?: return
val trackedTime = SystemClock.elapsedRealtimeNanos() - startTime
try {
reporter.sample(
key,
trackedTime,
TimeUnit.NANOSECONDS
)
} catch (_: Throwable) {
}
}
fun runTracking(key: String, action: () -> Unit) {
startTracking(key)
action()
endTracking(key)
}
suspend fun runTrackingSuspend(key: String, action: suspend () -> Unit) {
startTracking(key)
action()
endTracking(key)
}
}
Сами методы SDK оборачиваются в обращения к этому классу:
public fun libraryMethod() {
performanceTracker.runTracking("MetricName") {
...
}
}
По итогу в интерфейсе Tracer можно увидеть производительность метода:

Сбор перформанс-метрик от AppTracer пока что находится на бета-тестировании, но скоро станет доступен всем.
Подводные камни реализации
Обфускация
Как известно, почти все приложения обфусцируются с помощью ProGuard, из-за этого в дефолтном варианте non-fatal тоже приходят обфусцированные. У вас как авторов библиотек есть несколько вариантов решения этой проблемы:
Просить клиентов настроить Tracer в их приложениях.
Основной SDK Tracer, так же как Firebase Crashlytics, умеет деобфусцировать краши с помощью загрузки маппинга ProGuard при сборке. По маппингу названий классов в обфускированой APK к их названиям в коде сервисы восстанавливают стектрейс. Для библиотек это сделать нельзя, потому что в Maven Central хранятся уже собранные AAR-пакеты, и Tracer не может выполнить код без подключения его плагина в коде приложения клиента. Поэтому можно попросить клиентов, которые готовы участвовать в улучшении качества библиотеки, подключить основной SDK Tracer в их проекты, благодаря чему будут загружаться маппинг и для библиотеки. Им потребуется только минимальная настройка плагина в Gradle-скриптах с указанием токенов, саму инициализацию Tracer в коде делать не нужно.Не обфусцировать имена в библиотеке.
Я пошёл по более простому и удобному варианту, поскольку наш SDK в любом случае open-source. В consumerProguardFiles библиотеки можно добавить правила, которые будут применять клиенты при обфускации. Я добавил отмену обфускации имён, что сделало стектрейсы читаемыми:-keepnames class com.vk.id.** { *; }
-keepclassmembernames class com.vk.id.** { *; }
Crash-free интеграция Tracer
Лишняя зависимость в библиотеке может напугать пользователей, которые боятся лишних ошибок. Опять же, для решения проблемы есть два подхода:
Добавить фича-тоггл для отключения Tracer при потенциальных проблемах.
Механизм фича-тогглинга достаточно известен в разработке, подробно о нём я рассказывать не буду. Он позволяет отключить фичу и при достаточном уровне сервиса, с помощью которого он реализован, даже выключать работу кода на определённых версиях библиотеки. Но зависимость вроде Firebase Remote Config принесёт те же проблемы: не встраивать же фича-тоггл в инструмент для фича-тогглов. А самописное решение достаточно сложно в реализации.-
Более простой подход — обернуть обращения к Tracer в try-catch, чтобы избежать ошибок в коде из-за него. Хоть шанс на краши в чужом инструменте и маленький, этот подход обезопасит клиентов. Все обращения оборачиваются тривиально, главное — не забыть про конструкторы, в которых теоретически тоже может быть краш. А для фоновых потоков у Tracer есть параметр инициализации:
TracerLite( … configuration = TracerLite.Configuration.build { ioExecutor = Executors.newCachedThreadPool { object : Thread() { override fun run() { try { it.run() } catch (_: Throwable) { } } } } }, )
Поддержка отключения Tracer в библиотеке
Тем не менее некоторые крупные клиенты могут отказаться затягивать к себе стороннюю зависимость вместе с вашей библиотекой. Для решения этой проблемы у нас я сделал опциональный модуль с noop-трекингом. В итоге у меня получилось 3 модуля. Один с интерфейсами, которые подключаются в коде SDK, второй с реализацией для Tracer и DI-компонентом, который эти зависимости предоставляет, и третий с noop-реализацией и также с DI-компонентом для неё. После этого клиенты могут заменить Gradle-модуль с Tracer на noop-модуль:
subprojects {
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(module("com.vk.id:tracking-tracer")
.using(module("com.vk.id:tracking-noop"))
}
}
}
Заключение
С помощью Tracer можно замерять производительность любой библиотеки. На мой взгляд, в развивающемся мире Android-разработки за ним будущее. Качество библиотеки — важный аспект. Пользователям нравятся библиотеки, которые стараются быть производительнее. Напомню, что, если вы хотите внедрить AppTracer Lite к себе в библиотеку, стоит написать в чат: t.me/tracer_feedback — и обсудить такую возможность. Надеюсь, мой опыт интеграции Tracer вам пригодится. Буду рад ответить на ваши вопросы в комментариях к статье.