Сбор и анализ ошибок в приложениях с помощью Tracer

Введение

Одна из главных задач при разработке мобильных приложений (и в целом ПО) – создать работоспособную и отказоустойчивую систему. Приложение, которое стабильно работает и выполняет свою функцию так, как задумано, с большей вероятностью привлечёт пользователей, чем приложение, которое закрывается с ошибкой на старте. Более того, плохо работающее приложение может стать причиной негативных оценок, что может сказаться на дальнейшем продвижении приложения и увеличении аудитории.

Однако сразу написать идеально работающее приложение задача крайне сложная. Даже если во время разработки учесть все возможные варианты сбоев и ошибок, нет гарантии, что в проде не попадётся специфичное устройство, на котором по той или иной причине произойдёт сбой. И если мы никак не можем повлиять на различные системные ошибки, то исправление сбоев в рамках приложения это способ сделать приложение лучше.

Поэтому на стадии поддержки ПО важное место занимает поиск и анализ сбоев у пользователей. Если вы пишете мобильное приложение под Android и публикуетесь в Google Play, то, вероятнее всего, используете для этой задачи Android Vitals, который помогает собирать ошибки у пользователей, установивших приложение из Google Play. Но этого может быть недостаточно, если вы публикуетесь сразу в нескольких разных маркетах или пишете под мультиплатформу (Android/iOS). И тут на помощь приходят другие решения.

Одним из таких является сервис Tracer, разработанный технической командой OK.Tech. С его помощью можно удобно отлавливать и анализировать сбои и ошибки, находить утечки памяти и выявлять причины плохой работы приложения. Сервис относительно недавно добрался до первой стабильной версии и на данный момент доступен для Android и iOS, однако в будущем разработчики планируют добавить новые платформы.

Кроме того, Tracer представляет собой модульное решение. Разработчики приложений могут подключить и использовать только те части функционала Tracer, которые им нужны.

В этой статье мы разберёмся, как настроить Tracer и подключить его для работы в приложении на Android.

Регистрация и создание проекта

Перед началом работы нужно зарегистрироваться и создать свой первый проект. Для этого нужно перейти на сайт Tracer, где будет предложено создать аккаунт через VK ID. Если у вас нет VK ID, то существует опция создать его с помощью телефона/почты или войти через Яндекс ID.

После того, как аккаунт будет зарегистрирован, можно приступить к первоначальной настройке. Нужно создать организацию, к которой будут привязаны будущие приложения. В верхнем меню выбираем “Организация” -> “Создать организацию” и указываем название.

Теперь в рамках организации мы можем создавать проекты. По сути каждый проект представляет собой одно приложение, для которого мы будем отслеживать сбои. Чтобы создать новый проект, нажимаем на “Создать проект“, выбираем требуемую платформу и указываем название проекта.

После создания проекта мы попадаем на панель управления проектом, где, пока что, можно видеть только пустые графики и списки. Теперь мы готовы к тому, чтобы подключить наш проект к приложению.

Подключение в приложении

Чтобы Tracer начал отправлять ошибки и сбои из приложения, нужно подключить плагин и добавить требуемые библиотеки.

Для примера создадим пустой проект на Android. В libs.versions.toml добавим плагин Tracer и те компоненты, которые ходим использовать в приложении. Допустим, что нам нужно только отлавливать ошибки и сбои (в том числе нативные) и отслеживать утечки памяти.

[versions]
...
tracer="1.0.4"

[libraries]
...
tracer = { module = "ru.ok.tracer:tracer-platform", version.ref = "tracer" }
tracer-crash-report = { module = "ru.ok.tracer:tracer-crash-report" }
tracer-crash-report-native = { module = "ru.ok.tracer:tracer-crash-report-native" }
tracer-heap-dumps = { module = "ru.ok.tracer:tracer-heap-dumps" }

[plugins]
...
tracer = { id = "ru.ok.tracer", version.ref = "tracer" }

Актуальная версия Tracer на момент написания статьи 1.0.4, однако перед добавлением лучше проверить это в документации.

Tracer поддерживает BOM (Bill of Materials), благодаря чему нам достаточно только подключить зависимость “ru.ok.tracer:tracer-platform” с указанием версии, после чего выбрать только те артефакты, которые нам требуются. Версии этих артефактов будут автоматически указываться в соответствии с данными из BOM.

Ранее Tracer требовал добавить в settings.gradle.kts свой репозиторий для получения артефактов, однако в настоящее время SDK публикуется в Maven Central, поэтому просто убедимся, что у нас указан mavenCentral().

pluginManagement {
  repositories {
    google {
      content {
        includeGroupByRegex("com\\.android.*")
        includeGroupByRegex("com\\.google.*")
        includeGroupByRegex("androidx.*")
      }
    }
    mavenCentral()
    gradlePluginPortal()
  }
}
dependencyResolutionManagement {
  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
  repositories {
    google()
    mavenCentral()
  }
}

В build.gradle.kts модуля app подключим плагин и добавим зависимости.

plugins {
  ...
  // Подключаем плагин
  alias(libs.plugins.tracer)
}

android {
  ...
}

dependencies {
  ...

  // BOM Tracer
  implementation(platform(libs.tracer))
  // Сбор и анализ крешей и ANR
  implementation(libs.tracer.crash.report)
  // Сбор и анализ нативных крешей
  implementation(libs.tracer.crash.report.native)
  // Сбор и анализ хипдапмов при OOM
  implementation(libs.tracer.heap.dumps)
}

// Конфигурация Tracer
tracer {
  create("defaultConfig") {
    pluginToken = "PLUGIN_TOKEN"
    appToken = "APP_TOKEN"

    // Включает загрузку маппингов
    uploadMapping = true
    // Включает загрузку отладочной информации из нативных библиотек
    uploadNativeSymbols = true
  }
}

В блоке tracer { } также можно задать дополнительные настройки, например дополнительный путь для отладочных библиотек. Также можно настроить конфигурацию в зависимости от выбранного в приложении flavor. Более подробно с этим можно ознакомиться здесь.

Большинство настроек опциональны. Главное, что нам здесь нужно, это задать pluginToken и appToken. Чтобы получить их, вернёмся в созданный ранее проект в Tracer и зайдём в Настройки. Здесь, на вкладке Проект, мы увидим требуемые appToken и pluginToken, которые и вставляем в наше приложение.

Соберём приложение и проверим. Если Tracer сконфигурирован правильно, то в логах мы увидим соответствующее сообщение.

Обратите внимание на ворнинг “Application does not implement HasTracerConfiguration”. Мы подключили основные компоненты Tracer, однако теперь нам требуется настроить те модули, которые мы будем использовать в приложении.

Для этого в классе, наследующем от Application, реализуем интерфейс HasTracerConfiguration.

class App : Application(), HasTracerConfiguration {
  
}

Этот интерфейс содержит список tracerConfiguration, который нужно переопределить. В нём и будет происходит инициализация и настройка добавленных модулей.

class App : Application(), HasTracerConfiguration {
  override val tracerConfiguration: List
    get() = listOf(
      CoreTracerConfiguration.build {
        // Опции ядра Tracer
      },
      CrashReportConfiguration.build {
        // Опции сборщика крэшей
      },
      HeapDumpConfiguration.build {
        // Опции сборщика хипдампов при ООМ
      },
    )
}

Поскольку мы подключили только модуль для сбора ошибок и хипдампов, нам достаточно сконфигурировать только их. Однако если нужен остальной функционал, то вы также можете их подключить и настроить в классе Application.

Таким образом, мы можем настраивать каждый модуль Tracer независимо друг от друга. Эти модули имеют различные параметры для настройки работы, более подробно про них можно почитать в документации. В большинстве случаев эти настройки опциональны и не требуются.

Запустим приложение. На этот раз ворнинг пропал, а это значит, что подключение Tracer завершено и мы готовы отлавливать сбои.

Сбор и анализ ошибок

Чтобы увидеть, как Tracer работает со сбоями, нам нужно его спровоцировать в приложении. Допустим, мы “случайно” допустили ошибку в коде и где-то в приложении произошло деление на ноль.

binding.fab.setOnClickListener {
  // IDE покажет ворнинг на этой строке с предупреждением о делении на ноль, но допустим мы его проигнорировали
  val number = 5 / 0
}

Подобная ошибка должна спровоцировать исключение java.lang.ArithmeticException. И действительно, когда мы нажимаем на кнопку, то приложение закрывается с ошибкой. В это же время Tracer отлавливает это исключение и отправляет его на сервер.

Вернёмся к нашему проекту в Tracer и проверим. Теперь в списке сбоев у нас появилась только что произошедшая в приложении ошибка.

Tracer позволяет настраивать списки сбоев любым удобным для нас образом. Мы можем указать, для какой версии приложения нужно отобразить сбои, за какой промежуток времени, должен ли это быть debug или release. Также мы можем отфильтровать те сбои, которые произошли в фоне.

Аналогично мы можем кастомизировать графики для показа требуемой информации.

Основное, что нас интересует из списка, это имя метода, в котором произошёл сбой, и собственно текст ошибки. Tracer группирует похожие сбои, что позволяет узнать, на каком количестве устройств произошёл один и тот же сбой и на какой версии приложения.

При выборе сбоя откроется более подробное описание, относящееся к текущему сбою.

Здесь мы можем увидеть, на каком количестве устройств произошёл сбой, их основные параметры (модель, производитель, версия Android и т.д.), а также полный stacktrace с ошибкой. Как видно из стектрейса, сбой произошёл где-то в лямбда-выражении в MainActivity, т.е. в той самой лямбде, которую мы передали в setOnClickListener().

Как и в случае с общим списком ошибок, здесь мы также можем отфильтровать сбои по различным параметрам или настроить отображение графика.

Помимо stacktrace, здесь есть ещё несколько вкладок, которые могут содержать полезную для анализа информацию.

Вкладка Ключи содержит в себе пары “ключ-значение”, которые мы можем задавать самостоятельно в приложении. Tracer будет отображать процент ошибок, произошедших с заданным ключом. Это можно использовать, к примеру, при A/B тестировании. Если количество сбоев в тестовой группе будет сильно больше, чем в контрольной, то можно судить о том, что проблему нужно искать в эксперименте.

Например, установим ключ в зависимости от текущей ориентации приложения. Чтобы это сделать, вызовем метод Tracer.setKey(), который принимает два параметра ключ и значение.

if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
  Tracer.setKey("orientation", "portrait")
} else {
  Tracer.setKey("orientation", "landscape")
}

Спровоцируем сбой сначала в портретной ориентации, затем в альбомной. В отчёте мы увидим, что какой значение ключа была у выбранной сессии, а также соотношение сбоев с другими значениями этого ключа.

Вкладка Логи отображает последние записанные в приложении логи, которые предшествовали сбою. К примеру, мы можем залогировать переходы пользователя между экранами или работу с каким-то конкретным сегментом приложения. На основании этих данных можно составить картину того, какое состояние было у приложения на момент сбоя. Однако следует запомнить, что количество логов ограничено: Tracer собирает не все, а только последние 64 килобайта.

К примеру, в нашем тестовом приложении есть два фрагмента. Залогируем переходы пользователя между ними. Сделать это можно с помощью метода TracerCrashReport.log(). Добавим лог в первый фрагмент.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  super.onViewCreated(view, savedInstanceState)

  TracerCrashReport.log("Пользователь перешёл в Fragment 1")
  binding.buttonFirst.setOnClickListener {
    findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
  }
}

Аналогичным образом добавим во второй.

Теперь заново воспроизведём сбой и отправим новые данные на сервер. Как мы видим, теперь в логах вместе со сбоем мы можем посмотреть историю переходов между фрагментами.

Вкладка Данные содержит в себе общие данные об устройстве и операционной системе. Эти данные собирает Tracer, но мы также можем создавать свои собственные данные и передавать их на сервер. В этом случае они будут отображаться в этой вкладке после основных.

Допустим, мы хотим видеть в данных локаль устройства. Для этого воспользуемся методом Tracer.setCustomProperty().

Tracer.setCustomProperty("Language", resources.configuration.locales.get(0).displayLanguage)

Когда произойдёт сбой, в Данных мы увидим у выбранного пользователя, какая локаль была на его устройстве.

Мы также можем назначить пользователю некий ID, по которому можно отслеживать, какие сбои были связаны конкретно с этим пользователем.

Важно учесть, что сам по себе Tracer всю вышеперечисленную информацию не собирает, это нужно делать разработчику самостоятельно. Более подробно можно прочитать здесь.

Дополнительно, для анализа ошибок Tracer предлагает использовать ИИ. При запросе совета появится отдельное окно, в котором на основе стектрейса будет предложено возможное решение проблемы. Однако это решение не всегда правильное и информацию следует перепроверять, но как дополнительный инструмент может помочь с какими-нибудь простыми ошибками.

На любой сбой можно подписаться, чтобы получать уведомления. При этом можно настроить условия для срабатывания уведомления: отправлять его, если сбой стал самым массовым или при достижении определённого порога случаев. Также можно заглушить сбой, если мы не хотим его видеть по той или иной причине.

Вернёмся к списку сбоев. Помимо сбора ошибок в приложении бывают случаи, когда нам нужно проанализировать некритический сбой. Например, у нас возникла ситуация, при которой приложение повело себя не так, как планировалось, но к остановке приложения это не привело.

В этом случае мы можем воспользоваться методом TracerCrashReport.report(), в который нужно передать объект Throwable.

Поскольку группировать такие сбои как обычные не имеет смысла, для разделения их между собой есть опциональный параметр issueKey, по которому мы далее будем отслеживать сбои.

Расширим наш изначальный пример. Теперь мы проверяем, что второй операнд равен нулю, и в этом случае отправляем некритический сбой в Tracer, а приложение продолжает свою работу без ошибок.

binding.fab.setOnClickListener {
  val firstOperand = 5
  val secondOperand = 0
  if (secondOperand != 0) {
    val number = firstOperand / secondOperand
  } else {
    TracerCrashReport.report(IllegalArgumentException("Second operand is zero"), "ISSUE-MATH")
  }
}

Теперь в списке сбоев появилась новая ошибка. Помимо названия метода и описания ошибки к ней дописывается переданный issueKey, а тип обозначен как NON_FATAL.

С такими сбоями можно работать точно также, как с обычными, они содержат те же данные и логи. Дополнительно мы теперь можем фильтровать список сбоев по issueKey. Благодаря этому мы можем анализировать случаи, когда приложение работает некорректно.

Заключение

Подводя итоги, мы изучили настройку и работу Tracer на Android, а также изучили основные инструменты для анализа сбоя.

Благодаря сервису можно удобно работать со сбоями в приложении, анализировать причины их появления и находить решения. Инструментарий по логированию и добавлению дополнительной информации к сбою позволяет лучше видеть картину произошедшего и делать выводы на основе этого. Фильтры и графики помогают работать с большим количеством самых разных сбоев, выделяя среди них самые важные.

Мы не затронули в статье другие аспекты Tracer, такие как работу с памятью и профайлеры, однако вы можете изучить их работу более подробно в документации. Профайлеры не связаны напрямую со сбоями, однако они помогают определять, как приложение работает на том или ином устройстве, находить проблемные участки и решать их.

Хотя в будущем разработчики планируют добавить больше новых фичей и поддержку новых платформ, уже сейчас Tracer представляет собой мощный инструмент, который облегчает разработку и дальнейшую поддержку приложений.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *