Однажды нам захотелось узнать, сталкиваются ли наши пользователи с какими-нибудь ошибками. Ведь тестировщики божатся, что все проверили, а саппорт еле разгребает завалы обращений. И все бы ничего, но обращения приходят такие: «У меня ничего не работает, горите все в аду». Такие баги мы в работу брать не можем, хочется какой-то конкретики.
Мы начали попытки замутить систему сбора ошибок еще до 2022 года, но уже тогда в в компании тусовались ребята с грозным названием офицеры информационной безопасности, которые на корню зарубили идею хранить логи в каких-то забугорных облаках. Бомбило у меня тогда знатно, но как-то изменить это решение не вышло. Решили пойти другим путем — разворачивать Sentry в режиме on-prem. От идеи, как говорится, до реализации — один шаг. Всего-то нужно было выделить время SRE и сервачок. Мы еще тогда не знали что шагать мы будем пару лет, но уже тогда почувствовали, что дело не быстрое, и надо искать варианты.
Первые костыли
Перелопатив весь интернет мы так ничего и не нашли. В таких ситуациях на помощь пришел командный брейншторм: накидали кучу идиотских идей, просеяли через фильтр здравого смысла и обнаружили алмаз — передавать кастомные параметры в Яндекс.Метрику. А с ней все в порядке - российская компания, на российских серверах будет хранить российские ошибки в российском продукте российских же пользователей. Ну и тем более инфа обезличенная, передаем только то, что падает в консоль. Ну почти.
Для сбора ошибок рендеринга нам очень помогла фича реакта — Error Boundary. Мы обернули приложение в кастомную страничку с большой кнопкой "Обновить" и сожалениями. Пользователи перестали видеть белые экраны и писать в поддержку, а мы наконец получили первые боевые данные и стали понимать масштаб и природу проблем.
Код выглядел примерно так:
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
const analyticEvent = {
eventType: AnalyticEventType.UserError,
errorName: error.name,
errorMessage: error.message,
errorDescription: errorInfo.componentStack,
errorStack: error.stack,
};
analyticsService.sendEvent(analyticEvent);
}
Конечно, дебажить минифицированные логи было очень сложно, но какие-то критичные моменты мы все-таки находили и оперативно фиксили. Ну вот типа таких:
По графику понятно когда ошибка появилась, сколько пользователей аффектит и чего примерно искать в коде. По этому же графику можно понять пофиксили в релизе ошибку или нет. Очень удобно (нет).
Часть ошибок мы найти не могли и грезили о Sentry как о манне небесной и решении всех проблем. И вот этот день настал. Сервера включили, on-prem установили, архитектуру решения согласовали, аппрувы получили, осталось только интеграцию настроить, делов то. Но и без ограничений не обошлось.
Веб-приложения и клиентская часть в частности — штука очень бесячая для службы информационной безопасности, ведь код в открытом виде получает каждый пользователь. Кошмар, кошмар. Был бы я ИБшником, я бы вообще запретил код на прод заливать. Хорошо, что я не они. И нам код в продакшен заливать разрешают. Пока что. Но только минифицированный. Никаких sourcemap'ов конечно же. Если уж хакерам и достается код, то пусть хоть мучаются с деминификацией. Правда отзывов от хакеров я не слышал, но вот дебажить на проде добрым разработчикам действительно тяжело. Плюсы тоже есть — очень мотивирует писать хороший код, который никогда на проде не ломается, и дебажить там нам не приходится.
Интеграция
С такими вводными мы и пришли к интеграции с Sentry. Нужно чтобы в ней ошибки отображались как надо, но чтобы на проде исходники никто прочитать не мог.
У самой Sentry бизнес-модель очень понятная и прозрачная: они зарабатывают на хостинге логов и технической поддержке. Тут у меня родилась теория заговора, в рамках которой у Sentry ну просто обязана быть нормальная дока для внутреннего пользования, а для сообщества доступно то, что доступно. Так конверсия в платных пользователей выше.
Но платить за границу во времена, когда никто не хочет брать российских денег, не наш вариант, ну и мы все-таки инженеры как никак, а не просто курсы по реакту окончили. Начали думать как нам решить эту задачу.
По-умолчанию, интеграция с Sentry очень простая. Она сама сходит и за .js и за .map файлами к вам на сервис и если их найдет, то все покажет как надо. Не наш вариант. Но если мы не хотим чтобы Sentry ходила к нам на сервер, то можем ей помочь, и залить в нее наши .map файлы. Тогда она будет брать их, и не будет ходить на сайт. То что нужно, жаль не работает.
Точнее работает, но не так, как мы ожидали. Соурсмапы в JS устроены следующим образом. Есть собственно сам файл мапы .map и есть минифицированный .js, в котором в конце есть директива
//# sourceMappingURL=script.min.js.map
И вот по этой директиве собственно происходит метч между двумя этими файлами, чтобы восстановить в конце концов исходник.
Так ошибка выглядит, когда соурсмапы не подтянулись:
А так, когда подтянулись:
Так вот, мы придумали гениальный план: генерить билд с соурсмапами, грузить их в Sentry, а потом, перед деплоем, удалять. И все бы хорошо, но нам сразу завалило кибану 404 ошибками. А в девтулзах отсвечивало некрасивое:
Тут к нам на помощь пришел режим генерации SourceMaps в ViteJs!
Видимо Sentry без директивы //# sourceMappingURL=script.min.js.map
не умеет связывать .js и .map, хотя очевидно же, что возьми название файла, и добавь в конец .map. Вот было бы круто, если бы разработчики Sentry так умели.
Мы уже даже практически смирились, что будем жить с 404 в девтулзах. Типа обычный пользователь туда все-равно не полезет, а хакеры поржут, ну и ладно, настроение людям поднимем.
Спустя сутки ресеча внезапно на странице плагина для Vite.js, который мы не используем, глаз зацепился за фразу, что можно загружать в Sentry не только .map файлы, но и .js.
Попробовал два варианта генерации соурсмапов hidden
и true
пришел к выводу что работает только режим true
, то есть режим, в котором директива //# sourceMappingURL=script.min.js.map
присутствует в .js файлах. Но такие файлы, как я уже говорил, мы грузить на прод не хотим. Решаем делать отдельный билд, но каким-то чудом в интернете нас находит ишью, где парни решают эту проблему простым совецким
# delete sourcemaps refs
find .next -type f -name '*.js' -exec sed -i -E 's/sourceMappingURL=[^ ]*\.js\.map//g' {} +
То что нужно! Удаляем перед деплоем соурспамы из каталога и чистим рефы. Успех!
Итоги
Sentry в целом оправдала ожидания. Теперь наши релизы выглядят вот так:
Код
Полный код докерфайла со всеми присяданиями:
# Копируем все в отдельную папочку, хотя наверное можно это и не делать
RUN set -xe \
&& mkdir -p /tmp/sentry_map \
&& find /build -type f -name '*.*.map' -exec mv -t /tmp/sentry_map {} +
RUN set -xe \
&& find /build -type f -name '*.js' -exec mv -t /tmp/sentry_map {} +
# Качаем Sentry cli
RUN set -xe \
&& curl -sL https://sentry.io/get-cli/ | bash
# Настраиваем настроечки и секретные секреты
ARG SENTRY_API_KEY
ARG SENTRY_URL
ARG SENTRY_ORG
ARG SENTRY_PROJECT
ENV SENTRY_API_KEY=$SENTRY_API_KEY
ENV SENTRY_URL=$SENTRY_URL
ENV SENTRY_ORG=$SENTRY_ORG
ENV SENTRY_PROJECT=$SENTRY_PROJECT
# Наконец-то все грузим в Sentry
RUN set -xe \
&& sentry-cli releases new $RELEASE \
&& sentry-cli releases files $RELEASE upload /tmp/sentry_map
# Удоляем соурсмапы
RUN set -xe \
&& find /build -type f -name '*.*.map' -exec rm -f {} \;
# Удоляем рефы на них
RUN set -xe \
&& find /build -type f -name '*.js' -exec sed -i -E 's/sourceMappingURL=[^ ]*\.js\.map//g' {} \;
# Мы великолепны
Статья больше для хабра конечно. Ну и в целом эта проблема вот тут описывается: https://blog.sentry.io/4-reasons-why-your-source-maps-are-broken/#sentry-users-only-upload-your-source-files-as-artifacts
Не пробовали Sentry на своих серверах разворачивать?
У вас он безлимитный по количеству ивентов? Мы, как я не борюсь, все время врезаемся в квоту и Сентри не очень много делает, чтобы ты за нее не вылез