12 окт. 2025 г.·7 мин чтения

Логирование фронтенда, которое помогает воспроизводить проблемы пользователей

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

Логирование фронтенда, которое помогает воспроизводить проблемы пользователей

Почему отчеты пользователей заходят в тупик

Отчет пользователя часто начинается слишком поздно. Кто-то говорит: «страница сломалась» или «checkout завис», но экран, который он видел в конце, — это только последний шаг в длинной цепочке. К тому времени, когда support читает это сообщение, браузер уже успел пройти несколько страниц, загрузить новые данные и не раз изменить состояние.

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

Запросы только усложняют картину. Один неудачный API-вызов может тихо изменить то, что видит пользователь, без громкой ошибки. Например, запрос цены может истечь по таймауту, и тогда страница покажет устаревшую сумму. Или запрос корзины вернет неполные данные, и одна кнопка исчезнет. Пользователь пишет, что «страница сломалась», а реальная проблема — сбой запроса, который изменил интерфейс несколько секунд назад.

Feature flags добавляют еще один слой путаницы. Два пользователя могут открыть один и тот же маршрут и увидеть разные ветки кода. Один получит новую форму checkout. Другой — старую. Третий попадет в вариант эксперимента с дополнительным шагом. Если в отчете нет состояния флагов, инженеры могут проверить не ту версию и так и не наткнуться на тот же баг.

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

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

Что записывать при смене маршрута

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

Для каждой смены маршрута храните одно небольшое событие с коротким и стабильным набором полей:

  • текущий маршрут и маршрут, с которого пользователь только что ушел
  • timestamp и стабильный session ID
  • действие, которое вызвало смену, например клик по кнопке, кнопка назад в браузере или автоматический redirect
  • время, проведенное на предыдущем экране

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

Сам маршрут должен быть чистым. Логируйте нормализованный путь вроде /orders/:id, а не полный URL с сырыми ID, поисковыми запросами и tracking parameters. Инженеры все равно видят, куда пошел пользователь, но вы избегаете лишнего объема и лишних персональных данных.

Триггер часто и есть та деталь, которая решает загадку. Переход из корзины в checkout после клика по «Continue» означает одно. Тот же переход после тихого auth redirect или неудачной проверки прав означает совсем другое.

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

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

Простой пример показывает, почему это работает. Если пользователь говорит, что checkout «постоянно возвращает меня назад», журнал маршрутов может показать cart -> shipping -> payment -> shipping за восемь секунд, и все это запустилось автоматическим redirect после ошибки запроса. Такой короткий след дает инженерам то, что можно воспроизвести, вместо расплывчатой жалобы.

Что записывать при сбое запросов

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

Начните с имени запроса, которое не меняется со временем. «CreateOrder» или «LoadProfile» проще искать, чем сырой URL. Рядом добавьте HTTP method и status code. «POST 422» рассказывает совсем другую историю, чем «GET 401» или «POST 500», и быстро убирает догадки.

Храните группу endpoint, а не полный URL. Шаблон вроде "/api/orders/:id/pay" показывает форму вызова, не раскрывая ID, email, токены или query string. Полные URL часто содержат больше, чем люди ожидают, особенно когда браузерный запрос несет session data в параметрах.

Сообщение об ошибке должно быть коротким и простым. «timeout», «network error», «validation failed» или краткий текст ошибки от сервера — этого достаточно. Если backend возвращает request ID или trace ID, сохраните его без изменений. Одно это поле часто экономит 20 минут, потому что человек может сразу перейти к нужной строке в Sentry или server logs.

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

  • несколько boolean, например signed_in или coupon_applied
  • небольшие counts, например item_count или field_count
  • безопасные метки, например payment_method «card» или plan «pro»
  • размер ответа или bucket таймаута, если это полезно

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

Сбой в checkout хорошо показывает этот подход. Одно событие может записать request_name «CreateOrder», method «POST», status 422, endpoint_group "/orders", error «coupon expired», request_id «req_91A...», item_count 3, coupon_applied true. Этого достаточно, чтобы повторить путь, сравнить его с backend logs и исправить проблему, не превращая браузер в поток данных.

Как сохранить контекст feature flags

Feature flags часто объясняют, почему баг виден у одного пользователя и не виден у всех остальных. Если страница зависит от эксперимента с ценой, нового checkout flow или скрытого rollout, состояние этих флагов нужно записать в момент, когда пользователь увидел проблему.

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

Для каждого важного флага сохраняйте точное значение, которое использовал браузер. То есть variant, а не просто имя флага. «new_checkout=true» лучше, чем «new_checkout present». Если у флага несколько вариантов, записывайте тот, который был на самом деле, например «control», «variant_a» или «variant_b».

Компактному событию обычно достаточно четырех полей:

  • имя флага
  • значение или variant
  • timestamp
  • источник, если источник может влиять на поведение

Источник важнее, чем думают многие команды. Флаг может приходить из remote config service, из кэша, из query parameter для QA или из локального override, который выставил support. Два пользователя могут открыть одну и ту же страницу и получить разные результаты, потому что у одного браузера был устаревший кэш, а у другого — свежая конфигурация.

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

Небольшой пример это хорошо показывает. Допустим, пользователь сообщает, что блок «Apply coupon» исчез во время checkout. В логах видно, что страница открылась с "checkout_redesign=control" и "promo_box=enabled". Через три секунды приложение обновило настройки аккаунта и переключило "promo_box=disabled" из remote config. Теперь баг можно воспроизвести.

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

Соберите компактный план событий

Упростите отладку checkout
Разберите именно те события, которые объясняют сбои в корзине, промокодах и оплате.

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

Начните с самых проблемных сценариев

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

Простой план может выглядеть так:

  • Для входа в систему записывайте "login_started" и "login_failed"
  • Для checkout записывайте "checkout_viewed" и "payment_failed"
  • Для загрузок записывайте "upload_started" и "upload_failed"
  • Для настроек записывайте "settings_saved" и "settings_save_failed"

Этого уже достаточно, чтобы понять, в каком потоке находился пользователь и где именно все сломалось. Вам не нужны каждый клик, фокус, blur и scroll event. Такие логи быстро занимают место и редко помогают воспроизвести проблему пользователя.

Каждое событие должно нести один и тот же небольшой набор общих полей. Пусть они будут скучными и одинаковыми: session ID, user ID или anonymous ID, route, timestamp, версия приложения, браузер, тип устройства и environment. Добавляйте один request ID или trace ID, когда событие связано с сетевым запросом. Когда эти поля есть в каждом событии, инженеры могут быстро сравнивать записи, а не гадать, какой shape payload они получили.

Уберите шум до попадания в production

Шумные события нужно ограничивать до релиза, а не после того, как dashboard уже взорвался. Откройте staging, походите по приложению десять минут и посмотрите на поток событий. Если одна страница создает десятки почти одинаковых событий, ограничьте их или уберите.

Быстрая проверка перед релизом помогает:

  • посчитать количество событий на обычную пользовательскую сессию
  • ограничить высокочастотные UI events
  • проверить размер payload на медленных страницах
  • убрать поля, которые никогда не помогают во время triage

Последний шаг важнее, чем признают многие команды. Если при отладке никто не использует размер viewport, referrer или полный дамп feature flags, перестаньте это отправлять. Каждое поле должно отвечать на реальный вопрос, который инженер задает во время triage. Если оно никогда на него не отвечает, это просто выхлоп браузера.

Пример бага в checkout: от первого сообщения до воспроизведения

Покупатель пишет: «Страница оплаты продолжает крутиться после того, как я применяю промокод». Этот отчет расплывчатый, но хорошее логирование фронтенда может превратить его в короткую, понятную хронологию.

Сессия начинается на странице корзины. Пользователь нажимает «Checkout», и приложение записывает одну смену маршрута: /cart на /checkout/payment, а также request ID для загрузки новой страницы. Сразу после этого приложение сохраняет небольшой snapshot feature flags. Здесь важен один флаг: промо-эксперимент включен, поэтому стандартная форма оплаты не отображается. Вместо нее пользователь видит альтернативную форму.

Через секунду браузер отправляет tax request. Этот запрос завершается ошибкой. Полезна не огромная выгрузка payload, а компактная запись того, что приложение знало в тот момент:

  • текущий маршрут
  • variant формы checkout
  • имя запроса и status
  • cart total и country
  • можно ли было повторить попытку

Теперь у инженера есть реальная последовательность, а не догадка. Маршрут сменился нормально. Флаг промо изменил форму. tax-запрос упал после загрузки альтернативной формы. Это сначала указывает на variant формы, а не на платежного провайдера и не на страницу корзины.

Следующий шаг простой. Инженер включает тот же promo flag на тестовом аккаунте, открывает маршрут оплаты и смотрит на tax request. Альтернативная форма отправляет данные в другом формате, поэтому tax endpoint отвергает их. Спиннер не останавливается, потому что обработчик ошибки есть только в стандартной форме.

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

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

Ошибки, которые превращают браузер в свалку шума

Постройте lean observability
Получите помощь в связке браузерных событий с Sentry и backend logs.

Команды создают шум, когда относятся к браузеру как к бортовому самописцу. Если логировать каждый клик на каждой странице, получится огромная куча событий, которую никто не читает. Большинство кликов сами по себе ничего не значат. Логируйте поворотные моменты: смену маршрута, отправку формы, неудачный запрос или изменение feature flag, которое повлияло на то, что увидел пользователь.

Еще одна ошибка — сохранять слишком много payload. Полные API responses, request bodies и сырые персональные данные быстро заполняют хранилище и создают реальный риск. Когда запрос падает, инженерам обычно нужны endpoint, method, status code, latency, request ID и короткое сообщение об ошибке. Полный response body им нужен редко, а имена, email, токены или платежные данные — почти никогда.

Названия полей тоже расходятся чаще, чем ожидают команды. На одном экране пишут "userId", на другом — "uid", а на третьем — "account_id". После этого поиск ломается, dashboards становятся беспорядочными, и никто не доверяет данным. Выберите одну схему событий и держите ее простой. Если одна и та же сущность встречается в трех местах, записывайте ее каждый раз одинаково.

Контекст релиза тоже часто теряется. Отчет об ошибке без версии приложения или номера релиза отправляет инженеров в долгий поиск по старому коду и частичным rollout. Добавляйте version, build number, environment и активные feature flags к любому событию, связанному с реальной проблемой пользователя. Эта небольшая порция контекста часто объясняет проблему еще до того, как кто-то откроет DevTools.

Время может испортить историю более тихим способом. Если события приходят с несинхронизированных часов, хронология перестает иметь смысл. Неудачный запрос может выглядеть старше, чем маршрут, который его вызвал. Флаг может казаться включенным уже после того, как пользователь увидел неправильный интерфейс. Используйте один понятный формат timestamp, записывайте часовой пояс пользователя и по возможности сохраняйте server receive time.

Компактного плана обычно достаточно:

  • записывайте изменения состояния, а не каждый жест
  • удаляйте персональные данные до того, как они покинут браузер
  • используйте одну общую схему для основных полей
  • добавляйте version и release details
  • сохраняйте единый формат timestamp

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

Быстрые проверки перед запуском

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

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

Самая быстрая проверка простая: можно ли найти одну и ту же проблему по одному ID в browser logs, API logs и server logs? Если браузер использует session ID, request ID или trace ID, то то же значение должно появляться и на backend. Когда ID не совпадают, инженеры тратят время на ручную сборку улик.

Отчет об ошибке становится намного проще, когда данные о маршруте, запросе и feature flags стоят рядом. Если пользователь попадает на страницу с ценой, запускает запрос и видит сломанную кнопку под определенным variant, эти факты должны оказаться в одной временной линии. Когда они разнесены по разным инструментам, воспроизведение проблемы превращается в гадание.

Пять проверок перед релизом

  • Пройдите одну полную сессию и убедитесь, что события читаются как понятная история.
  • Найдите один общий ID в client и server logs.
  • Проверьте, что каждый неудачный запрос показывает маршрут, детали запроса и активные флаги вместе.
  • Убедитесь, что в логах нет токенов, email-адресов или полных request и response bodies.
  • Смоделируйте медленную сеть и проверьте, что повторные попытки, таймауты и изменения состояния интерфейса по-прежнему выглядят логично.

Приватность нужно тщательно проверить до запуска. Уберите все, по чему можно идентифицировать пользователя, если только это действительно не нужно. В большинстве случаев достаточно хеша user ID, имени маршрута, кода ошибки и краткой сводки запроса. Полные payload сначала кажутся полезными, а потом просто забивают хранилище и создают риск.

Специально протестируйте медленный сценарий. Ограничьте сеть, заставьте один запрос упасть и смените variant флага в середине сессии. Хорошее логирование фронтенда все равно должно показывать, что пользователь нажал, что запросило приложение, что изменилось на экране и почему сбой был важен. Если эта последовательность читается ясно, значит, система достаточно компактна, чтобы выпускать ее в production.

Что делать дальше вашей команде

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

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

Начните с узкого набора событий. Для одного болезненного сценария большинству команд обычно нужны только:

  • смены маршрута с названием маршрута, предыдущим маршрутом, timestamp и session или trace ID
  • неудачные запросы с method, именем endpoint, status code, request ID и коротким типом ошибки
  • активные feature flags или experiment variants в момент возникновения проблемы
  • несколько действий пользователя рядом со сбоем, например submit, retry или back
  • версия браузера и версия приложения, чтобы инженеры могли группировать похожие отчеты

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

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

Оставляйте только те поля, которые реально помогли в конкретном случае. Если какой-то недостающей детали мешает будущему расследованию, добавьте ее с понятной причиной. Это лучше, чем собирать все подряд и надеяться, что потом пригодится.

Если команда все еще не уверена, что именно записывать, поможет короткий внешний review. Oleg Sotnikov занимается таким debugging и system design work как fractional CTO, и он может вместе с вашей командой разобрать один сценарий, показать, что должно быть в логах, и убрать все лишнее.

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