{ "index": 90, "slug": "editorial-2025-07-practice-product-metrics", "title": "Метрики продукта для инженера: связать изменение с решением", "excerpt": "Рост conversion не доказывает пользу изменения. Разбираем, как связать технический сигнал, cohort, denominator, пользовательский outcome и guardrail, чтобы ошибка измерения остановила решение до релиза.", "contentHtml": "
После ускорения checkout на графике выросла conversion. Команда готовит rollout. Через несколько дней выясняется: повторные открытия перестали попадать в denominator, а часть ошибок рендера исчезла из отчёта вместе с событием. Пользователи не стали чаще подтверждать заказ. Изменился способ счёта.
Цена ошибки — решение по ложному сигналу. Команда выпускает изменение, тратит время на обратное расследование и теряет возможность сравнить варианты в одном окне. Если ошибка затрагивает платежный или регистрационный путь, к этому добавляются незавершённые операции и обращения в поддержку.
Тезис: продуктовая метрика для инженера — это не имя на дашборде, а контракт. Он связывает техническое изменение с наблюдаемым действием, задаёт cohort, период и denominator, а рядом держит guardrail. При разрыве связи расчёт должен остановиться. Число без этих условий не становится доказательством.
Техническое изменение само по себе не является продуктовым результатом. Предзагрузка формы может сократить ожидание. Сокращённое ожидание может изменить долю открывших checkout, которые нажали confirm. Но между этими утверждениями стоят события, идентификаторы и правила включения.
Для каждого измерения назовите пять звеньев:
Например, гипотеза звучит так: «Предзагрузка формы увеличит долю подтверждений среди пользователей, открывших checkout, но не повысит долю render failure». Это проверяемая цепочка. Формулировка «сделаем экран быстрее и поднимем conversion» цепочки не содержит.
Событие отвечает на вопрос «что произошло», а его поля — на вопросы «с кем», «в каком варианте», «когда» и «как связать шаги». Имя вроде checkout_confirmed полезнее произвольного button_click, но одного имени мало. Два одинаковых события могут относиться к разным вариантам и разным попыткам.
Минимальный учебный контракт может выглядеть так:
const event = {\n name: 'product.checkout_confirmed',\n subjectId: 'u-17',\n cohort: 'treatment',\n period: '2025-07-14',\n requestId: 'r-204',\n schemaVersion: 1,\n};\n\n// Учебный объект в памяти. Он не отправляет telemetry\n// и не показывает результат реального продукта.subjectId нужен, чтобы повторная доставка события не увеличила denominator. cohort не следует восстанавливать по текущему флагу: пользователь мог увидеть один вариант, а запросить данные после переключения флага. period не даёт смешать окна. requestId связывает открытие, подтверждение и техническую ошибку одной попытки.
OpenTelemetry разделяет traces, metrics и logs как разные сигналы наблюдаемости. Это полезная граница: latency можно увидеть в span, число ошибок — в metric, а контекст конкретной попытки — в log или event. Но сама телеметрия не создаёт product contract. Владелец решения должен заранее определить, какие сигналы отвечают на его вопрос.
Учебная локальная метрика может быть записана так:
conversion(cohort, period) =\n unique subjects with checkout_confirmed\n /\n unique subjects with checkout_opened\n\nrenderFailureRate =\n unique subjects with render_failed\n /\n unique subjects with checkout_openedОбе дроби используют одну базу opened, один cohort и один период. Это не универсальное определение conversion. Реальный продукт может считать заказ, оплату или завершённую сессию иначе. Важно другое: правило нельзя менять между вариантами, а его состав нужно хранить рядом с результатом.
Если один пользователь открыл checkout три раза, denominator по subjects равен одному, а не трём. Если повторная попытка имеет другой смысл для продукта, это решение нужно зафиксировать до подсчёта. Нельзя выбрать удобный вариант после просмотра результата.
Attribution связывает outcome с показанным вариантом. Если подтверждение пришло без requestId, расчёт не должен молча принять его. Оно могло относиться к старому экрану, другой вкладке или повторной попытке. В этом случае правильный статус — остановка с причиной missing-attribution-rule, а не нулевая conversion.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Conversion выросла сразу после изменения | Из denominator исчезли повторные или ошибочные открытия | Сравнить множества unique subjects и правило включения до и после | Остановить интерпретацию и восстановить сопоставимый denominator |
| Confirm есть, но вариант неизвестен | Нет attribution или requestId | Проверить связь opened, confirmed и cohort для каждой попытки | Вернуть hold, добавить ключ связи и тест отрицательного пути |
| Treatment лучше control, но даты различаются | Смешаны cohort или period | Сверить период каждого события и источник cohort | Пересобрать окна и не сравнивать текущие числа |
| Локальная метрика растёт вместе с отказами | Guardrail не включён в решение | Посчитать render failure на той же базе opened | Остановить rollout и разобрать технический путь отказа |
| На графике появились нули | Пайплайн не отличает отсутствие данных от нулевого результата | Проверить статус расчёта и причины отклонения | Показывать stop reason отдельно от числового значения |
| Число меняется после повторного запуска | Дубликаты событий или плавающее окно | Проверить idempotency по subjectId, requestId и period | Зафиксировать дедупликацию и повторить расчёт |
Предположим, есть два cohort и один день наблюдения. В каждом варианте два пользователя открыли checkout. В treatment один пользователь подтвердил действие. В control один пользователь подтвердил действие. У treatment дополнительно зафиксирован один render failure.
В таком маленьком наборе обе conversion равны 0,5. Это не результат эксперимента и не основание для запуска. Он показывает только форму контракта: одинаковые знаменатели, явный cohort и guardrail рядом. Если удалить cohort у одного confirm, расчёт должен остановиться, даже если арифметика всё ещё возможна.
function evaluate(events) {\n const required = events.every((event) =>\n event.subjectId && event.cohort && event.period && event.requestId\n );\n\n if (!required) {\n return { status: 'hold', reason: 'missing-attribution-rule' };\n }\n\n const opened = unique(events, 'checkout_opened', 'subjectId');\n const confirmed = unique(events, 'checkout_confirmed', 'subjectId');\n const failures = unique(events, 'render_failed', 'subjectId');\n\n return {\n status: 'eligible-for-human-review',\n conversion: confirmed.size / opened.size,\n renderFailureRate: failures.size / opened.size,\n };\n}Функция учебная. В ней нет проверки случайного распределения, задержки доставки, часовых поясов, privacy-политики или достаточного размера выборки. Она иллюстрирует отрицательный путь: отсутствие обязательного поля переводит расчёт в hold до деления. В production это правило должно жить в проверяемом pipeline, а не только в тексте.
Такая схема не заменяет экспериментальный дизайн. Она не доказывает randomization, причинность, статистическую значимость или долгосрочный outcome. Она не исправляет потерю событий и не знает, был ли пользователь заблокирован сетью. Она только делает условия сравнения явными и не даёт незаметно продолжить при нарушенном контракте.
Один guardrail не покрывает все риски. Для платежа важны отказ и незавершённая операция. Для регистрации — доступность и повторная отправка. Для медленного интерфейса — время до действия и ошибки клиента. Выбирайте guardrail по цене конкретного ухудшения, а не по удобству существующего дашборда.
Нельзя выдавать рост local metric за рост выручки или удовлетворённости. Нельзя считать отсутствие события нулевым значением без проверки доставки. Нельзя сравнивать cohort, собранные разными версиями схемы, если вы не доказали сопоставимость. Если это невозможно, честный результат — hold и план исправления данных.
Проверка готова, когда другой инженер может по decision record восстановить гипотезу, cohort, период, numerator, denominator, attribution, guardrail и owner. Для каждого числа есть источник событий. Для каждого stop reason есть воспроизводимый вход. Повторный запуск на том же окне даёт тот же результат.
Минимальный набор доказательств — контракт событий, пример успешного расчёта, три отрицательных проверки, сравнение local metric с guardrail и запись ограничения. Только после этого human owner выбирает rollout, hold или rollback. Если связь между изменением и outcome не доказана, система не обязана выдавать красивую цифру. Она обязана показать, где цепочка оборвалась.