Тема
ТЗ — Sentry/трекинг ошибок (cms/sentry)
Слой: 🔵 инфра-модуль (обязательный в managed-парке) · Зрелость доноров: ★★ · Донор: — Статус: ТЗ к разработке
Назначение и возможности
Интеграция со Sentry для трекинга исключений и производительности. Отдельный DSN на каждый сайт студии, релиз-теги привязаны к версии ядра и модулей — позволяет отследить всплеск ошибок сразу после обновления и использовать это как гейт.
- DSN per-сайт из
.env, без хранения секрета в БД - Release-теги = версия ядра + список версий модулей на момент релиза
- Гейт
cms:upgrade: сравнение частоты ошибок до/после обновления - Breadcrumbs без ПДн (маскирование email/телефон/токены перед отправкой)
- Группировка ошибок по модулю-источнику (из stack trace и манифестов)
- Алерт при превышении порога новых ошибок за окно времени
- Просмотр последних ошибок прямо в Filament без перехода в Sentry
- Таймаут отправки, не позволяющий недоступности DSN замедлить пользовательский запрос
- Обязателен в managed-парке как источник данных для гейта апгрейда наравне с
cms/health - Не дублирует
cms/health:cms/sentry— трейсинг конкретных ошибок/производительности (stack trace, breadcrumbs, релиз-гейт по частоте ошибок),cms/health— агрегат доступности и порогов (аптайм, health-чеки); сбой самого обращения к API Sentry эскалируется как отказ провайдера черезcms/health(стандарт §12), отдельного канала эскалации у sentry-модуля для этого нет
Зависимости и выключение
requires: ядро · provides: — (не декларирует канонического контракта — модуль единственный в своей роли, конкурирующей реализации нет)
Модуль помечен обязательным в managed-парке (см. заголовок ТЗ); зафиксировано ревизией 14.07.2026 и §3 стандарта (подраздел «Обязательные модули managed-парка»): disable cms/sentry недоступен клиентским ролям — доступен только studio-роли, с обязательным подтверждением и записью в аудит; кнопка выключения в админке клиента показывает «модуль обязателен по условиям поддержки». Правило «выключение = деградация» при этом не отменяется: если модуль всё же выключен студией или упал, сайт остаётся жив. См. ревизия ядра 14.07.2026.
Поведение при выключении: исключения продолжают падать в штатный лог Laravel, отправка в Sentry не выполняется — деградация внешней трассировки, приложение работает штатно. cms/updates обязан деградировать симметрично: при выключенном sentry гейт cms:sentry:gate внутри cms:upgrade пропускается с явным предупреждением в лог, апгрейд не блокируется отсутствием данных, которые физически некому собрать.
Стоимость внешнего сервиса (критерий матрицы v2.2): у Sentry-плана студии есть квота событий/трасс в месяц — общая на все сайты парка, не индивидуальная per-DSN. sentry.sample_rate и sentry.traces_sample_rate — не только рычаг шума, а прямой рычаг расхода квоты. При исчерпании квоты Sentry на своей стороне молча дропает или rate-limit'ит новые события — модуль не считает это сбоем приложения (исключения всё равно попадают в штатный лог Laravel), но администратор должен иметь возможность узнать о приближении к лимиту: если Sentry API отдаёт остаток квоты — эскалация в cms/fleet-dashboard/уведомление; если нет — ограничение видимости документируется явно как факт (студия не может гарантировать своевременное предупреждение об исчерпании квоты стороннего сервиса).
Модель данных
Своих таблиц нет — использует Sentry как внешнее хранилище; локально кешируется только сводка последних событий (Redis, TTL) для виджета Filament. Кеш-ключ сводки — с TTL не более sentry.spike_window_minutes, без отдельной БД-таблицы и, соответственно, без миграций. Release-теги не хранятся в БД CMS — источник истины версий на момент релиза читается из реестра модулей ядра в момент вызова cms:sentry:release, сам Sentry хранит теги на своей стороне как атрибут события/транзакции.
ПДн-паспорт (матрица v2.2): своих таблиц с ПДн в БД CMS нет — единственное потенциальное касание ПДн до скраббинга — breadcrumbs (см. «Безопасность»). Удалённое хранение событий (с уже замаскированными ПДн, если скраббинг не пропустил утечку) — на стороне Sentry, внешнего сервиса студии; ретеншн событий там управляется тарифом Sentry, не настройками CMS. Хуки ядра «выгрузить всё по субъекту» и «забыть по запросу» (152-ФЗ) к самому модулю не применимы — ему нечего выгружать/забывать локально; обязанность модуля — не допустить попадания ПДн в Sentry вовсе (обязательный скраббинг), а не управлять их последующим хранением там. PrivacyRegistry (п.15 ревизии ядра): модуль не регистрирует обработчик — cms:privacy:export/cms:privacy:forget по субъекту Sentry-события не затрагивают, так как они уже обезличены скраббингом до отправки и локально агрегировать по субъекту нечего.
Входные и выходные данные
Входы
| Источник | Данные/поля | Чем валидируется |
|---|---|---|
| Необработанное исключение ядра/модуля (внутренний перехват SDK) | stack trace, breadcrumbs, контекст запроса | SDK-перехват, не пользовательский ввод; проходит обязательный PII-скраббинг перед отправкой |
Хук деплоя → cms:sentry:release | версия ядра, список версий модулей | команда читает состояние из реестра модулей ядра, не принимает произвольный ввод |
cms:sentry:gate (вызов из cms:upgrade) | release до/после | сравнение частоты ошибок по API Sentry, таймаут на внешний вызов обязателен |
| Filament: просмотр сводки | без полей | permission sentry.view |
.env → config('sentry.*') | DSN, окружение | не проходит через settings-store; секрет только из .env |
Выходы
| Потребитель | Данные | Формат |
|---|---|---|
| Sentry (внешний сервис) | событие исключения/транзакции с release-тегом, breadcrumbs после скраббинга | HTTPS, формат Sentry SDK |
| Filament-виджет «Ошибки за 24 часа» | сводка из Redis-кеша + ссылка на Sentry | Blade через сервис модуля |
cms/updates (гейт cms:upgrade) | итог сравнения error-rate до/после | сервис-вызов (requires, канал 4) |
| Redis-кеш сводки | последние события, TTL | внутреннее хранилище виджета |
Настройки (группа sentry)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
sentry.enabled | bool | true | нет | Включить отправку событий |
sentry.sample_rate | float | 1.0 | нет | Доля событий-исключений для отправки |
sentry.traces_sample_rate | float | 0.1 | нет | Доля трасс производительности |
sentry.pii_scrub_enabled | bool | true | нет | Маскирование ПДн в breadcrumbs |
sentry.upgrade_gate_enabled | bool | true | нет | Учитывать всплеск ошибок как гейт апгрейда |
sentry.error_spike_threshold | int | 20 | нет | Порог новых ошибок за окно для алерта |
sentry.spike_window_minutes | int | 30 | нет | Окно подсчёта всплеска ошибок для алерта и гейта |
sentry.dsn_timeout_ms | int | 500 | нет | Таймаут отправки события в Sentry — не должен задерживать основной запрос |
sentry.scrub_fields | array | ["email","phone","password","token","authorization"] | нет | Список полей, обязательных к маскированию в breadcrumbs |
sentry.sample_rate и sentry.traces_sample_rate — не только регулятор шума, но и прямой рычаг расхода квоты Sentry-плана студии (критерий «Стоимость внешних API» матрицы v2.2, детали — «Зависимости и выключение»); sentry.error_spike_threshold — лимит матрицы «Лимиты и квоты» на срабатывание алерта, не квота самого Sentry.
Kill-switch (критерий матрицы v2.2): sentry.enabled — штатная реализация этого критерия: аварийное отключение отправки событий без выключения модуля целиком (сводка, права, гейт-логика остаются доступны/консистентны, просто новые события не летят) — дешевле и быстрее полного disable, доступного только studio-роли.
API
Отдельного API нет — админ-CRUD через Filament (просмотр сводки последних ошибок). Гейт cms:sentry:gate и фиксация релиза доступны только как artisan-команды (--json), не как HTTP-эндпоинты — вызываются из пайплайна cms:upgrade и CI/деплой-скрипта соответственно, не из браузера.
Компоненты
Виджеты: Filament-виджет «Ошибки за 24 часа» со ссылкой на Sentry (группировка по модулю-источнику, топ-N по частоте). Filament: страница сводки по релизам (список ReleaseTagged с count ошибок после каждого, тренд «стало лучше/хуже» относительно предыдущего релиза). Команды: cms:sentry:release --json (фиксация release-тега при деплое — версия ядра + версии всех модулей на момент релиза), cms:sentry:gate --json (проверка всплеска ошибок для cms:upgrade, таймаут на вызов API Sentry обязателен), cms:sentry:doctor --json (валидность DSN, доступность SDK).
Демо-контент: не применимо — модулю нечего сидировать demo-данными (события приходят из реальных исключений, не из фикстур), галерея/playground показывают виджет с пустым состоянием «ошибок не было» (см. «UX-требования»).
События и обмен
| Событие | Когда | Payload |
|---|---|---|
ErrorSpikeDetected | превышен порог новых ошибок после релиза | release, count, threshold |
ReleaseTagged | зафиксирован новый release-тег | release, core_version, modules |
FilterBus и provides-контракты не используются. Слушает: не подписывается на события других модулей — источник данных внешний (Sentry SDK перехватывает исключения ядра).
Таблица взаимодействий
| Сущность/модуль | Канал | Направление | Что происходит |
|---|---|---|---|
| Исключения ядра и всех модулей | внутренний перехват SDK (вне пяти каналов, глобальный exception handler) | in | Каждое необработанное исключение становится событием Sentry с release-тегом |
cms/updates | requires → сервис-вызов (канал 4), cms:sentry:gate | out | Сравнение error-rate до/после — итог используется как часть гейта апгрейда |
| Sentry (внешний сервис) | внешняя интеграция (HTTPS, вне пяти каналов) | out | Приём событий/трасс, источник сводки для виджета |
| Хук деплоя | внешний относительно рантайма (CI/деплой-скрипт вызывает команду) | in | Фиксация ReleaseTagged при каждом деплое |
Фоновая работа
Своей именованной очереди нет: отправка событий в Sentry выполняется SDK асинхронно (внутренний транспорт пакета), фиксация release-тега — по хуку деплоя, не по расписанию ядра.
Эксплуатация (§15). Метрики: rate отправленных/сброшенных по таймауту событий, доля таймаутов DSN (dsn_timeout_ms от общего числа попыток отправки), задержка выполнения cms:sentry:gate относительно cms:upgrade. Алерты: ErrorSpikeDetected — событие уже формализовано (см. «События и обмен»).
| Симптом | Что проверить | Команда |
|---|---|---|
| DSN недоступен, события не долетают | сеть до Sentry, валидность DSN в .env | cms:sentry:doctor --json |
| Всплеск ошибок после релиза | сводка ошибок в Filament/Sentry по release-тегу | откат через пайплайн cms/updates (cms:upgrade --rollback) |
sample_rate/traces_sample_rate слишком высоки, заметная нагрузка на сайт | объём трафика на traces_sample_rate, доля запросов с трассировкой | понизить в настройках sentry, без деплоя |
| Первый релиз без базы для сравнения | история релизов пуста в cms:sentry:release | не инцидент — гейт помечает «нет базы для сравнения», не блокирует |
Гейт cms:sentry:gate падает по таймауту API Sentry | доступность API Sentry, dsn_timeout_ms | cms:sentry:doctor --json, при системной недоступности — временно снизить upgrade_gate_enabled |
Бэкап/рестор: своих таблиц и файлов нет — модулю нечего добавлять в бэкап сайта и нечего восстанавливать после рестора; вся история событий живёт только на стороне Sentry.
Производительность и кеш
Объёмы: на активном сайте — от единиц до сотен исключений в сутки (при sample_rate=1.0 все идут в Sentry), трассы производительности — доля traces_sample_rate от запросов, на нагруженном сайте это может быть заметный процент трафика — держать 0.1 как дефолт и не поднимать бездумно. Горячий путь — не сама отправка (она асинхронна и не должна блокировать ответ), а вызов dsn_timeout_ms: недоступность DSN не должна замедлять пользовательский запрос — таймаут строгий, сбой отправки проглатывается модулем (лог уровня warning, не exception пользователю). Транспорт SDK — асинхронный, с внутренним пулом/буфером ограниченного размера: HTTP-ответ пользователю не ждёт результата отправки независимо от исхода. Если DSN недоступен дольше, чем вмещает буфер транспорта, событие теряется безвозвратно — приемлемая потеря (диагностика, не бизнес-транзакция), не требующая собственной retry-очереди модуля. Redis-сводка виджета — TTL короче spike_window_minutes, инвалидация естественная по истечении, без событий ядра. Собственных тегов page-cache не объявляет, на публичный рендер сайта не влияет. Виджет сводки в Filament — единственный горячий путь модуля внутри админки: агрегация из Redis, не прямой запрос к API Sentry на каждый рендер дашборда (иначе открытие админки становится зависимым от доступности стороннего сервиса).
Безопасность
Границы входа: DSN и креды Sentry — только .env → config('sentry.*'), хранение в БД запрещено. Breadcrumbs проходят обязательное маскирование ПДн (scrub_fields) перед отправкой — двойной барьер: маскирование на стороне модуля + отключаемые интеграции SDK, собирающие тело запроса. Специфичные векторы: SQL-бindings в breadcrumbs могут содержать пароли/токены пользователей из форм — скраббинг применяется не только к явным полям контекста, но и к параметрам запросов и заголовкам (Authorization, Cookie); утечка DSN в клиентский JS (при ошибочной публикации frontend-конфига) позволяет постороннему слать мусорные события — DSN серверный, во frontend-бандл не попадает. Доступ к сводке в Filament — только под явными permissions.
Скраббинг ПДн — два уровня, не один: (1) точное совпадение имени поля из scrub_fields (email, phone, password, token, authorization) в именованных контекстах (request-параметры, заголовки, extra-контекст breadcrumb) — маскируется всё значение; (2) эвристика по значению в свободном тексте (сообщение исключения, произвольные строки контекста), где ПДн не привязаны к имени поля — например email-подобная подстрока внутри текста сообщения об ошибке ищется regex-паттерном (через ReDoS-валидатор ядра) и маскируется независимо от того, в каком поле она оказалась. Оба уровня обязательны: скраббинг только по имени поля пропускает ПДн, случайно попавшие в свободный текст (например интерполированный email в сообщении исключения валидатора).
Матрица ролей (permissions sentry.view, sentry.manage):
| Роль | sentry.view | sentry.manage |
|---|---|---|
| Администратор | да | да |
| Менеджер | да | нет |
| Редактор | нет | нет |
| Studio | да | да (+ право выключить модуль в managed-парке, см. «Зависимости и выключение») |
UX-требования
Админ: пустое состояние «ошибок за последние 24 часа не было» вместо пустого списка; ссылка «открыть в Sentry» рядом с каждой ошибкой сводки — не требует ручного поиска по release/timestamp; сообщение о недоступности Sentry SDK/DSN — «отправка ошибок временно недоступна, исключения пишутся в штатный лог» на человеческом языке, не трассировка; группировка ошибок по модулю-источнику видна сразу в сводке, не требует перехода в Sentry для базовой диагностики «что чаще всего ломается».
Посетитель: не применимо — отправка в Sentry не должна быть видна пользователю ни при каких условиях (в т.ч. при таймауте DSN — деградация тихая).
Крайние случаи и типовые баги
- утечка ПДн/секретов в трейсы → breadcrumbs обязаны проходить скраббинг (
scrub_fields) до сериализации в SDK, не после — тест на утечку конкретно проверяет, что замаскированное значение никогда не попадает в исходящий HTTP-payload, включая вложенные массивы и SQL-bindings; - недоступность DSN не должна замедлять запросы →
dsn_timeout_ms— жёсткий таймаут; при недоступности Sentry основной запрос пользователя не ждёт ответа от внешнего сервиса, ошибка отправки логируется, не пробрасывается наверх; - двойная отправка одного и того же исключения (retry SDK при флапе сети) → идемпотентность на стороне Sentry по fingerprint события — модуль не обязан дедуплицировать сам, но не должен создавать искусственные дубли повторной ручной отправкой;
- выключение модуля посреди обработки исключения → недопустимая гонка: если
sentry.enabledменяется в момент отправки, текущая отправка либо завершается, либо тихо отбрасывается — исключение в любом случае попадает в штатный лог Laravel (двойная гарантия, не полагаться только на Sentry); - отсутствие requires/suggests-зависимостей — модуль ни от кого не зависит, но сам является зависимостью гейта
cms:upgradeчерезcms/updates: еслиsentryвыключен,cms/updatesобязан пропускать error-rate-гейт с предупреждением, а не падать при вызове недоступного сервиса; - сбой/таймаут внешнего API Sentry при
cms:sentry:gate→ таймаут обязателен и здесь отдельно отdsn_timeout_msотправки событий; при недоступности API гейт возвращает «неизвестно» с явным предупреждением вcms:upgrade, не блокирует и не пропускает молча апгрейд как «чисто»; - пустая история релизов (первый деплой) →
ErrorSpikeDetectedне может сравнивать «до/после» без предыдущего релиза — гейт для первого релиза инсталляции пропускается с пометкой «нет базы для сравнения», не считается ложным провалом; - огромный всплеск ошибок (например, взрыв 500 на всех страницах сразу после релиза) →
error_spike_threshold/spike_window_minutesдолжны триггерить алерт быстро, но не заспамить — один агрегированный алерт на всплеск, не по одному на каждую ошибку внутри окна; - измерения locale/city/site — Sentry-события не несут этих измерений явно; на
cms/multisiteстоит явно зафиксировать: DSN общий на инсталляцию или per-сайт — при общем DSN ошибки разных сайтов парка смешиваются в одном проекте Sentry без тегаsite_id, что затрудняет диагностику конкретного сайта; - противоречивая комбинация настроек:
sentry.upgrade_gate_enabled=trueприsentry.enabled=false→ аналогичноhealth, гейт не может опираться на данные, которые не собираются;cms:upgradeобязан явно предупреждать, а не тихо считать гейт пройденным; - обязательность модуля в managed-парке и disable — зафиксировано ревизией 14.07.2026: disable
cms/sentryв managed-парке недоступен клиентским ролям — доступен только studio-роли с обязательным подтверждением и записью в аудит; правило «выключение = деградация» при этом сохраняется. См. ревизия ядра 14.07.2026 и §3 стандарта (подраздел «Обязательные модули managed-парка»); - исчерпание квоты Sentry-плана → Sentry на своей стороне молча дропает/rate-limit'ит новые события — не считается сбоем приложения (исключения всё равно попадают в штатный лог Laravel), но не должно оставаться незамеченным: при доступности остатка квоты через API Sentry — эскалация в
cms/fleet-dashboard, иначе — задокументированное ограничение видимости (см. «Зависимости и выключение»); - потеря события при длительной недоступности DSN → внутренний буфер транспорта SDK ограничен по размеру; если DSN недоступен дольше, чем вмещает буфер, событие теряется безвозвратно — приемлемо (диагностика, не бизнес-транзакция), HTTP-ответ пользователю при этом не блокируется независимо от исхода отправки (детали — «Производительность и кеш»);
Донорский код
Донор: — (новая разработка, обёртка над sentry/sentry-laravel)
Миграция legacy-данных (§16): не применимо — модуль не имеет доменных данных для переноса с донорских сайтов (события живут в Sentry, не в БД CMS), cms:sentry:import-legacy не требуется.
Тесты и приёмка
- [ ] Контрактный тест: DSN берётся из
.envчерезconfig(), не хранится в БД - [ ] Health-чек модуля отражает доступность Sentry SDK и валидность DSN
- [ ] Гейт
cms:upgradeблокирует релиз при всплеске ошибок выше порога - [ ] При выключении модуля исключения не теряются — попадают в штатный лог
- [ ] Breadcrumbs проходят маскирование ПДн перед отправкой (тест на утечку), включая SQL-bindings и заголовки авторизации
- [ ] Недоступность/таймаут Sentry (отправка и
cms:sentry:gate) не увеличивает время ответа пользователю и не роняетcms:upgrade - [ ] Первый релиз инсталляции не считается ложным провалом гейта из-за отсутствия базы для сравнения
- [ ] Права
sentry.view/sentry.manageразграничивают доступ к сводке - [ ] DSN не попадает в клиентский JS-бандл (контрактная проверка frontend-сборки)
- [ ] Виджет сводки в Filament не делает синхронный запрос к API Sentry при каждом рендере дашборда
- [ ] Disable модуля в managed-парке недоступен клиентским ролям — доступен только studio-роли с подтверждением и записью в аудит (контрактный тест на permission-гейт)
- [ ] Исчерпание квоты Sentry-плана не считается сбоем приложения: исключения остаются в штатном логе Laravel, ограничение видимости расхода квоты задокументировано
- [ ] Потеря события при недоступности DSN дольше внутреннего буфера транспорта не блокирует и не замедляет HTTP-ответ пользователю
- [ ] Контрактный набор
cms-testingпройден, пакет протестирован в testbench-изоляции - [ ] Feature-тест на каждый роут модуля; тестовая БД только
sentry_test,migrate:freshзапрещён