В прошлый раз мы рассматривали внедрение Sentry со стороны эксплуатации: устанавливали на сервер self-hosted, делали его высокодоступным при помощи сети доставки td-agent, настраивали мониторинг. Ожидаются небольшие дополнения.

Теперь рассмотрим процесс внедрения Sentry со стороны команды разработки.

Релизы и интеграция с gitlab

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

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

Если кратко, то релиз состоит из названия, коммитов, деплоев и конечно же статистики.

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

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

Подробнее про релизы для backend на PHP и frontend на Javascript поговорим ниже.

Общие приемы клиентов

Сначала мы рассмотрим общие моменты, которые необходимо реализовать, а затем затронем использование конкретного клиента на конкретной платформе.

Автоматический перехват ошибок и ручная отправка

Клиенты Sentry поддерживают ручную отправку событий, например как это сделано в тестовом стенде php-tdagent-sentry:

try {
    $a = new B();
} catch (\Throwable $exception) {
    \Sentry\captureException($exception);
}

Но могут и самостоятельно отслеживать ошибки в приложении. В пакете sentry-php есть файл ErrorHandler.php, который использует функции обработки ошибок:

  • set_error_handler - для перехвата ошибок

  • set_exception_handler - для перехвата выброшенных, но неперехваченных исключений

  • register_shutdown_function - для перехвата критических ошибок, при завершении работы скрипта

Однако, как это будет работать зависит от того с какими интеграциями мы сконфигурируем клиент.

Для указания кастомного набора интеграций необходимо в конструктор передать default_integrations => false, а в ключ integrations массив объектов интеграций.

За автоматический перехват ошибок отвечаются интеграции ExceptionListenerIntegration, ErrorListenerIntegration, FatalErrorListenerIntegration. Детали по ссылке выше.

Кстати, примерно тоже самое относится и к Javascript, набор интеграций больше, однако там своя история, количество интеграций влияет на размер конечного бандла, но об этом позже.

Обогащение событий

События можно обогощать:

  • тегами, для своего backend мы используем тег application потому что у нас один проект реализует несколько приложений, а для фоновых процессов используем название профиля, имя воркера и таска. Кроме того в тэгах можно передавать пользователя. Прошло время и разработчики сами начали засовывать в теги нужные классификаторы багов.

Backend

У нас есть продукт на PHP, условный монолит - единая кодовая база, в который мы хотели внедрить Sentry.

Клиент

Для PHP у Sentry есть пакет sentry/sdk, который является оберткой над другим клиентом.

Стоит акцентировать внимание на версии PHP. Более-менее свежие версии клиента требуют PHP 7.2, начиная с версии клиента 3.0.0, а более старый клиент версии 2.0.0 требует PHP 7.1. Поэтому перед внедрением стоит сверить версии.

Клиент использует store endpoint. Использование самого клиента описано в документации.

Но у нас были свои требования к клиенту:

  • нам нужно было внедрять в наш проект где все строится на объектах и предоставить разработчикам минимально-необходимый интерфейс, чтобы им не пришлось знакомиться с философией Sentry SDK

  • нам нужна надежная транспортировка событий через td-agent, а sentry/sdk поддерживает только http

  • в разных проектах у нас разные обработчики, где-то мы полагаемся на фреймворк, а где-то хотим руками ловить ошибки

Все эти детали мы скрыли за простым интерфейсом в нашем клиенте sentry-client. Разработчики переходя из команды в команду будут работать с тем же интерфейсом.

Релизы

Доставляем релизы проекта на php до Sentry из кода при помощи sentry-cli в CI/CD конвейере при деплое. А с образом sentry-cli в docker это становится проще:

docker run --rm \
  -v $(pwd):/work \
  -e SENTRY_URL=$SENTRY_URL \
  -e SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN \
  -e SENTRY_ORG=$SENTRY_ORG \
  -u "$(id -u):$(id -g)" \
  getsentry/sentry-cli \
  releases set-commits --initial-depth 10000 --ignore-missing $CI_PIPELINE_CREATED_AT

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

Frontend

Интеграция Sentry в проект на PHP была простой, чего не скажешь про интеграцию с Javascript. В дополнение к нашим бэкэндам на PHP имеется красивый web-интерфейс на Vue. Внедряя @sentry/browser перед нами встали некоторые проблемы.

Доставка sourcemaps

На продакшене у нас обфусцированный и минифицированный код, и в issue в Sentry мы будем наблюдать нечитаемый стектрейс без деталей. Чтобы исправить ситуацию и получить такой же красивый и понятный стектрейс как на PHP (ну хоть в чем-то он хорошо) нам нужно отправлять в Sentry sourcemaps.

В разных проектах мы сделали по разному: где-то отправка осуществляется при билде проекта через Vite с использованием sentryVitePlugin, а где-то пришлось повозиться и написать ручную настройку для пайплайна через sentry-cli. Кратко рассмотрим последний вариант.

Сначала нам нужно разметить минифицированный и исходный код идентификатором отладки debugId:

sentry-cli sourcemaps inject ./public_html/spa_wm/

Затем отправляем размеченные sourcemaps в Sentry:

# переменные должны быть инициализированы
# SENTRY_URL SENTRY_ORG SENTRY_PROJECT SENTRY_AUTH_TOKEN
sentry-cli sourcemaps upload --release="$FRONTEND_RELEASE" --log-level debug ./public_html/spa_wm/

Загруженные sourcemaps можно посмотреть на странице настроек проекта.

Размер бандла

Бандл до внедрения sentry client: 97.07kB, после: 205.94kB

Такое положение дел печалит не устраивает и нужно что-то предпринимать. Поиски в интернетах навели на дискуссию по уменьшению размера, что вылилось в статью и инструкцию на сайте Sentry. Однако, рекомендации помогли уменьшить бандл только на 25kB:

  • __SENTRY_DEBUG__: false и __SENTRY_TRACING__: false уменьшили на 13kB

  • ручная сборка клиента с необходимыми интеграциями уменьшила на 12kB

Уже лучше, но кажется что еще недостаточно. И тут мы решили провести исследование чтобы узнать какова нагрузка по времени на клиентов. Для этого взяли браузер Firefox и на разных типах соединения перезагружали страницу без кэша по 10 раз, брали среднее значение. Оказалось что дополнительное время загрузки всего +~12% ко времени загрузки скрипта.

При внедрении sentry client мы получаем +108кб и +12% ко времени загрузки скрипта чтобы отслеживать ошибки.

Есть репозиторий Tinkoff/micro-sentry с меньшим количеством возможностей, но +35kB к бандлу.

Скрепя зубами мы оставили как есть.

Блокировщик рекламы

Один из наших проектов предоставляет хостинг с конструктором сайтов, куда мы встраиваем трекинг ошибок. В этом плане, для нас печаль в том, что существуют блокировщики рекламы, которые блокируют запросы к серверу Sentry распознавая части endpoint. Cамым лютым из блокировщиков оказался uBlock Origin.

Включаем лог и смотрим: блокировщик ругается на параметр запроса sentry_key.

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

В документации по использование sentry с javascript есть решение: использовать опцию tunnel чтобы скрыть параметры запроса, а на туннелированном endpoint преобразовать запрос и перенаправить его на сервер Sentry. Мы слегка подправили lua скрипт для нашего gateway и спокойно гоняем через наш кастомный endpoint запросы для Sentry, без блокировки со стороны uBlock Origin и подобных.

Решения на стороне Sentry

Правила фильтрации stacktrace

У каждого issue есть стектрейс в виде набора спойлеров с указанием файла и нескольких строк около вызова функции. Это один из инстурментов в расследовании проблемы, и важно чтобы эта часть была верной.

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

Была проблема как на скрине: в стектрейс зашли внутренние механизмы обработки ошибок со стороны фреймворка, а на верху не тот файл ошибки что нужно. Нам нужен стектрейс только до строки выделенной синим цветом.

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

У каждого проекта в Sentry есть такая штука как stack trace rules, где мы можем отбрасывать какие-то части стектрейса или помечать их, относятся ли они к отслеживаемому приложению.

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

Правило "все что выше этой строки убираем из стектрейса" решило нашу проблему:

stack.function:CApplication::handleError ^-app -app ^-group -group

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

Фильтрация событий для Javascript

История с Javascript на самом деле содержит много нюансов. На практике мы накопили достаточное количество issue связанных с расширениями браузеров, на которые мы никак влиять не можем. Еще есть ну очень старые браузеры. А при разработке можно увидеть события в Sentry с localhost.

Для подобных классов ошибок можно настроить фильтрацию. Есть такая страница настроек проекта:

На графике видно динамику отброшенных событий. А вот все доступные фильтры:

Еще есть фильтрация при конфигурировании клиента используя параметры ignoreErrors и ignoreUrls, а еще есть beforeSend, который вызывается перед отправкой события в Sentry, если он вернет null тогда событие не будет отправленно.

Когда отправлять события в Sentry?

Мы проделали большую работу, разобрались со множеством проблем, можно брать и слать ошибки в Sentry, но у разработчиков назревает вопрос: когда нужно отправлять события в Sentry?

И это важный вопрос, на который мы должны ответить, потому что иначе могут быть проблемы ...

У нас была одна весьма производительная фоновая задача, которая обрабатывала входящую очередь событий. В ней намерено отслеживались ошибки и направлялись в Sentry, сотни больших json'ов летели в Sentry (неладная doctine со своим циклическими ссылками), которые успешно отклонялись сервером Sentry из-за большого тела.

Часть отклоненных событий ложилась в /tmp/fluent, другая часть отклоенная из-за частоты запросов буферизировалась на диск самим td-agent в /var/log/td-agent/sentry.store.buffer. Все это происходило со скоростью 1.5gb в минуту на протяжении ~часа. В итоге место на диске на сервере Sentry закончилось.

Последствия незначительные, мы всего-лишь не могли принимать новые ошибки в Sentry, но масштаб трагедии мог быть колосальным учитывая тот факт что наши события проходят через сеть узлов td-agent, которая предназначена не только для Sentry. Отсюда можно получить забитый интернет-канал, заполненные диски на промежуточных узлах td-agent и много чего еще неприятного.

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

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

Теперь мы приходим к выводу что:

События должны иметь ценность.

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

Здесь 1 и 2 варианты являются частью бизнес-логики и это вполне ожидаемое поведение от кода размещенного в блоке try. Однако, вариант 3 является непредвиденным поведением и требует отслеживания - нужно транспортировать в Sentry.

Также следует обратить внимание на разницу между Exception и Throwable. Кроме Exception наш код в try может сгенерировать Error, который также будет являться частью непредвиденной логики, которую необходимо исправлять в Sentry:

try {
  // класс b не существует
  $a = new b();
} catch (Exception $e) {
  // сюда не придет
} catch (Throwable $e) {
  // сюда придет
}

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

Заключение

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

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

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

В следующий раз рассмотрим менеджмент issue в Sentry и расскажем о тех преткновениях, с которыми мы столкнулись после всего цикла внедрения.

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