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

Привет! Меня зовут Тимур Юсипов, я работаю над мобильной версией Авито. В статье рассказываю, как мы логируем бэкендовые и картиночные ошибки, и какую пользу от этого получили.

Зачем нужно логирование

В мобильном приложении ошибки могут случаться в нескольких точках: на уровне бэкенда, CDN или DDoS-щита, сети или кода мобильного приложения.

Варианты точек отказа в приложении
Варианты точек отказа в приложении

На бэкенде причины ошибок видны хорошо — у разработчика есть к нему доступ. DDoS-щиты и CDN для большинства компаний являются сторонними инструментами, поэтому в них всё уже не так прозрачно. Сеть и вовсе априори ненадежна. Хорошо хоть, что в коде мобильного приложения ошибок почти никогда не бывает (всегда же виноват бэкенд…). Короче говоря, точек отказа много, поэтому без логирования ошибок сложно понять, что же на самом деле ощущает пользователь.

Ок, давайте сделаем логирование. Есть три вида задач, которые можно решать с помощью логирования технических метрик: 

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

  2. Обнаруживать изменения во время AB-тестирования на небольшой аудитории. Например, если AB-тест запущен на 5% пользователей и в нём растут ошибки на 1%, то на общем графике реалтайм-мониторинга они будут незаметны. 

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

Чтобы решать эти задачи, нужно логировать разные данные и обрабатывать их по-разному.

Какой инструмент логирования используется в Авито

Готовых решений для логирования много, например Firebase, Grafana или Sentry. Среди них нужно выбрать то, которое подходит для конкретного проекта. При этом стоит смотреть:

  • какие из трёх видов задач решает выбранный инструмент;

  • какая версия доступна — платная или бесплатная; 

  • какие ограничения есть у инструмента, например, по длительности хранения данных и их объёму;

  • облачный ли инструмент или устанавливается на локальный сервер.

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

В Авито мы используем несколько инструментов под разные задачи. 

Инструменты логирования в Авито
Инструменты логирования в Авито

Для логирования ошибок в мобильном приложении мы пользуемся своей системой аналитики — с её помощью отслеживаем продуктовые события. Она состоит из мобильного SDK для отправки событий, транспорта и аналитического хранилища данных. Его дополняет система AB-тестирования — она показывает, в какой группе пользователей и с какой статистической значимостью изменилась каждая метрика.

У нашего инструмента логирования ошибок есть плюсы и минусы.

Плюсы

Минусы

— Мы хорошо понимаем, что именно и откуда логируем. Поэтому нам легко интерпретировать метрики и выдвигать гипотезы, почему что-то изменилось.

— Не нужно интегрироваться со сторонними системами, у которых есть много ограничений.

— Мы можем максимально снизить влияние логирования на производительность приложения.

— Нужно самим писать код и отслеживать, что инструмент работает корректно.

— Приходится обходить ограничения аналитического хранилища: например, в него нельзя загрузить полный ответ бэкенда.

— Нужно избегать излишне уникальных данных, иначе аналитические отчеты могут начать тормозить и занимать сильно больше места на диске.

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

Ошибки мы решили разделить по типам и подтипам — так удобнее их мониторить. Мы понимаем, в какой зоне случилась беда.

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

Ошибки бэкенда классифицируем и разделяем по статус-кодам HTTP и кастомным ответам, которые используются внутри Авито.

Подтипы ошибок бэкенда
Подтипы ошибок бэкенда

Ошибки сети определяем по каталогу NSURLErrorDomain. В нём есть понятные описания каждого кода ошибки, поэтому логировать по ним удобно. Ещё учитываем в логах низкоуровневые недокументированные ошибки из других доменов — они появляются редко. 

На Android определять ошибки сети оказалось не так удобно. Нужно отловить java.lang.Exception, потом определить, какое описание можно дать конкретному подтипу Exception, и достать из этого подтипа дополнительную информацию, если это возможно. Для того, чтобы подобрать хорошее описание всем таким подтипам Exception, мы заранее из кода библиотек и фреймворка достали вероятные ошибки на разных слоях и проставили им читаемые названия вручную. Это ошибки слоя OkHttp (все Exception'ы из пакетов okhttp.*), слоя ssl-соединения (ошибки из пакета javax.net.ssl), и «системного» слоя (ошибки из пакета java.net). Этого оказалось достаточно, и самые частые ошибки сети сейчас попадают в наш заготовленный список типов. 

Подтипы ошибок сети
Подтипы ошибок сети

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

Подтипы ошибок, связанных с картинками
Подтипы ошибок, связанных с картинками

Разобрались с типами и подтипами ошибок — этой информации достаточно для организации первой версии реалтайм мониторинга. Ещё можно добавить маску url адреса, по которму случилась ошибка. Например, если возросли ошибки парсинга, хочется сразу видеть, на каком семействе урлов. Мы эту информацию смотрим на отдельном графике.

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

А чтобы проанализировать саму причину ошибки, понадобится более подробная информация: текст ошибки, полный URL, параметры устройства, версия приложения, имя экрана, IP и тип соединения, имя оператора, response header CDN и так далее.

Откуда логируются ошибки в мобильном приложении

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

Схема передачи запросов и ответов в приложении
Схема передачи запросов и ответов в приложении

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

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

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

Схема передачи запросов и ответов в приложении, если ошибку удаётся решить повторным запросом
Схема передачи запросов и ответов в приложении, если ошибку удаётся решить повторным запросом

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

Зачем логировать ошибки с экранов приложения

Допустим, мы видим по дашборду, что некоторые ошибки выглядят частыми, поэтому мы считаем их значимыми. Но так ли это? Чтобы ответить на этот вопрос, мы должны научиться понимать, затрагивают ли они пользователей. Например, если один и тот же запрос регулярно отправляется по таймеру, чтобы обновить бейджики, то мы можем увидеть много ошибок просто из-за высокой частотности запроса, но при этом на пользователя они почти не будут влиять. 

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

Еще есть ошибки бэкенда, которые на самом деле непонятно, являются ли ошибкой с точки зрения бизнес логики приложения. Например, мы получили ответ Not Found при попытке открыть объявление. Возможно, объявления никогда не было, значит его ID попал в приложение по ошибке, и это проблема. Но если владелец удалил объявление, а мы пытаемся его открыть, и поэтому у нас не получается, то мы это уже не считаем за ошибку — это исключительная ситуация, которую приложение должно уметь правильно обрабатывать и рисовать пользователю соответствующий UI.

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

Еще может быть так, что пользователь будет по-разному видеть ошибки на разных экранах, хотя на сетевом слое ошибка может быть одной и той же. Так может происходить из-за продуктовой логики или неконсистентности дизайна.

Примеры отображения одной и той же ошибки в приложении
Примеры отображения одной и той же ошибки в приложении

Например, на всех изображениях сверху причина ошибки одна и та же — пропало соединение с интернетом, но отображается она по-разному. И тут мы подходим ко второй причине, почему важно логировать и видимые пользователю ошибки: именно об увиденном на экране тексте пользователь и будет сообщать в поддержку. Если мы сможем фильтровать ошибки сетевого слоя на основе текста, который видит пользователь, то нам будет легче отлаживать проблемы.

Если подытоживать здесь, то в Авито мы смотрим не только на метрики сетевого слоя (низкоуровневые), то и на видимые пользователю ошибки (верхнеуровневые). Венцом здесь являются царь-метрики, как мы их называем. Это число и доля пользователей, которые увидели в интерфейсе одну, больше двух, пяти и десяти ошибок. Аналогично с ошибками картинок.

Как логировать ошибки с экранов приложения

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

Схема передачи запросов-ответов и необходимой связи между событиями в логах
Схема передачи запросов-ответов и необходимой связи между событиями в логах

Кроме ID ошибки на экранном слое логируются идентификатор экрана, текст сообщения, которое увидел пользователь, и способ его отображения — внутри экрана, в виде алерта или всплывающего баннера (тоста).

Почему сложно логировать ошибки на экранах приложения

Есть две причины:

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

  2. В приложении Авито есть несколько экранов, где не отображается диалог об ошибке. Вместо этого появляется экран в пустом состоянии, как будто с бэкенда пришла не ошибка, а пустой массив данных. 

Пример экрана, который может показан как в случае ошибки, так и в случае отсутствия полезных данных
Пример экрана, который может показан как в случае ошибки, так и в случае отсутствия полезных данных

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

Короче говоря, логирование ошибок с экранов — довольно нудный ручной труд (потная катка).

Как залогировать ошибки с экранов приложения и не сойти с ума

Скорее всего у вас есть топ 10–15 экранов, через которые проходит больше 90% пользовательского трафика. Если залогировать видимые пользователю ошибки с этих экранов, то вы уже получите хорошую приближенную оценку реальных ощущений пользователя. С этого можно начать, это дешево.

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

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

Итого, мы приходим к потребности реализовать еще одну метрику (success rate’а). Добавить логирование для нее так же просто, как и в случае логирование низкоуровневых ошибок, тк все сетевые запросы у вас скорее всего проходят через общий сетевой слой. 

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

Что у нас получилось в итоге

У нас есть число ошибок сетевых запросов по их типам, success rate по каждому URL, детали ошибок, привязка ошибок к экранам и способам их отображения пользователю, число и доля пользователей, которые сталкиваются с ошибкой. Теперь мы можем мониторить состояние приложения в продакшене, обнаруживать рост ошибок в AB-тестах и разбираться в их причинах.

Какую пользу принесло логирование ошибок в мобильном приложении

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

Удалось отловить и исправить несколько частых ошибок: 
  • Бэкенд больше не присылает неверный URL картинки (250к в день).

  • Обнаружили, что отменяются некоторые первые сетевые запросы на запуске приложения — причиной стала проблема в многопоточности (300к в день).

  • Увидели и исправили проблему с неравномерным распределением CPU на воркеры NGINX. Оно приводило к большому количеству 502-ых ошибок, даже когда upstream’овый сервис отвечал успешно.

  • CDN больше не отправляет картинки в формате webp на iOS 13 и ниже — они в них не поддерживаются (200к в день).

  • Исправили ошибку парсинга JSON на главной странице (30к в день). При этом парсинг падал на поле, которое приложение даже не использовало. 

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

Ещё мы узнали, насколько стабильно работают CDN и DDoS-щит. И что иногда приложение теряет доступ к мобильной сети, и пользователей нужно направлять в системные настройки. 

Также мы исправили большое количество видимых пользователю ошибок. Для этого добавили несколько повторных попыток на ошибки установки SSL-соединения и разрешении DNS. В AB-тесте мы не увидели, как они влияют на продуктовые метрики. Возможно, потому что пользователь и так не уходил из приложения в случае ошибки. Он просто повторно отправлял запрос вручную, а теперь мы стали это делать за него автоматически. Будьте внимательны с повторными попытками, т.к. они могут окончательно завалить ваш бэкенд, когда он и так тяжело работает (читай про exponential backoff retry). 

В планах осталось:
  • Подсчитать, как влияют ошибки на конверсию в продуктовых воронках. 

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

  • Подробнее изучить ошибки DNS, SSL, сетевые ошибки на ряде подсетей, и мутные, редкие ошибки парсинга по причине получения чуток обрезанного JSON

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

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


  1. storoj
    12.08.2022 15:59
    +1

    Какой смысл логировать HTTP ошибки на клиенте? Их же отдал сервер, пусть он и логирует.


    1. Hardcoin
      12.08.2022 19:32
      +1

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


  1. zatim
    12.08.2022 16:01
    +1

    Уже больше месяца авитовцы не могут мне исправить ошибку - в архиве "зависло" одно объявление, и не удаляется и зайти в него нельзя, потому что его нет. ТП сначала нафиг посылала, теперь завтраками кормит. Может через хабру можно достучаться до того программиста, который может это объявление удалить?


    1. avitocare
      12.08.2022 16:07
      +1

      Здравствуйте. Действительно, сейчас в аккаунте могут показываться не все объявления. Эта проблема, которую мы уже решаем. Неполадка никак не влияет на работу с активными объявлениями: другие пользователи видят их в поиске и на странице профиля, поэтому вы можете продолжить использовать сервис.


  1. storoj
    12.08.2022 16:04

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


    1. TachikomaGT
      12.08.2022 19:53
      +1

      Хоть IO и составляют основную массу ошибок, ошибки приложения, библиотек и разношёрстных девайсов никуда не деваются. Да и IO – факт плохого пользовательского опыта и повод не забывать, в какой среде используется приложение. К тому же, например, может оказаться что какой-нибудь из HTTP-запросов отдаёт верный ответ, но на прокси ломается или в таймаут не вписывается и его нужно исправлять. Пусть логгируется как можно больше, а что нужно, а что не нужно уже потом видно будет.


      1. storoj
        12.08.2022 19:56

        А ошибки отправки аналитики об ошибках считаются равноправными ошибками?


        1. TachikomaGT
          12.08.2022 20:10

          нет, как сможем – отправим то что сохранили


  1. TachikomaGT
    12.08.2022 20:40

    По опыту Android, хорошо себя показывает Sentry, не только для крешей, но и при обработке прочих ошибок, с логгированием контекста (логи приложения уровня info и выше дублируются как breadcrumb, профиль пользователя и extra – в Sentry Context). Особенно, при использовании Logback, раз настроил appender и забыл. В случае iOS, хорошим вариантом мне видится SwiftLog с соответственно настроенным бэкендом логгера.