Как мы внедряли Sentry. Часть 2 — внедрение в разработку
В прошлый раз мы рассматривали внедрение 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 и расскажем о тех преткновениях, с которыми мы столкнулись после всего цикла внедрения.