{ "index": 89, "slug": "editorial-2025-07-mechanism-product-metrics", "title": "Рост метрики не равен улучшению продукта: проверяем denominator и guardrail", "excerpt": "Практический способ проверить продуктовую метрику до решения: зафиксировать событие, cohort, attribution, окно и denominator, затем сопоставить локальный сигнал с guardrail и остановить вывод при разрыве данных.", "contentHtml": "
На дашборде treatment показывает conversion 52%, а control — 48%. Команда готовит выпуск. Через день выясняется, что в treatment считали уникальных открывших экран, а в control — все строки события. Ещё часть подтверждений пришла без связи с вариантом. Числа выглядят аккуратно, но сравнивают разные множества.
\nЦена ошибки — не только неверный график. Команда может раскатить изменение, которого пользователь не заметил, потерять доверие к аналитике и потратить следующий спринт на поиск причины. Если рядом выросли отказы, задержка или отмены, локальный рост conversion скрывает ущерб.
\nТезис простой: метрика становится основанием для решения только вместе с контрактом измерения. Контракт называет субъектов, событие, cohort, attribution, период, numerator, denominator и guardrail. Любое нарушение контракта должно остановить product decision. Пустой или неполный результат не следует трактовать как нулевой эффект.
\nЗапись 52 / 100 ничего не говорит без описания ста субъектов и пятидесяти двух действий. В продуктовой метрике нужно сначала определить population, затем выбрать единицу счёта. Если один пользователь повторил событие три раза, число строк и число пользователей отвечают на разные вопросы.
Учебный пример ниже считает conversion по уникальным субъектам. Numerator — субъекты с checkout_confirmed. Denominator — субъекты с checkout_opened. Обе группы ограничены одним cohort и одним днём. Guardrail считает render_failed среди открывших. Это фиксированные значения для иллюстрации. Они не описывают production и не доказывают эффект.
const events = [\n { event: 'checkout_opened', subject: 'u-1', cohort: 'control', period: '2025-07-14' },\n { event: 'checkout_confirmed', subject: 'u-1', cohort: 'control', period: '2025-07-14' },\n { event: 'checkout_opened', subject: 'u-2', cohort: 'control', period: '2025-07-14' },\n { event: 'render_failed', subject: 'u-2', cohort: 'control', period: '2025-07-14' },\n { event: 'checkout_opened', subject: 'u-3', cohort: 'treatment', period: '2025-07-14' },\n { event: 'checkout_confirmed', subject: 'u-3', cohort: 'treatment', period: '2025-07-14' },\n { event: 'checkout_opened', subject: 'u-4', cohort: 'treatment', period: '2025-07-14' },\n];\n\nconst unique = (name, cohort) => new Set(\n events.filter((x) => x.event === name && x.cohort === cohort)\n .map((x) => x.subject),\n).size;\n\nconst conversion = unique('checkout_confirmed', 'treatment')\n / unique('checkout_opened', 'treatment');\nconst guardrail = unique('render_failed', 'control')\n / unique('checkout_opened', 'control');\n\n// Учебный результат: treatment conversion = 0.5,\n// control guardrail = 0.5. Это не production-вывод.\nВ этом наборе treatment conversion равна 1 из 2, а control guardrail — 1 из 2. Эти дроби нужны, чтобы показать форму вычисления, а не чтобы объявить treatment лучше. В реальной системе дополнительно проверяют распределение вариантов, задержку доставки, повторные события, идентификаторы, окно наблюдения и статистическую неопределённость.
\nattribution связывает действие с вариантом. Например, подтверждение можно отнести к treatment, если у события есть тот же subject и request или сохранённый exposure key. Если связи нет, система не должна угадывать. Она возвращает hold и причину missing-attribution.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Conversion выросла сразу после изменения tracking | Пропали открытия или изменился способ deduplication | Сравнить число субъектов, строк и долю доставки каждого события до и после изменения | Остановить интерпретацию; проверить instrumentation и denominator |
| Treatment и control имеют разные размеры | Нарушилось распределение вариантов или одна группа потеряла события | Сверить ожидаемое и наблюдаемое соотношение, exposure и data-quality metric | Не объявлять победителя; найти источник mismatch |
| Подтверждение не содержит cohort или request | Нет правила attribution | Проверить event schema и цепочку от exposure до outcome | Вернуть hold; не приписывать outcome варианту |
| Local metric растёт, но растут ошибки рендера | Выигрыш куплен ухудшением соседнего шага | Посчитать guardrail по той же population и тому же окну | Сверить порог с владельцем риска и остановить выпуск при нарушении |
| В одном отчёте смешаны два дня | Query собрал разные окна | Проверить period на каждой записи и границы окна | Пересобрать выборку; не усреднять разрыв молча |
Положительный путь означает не «метрика хорошая». Он означает, что измерение прошло базовые проверки и может попасть к владельцу решения. Минимальный результат содержит definition, population, период, attribution, guardrail и список ограничений.
\nfunction inspect(report) {\n const reasons = [];\n\n if (!report.attribution) reasons.push('missing-attribution');\n if (report.denominator !== 'unique-opened-subjects') {\n reasons.push('wrong-denominator');\n }\n if (report.periods.length !== 1) reasons.push('mixed-period');\n if (report.guardrailRate > report.guardrailLimit) {\n reasons.push('guardrail-breached');\n }\n\n return reasons.length === 0\n ? { status: 'eligible-for-human-review', reasons: [] }\n : { status: 'hold', reasons };\n}\n\n// Код иллюстрирует stop conditions.\n// Он не читает telemetry и не принимает решение о выпуске.\nВ отрицательном пути нет попытки «починить» данные средним значением или подстановкой default cohort. Если период смешан, выборку пересобирают. Если denominator изменился, заново описывают метрику. Если нет attribution, чинят схему события или правила связи. Если guardrail превышен, владелец риска решает, допустимо ли продолжать. Функция не должна скрывать эти причины в null или в зелёном статусе.
Такое разделение защищает от двух подмен. Первая — движение локальной метрики превращают в причинное объяснение. Вторая — техническую проверку превращают в автоматический ship. Инспектор может сказать «условия расчёта выполнены» или «расчёт остановлен». Он не может доказать, что изменение вызвало результат, если дизайн и данные этого не показывают.
\nSuccess metric отвечает на вопрос о желаемом результате. Local metric помогает понять ближайший шаг. Guardrail ограничивает цену улучшения. Например, форма может увеличить число подтверждений, но одновременно повысить ошибки рендера или отмены. Если guardrail появляется только после обсуждения успеха, команда уже выбрала удобную рамку.
\nGuardrail должен иметь population, окно, единицу счёта и владельца порога. «Ошибок стало больше» недостаточно. Нужны доля, база и правило: например, render_failed unique subjects / checkout_opened unique subjects в том же cohort и периоде. Порог задают до интерпретации результата. Его не следует подбирать после того, как local metric уже выросла.
Отдельная data-quality metric проверяет, можно ли доверять самой выборке. Она не является guardrail пользовательского опыта. Sample-ratio mismatch, потеря exposure или резкий провал доставки событий могут остановить анализ раньше, чем команда посмотрит conversion. Это отрицательный путь измерения, а не доказательство плохого продукта.
\nКонтракт метрики не заменяет дизайн эксперимента. Он не доказывает случайное распределение, достаточную мощность, отсутствие сезонности или причинный эффект. Небольшой fixed набор в примере не моделирует реальный трафик. Имена u-1 и u-2 не являются советом хранить открытые идентификаторы пользователя.
Событие с корректным именем всё равно может потеряться в клиенте, задержаться в очереди или попасть в другую систему времени. Поэтому проверка схемы должна сопровождаться проверкой доставки и задержки. Для финансовых, медицинских и других чувствительных сценариев нужны отдельные правила приватности, retention и доступа.
\nПроверяемый критерий готовности такой: независимый инженер по записи может восстановить population, numerator, denominator, cohort, период, attribution и guardrail. На валидном наборе система возвращает eligible-for-human-review, а на каждом специально испорченном наборе — hold с причиной. Ни один путь не публикует результат и не запускает rollout автоматически. Если критерий не выполняется, сначала ремонтируют измерение.