{ "index": 88, "slug": "editorial-2025-07-field-product-metrics", "title": "Когда рост conversion не означает улучшение продукта", "excerpt": "Conversion имеет смысл только вместе с определением событий, cohort, периодом, denominator и guardrail. Разбираем механизм ошибки, отрицательные пути и проверяемый критерий готовности.", "contentHtml": "
На дашборде растёт conversion, а число ошибок рендера растёт вместе с ним. Пользователь открывает форму повторно, но повторное открытие уже не попадает в denominator. Команда видит красивую дробь и оставляет новый вариант. Через день выясняется, что сравнивались разные множества событий. Цена ошибки — неверное решение, повторный сбор данных и часы спора о том, что именно измерила система.
\nТезис простой: метрика становится основанием для решения только вместе с условиями её получения. Нужно связать изменение, событие, cohort, period, numerator, denominator и guardrail. Если связь не доказана, расчёт должен остановиться с понятной причиной. Неполный результат лучше честного на вид числа, которое отвечает на другой вопрос.
\nConversion — это отношение numerator к denominator. Например, numerator может считать уникальных субъектов с событием checkout_confirmed, а denominator — уникальных субъектов с событием checkout_opened. Дробь отвечает на вопрос «какая доля открывших подтвердила действие» только при одинаковых правилах отбора.
Cohort задаёт сравниваемую группу: control или treatment. Period задаёт единое окно времени. Attribution связывает подтверждение с конкретным открытием и вариантом. Guardrail показывает ущерб, который не должен расти ради локального улучшения. Если один элемент выпадает, значение conversion меняет смысл.
\nПредзагрузка формы может сократить ожидание. Она не доказывает, что пользователь чаще завершает действие. Для такого вывода нужны как минимум открытие, подтверждение и правило их связи. Отдельно нужно считать сбой рендера, отмену или другой риск, который может скрыться за ростом локальной метрики.
\nСобытие должно иметь стабильное имя и отдельные атрибуты. Динамические значения нельзя зашивать в имя: запрос не сможет надёжно сгруппировать такие записи. OpenTelemetry формулирует то же правило для semantic conventions: имя события должно однозначно описывать структуру, а переменные значения должны жить в attributes.
\nevent: product.checkout_opened; subject: subject-a; cohort: treatment; period: 2025-07-14; requestId: request-2;\nВызов подтверждения должен содержать совместимые поля: event, subject, cohort, period и requestId. Тогда запрос может проверить, что открытие и подтверждение относятся к одному субъекту, одной попытке и одному окну. Если requestId отсутствует, запрос не должен угадывать связь.
Значения в примере фиксированы и нужны только для объяснения механизма. Они не задают политику идентификации. В реальной системе отдельно определяют допустимый идентификатор, срок хранения, доступ к данным и правила обработки задержанных событий. Нельзя переносить строку subject-a в действующий сбор без такой проверки.
Возьмём малый набор событий. В control два открытия и одно подтверждение. В treatment два открытия и одно подтверждение. В control один экран завершился событием product.render_failed. Расчёт считает уникальных субъектов внутри cohort и period.
const events = [\n { event: 'product.checkout_opened', subject: 'a', cohort: 'control', period: '2025-07-14', requestId: 'r-1' },\\n { event: 'product.checkout_confirmed', subject: 'a', cohort: 'control', period: '2025-07-14', requestId: 'r-1' },\\n { event: 'product.checkout_opened', subject: 'b', cohort: 'treatment', period: '2025-07-14', requestId: 'r-2' },\\n { event: 'product.checkout_confirmed', subject: 'b', cohort: 'treatment', period: '2025-07-14', requestId: 'r-2' },\\n { event: 'product.checkout_opened', subject: 'c', cohort: 'treatment', period: '2025-07-14', requestId: 'r-3' },\\n { event: 'product.checkout_opened', subject: 'd', cohort: 'control', period: '2025-07-14', requestId: 'r-4' },\\n { event: 'product.render_failed', subject: 'd', cohort: 'control', period: '2025-07-14', requestId: 'r-4' }\n];\nВ treatment conversion равна 1/2. В control conversion тоже равна 1/2. Для control guardrail равен 1/2. Эти значения показывают только работу дроби на фиксированных данных. Они не оценивают эффект, статистическую значимость или поведение пользователей.
\nСмысл примера раскрывается на ошибочном пути. Если подтверждение приходит без requestId, его нельзя приписать открытию. Если одно событие имеет следующий period, его нельзя молча объединить с предыдущим. Если denominator заменили на all-events, старое имя conversion больше не описывает новый расчёт.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Conversion выросла после изменения запроса | Изменился denominator или deduplication | Вывести numerator и denominator по cohort | Остановить сравнение и исправить правило inclusion |
| Подтверждение есть, вариант не определён | Нет attribution или requestId | Проверить subject, requestId и cohort | Вернуть ошибку владельцу событий |
| Группы имеют разные даты | Смешан period или пришли задержанные события | Проверить period каждой записи до агрегации | Разделить окна или собрать данные заново |
| Local metric растёт вместе с ошибками | Guardrail считает другую population | Посчитать failure на том же срезе | Не считать локальный рост улучшением |
| Расчёт нельзя повторить | Definition хранится только в сообщениях | Восстановить запрос по полям метрики | Зафиксировать definition, owner и limitation |
Таблица задаёт маршрут диагностики. Она не заменяет проверку данных. Каждый ответ должен вести к действию: пересчитать дробь, исправить событие, разделить period, пересмотреть guardrail или остановить решение.
\nЧеловек находится в конце цепочки не случайно. Код может проверить обязательные поля, период, denominator и известные отрицательные пути. Он не может по одной дроби выбрать допустимый продуктовый риск. Локальный рост может сопровождаться отказами, отменами или недоступностью. Guardrail делает такую цену видимой, но не выбирает порог вместо владельца.
\nПроверяющая функция должна отклонять неполный контракт. Ниже приведён ограниченный пример с фиксированными данными в памяти. Он не читает сеть или файл и не отправляет события.
\nconst report = inspectProductMetric(\\n createProductMetricInput('mixed-period-example'),\\n);\\n\\nconst decision = prepareProductDecision(report);\\n\\nconsole.log(decision);\\n// {\\n// status: 'stop-before-decision',\\n// reasons: ['mixed-cohort-or-period']\\n// }\nСмешанный period не превращается в null и не замазывается средним. Проверка возвращает короткую причину. Она направляет действие: владелец событий проверяет instrumentation, владелец запроса проверяет population, владелец варианта проверяет cohort и окно.
То же правило действует для неверного denominator и отсутствующего attribution. Ветка wrong-denominator не чинит запрос автоматически и не разрешает сохранить старое имя метрики. Ветка missing-attribution-rule не угадывает связь между открытием и подтверждением. Такая строгость защищает от убедительного числа, собранного из несвязанных фактов.
Контракт событий не доказывает причинность. Он не заменяет randomization, расчёт мощности, проверку задержки доставки, privacy, retention и качества identity. Guardrail не делает эксперимент безопасным автоматически. Он заранее называет ущерб, который нельзя скрыть локальным ростом.
\nМалый набор не говорит о размере эффекта. В нём нет реальных пользователей, денег, сети, clock, действующего запроса или инцидента. Его можно использовать для детерминированной проверки веток и отсутствия побочных действий. Нельзя выдавать его значения за наблюдение действующего продукта.
\nКорректный контракт тоже может устареть после изменения клиента, схемы или pipeline. Проверяйте смысл события рядом с кодом отправки и запросом. Если событие меняет смысл, меняйте definition и отмечайте несовместимость. Молчаливое сохранение старого названия опаснее явной остановки.
\nИзмерение готово к продуктовому решению, если другой инженер по одной карточке восстанавливает population, numerator, denominator, attribution, period и guardrail. Он видит входящие события, условие остановки и владельца каждой ошибки. Если нужен устный контекст, контракт измерения ещё не готов.
\nМинимальный проверяемый результат таков: корректный фиксированный пример получает статус needs-human-decision; missing attribution, mixed period и wrong denominator получают stop-before-decision с разными причинами; ни один вызов не отправляет данные и не меняет состояние системы. Для действующего проекта добавьте отдельные проверки схемы, задержки, прав доступа и повторяемости запроса.