0065. Периоды активности вариантов A/B-теста и сбор CTR по окнам
Копия ADR из репозитория:
backend/docs/adr/0065-ab-test-variant-periods.md. Канонический текст — в репозитории; портал обновляется командойnpm run sync:adr.
- Статус: accepted
- Дата: 2026-09-28
- Связанные задачи: OP-143, OP-144, OP-145, OP-146 (5.14)
Контекст и проблема
AbTestMetricsService.collect() (OP-144) нигде не вызывался, кроме unit-тестов:
POST /ab-tests/:id/analyze читал пустую ab_test_variant_metrics и почти всегда
отдавал insufficient-data. Метрики считаются из витрины рекламы
ad_campaign_stats (синк автобиддера), а она подневная и привязана к nmId
карточки, а не к варианту. Карточка WB показывает один вариант за раз, поэтому
без знания «какой вариант когда стоял» все варианты получали бы одинаковые
суммы показов/кликов.
Рассмотренные варианты
- Окна задаёт клиент (
collect(variantId, from, to)через API) — просто, но фронт должен помнить историю переключений; ошибки окна → двойной счёт. - Периоды активности на бэкенде (
ab_test_variant_periods) — бэкенд сам фиксирует моменты активации/переключения и собирает метрики по ним. - Разнесение по дням на лету в
analyzeбез хранения — нельзя показать промежуточный CTR и нет аудита переключений.
Решение
Вариант 2.
- Таблица
ab_test_variant_periods(ab_test_id,variant_id,started_at,ended_atNULL = активен). Частичный уникальный индекс — не более одного открытого периода на тест; смена периода — подpessimistic_writeстроки теста. ab_test_variant_metrics.period_id(FK, UNIQUE): одна строка метрик на период, повторный сбор открытого периода сдвигаетperiod_end(upsert поperiod_id), а не плодит пересекающиеся окна, которые анализ сложил бы дважды.- Окно периода — дни MSK (витрина WB в MSK). День переключения целиком
отходит новому варианту: окно закрытого периода с преемником заканчивается
накануне дня
ended_at; последний период (закрыт stop/analyze) включает день закрытия; открытый — до «сегодня». Период внутри одного дня (активирован и переключён в тот же день) полного дня не содержит — метрик нет. - Жизненный цикл:
startоткрывает первый период:body.variantId→ контрольный → первый созданный;POST /ab-tests/:id/variants/:variantId/activate— переключение: закрыть текущий период + собрать его, открыть новый (повтор для активного — идемпотентен);stop— закрыть открытый период + собрать;analyze— закрыть открытый период, пересобрать все периоды, затем расчёт.
- Cron
25 */3 * * *(Europe/Moscow) через реестр расписаний (OP-42), очередьab-test-metrics: пересобирает периоды всехrunning-тестов (витрина рекламы догружается с задержкой — закрытые окна тоже уточняются). Одно глобальное расписание — новые тесты подхватываются без рестарта. GET /ab-tests/:id/periods— периоды с метриками для UI.
Последствия
- Положительные: CTR честно разнесён по вариантам;
analyzeполучает данные; есть история переключений для аудита и графиков. - Отрицательные / компромиссы: точность — сутки (день переключения целиком у
нового варианта; утро дня старта теста засчитывается первому варианту).
Витрина — только рекламный трафик (
ad_campaign_stats), органика не учтена. Реальную смену фото на карточке переключение не делает — это отдельный эндпоинт apply (ADR-0062). - Дальше: при появлении воронки карточки (
nm-report) — перейти на полный трафик.