Привет! Меня зовут Даниил Климчук, я работаю в команде, занимающейся 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

Логотип Tracer
Логотип Tracer

Основной 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 можно проанализировать отчёт, увидев частоту воспроизводимости ошибки, распределение по параметрам девайса, полный стектрейс и другие данные:

Интерфейс Tracer
Интерфейс Tracer

Логируем аналитику

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

Отображение аналитики в Tracer
Отображение аналитики в Tracer

Собираем метрики производительности

Помимо крашей в 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 можно увидеть производительность метода:

Перформанс метрики в Tracer
Перформанс метрики в Tracer

 Сбор перформанс-метрик от AppTracer пока что находится на бета-тестировании, но скоро станет доступен всем.

Подводные камни реализации

Обфускация

Как известно, почти все приложения обфусцируются с помощью ProGuard, из-за этого в дефолтном варианте non-fatal тоже приходят обфусцированные. У вас как авторов библиотек есть несколько вариантов решения этой проблемы:

  1. Просить клиентов настроить Tracer в их приложениях.
    Основной SDK Tracer, так же как Firebase Crashlytics, умеет деобфусцировать краши с помощью загрузки маппинга ProGuard при сборке. По маппингу названий классов в обфускированой APK к их названиям в коде сервисы восстанавливают стектрейс. Для библиотек это сделать нельзя, потому что в Maven Central хранятся уже собранные AAR-пакеты, и Tracer не может выполнить код без подключения его плагина в коде приложения клиента. Поэтому можно попросить клиентов, которые готовы участвовать в улучшении качества библиотеки, подключить основной SDK Tracer в их проекты, благодаря чему будут загружаться маппинг и для библиотеки. Им потребуется только минимальная настройка плагина в Gradle-скриптах с указанием  токенов, саму инициализацию Tracer в коде делать не нужно.

  2. Не обфусцировать имена в библиотеке.
    Я пошёл по более простому и удобному варианту, поскольку наш SDK в любом случае open-source. В consumerProguardFiles библиотеки можно добавить правила, которые будут применять клиенты при обфускации. Я добавил отмену обфускации имён, что сделало стектрейсы читаемыми:

  3. -keepnames class com.vk.id.** { *; }

  4. -keepclassmembernames class com.vk.id.** { *; }

Crash-free интеграция Tracer

Лишняя зависимость в библиотеке может напугать пользователей, которые боятся лишних ошибок. Опять же, для решения проблемы есть два подхода:

  1. Добавить фича-тоггл для отключения Tracer при потенциальных проблемах.
    Механизм фича-тогглинга достаточно известен в разработке, подробно о нём я рассказывать не буду. Он позволяет отключить фичу и при достаточном уровне сервиса, с помощью которого он реализован, даже выключать работу кода на определённых версиях библиотеки. Но зависимость вроде Firebase Remote Config принесёт те же проблемы: не встраивать же фича-тоггл в инструмент для фича-тогглов. А самописное решение достаточно сложно в реализации.

  2. Более простой подход — обернуть обращения к 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 вам пригодится. Буду рад ответить на ваши вопросы в комментариях к статье.

Комментарии (0)