Skip to content

ТЗ — A/B-тесты (cms/ab-testing)

Слой: 🟡 функц-модуль · Зрелость доноров: ★ · Донор: — Статус: ТЗ к разработке

Назначение и возможности

Сплит-тестирование вариантов страниц и блоков поверх ревизий контента ядра. Вариант выбирается стабильно по cookie-бакету ещё до формирования page-cache, чтобы кеш не смешивал варианты между посетителями. Цели берутся из cms/analytics, значимость и автозавершение считаются по накопленной статистике.

  • Варианты страниц/блоков как альтернативные ревизии контента ядра
  • Стабильный сплит по проценту через cookie-бакет (один посетитель — один вариант всю сессию)
  • Выбор варианта на edge/middleware до кеша — исключает утечку варианта B посетителю варианта A
  • Ключ page-cache включает идентификатор варианта (сегментация кеша по тесту)
  • Цели конверсии берутся из реестра cms/analytics, без дублирования учёта событий
  • Расчёт статистической значимости по накопленным данным теста
  • Автозавершение теста при достижении значимости или лимита длительности
  • Совместимость со scheduled-publishing: тест не переживает публикацию новой ревизии втихую

Зависимости и выключение

requires: ядро (ревизии контента), cms/analytics · suggests:

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

Модель данных

ТаблицаКлючевые поляПримечание
cms_ab_testsid, name, subject_type, subject_id, status, split_percent, started_at, ends_at, lock_versionтест на странице/блоке
cms_ab_variantsid, test_id, code, revision_id, is_controlварианты теста, ссылка на ревизию ядра
cms_ab_assignmentsid, test_id, visitor_token, variant_id, assigned_atстабильное закрепление посетителя за вариантом
cms_ab_conversionsid, test_id, variant_id, goal_code, visitor_token, converted_atзафиксированные конверсии по цели cms/analytics

FK test_id, variant_id, revision_idconstrained() + index(); status — PHP Enum; уникальный индекс (test_id, visitor_token) в cms_ab_assignments гарантирует стабильность закрепления; cms_ab_conversions — журнальная таблица, кандидат на BRIN по converted_at; lock_version на cms_ab_tests — конкурентная правка теста.

ПДн-паспорт: visitor_token — обезличенный идентификатор, ПДн не хранится ни в одной из четырёх таблиц. Ретеншн: cms_ab_assignments/cms_ab_conversions — журналы завершённого теста хранятся как исторические данные (полезны для ретроспективы), явного TTL нет по умолчанию — при накоплении множества завершённых тестов рекомендуется архивация через общую политику ретеншна журналов ядра.

Входные и выходные данные

Входы:

ИсточникПоляЧем валидируется
Форма создания теста (Filament)name, subject_type, subject_id, split_percent, variants[], goal_codeFormRequest, subject_type — whitelist поддерживаемых сущностей ядра, goal_code — сверяется с реестром cms/analytics
Публичный запрос страницы (посетитель)cookie-бакет / его отсутствиеmiddleware: детерминированный хэш visitor_token → бакет 0–99, без пользовательского ввода
Событие цели конверсии (cms/analytics)goal_code, contextконтракт события AnalyticsGoalTriggered, слушатель проверяет активное закрепление посетителя за тестом
Команда/API завершения тестаtest_idab-testing.manage, FormRequest

Выходы:

ПотребительДанныеФормат
Посетительвариант контента (ревизия)Blade-рендер выбранной ревизии, идентификатор варианта — в ключе кеша
Filament adminсписок тестов, дашборд значимостиkeyset-JSON через API
page-cache ядраизмерение variant_id для ключа кешавнутренний контракт кеша (не API)
Подписчики событийAbTestVariantAssigned, AbTestConversionRecorded, AbTestFinishedpayload по таблице ниже

Настройки (группа ab-testing)

КлючТипДефолтaffectsPageCacheОписание
ab-testing.enabledbooltrueдаВключение сплит-тестирования
ab-testing.default_split_percentint50даДефолтный процент сплита для новых тестов
ab-testing.significance_thresholdfloat0.95нетПорог статистической значимости для автозавершения
ab-testing.max_duration_daysint30нетЛимит длительности теста до принудительного завершения
ab-testing.min_sample_sizeint100нетМинимум конверсий на вариант до расчёта значимости (защита от ранней остановки на шуме)
ab-testing.max_concurrent_testsint10нетЛимит одновременно активных тестов на сайт
ab-testing.kill_switchboolfalseдаKill-switch: аварийная остановка всех активных тестов (все посетители видят контроль) без выключения модуля

Лимиты и квоты: max_concurrent_tests защищает от неконтролируемого роста числа активных сегментов кеша (каждый тест — новое измерение ключа); достижение лимита отклоняет создание нового теста 422 с понятной ошибкой, не превышает лимит молча.

API

МетодПутьДоступНазначение
GET/api/v1/admin/ab-testing/testsadmin (ab-testing.view)Список тестов со статусами и статистикой
POST/api/v1/admin/ab-testing/testsadmin (ab-testing.manage)Создание теста с вариантами и сплитом
POST/api/v1/admin/ab-testing/tests/{id}/finishadmin (ab-testing.manage)Принудительное завершение теста

Компоненты

Filament: конструктор теста (варианты, сплит, цели), дашборд значимости по тесту. Команды: cms:ab-testing:evaluate --json (расчёт значимости и автозавершение).

Демо-контент: AbTestingDemoSeeder создаёт один активный демо-тест на демо-странице с двумя вариантами (контроль + вариант B) и синтетическими закреплениями/конверсиями — дашборд значимости в галерее /_gallery показывает график без реального трафика.

Фронтенд-бюджет: выбор варианта — серверный (middleware), клиентского JS для переключения контента не требуется — 0 влияния на фронтенд-бюджет; сам вариант рендерится как обычная ревизия темой без дополнительных ассетов.

Эксплуатация (ранбук): метрики ab_testing_assignments_total{test_id}, ab_testing_conversions_total{test_id,variant_id}, ab_testing_evaluate_duration_seconds; алерт — тест превысил max_duration_days без автозавершения (джоба evaluate не отрабатывает).

СимптомЧто проверить / команда
Посетители видят «прыгающий» контент между визитамипроверить TTL cookie-бакета, уникальность (test_id, visitor_token)
Тест не завершается автоматическиcms:ab-testing:evaluate --json, проверить min_sample_size/significance_threshold
Кеш смешивает вариантыубедиться, что variant_id в ключе page-cache, см. «Производительность и кеш»
Слишком много активных тестов замедляет кешсверить max_concurrent_tests, оценить число сегментов кеша

Бэкап/рестор: все четыре таблицы — в бэкапе целиком (включая журналы конверсий — статистика теста должна переживать рестор без потерь); cms:ab-testing:evaluate безопасно перезапускается после рестора для пересчёта незавершённых тестов.

События и обмен

СобытиеКогдаPayload
AbTestVariantAssignedпосетителю закреплён вариантtest_id, variant_id, visitor_token
AbTestConversionRecordedзафиксирована конверсия по целиtest_id, variant_id, goal_code
AbTestFinishedтест завершён (значимость/лимит)test_id, winning_variant_id, reason

Слушает: события целей cms/analytics для учёта конверсий по вариантам (requires, без дублирования собственного реестра целей).

Таблица взаимодействий:

Сущность/модульКаналНаправлениеЧто происходит
cms/analytics (AnalyticsGoalTriggered)событие (1, requires)analytics → ab-testingконверсия посетителя засчитывается варианту, за которым он закреплён
Ревизии контента ядрасервис-вызов (4, requires)ab-testing → ядровариант ссылается на конкретную ревизию, рендерится как обычная опубликованная страница
page-cache ядраизмерение ключа кеша (внутренний контракт)ab-testing → ядроvariant_id сегментирует кеш аналогично city_id/locale
очередь ab-testingочередь (5)ab-testing → воркерevaluate — расчёт значимости и автозавершение
Подписчики AbTestFinishedсобытие (1)ab-testing → любойнапример уведомление ответственного менеджера о завершении теста

Фоновая работа

Очередь ab-testing: evaluate — плановый расчёт статистической значимости и автозавершение теста через ScheduleRegistrar. Выбор варианта — синхронный на middleware/edge, до формирования кеша.

Производительность и кеш

Ожидаемые объёмы: единицы–десятки одновременно активных тестов (max_concurrent_tests), тысячи-десятки тысяч cms_ab_assignments на популярный тест. Горячий путь — разрешение варианта на входе запроса (middleware, до кеша): чтение cookie-бакета — 0 запросов к БД при наличии cookie; при первом визите — один insert в cms_ab_assignments с уникальным индексом (test_id, visitor_token) (upsert-безопасно при гонке).

Собственный тег ab-testing:{test_id} для сброса кеша сегментированных вариантов при завершении/остановке теста. Ключ page-cache ядра обязан включать идентификатор варианта теста — иначе кеш смешает контрольную и тестовую версии между посетителями; это контрактный тест, не рекомендация. При нескольких одновременных тестах на разных сущностях ключ кеша включает variant_id каждого применимого к странице теста — max_concurrent_tests ограничивает комбинаторный рост числа сегментов.

Безопасность

Выбор варианта происходит на сервере (middleware) — клиент не может подменить вариант напрямую; создание/завершение теста — только под ab-testing.manage через FormRequest. visitor_token не связан с персональными данными.

Права: ab-testing.view, ab-testing.manage.

Матрица ролей:

ДействиеАдминистраторМенеджерРедакторStudio
Просмотр тестов и статистики (view)
Создание теста, привязка вариантов к ревизиям (manage)
Принудительное завершение теста
ab-testing.kill_switch (аварийная остановка всех тестов)

UX-требования

Админ: пустое состояние списка тестов — «Активных тестов нет» со ссылкой «Создать тест»; массовое завершение нескольких тестов сразу; человеческая ошибка при попытке создать вариант со ссылкой на неопубликованную/отсутствующую ревизию («Ревизия ещё не опубликована — тест не может стартовать»); подтверждение перед принудительным завершением теста, не достигшего значимости («Тест завершится без статистически значимого результата — продолжить?»).

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

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

  1. Липкость варианта (cookie) — истечение/удаление cookie посетителем → новое закрепление по (test_id, visitor_token), если visitor_token восстановим (например из UTM/сессии), иначе — новый бакет-хэш; тест допускает единичные «пересдвиги» варианта при потере cookie, это не считается критичным дефектом.
  2. Page-cache и сегментация вариантов — без сегментации посетитель варианта A увидит закешированный вариант B. Механизм зафиксирован ревизией ядра 14.07.2026 (п. 7): тест отдельных блоков — блок объявляет себя personalized (каркас страницы в общем кеше, вариант — фрагментом/островом); полностраничный тест (layout, лендинг целиком) — variant_id как измерение ключа page-cache тестируемой страницы через тот же механизм сегментации ядра, самодельные ключи запрещены. Контрактный тест на утечку кеша между вариантами обязателен, как для cms/multicity.
  3. Стат-значимость и ранняя остановка — автозавершение при первом же достижении significance_threshold без учёта размера выборки даёт ложные срабатывания (проблема «peeking») → min_sample_size — обязательный гейт перед расчётом значимости, evaluate не завершает тест раньше накопления минимальной выборки на каждый вариант.
  4. Тест на странице со scheduled-publishing — плановая публикация новой ревизии базовой страницы во время активного теста → публикация контрольной ревизии не должна тихо ломать привязку варианта-контроля к устаревшей ревизии; тест либо продолжает использовать зафиксированную на старте ревизию (детерминированность эксперимента), либо явно требует ручного решения редактора — конфликт логируется, не применяется автоматически без уведомления.
  5. Гонка первого визита — два параллельных запроса одного нового посетителя (например preload + основной документ) пытаются создать cms_ab_assignments одновременно → уникальный индекс (test_id, visitor_token) + upsert разрешают гонку без дублей, оба запроса получают один и тот же variant_id.
  6. Выключение модуля посреди активного тестаkill_switch или полное отключение модуля → все посетители мгновенно видят контрольный вариант, накопленная статистика не удаляется, при повторном включении тест можно возобновить (не обязан начинаться заново).
  7. Отсутствие requires-модуля cms/analytics (жёсткая зависимость) → модуль не может стартовать новые тесты без реестра целей — self-test при enable заваливается с понятной ошибкой «требуется cms/analytics»; уже идущие тесты (если cms/analytics выключили посреди) не могут фиксировать новые конверсии — деградация до сбора только AbTestVariantAssigned без ConversionRecorded.
  8. Пустой/огромный тест — тест без конверсий за весь max_duration_days → принудительно завершается по лимиту длительности с reason = duration_limit, не висит бесконечно; тест с огромным трафиком (миллионы assignments) — таблица cms_ab_assignments проектируется под партиционирование по test_id/времени при росте.
  9. Измерение city/locale — тест на региональной странице должен работать независимо от city_id/locale как дополнительных измерений: ключ кеша включает и variant_id, и city_id/locale одновременно (композитное измерение, не взаимоисключающее) — контрактный тест проверяет оба режима (с мультигородом и без).
  10. Противоречивые настройки: split_percent вариантов в сумме не равен 100% → отклоняется на создании (422 «сумма сплитов должна быть 100%»), не тихая нормализация значений.
  11. Конкурентное редактирование теста двумя админами (например один меняет сплит, другой завершает тест) → lock_version на cms_ab_tests, конфликт — 409 с человеческим сообщением, не «последний победил».
  12. Suggests отсутствует и жёсткая зависимость одновременно — у модуля нет suggests, только requires: cms/analytics; это осознанно (без реестра целей тест бессмысленен) — задокументировано, чтобы не путать с типовым паттерном мягкой зависимости остальных модулей блока «Маркетинг».

Донорский код

Донор: — (новая разработка)

Тесты и приёмка

  • [ ] Контрактный тест: один и тот же посетитель получает один и тот же вариант повторно
  • [ ] Ключ page-cache включает вариант — посетители разных вариантов не видят чужой кеш
  • [ ] Выбор варианта происходит до формирования кеша (middleware/edge), не после
  • [ ] Гонка первого визита не создаёт дублей cms_ab_assignments (уникальный индекс + upsert)
  • [ ] min_sample_size не даёт завершить тест раньше накопления минимальной выборки
  • [ ] Автозавершение срабатывает при достижении significance_threshold или max_duration_days
  • [ ] kill_switch мгновенно переключает всех посетителей на контроль без выключения модуля
  • [ ] Сумма split_percent вариантов, не равная 100%, отклоняется при создании теста
  • [ ] При выключении модуля все посетители видят контрольный вариант без ошибок
  • [ ] Композитная сегментация кеша (variant_id + city_id/locale) работает без утечек
  • [ ] Конкурентная правка одного теста двумя админами отдаёт 409, не «последний победил»
  • [ ] Права ab-testing.view/ab-testing.manage разграничивают просмотр и управление тестами
  • [ ] Контрактный набор cms-testing зелёный, пакет тестируется в testbench-изоляции
  • [ ] Feature-тест на каждый роут API; тестовая БД только ab-testing_test, migrate:fresh запрещён

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