Тема
ТЗ — Аналитика/счётчики (cms/analytics)
Слой: 🟡 функц-модуль · Зрелость доноров: ★★ · Донор: — Статус: ТЗ к разработке
Назначение и возможности
Подключение счётчиков веб-аналитики (Яндекс.Метрика, Google Analytics 4) из настроек без правки шаблонов, единый JS-API для отправки целей/событий конверсий и базовый дашборд метрик в админке. Персональные данные в события не передаются.
- Подключение ID счётчиков Метрики/GA4 из настроек, без хардкода в шаблонах
- Единая JS-обёртка целей/событий конверсий поверх разных счётчиков (один вызов — все системы)
- Серверные события конверсий для SPA-переходов (React-острова, Inertia-кабинеты)
- Дашборд базовых метрик в админке (визиты, конверсии) через
cms/integrations-bus - Вставка кодов счётчиков CSP-совместимо (nonce), см.
/cms-v2/security - Явный запрет передачи персональных данных в события
- Загрузка счётчиков только после согласия на обработку данных (см. «Крайние случаи»)
- Серверная статистика лидов/заказов как fallback-источник конверсий при заблокированных счётчиках
Зависимости и выключение
requires: ядро · suggests: cms/integrations-bus (получение метрик по API счётчиков), cms/cookie-consent (гейт загрузки по согласию)
Поведение при выключении: счётчики и JS-обёртка не выводятся на страницы, вызовы целей из кода становятся no-op — остальной функционал сайта не затрагивается, деградация без поломки. Внешние API счётчиков (получение метрик дашборда) тарифицируются лимитами провайдера — см. «Настройки»; исчерпание квоты не влияет на сам сбор данных (это делает клиентский JS счётчиков, не модуль), только на дашборд.
Модель данных
| Таблица | Ключевые поля | Примечание |
|---|---|---|
cms_analytics_goals | id, code, title, provider (yandex|ga4), is_active | реестр целей/событий конверсий |
cms_analytics_daily_stats | id, date, provider, metric, value | кеш базовых метрик дашборда из API счётчиков |
provider — PHP Enum; cms_analytics_daily_stats — журнальная посуточная таблица, кандидат на BRIN-индекс по date при накоплении истории; уникальный индекс (date, provider, metric) от дублей при повторной синхронизации.
ПДн-паспорт: модуль ПДн намеренно не хранит — context события конверсии фильтруется на входе (email/телефон/ФИО запрещены, см. «Безопасность»). daily_stats — агрегаты счётчиков, не привязаны к личности. Декларация: «ПДн не храню».
Входные и выходные данные
Входы:
| Источник | Поля | Чем валидируется |
|---|---|---|
Серверное событие конверсии (SPA, POST /api/v1/analytics/event) | goal_code, provider, context (json) | FormRequest-whitelist полей, context фильтруется от ПДн-паттернов (email/телефон/ФИО-регексы), rate-limit |
| Форма цели конверсии (Filament) | code, title, provider, is_active | FormRequest, code — уникальный slug |
Событие ядра/коммерции (OrderCompleted, LeadCreated) | стандартный payload события | контракт события ядра, слушатель тонкий — только регистрация стандартной цели |
| Синхронизация метрик из API счётчика (джоба) | ответ API Яндекс.Метрики/GA4 | валидация формата ответа провайдера перед upsert в daily_stats |
Выходы:
| Потребитель | Данные | Формат |
|---|---|---|
| Посетитель (браузер) | JS-обёртка вставки счётчиков + вызов целей | inline-скрипт с nonce (CSP-совместимо) |
| Filament admin | дашборд визитов/конверсий | keyset-JSON через API |
cms/pixels, cms/ab-testing (потребители реестра целей) | список целей cms_analytics_goals | сервис-вызов (канал 4, requires) |
| Подписчики событий | AnalyticsGoalTriggered | payload по таблице ниже |
cms/integrations-bus (suggests) | запрос метрик по API счётчика | сервис-вызов, только из очереди |
Настройки (группа analytics)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
analytics.enabled | bool | true | да | Включение вставки счётчиков на страницы |
analytics.yandex_metrika_id | string | null | да | ID счётчика Яндекс.Метрики |
analytics.ga4_measurement_id | string | null | да | Measurement ID Google Analytics 4 |
analytics.spa_events_enabled | bool | true | нет | Отправка серверных событий для SPA-переходов |
analytics.dashboard_enabled | bool | true | нет | Показ дашборда метрик в админке |
analytics.require_consent | bool | true | нет | Требовать согласие (cms/cookie-consent) перед загрузкой счётчиков |
analytics.no_consent_module_behavior | string | block | нет | Поведение при отсутствии cms/cookie-consent: block (не грузить) или allow (грузить без гейта, юридический риск на владельце сайта) |
analytics.event_rate_limit_per_minute | int | 60 | нет | Лимит серверных событий конверсии с одного visitor-токена в минуту |
Лимиты и квоты: event_rate_limit_per_minute защищает /api/v1/analytics/event от спама/накрутки; API счётчиков-провайдеров тарифицируются отдельно — расход синхронизации дашборда виден в Filament, исчерпание квоты провайдера деградирует только дашборд (последние доступные данные + пометка «устарело»), не публичный сбор аналитики.
API
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| POST | /api/v1/analytics/event | public (rate-limit) | Приём серверного события конверсии от SPA |
| GET | /api/v1/admin/analytics/dashboard | admin (analytics.view) | Базовые метрики для дашборда |
| POST | /api/v1/admin/analytics/goals | admin (analytics.manage) | Создание/правка цели конверсии |
Компоненты
Виджеты: JS-обёртка отправки целей (глобальный объект на странице). Filament: дашборд базовых метрик, реестр целей конверсий. Команды: cms:analytics:sync-stats --json.
Демо-контент: AnalyticsDemoSeeder создаёт 2 демо-цели (lead_created, order_completed) и неделю демо-daily_stats — дашборд в галерее /_gallery показывает график без ручной настройки счётчиков.
Фронтенд-бюджет: JS-обёртка — единственный файл ≤3 КБ gzip, загружается асинхронно, не блокирует рендер (defer); сами скрипты счётчиков (Метрика/GA4) грузятся лениво после согласия — не в критическом пути первой отрисовки.
Эксплуатация (ранбук): метрики analytics_events_received_total, analytics_events_rejected_total{reason} (rate-limit/ПДн-фильтр), analytics_sync_stats_duration_seconds; алерт — синхронизация дашборда не выполнялась дольше 2 плановых периодов подряд.
| Симптом | Что проверить / команда |
|---|---|
| Счётчики не грузятся у посетителей | проверить require_consent и статус согласия в cms/cookie-consent, yandex_metrika_id/ga4_measurement_id не пустые |
| Дашборд не обновляется | cms:analytics:sync-stats --json, проверить квоту API счётчика |
| SPA-конверсии не долетают | проверить event_rate_limit_per_minute, CSP-заголовки на фронте |
Резкий рост events_rejected_total | смотреть reason — ПДн-фильтр (ошибка в форме) или rate-limit (накрутка/бот) |
Бэкап/рестор: cms_analytics_goals — в бэкапе (конфигурационная сущность); cms_analytics_daily_stats — денормализованный кеш метрик, после рестора пересоздаётся командой cms:analytics:sync-stats за пропущенный период, не считается потерей данных.
События и обмен
| Событие | Когда | Payload |
|---|---|---|
AnalyticsGoalTriggered | зафиксировано событие конверсии | goal_code, provider, context (json, без ПДн) |
Слушает: события ядра/коммерции для автоматической регистрации стандартных целей (заказ оформлен, лид создан). Цели-реестр модуля потребляется cms/ab-testing и cms/pixels как общий справочник событий конверсии.
Таблица взаимодействий:
| Сущность/модуль | Канал | Направление | Что происходит |
|---|---|---|---|
Ядро (OrderCompleted, LeadCreated) | событие (1) | ядро → analytics | автоматическая регистрация срабатывания стандартной цели |
cms/cookie-consent (suggests) | сервис-вызов (4) | analytics → cookie-consent | проверка статуса согласия перед вставкой счётчиков на страницу |
cms/pixels (потребитель) | сервис-вызов (4, requires у pixels) | pixels → analytics | чтение реестра целей cms_analytics_goals вместо дублирования |
cms/ab-testing (потребитель) | сервис-вызов (4, requires у ab-testing) | ab-testing → analytics | учёт конверсий по вариантам теста через тот же реестр целей |
cms/integrations-bus (suggests) | очередь (5) | analytics → воркер | синхронизация daily_stats из API счётчика по расписанию |
Подписчики AnalyticsGoalTriggered | событие (1) | analytics → любой | например будущий модуль отчётности маркетинга |
Фоновая работа
Очередь analytics: синхронизация cms_analytics_daily_stats из API счётчиков через cms/integrations-bus — только по расписанию ScheduleRegistrar, без синхронных внешних HTTP-вызовов в веб-потоке.
Производительность и кеш
Ожидаемые объёмы: единицы целей на сайт, десятки-сотни daily_stats-строк в месяц (1 провайдер × метрики × дни). Горячий путь — вставка счётчиков на публичной странице: 0 запросов к БД (ID читаются из кеша группы настроек). Приём события /analytics/event — один insert, без чтения перед записью.
Критичные индексы: уникальный (date, provider, metric) на daily_stats от дублей синхронизации; code уникальный на goals. Собственных тегов кеша нет — участвует только в page-cache ядра. yandex_metrika_id и ga4_measurement_id помечены affectsPageCache: изменение ID сбрасывает кеш страниц со вставкой счётчиков.
Построение дашборда читает daily_stats за период агрегированно (group by date, metric) — без построчной обработки в PHP; при больших периодах (год и более) отчёт использует ту же денормализованную таблицу, не пересчитывает исходные события заново.
Безопасность
/api/v1/analytics/event — публичный эндпоинт под rate-limit, FormRequest-whitelist полей события; персональные данные (email, телефон, ФИО) в context запрещены и фильтруются на входе. Скрипты счётчиков вставляются CSP-совместимо через nonce.
Права: analytics.view, analytics.manage.
Матрица ролей:
| Действие | Администратор | Менеджер | Редактор | Studio |
|---|---|---|---|---|
Просмотр дашборда (view) | ✅ | ✅ | ✅ | ✅ |
Создание/правка целей конверсии (manage) | ✅ | ✅ | — | ✅ |
Правка ID счётчиков (yandex_metrika_id, ga4_measurement_id) | ✅ | — | — | ✅ |
Изменение no_consent_module_behavior (юридически значимо) | ✅ | — | — | ✅ |
UX-требования
Админ: пустое состояние дашборда — «Нет данных за период — проверьте ID счётчиков» со ссылкой на настройки; массовое включение/отключение целей списком; человеческая ошибка при попытке сохранить context с похожим на email/телефон значением («Похоже на персональные данные — уберите поле перед сохранением цели»); подтверждение перед сменой no_consent_module_behavior на allow (юридический риск, явное предупреждение текстом).
Посетитель: счётчики не создают видимой задержки первой отрисовки (асинхронная загрузка); при отклонённом согласии сайт не показывает ошибок и не блокирует функциональность — аналитика просто не собирается.
Крайние случаи и типовые баги
- Счётчики грузятся до согласия — прямая вставка в
<head>без гейта нарушает ФЗ/GDPR-требования → скрипты вставляются только после подтверждённого согласия изcms/cookie-consent; до согласия — счётчик не инициализирован, вызовы целей — no-op. - Отсутствие
cms/cookie-consent(suggests не установлен) → поведение управляетсяno_consent_module_behavior: по умолчаниюblock(безопасный дефолт — не грузить без модуля согласия), явныйallow— осознанный выбор владельца сайта с предупреждением в UI. - SPA-переходы без перезагрузки страницы → серверные события конверсии (
spa_events_enabled) компенсируют невозможность client-sidepageviewдля React-островов/Inertia; дублирование с client-side счётчиком исключается разнымиgoal_codeдля server/client источников. - Дубль события конверсии при повторной отправке формы (двойной сабмит) →
event_rate_limit_per_minuteне дедуплицирует полностью, дедупликация — поIdempotency-Keyна клиенте при повторной отправке той же формы (см. такжеcms/pixelsдля event_id-дедупликации покупок). - Модуль выключен, но код продолжает вызывать цели → JS-обёртка деградирует до no-op функции без ошибок в консоли, серверный
/analytics/eventотвечает 404 маршрута отключённого модуля (роут не зарегистрирован), клиент обязан обрабатывать это тихо (fire-and-forget, без retry-шторма). - API счётчика недоступен/квота исчерпана при синхронизации дашборда → джоба логирует и завершается штатно,
daily_statsне обновляется за период, дашборд показывает последние доступные данные с пометкой «данные устарели», не 500. - Пустой/огромный
contextв событии конверсии → whitelist полей ограничивает размер, превышение лимита — 422, не тихая обрезка данных. - Серверная статистика как fallback — при заблокированном рекламными блокировщиками клиентском счётчике конверсия всё равно фиксируется через
LeadService/OrderCompletedслушатель (серверная сторона), не теряется полностью даже без JS-счётчика у посетителя. - Измерение city/locale: цель конверсии одинакова для всех городов/локалей (нет собственного измерения в
cms_analytics_goals) — при необходимости регионального среза дашборд агрегирует поRequestContextна момент события, не хранитcity_idвdaily_stats(ограничение зафиксировано явно, не баг). - Противоречивые настройки:
spa_events_enabled=falseпри активных React-островах → конверсии SPA-переходов не фиксируются вовсе. ⚠️ Противоречие: тихая потеря данных без предупреждения. Разрешение: Filament показывает предупреждение при сохраненииspa_events_enabled=false, если в системе зарегистрированы SPA-виджеты. - Конкурентное редактирование цели конверсии двумя админами →
lock_versionнаcms_analytics_goals, конфликт — 409 с человеческим сообщением. - Гонка синхронизации: два параллельных плановых прогона
sync-stats(повторный запуск джобы после ретрая) → уникальный индекс(date, provider, metric)+ upsert делают повтор идемпотентным, не создают дублей строк.
Донорский код
Донор: — (новая разработка)
Тесты и приёмка
- [ ] Контрактный тест:
/api/v1/analytics/eventпринимает событие и не логирует ПДн - [ ] Вставка счётчиков не ломает page-cache (ID подставляются из настроек, не хардкод)
- [ ] Скрипты счётчиков вставлены CSP-совместимо (nonce), см.
/cms-v2/security - [ ] Счётчики не грузятся до согласия при
require_consent=trueи включённомcms/cookie-consent - [ ]
no_consent_module_behavior=block— счётчики не грузятся при отсутствииcms/cookie-consent - [ ] При выключении модуля счётчики не рендерятся, вызовы целей — no-op без ошибок JS
- [ ] Права
analytics.view/analytics.manageразграничивают чтение дашборда и правку целей - [ ] Изменение
yandex_metrika_id/ga4_measurement_idсбрасывает page-cache - [ ] Повторный
sync-statsне создаёт дублейdaily_stats(идемпотентность upsert) - [ ] Серверная фиксация лида/заказа работает как fallback при отсутствии клиентского события
- [ ] Контрактный набор
cms-testingзелёный, пакет тестируется в testbench-изоляции - [ ] Feature-тест на каждый роут API; тестовая БД только
analytics_test,migrate:freshзапрещён