Тема
ТЗ — Пиксели/tag manager (cms/pixels)
Слой: 🟡 функц-модуль · Зрелость доноров: ★★ · Донор: — Статус: ТЗ к разработке
Назначение и возможности
Вставка рекламных пикселей (VK Pixel, Top.Mail.Ru) и Google Tag Manager из настроек, привязка событий конверсий ядра/коммерции к пикселям и загрузка скриптов только после согласия пользователя на обработку данных.
- Вставка кодов VK Pixel, Top.Mail.Ru, GTM по ID из настроек, без правки шаблонов
- Привязка стандартных событий конверсий (покупка, лид) к событиям ядра/коммерции
- Consent-зависимая загрузка: скрипты не грузятся до согласия пользователя (
cms/cookie-consent) - Настраиваемое сопоставление событие ядра → событие пикселя (без хардкода в коде)
- CSP-совместимая вставка: источники (домены VK Pixel/Top.Mail.Ru/GTM) регистрируются через канонический фильтр FilterBus
security.csp, инлайн-код — с nonce ядра, см./cms-v2/security - Запрет передачи персональных данных в параметрах событий
- Дедупликация события покупки по
event_idмежду client-side и server-side отправкой - Фиксированный порядок загрузки пикселей и исключение вставки в admin-зоне
Зависимости и выключение
requires: ядро, cms/cookie-consent · suggests: cms/analytics (общий реестр целей)
Поведение при выключении: пиксели не вставляются на страницы, события конверсий не отправляются во внешние системы — остальной сайт и оформление заказов работают без изменений, деградация без поломки. Внешних платных API нет — пиксели работают через клиентский JS провайдера, модуль не тарифицируется по числу событий.
Модель данных
| Таблица | Ключевые поля | Примечание |
|---|---|---|
cms_pixels_providers | id, code (vk|topmail|gtm), container_id, is_active, load_order | подключённые пиксели/контейнеры, порядок загрузки |
cms_pixels_event_map | id, provider_id, core_event, pixel_event_name | сопоставление событие ядра → событие пикселя |
FK provider_id — constrained() + index(); code — PHP Enum; уникальный индекс (provider_id, core_event) в cms_pixels_event_map защищает от дублей сопоставления.
ПДн-паспорт: модуль ПДн не хранит — container_id/event_map не персональные данные. context событий (payload в пиксель) фильтруется на входе от email/телефона/ФИО (см. «Безопасность»). Декларация: «ПДн не храню».
Входные и выходные данные
Входы:
| Источник | Поля | Чем валидируется |
|---|---|---|
| Форма провайдера (Filament) | code, container_id, is_active, load_order | FormRequest, code — whitelist поддерживаемых провайдеров |
| Форма сопоставления событий (Filament) | provider_id, core_event, pixel_event_name | FormRequest, core_event — whitelist известных событий ядра/коммерции/cms/analytics |
Событие ядра/коммерции (OrderCompleted, LeadCreated) | стандартный payload события | контракт события; слушатель тонкий — только маппинг на пиксель-событие |
| Согласие пользователя (чтение) | статус согласия | cms/cookie-consent (requires), не собственная валидация |
Выходы:
| Потребитель | Данные | Формат |
|---|---|---|
| Посетитель (браузер) | скрипты пикселей + вызовы событий | inline/async-скрипт с nonce, в фиксированном load_order |
| Filament admin | список провайдеров и сопоставлений | keyset-JSON через API |
| Внешние системы пикселей (VK/Top.Mail.Ru/GTM) | событие конверсии | клиентский вызов API провайдера с event_id для дедупликации |
| Подписчики событий | PixelEventFired | payload по таблице ниже |
Настройки (группа pixels)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
pixels.enabled | bool | true | да | Включение вставки пикселей |
pixels.vk_pixel_id | string | null | да | ID VK Pixel |
pixels.topmail_id | string | null | да | ID счётчика Top.Mail.Ru |
pixels.gtm_container_id | string | null | да | ID контейнера Google Tag Manager |
pixels.require_consent | bool | true | нет | Требовать согласие перед загрузкой скриптов |
pixels.dedupe_window_minutes | int | 30 | нет | Окно дедупликации event_id для события покупки |
pixels.admin_zone_kill_switch | bool | true | нет | Kill-switch: запрет вставки пикселей в admin-зоне (защита от учёта служебных визитов как трафика) |
Лимиты и квоты: у внешних провайдеров пикселей нет платных лимитов количества событий (в отличие от API счётчиков cms/analytics) — квот в этом модуле нет; единственный защитный барьер — dedupe_window_minutes против задвоения конверсий в отчётах партнёра.
API
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET | /api/v1/admin/pixels/providers | admin (pixels.view) | Список подключённых пикселей |
| POST | /api/v1/admin/pixels/event-map | admin (pixels.manage) | Правка сопоставления событий |
Компоненты
Виджеты: вставка скриптов пикселей с consent-гейтом. Filament: список провайдеров, редактор сопоставления событий. Чего нет: отдельного дашборда метрик (см. cms/analytics).
Демо-контент: PixelsDemoSeeder создаёт демо-провайдер vk (тестовый container_id) и одно сопоставление OrderCompleted → purchase — галерея /_gallery показывает вставку скрипта с consent-гейтом без реального рекламного кабинета.
Фронтенд-бюджет: скрипты пикселей грузятся async/defer в объявленном load_order, ни один не блокирует первую отрисовку; отсутствие пикселя (нет container_id) не оставляет пустых <script>-тегов на странице.
Эксплуатация (ранбук): метрики pixels_events_fired_total{provider}, pixels_events_deduped_total (сколько задвоений отсеяно); алерт — резкий обвал events_fired_total при живом трафике (подозрение на сбой скрипта/CSP-блокировку).
| Симптом | Что проверить / команда |
|---|---|
| Пиксель не грузится у посетителей | require_consent + статус согласия в cms/cookie-consent, container_id не пустой |
| Событие покупки задвоено в кабинете партнёра | проверить dedupe_window_minutes, event_id генерируется детерминированно от заказа |
| Пиксель виден в admin-зоне | admin_zone_kill_switch=true, проверить middleware исключения admin-роутов |
| Сопоставление события не срабатывает | сверить core_event в cms_pixels_event_map со списком реально издаваемых событий ядра/коммерции |
Бэкап/рестор: cms_pixels_providers/event_map — конфигурационные таблицы, входят в обычный бэкап целиком; денормализованных агрегатов нет — рестор не требует пересчёта.
События и обмен
| Событие | Когда | Payload |
|---|---|---|
PixelEventFired | событие ядра/коммерции отправлено в пиксель | provider_code, pixel_event_name, event_id, context (json, без ПДн) |
Слушает: события заказа/лида ядра и коммерции для отправки конверсий; при наличии cms/analytics использует его реестр целей вместо собственного дублирования (suggests).
Таблица взаимодействий:
| Сущность/модуль | Канал | Направление | Что происходит |
|---|---|---|---|
Ядро/коммерция (OrderCompleted, LeadCreated) | событие (1) | ядро → pixels | триггер маппинга на событие пикселя по cms_pixels_event_map |
cms/cookie-consent | сервис-вызов (4, requires) | pixels → cookie-consent | проверка статуса согласия перед инициализацией загрузчика скриптов |
cms/analytics (suggests) | сервис-вызов (4) | pixels → analytics | чтение общего реестра целей вместо дублирующего справочника |
| Внешние пиксели (VK/Top.Mail.Ru/GTM) | внешний канал (вне 5) | pixels(клиент) → провайдер | клиентский JS отправляет событие с event_id напрямую в API провайдера |
Подписчики PixelEventFired | событие (1) | pixels → любой | наблюдаемость/отчётность модулей, если появятся |
Фоновая работа
Фоновой работы нет — вставка скриптов и отправка событий конверсии происходит синхронно на рендере страницы (клиентский JS) и в обработчике события ядра.
Производительность и кеш
Ожидаемые объёмы: единицы провайдеров на сайт, десятки сопоставлений событий — таблицы малы, полностью читаются из кеша группы настроек (0 запросов на горячем пути рендера). Горячий путь — вставка скриптов на публичной странице: 0 запросов к БД, load_order и ID читаются из кеша настроек.
Критичные индексы: уникальный (provider_id, core_event) от дублей сопоставления. Собственных тегов кеша нет — участвует только в page-cache ядра. ID пикселей и enabled помечены affectsPageCache: изменение сбрасывает кеш страниц со вставкой скриптов.
Ожидаемая нагрузка на приём событий: клиентская отправка идёт напрямую в API провайдера пикселя, минуя сервер CMS — модуль не является промежуточным звеном для самого события, поэтому не создаёт дополнительного горячего пути на сервере сверх обычного рендера страницы. Единственная серверная работа — обработчик события ядра, формирующий PixelEventFired (лёгкий, без внешних вызовов; клиентская отправка события происходит уже отрисованным на странице JS-стеком).
Безопасность
Скрипты пикселей грузятся только после согласия из cms/cookie-consent (require_consent) — без него модуль не инициализирует загрузчик; источники доменов провайдеров (VK Pixel/Top.Mail.Ru/GTM) регистрируются модулем через канонический фильтр FilterBus security.csp (ревизия №2 ядра, п. 5) — это единственный способ разрешить домен, незарегистрированный <script> блокируется CSP-middleware ядра в режиме enforce; инлайн-код инициализации — с nonce ядра, не собственным. Персональные данные (email, телефон, ФИО) в параметрах событий запрещены и не передаются в payload.
Права: pixels.view, pixels.manage.
Провайдер отключён (is_active=false) — его скрипт не вставляется и его сопоставления событий не срабатывают, но сами строки cms_pixels_event_map не удаляются: включение провайдера обратно восстанавливает прежние сопоставления без повторной настройки.
Матрица ролей:
| Действие | Администратор | Менеджер | Редактор | Studio |
|---|---|---|---|---|
Просмотр провайдеров/сопоставлений (view) | ✅ | ✅ | ✅ | ✅ |
Правка сопоставления событий (manage) | ✅ | ✅ | — | ✅ |
Подключение/отключение провайдера, container_id | ✅ | — | — | ✅ |
admin_zone_kill_switch | ✅ | — | — | ✅ |
UX-требования
Админ: пустое состояние списка провайдеров — «Пиксели не подключены» со ссылкой «Добавить провайдер»; массовое включение/отключение провайдеров списком; человеческая ошибка при попытке сопоставить несуществующее core_event («Событие X не издаётся ни одним установленным модулем»); подтверждение перед отключением admin_zone_kill_switch (риск учёта служебного трафика).
Посетитель: скрипты пикселей не создают видимой задержки (async-загрузка, фиксированный load_order не создаёт гонки инициализации); отклонённое согласие не блокирует остальной функционал страницы.
Крайние случаи и типовые баги
- Порядок загрузки пикселей — GTM должен инициализироваться раньше пикселей, которые он может проксировать, либо независимо, если настроен напрямую →
load_orderявно задаётся в Filament, не выводится из порядка вставки в БД по умолчанию. - Дубль события покупки между client-side (браузер) и server-side (webhook/событие ядра) отправкой → дедупликация по
event_id, детерминированно вычисляемому от id заказа (не случайному), с окномdedupe_window_minutes; повторная отправка в пределах окна — no-op с логом, не повторная конверсия в кабинете партнёра. - Пиксель грузится в admin-зоне — без исключения администратор считается «посетителем» в статистике партнёра, искажая метрики →
admin_zone_kill_switchисключает/admin/*-роуты из вставки скриптов на уровне middleware, не на уровне разметки шаблона (нельзя забыть в новом admin-виде). - Отсутствие
cms/cookie-consent(жёсткая зависимостьrequires, не suggests) → модуль не активируется вовсе без согласия (fail-safe: без реализации consent провайдера — деградация до полного отсутствия пикселей, не «грузить без гейта» как вcms/analytics, потому чтоcms/cookie-consentздесьrequires, а не suggests). - CSP блокирует скрипт пикселя (неверный nonce/домен не в whitelist CSP) → пиксель тихо не загружается в браузере посетителя, ошибка видна в консоли разработчика, не ломает остальной рендер страницы; health-чек модуля не может проверить клиентскую загрузку напрямую — только валидность конфигурации ID/nonce на сервере.
- Suggests-модуль выключен (
cms/analyticsотсутствует) → сопоставление событий работает по собственномуcore_event-whitelist без общего реестра целей — функционал не деградирует, просто нет переиспользования справочника. - Модуль выключен посреди отправки события (например
PixelEventFiredсоздан, но модуль выключен до клиентской отправки) → клиентский JS для выключенного модуля не инициализирован вовсе (скрипт не вставлен), событие теряется на клиенте безопасно — не создаёт ошибок оформления заказа. - Пустой/огромный
contextв событии → whitelist полей ограничивает состав, превышение — событие всё равно отправляется в пиксель без лишних полей (усечение по whitelist, не 422 — конверсия важнее строгости на этом канале). - Измерение locale/city: событие конверсии одинаково для всех измерений — маппинг
core_event → pixel_event_nameне зависит отcity_id/locale; при необходимости разных пикселей на разные регионы — отдельные провайдеры с разнымcontainer_id, не встроенная логика измерений в этом модуле. - Противоречивые настройки:
require_consent=falseпри включённомcms/cookie-consent→ пиксели грузятся без ожидания согласия, хотя consent-модуль установлен. ⚠️ Противоречие: обходит смысл наличия consent-модуля и создаёт юридический риск. Разрешение: Filament показывает явное предупреждение при сохраненииrequire_consent=falseс установленнымcms/cookie-consent, требует отдельного подтверждения. - Конкурентное редактирование сопоставления событий двумя админами →
lock_version(добавляется кcms_pixels_event_map), конфликт — 409, не «последний победил». - Гонка инициализации: два пикселя с одинаковым
load_order→ детерминированный вторичный порядок поid(не случайный порядок из БД), чтобы поведение было воспроизводимо между запросами при одинаковом кеше.
Донорский код
Донор: — (новая разработка)
Тесты и приёмка
- [ ] Контрактный тест: событие
OrderCompletedтранслируется в сопоставленное событие пикселя - [ ] Скрипты пикселей не грузятся до согласия пользователя (consent-гейт,
requires) - [ ] Вставка не ломает page-cache (ID из настроек, источники доменов зарегистрированы через фильтр
security.csp, инлайн — nonce ядра) - [ ] При выключении модуля или отсутствии
cms/cookie-consentпиксели не вставляются, ошибок нет - [ ] Дедупликация
event_idпредотвращает задвоение конверсии покупки в окнеdedupe_window_minutes - [ ]
admin_zone_kill_switchисключает вставку скриптов в/admin/* - [ ] Права
pixels.view/pixels.manageразграничивают чтение и правку сопоставлений - [ ] Персональные данные (email/телефон/ФИО) отсутствуют в payload событий
- [ ] Конкурентная правка одного сопоставления двумя админами отдаёт 409
- [ ] Контрактный набор
cms-testingзелёный, пакет тестируется в testbench-изоляции - [ ] Feature-тест на каждый роут API; тестовая БД только
pixels_test,migrate:freshзапрещён