Files
progcode/editorial/agent-rewrites/090.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
19 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 90,
"slug": "editorial-2025-07-practice-product-metrics",
"title": "Метрики продукта для инженера: связать изменение с решением",
"excerpt": "Рост conversion не доказывает пользу изменения. Разбираем, как связать технический сигнал, cohort, denominator, пользовательский outcome и guardrail, чтобы ошибка измерения остановила решение до релиза.",
"contentHtml": "<p>После ускорения checkout на графике выросла conversion. Команда готовит rollout. Через несколько дней выясняется: повторные открытия перестали попадать в denominator, а часть ошибок рендера исчезла из отчёта вместе с событием. Пользователи не стали чаще подтверждать заказ. Изменился способ счёта.</p><p>Цена ошибки — решение по ложному сигналу. Команда выпускает изменение, тратит время на обратное расследование и теряет возможность сравнить варианты в одном окне. Если ошибка затрагивает платежный или регистрационный путь, к этому добавляются незавершённые операции и обращения в поддержку.</p><p><strong>Тезис:</strong> продуктовая метрика для инженера — это не имя на дашборде, а контракт. Он связывает техническое изменение с наблюдаемым действием, задаёт cohort, период и denominator, а рядом держит guardrail. При разрыве связи расчёт должен остановиться. Число без этих условий не становится доказательством.</p><h2>Механизм: от изменения к решению</h2><p>Техническое изменение само по себе не является продуктовым результатом. Предзагрузка формы может сократить ожидание. Сокращённое ожидание может изменить долю открывших checkout, которые нажали confirm. Но между этими утверждениями стоят события, идентификаторы и правила включения.</p><p>Для каждого измерения назовите пять звеньев:</p><ul><li><strong>Изменение.</strong> Что именно поменялось, в каком варианте и кто отвечает за него.</li><li><strong>Наблюдаемый шаг.</strong> Какое событие показывает, что пользователь дошёл до нужной точки.</li><li><strong>Правило сравнения.</strong> Как формируются cohort, период, population и attribution.</li><li><strong>Outcome.</strong> Какое действие пользователя имеет смысл для принятия решения.</li><li><strong>Guardrail.</strong> Какое ухудшение отменяет локальный выигрыш.</li></ul><p>Например, гипотеза звучит так: «Предзагрузка формы увеличит долю подтверждений среди пользователей, открывших checkout, но не повысит долю render failure». Это проверяемая цепочка. Формулировка «сделаем экран быстрее и поднимем conversion» цепочки не содержит.</p><figure><img src=\"/assets/editorial/2025/product-metrics-2025-causal-funnel.svg\" alt=\"Причинная цепочка от технического изменения к открытию checkout, подтверждению и решению, с отдельным guardrail для ошибок рендера\" loading=\"lazy\" /><figcaption>Схема разделяет локальный сигнал и побочную цену. Она не доказывает причинность, но показывает, какие связи нужно проверить до интерпретации числа.</figcaption></figure><h2>Событие должно сохранять контекст</h2><p>Событие отвечает на вопрос «что произошло», а его поля — на вопросы «с кем», «в каком варианте», «когда» и «как связать шаги». Имя вроде <code>checkout_confirmed</code> полезнее произвольного <code>button_click</code>, но одного имени мало. Два одинаковых события могут относиться к разным вариантам и разным попыткам.</p><p>Минимальный учебный контракт может выглядеть так:</p><pre><code>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// и не показывает результат реального продукта.</code></pre><p><code>subjectId</code> нужен, чтобы повторная доставка события не увеличила denominator. <code>cohort</code> не следует восстанавливать по текущему флагу: пользователь мог увидеть один вариант, а запросить данные после переключения флага. <code>period</code> не даёт смешать окна. <code>requestId</code> связывает открытие, подтверждение и техническую ошибку одной попытки.</p><p>OpenTelemetry разделяет traces, metrics и logs как разные сигналы наблюдаемости. Это полезная граница: latency можно увидеть в span, число ошибок — в metric, а контекст конкретной попытки — в log или event. Но сама телеметрия не создаёт product contract. Владелец решения должен заранее определить, какие сигналы отвечают на его вопрос.</p><h2>Denominator важнее красивой дроби</h2><p>Учебная локальная метрика может быть записана так:</p><pre><code>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</code></pre><p>Обе дроби используют одну базу opened, один cohort и один период. Это не универсальное определение conversion. Реальный продукт может считать заказ, оплату или завершённую сессию иначе. Важно другое: правило нельзя менять между вариантами, а его состав нужно хранить рядом с результатом.</p><p>Если один пользователь открыл checkout три раза, denominator по subjects равен одному, а не трём. Если повторная попытка имеет другой смысл для продукта, это решение нужно зафиксировать до подсчёта. Нельзя выбрать удобный вариант после просмотра результата.</p><p>Attribution связывает outcome с показанным вариантом. Если подтверждение пришло без <code>requestId</code>, расчёт не должен молча принять его. Оно могло относиться к старому экрану, другой вкладке или повторной попытке. В этом случае правильный статус — остановка с причиной <code>missing-attribution-rule</code>, а не нулевая conversion.</p><h2>Симптом → причина → проверка → действие</h2><div class=\"table-scroll\"><table><caption>Диагностика продуктовой метрики</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Conversion выросла сразу после изменения</td><td>Из denominator исчезли повторные или ошибочные открытия</td><td>Сравнить множества unique subjects и правило включения до и после</td><td>Остановить интерпретацию и восстановить сопоставимый denominator</td></tr><tr><td>Confirm есть, но вариант неизвестен</td><td>Нет attribution или requestId</td><td>Проверить связь opened, confirmed и cohort для каждой попытки</td><td>Вернуть hold, добавить ключ связи и тест отрицательного пути</td></tr><tr><td>Treatment лучше control, но даты различаются</td><td>Смешаны cohort или period</td><td>Сверить период каждого события и источник cohort</td><td>Пересобрать окна и не сравнивать текущие числа</td></tr><tr><td>Локальная метрика растёт вместе с отказами</td><td>Guardrail не включён в решение</td><td>Посчитать render failure на той же базе opened</td><td>Остановить rollout и разобрать технический путь отказа</td></tr><tr><td>На графике появились нули</td><td>Пайплайн не отличает отсутствие данных от нулевого результата</td><td>Проверить статус расчёта и причины отклонения</td><td>Показывать stop reason отдельно от числового значения</td></tr><tr><td>Число меняется после повторного запуска</td><td>Дубликаты событий или плавающее окно</td><td>Проверить idempotency по subjectId, requestId и period</td><td>Зафиксировать дедупликацию и повторить расчёт</td></tr></tbody></table></div><h2>Учебный пример отрицательного пути</h2><p>Предположим, есть два cohort и один день наблюдения. В каждом варианте два пользователя открыли checkout. В treatment один пользователь подтвердил действие. В control один пользователь подтвердил действие. У treatment дополнительно зафиксирован один render failure.</p><p>В таком маленьком наборе обе conversion равны 0,5. Это не результат эксперимента и не основание для запуска. Он показывает только форму контракта: одинаковые знаменатели, явный cohort и guardrail рядом. Если удалить cohort у одного confirm, расчёт должен остановиться, даже если арифметика всё ещё возможна.</p><pre><code>function evaluate(events) {\n const required = events.every((event) =&gt;\n event.subjectId &amp;&amp; event.cohort &amp;&amp; event.period &amp;&amp; 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}</code></pre><p>Функция учебная. В ней нет проверки случайного распределения, задержки доставки, часовых поясов, privacy-политики или достаточного размера выборки. Она иллюстрирует отрицательный путь: отсутствие обязательного поля переводит расчёт в <code>hold</code> до деления. В production это правило должно жить в проверяемом pipeline, а не только в тексте.</p><h2>Порядок работы</h2><ol><li><strong>Назовите решение.</strong> Запишите действие: rollout, hold, rollback или дополнительная проверка. Не начинайте с названия графика.</li><li><strong>Сформулируйте цепочку.</strong> Укажите изменение, наблюдаемый шаг, outcome и guardrail одним абзацем.</li><li><strong>Зафиксируйте contract.</strong> Опишите cohort, period, population, numerator, denominator и attribution до первого сравнения.</li><li><strong>Проверьте данные.</strong> Сверьте уникальность subjectId, связь requestId, полноту полей и единое окно времени.</li><li><strong>Посчитайте локальный сигнал.</strong> Отдельно выведите числитель, знаменатель и правила, по которым они получены.</li><li><strong>Посчитайте guardrail.</strong> Используйте сопоставимую базу и заранее названный порог риска.</li><li><strong>Пройдите отрицательный путь.</strong> Подайте событие без attribution, с другим period и с неверным denominator. Ожидайте разные stop reasons.</li><li><strong>Передайте решение владельцу.</strong> Код может вернуть eligible или hold, но product decision принимает человек с указанным owner и ограничениями.</li></ol><h2>Ограничения</h2><p>Такая схема не заменяет экспериментальный дизайн. Она не доказывает randomization, причинность, статистическую значимость или долгосрочный outcome. Она не исправляет потерю событий и не знает, был ли пользователь заблокирован сетью. Она только делает условия сравнения явными и не даёт незаметно продолжить при нарушенном контракте.</p><p>Один guardrail не покрывает все риски. Для платежа важны отказ и незавершённая операция. Для регистрации — доступность и повторная отправка. Для медленного интерфейса — время до действия и ошибки клиента. Выбирайте guardrail по цене конкретного ухудшения, а не по удобству существующего дашборда.</p><p>Нельзя выдавать рост local metric за рост выручки или удовлетворённости. Нельзя считать отсутствие события нулевым значением без проверки доставки. Нельзя сравнивать cohort, собранные разными версиями схемы, если вы не доказали сопоставимость. Если это невозможно, честный результат — hold и план исправления данных.</p><h2>Критерий готовности</h2><p>Проверка готова, когда другой инженер может по decision record восстановить гипотезу, cohort, период, numerator, denominator, attribution, guardrail и owner. Для каждого числа есть источник событий. Для каждого stop reason есть воспроизводимый вход. Повторный запуск на том же окне даёт тот же результат.</p><p>Минимальный набор доказательств — контракт событий, пример успешного расчёта, три отрицательных проверки, сравнение local metric с guardrail и запись ограничения. Только после этого human owner выбирает rollout, hold или rollback. Если связь между изменением и outcome не доказана, система не обязана выдавать красивую цифру. Она обязана показать, где цепочка оборвалась.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://opentelemetry.io/docs/concepts/signals/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Signals</a> — официальное описание traces, metrics и logs как разных сигналов наблюдаемости.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/logs/data-model/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry Logs Data Model</a> — официальная модель полей и контекста записи; она не задаёт product metric или порог rollout.</li><li><a href=\"https://www.microsoft.com/en-us/research/articles/a-b-testing-infrastructure-changes-at-microsoft-exp\" target=\"_blank\" rel=\"noopener noreferrer\">Microsoft Research: A/B Testing Infrastructure Changes at Microsoft ExP</a> — первичная работа о выборе и интерпретации метрик; учебные числа выше не являются её production-результатами.</li></ul>"
}