Skip to content

ТЗ — Аналитика/счётчики (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_goalsid, code, title, provider (yandex|ga4), is_activeреестр целей/событий конверсий
cms_analytics_daily_statsid, 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_activeFormRequest, 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)
Подписчики событийAnalyticsGoalTriggeredpayload по таблице ниже
cms/integrations-bus (suggests)запрос метрик по API счётчикасервис-вызов, только из очереди

Настройки (группа analytics)

КлючТипДефолтaffectsPageCacheОписание
analytics.enabledbooltrueдаВключение вставки счётчиков на страницы
analytics.yandex_metrika_idstringnullдаID счётчика Яндекс.Метрики
analytics.ga4_measurement_idstringnullдаMeasurement ID Google Analytics 4
analytics.spa_events_enabledbooltrueнетОтправка серверных событий для SPA-переходов
analytics.dashboard_enabledbooltrueнетПоказ дашборда метрик в админке
analytics.require_consentbooltrueнетТребовать согласие (cms/cookie-consent) перед загрузкой счётчиков
analytics.no_consent_module_behaviorstringblockнетПоведение при отсутствии cms/cookie-consent: block (не грузить) или allow (грузить без гейта, юридический риск на владельце сайта)
analytics.event_rate_limit_per_minuteint60нетЛимит серверных событий конверсии с одного visitor-токена в минуту

Лимиты и квоты: event_rate_limit_per_minute защищает /api/v1/analytics/event от спама/накрутки; API счётчиков-провайдеров тарифицируются отдельно — расход синхронизации дашборда виден в Filament, исчерпание квоты провайдера деградирует только дашборд (последние доступные данные + пометка «устарело»), не публичный сбор аналитики.

API

МетодПутьДоступНазначение
POST/api/v1/analytics/eventpublic (rate-limit)Приём серверного события конверсии от SPA
GET/api/v1/admin/analytics/dashboardadmin (analytics.view)Базовые метрики для дашборда
POST/api/v1/admin/analytics/goalsadmin (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 (юридический риск, явное предупреждение текстом).

Посетитель: счётчики не создают видимой задержки первой отрисовки (асинхронная загрузка); при отклонённом согласии сайт не показывает ошибок и не блокирует функциональность — аналитика просто не собирается.

Крайние случаи и типовые баги

  1. Счётчики грузятся до согласия — прямая вставка в <head> без гейта нарушает ФЗ/GDPR-требования → скрипты вставляются только после подтверждённого согласия из cms/cookie-consent; до согласия — счётчик не инициализирован, вызовы целей — no-op.
  2. Отсутствие cms/cookie-consent (suggests не установлен) → поведение управляется no_consent_module_behavior: по умолчанию block (безопасный дефолт — не грузить без модуля согласия), явный allow — осознанный выбор владельца сайта с предупреждением в UI.
  3. SPA-переходы без перезагрузки страницы → серверные события конверсии (spa_events_enabled) компенсируют невозможность client-side pageview для React-островов/Inertia; дублирование с client-side счётчиком исключается разными goal_code для server/client источников.
  4. Дубль события конверсии при повторной отправке формы (двойной сабмит) → event_rate_limit_per_minute не дедуплицирует полностью, дедупликация — по Idempotency-Key на клиенте при повторной отправке той же формы (см. также cms/pixels для event_id-дедупликации покупок).
  5. Модуль выключен, но код продолжает вызывать цели → JS-обёртка деградирует до no-op функции без ошибок в консоли, серверный /analytics/event отвечает 404 маршрута отключённого модуля (роут не зарегистрирован), клиент обязан обрабатывать это тихо (fire-and-forget, без retry-шторма).
  6. API счётчика недоступен/квота исчерпана при синхронизации дашборда → джоба логирует и завершается штатно, daily_stats не обновляется за период, дашборд показывает последние доступные данные с пометкой «данные устарели», не 500.
  7. Пустой/огромный context в событии конверсии → whitelist полей ограничивает размер, превышение лимита — 422, не тихая обрезка данных.
  8. Серверная статистика как fallback — при заблокированном рекламными блокировщиками клиентском счётчике конверсия всё равно фиксируется через LeadService/OrderCompleted слушатель (серверная сторона), не теряется полностью даже без JS-счётчика у посетителя.
  9. Измерение city/locale: цель конверсии одинакова для всех городов/локалей (нет собственного измерения в cms_analytics_goals) — при необходимости регионального среза дашборд агрегирует по RequestContext на момент события, не хранит city_id в daily_stats (ограничение зафиксировано явно, не баг).
  10. Противоречивые настройки: spa_events_enabled=false при активных React-островах → конверсии SPA-переходов не фиксируются вовсе. ⚠️ Противоречие: тихая потеря данных без предупреждения. Разрешение: Filament показывает предупреждение при сохранении spa_events_enabled=false, если в системе зарегистрированы SPA-виджеты.
  11. Конкурентное редактирование цели конверсии двумя админами → lock_version на cms_analytics_goals, конфликт — 409 с человеческим сообщением.
  12. Гонка синхронизации: два параллельных плановых прогона 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 запрещён

Внутренняя база знаний студии