Files
progcode/editorial/agent-rewrites/122.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
22 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": 122,
"slug": "editorial-2024-08-mechanism-feature-flags",
"title": "Feature flags без рассинхрона: где принимать решение и что считать показом",
"excerpt": "Как разделить серверное решение, клиентское отображение и аналитическое событие, чтобы rollout оставался объяснимым, а временная ветка действительно исчезла.",
"contentHtml": "<p>Новый экран включили для десяти процентов пользователей. Часть из них увидела кнопку, но API продолжил выполнять старую ветку. У другой части браузер показал новый вариант после обновления страницы, хотя сервер ещё отдавал старый. В аналитике при этом появился один общий event exposure. По нему нельзя понять, было ли решение вычислено, дошёл ли ответ до браузера и отрендерился ли экран.</p>\n<p>Цена такой ошибки растёт после первого успешного релиза. Команда тратит время на поиск слоя, который изменил вариант. В защищённый клиентский контекст могут попасть лишние сведения. Сегмент получает разные правила на соседних запросах. После эксперимента остаются две ветки кода, старый ключ конфигурации и тесты, которые поддерживают уже несуществующее решение.</p>\n<p>Feature flag — не просто boolean. Это контракт из входов, владельца решения, версии конфигурации, безопасного значения и пути удаления. Главный принцип прост: один слой владеет eligibility, а остальные получают только тот результат, который им нужен. Evaluation, render и business effect нужно считать разными фактами.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Диагностика рассинхрона feature flag</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>UI показывает новый вариант, API выполняет старый</td><td>Сервер и браузер независимо вычисляют один flag</td><td>Сравнить evaluator, targeting key и config version в одном запросе</td><td>Оставить eligibility на сервере; клиенту передавать verdict и version</td></tr><tr><td>Один пользователь меняет вариант после логина</td><td>До логина используется anonymous ID, после логина — account ID</td><td>Построить timeline ключа для одной сессии</td><td>Описать переход ключа или закрепить вариант на время сессии</td></tr><tr><td>Exposure есть, но экран не показан</td><td>Событие отправляется сразу после evaluation</td><td>Сопоставить событие с точкой рендера и ошибками UI</td><td>Разделить evaluation record и render confirmation</td></tr><tr><td>Отключение флага не меняет всех клиентов сразу</td><td>Клиент держит кэш или обновляет конфигурацию по расписанию</td><td>Проверить effective config version и границу refresh</td><td>Задать допустимое окно устаревания и fallback</td></tr></tbody></table>\n<h2>Сначала разделите входы</h2>\n<p>Оценка флага получает context. В него могут входить идентификатор субъекта, приложение, окружение, локаль и другие признаки. Не каждый такой признак можно передавать в браузер. Право доступа, индивидуальная цена, fraud signal и внутреннее состояние аккаунта должны оставаться за доверенной границей. Клиенту нужен ответ о доступности функции, а не правило, по которому сервер его получил.</p>\n<p>Публичный layout preference или локаль часто подходят для client-side решения. Это верно только тогда, когда они уже доступны клиенту и не скрывают защищённую политику. Если один flag зависит и от локали, и от права доступа, его нельзя безопасно перенести в браузер целиком. Сервер может вычислить eligibility, а клиент — выбрать разрешённое представление внутри полученного контракта.</p>\n<h2>Один authoritative evaluator</h2>\n<p>Evaluator отвечает на вопрос: какой вариант вернуть для данного flag key и context. В архитектуре должен быть один владелец этого ответа. Если сервер и браузер повторяют одно правило, они должны либо использовать один явно согласованный контракт, либо считаться независимыми решениями с отдельными названиями. Скрытая копия правила почти всегда приводит к расхождению.</p>\n<p>Пример ниже учебный. Он не подключается к provider, не читает реальную конфигурацию и не отправляет события. В нём показана граница: сервер принимает защищённый вход, возвращает уже принятое решение и версию, а клиент не переоценивает право доступа.</p>\n<pre><code>type FlagDecision = {\n variant: 'control' | 'candidate';\n enabled: boolean;\n configVersion: string;\n};\n\nfunction resolveCheckoutBanner(input: {\n accountId: string;\n hasNewCheckoutAccess: boolean;\n}): FlagDecision {\n return {\n variant: input.hasNewCheckoutAccess ? 'candidate' : 'control',\n enabled: input.hasNewCheckoutAccess,\n configVersion: 'checkout-banner-v3',\n };\n}\n\nconst decision = resolveCheckoutBanner({\n accountId: 'account-42',\n hasNewCheckoutAccess: false,\n});\n\n// Browser receives the result, not hasNewCheckoutAccess or the rule.\nrenderBanner({ enabled: decision.enabled, variant: decision.variant });</code></pre>\n<p>В реальной системе значение <code>accountId</code> не нужно отправлять в клиент только ради отображения. Поле <code>configVersion</code> помогает объяснить результат и сопоставить его с журналом. Его нельзя считать глобальным доказательством: другой сервис или кэш может работать с другой версией.</p>\n<p>Отрицательный путь важнее счастливого. Если provider недоступен, context не содержит обязательного ключа или версия конфигурации устарела сверх допустимого окна, система должна вернуть явно выбранный fallback. Не следует молча вычислять тот же flag вторым алгоритмом в браузере. Иначе отказ превращается в незаметное изменение аудитории.</p>\n<h2>Когорта начинается с targeting key</h2>\n<p>Процент rollout стабилен только относительно ключа. Сервер может распределять пользователей по account ID, а браузер — по cookie ID. Тогда один человек получит разные варианты. После входа ключ может измениться снова. Это не мелкая деталь хеширования. Это изменение субъекта, которому принадлежит решение.</p>\n<p>Зафиксируйте четыре вещи: кто является subject, когда появляется его ключ, как проходит переход anonymous → authenticated и нужно ли закреплять вариант на активную сессию. Если конфигурация меняется, решите, может ли следующий запрос пересчитать когорту. Для некоторых экранов это допустимо. Для оплаты, миграции данных или последовательного сценария может потребоваться pinning до конца операции.</p>\n<p>Не включайте персональные данные в context без необходимости. Провайдер может сериализовать или сохранять его для таргетинга. Используйте стабильный идентификатор или заранее определённый хеш, если это соответствует модели угроз и правилам хранения. Хеш сам по себе не делает данные безопасными.</p>\n<h2>Evaluation не равно exposure</h2>\n<p>Evaluation означает, что evaluator получил входы и вернул вариант или fallback. Это технический факт вычисления. Exposure candidate означает, что приложение подготовило запись о выбранном варианте. Render confirmation означает, что клиент дошёл до конкретной точки показа. Business effect означает отдельное действие пользователя или доменный результат. Эти события нельзя сливать в одно поле <code>exposure</code>.</p>\n<table><caption>Что означает запись о flag</caption><thead><tr><th scope=\"col\">Факт</th><th scope=\"col\">Минимальные поля</th><th scope=\"col\">Чего он не доказывает</th></tr></thead><tbody><tr><td>Evaluation</td><td>flag key, variant, evaluator, config version</td><td>Что UI отрендерился</td></tr><tr><td>Exposure candidate</td><td>subject boundary, variant, idempotency key</td><td>Что событие доставлено ровно один раз</td></tr><tr><td>Render confirmation</td><td>screen, display point, client timestamp</td><td>Что пользователь прочитал экран</td></tr><tr><td>Business effect</td><td>Доменное действие и его собственный contract</td><td>Что действие вызвал только flag</td></tr></tbody></table>\n<p>Слово exactly-once здесь опасно. Повторная отправка, таймаут, падение consumer и повторный запуск страницы создают разные сценарии. Если аналитике нужна дедупликация, downstream должен получить устойчивый idempotency key и окно хранения ключей. Если нужно знать факт рендера, отправляйте событие в точке рендера. Даже это не доказывает, что пользователь увидел или использовал результат.</p>\n<h2>Сервер, клиент или разделённое решение</h2>\n<p>Серверное решение подходит, когда flag зависит от защищённых данных или должно одинаково влиять на API и UI. Недостаток — сетевой путь и возможность увидеть старый результат из кэша. Его компенсируют явная версия, короткий контракт ответа и проверяемый fallback.</p>\n<p>Клиентское решение подходит для уже публичной конфигурации, например локали или разрешённой темы. Недостаток — контекст и правила становятся частью доверенной границы браузера. Нельзя использовать этот вариант для скрытого entitlement только потому, что так быстрее собрать интерфейс.</p>\n<p>Разделённое решение полезно, когда сервер отвечает за eligibility, а клиент выбирает только presentation. Например, сервер возвращает <code>candidate</code> и версию, а клиент решает, какой из двух разрешённых layout показать. Граница должна быть написана рядом с контрактом. Иначе presentation постепенно начнёт повторять policy.</p>\n<figure><img src=\"/assets/editorial/2024/feature-flags-2024-rollout-timeline.svg\" alt=\"Поток feature flag: сервер получает защищённый контекст, возвращает вариант и версию конфигурации, клиент отображает разрешённое представление и отдельно фиксирует события\" loading=\"lazy\" /><figcaption>Схема разделяет decision, render и event delivery. Она учебная: не показывает реальный трафик, задержки, размер когорты или результаты эксперимента.</figcaption></figure>\n<h2>Изменение конфигурации имеет границу свежести</h2>\n<p>Отключение flag не обязано мгновенно менять каждый клиент. Кэш, refresh interval, service worker, edge и повторная загрузка страницы создают окно устаревшего состояния. Поэтому контракт должен отвечать на вопрос: какая версия вернулась этому evaluator и сколько времени такой результат допустим.</p>\n<p>Если UI получает вариант с сервера, не заставляйте браузер заново применять скрытое targeting rule. Передавайте verdict, variant и config version. Если provider сообщает об изменении конфигурации, это событие помогает запустить refresh или диагностику, но само по себе не является бизнес-exposure. При ошибке обновления используйте заранее выбранное поведение: сохранить последний допустимый вариант, перейти на control или заблокировать опасную операцию. Выбор зависит от риска функции.</p>\n<h2>Порядок проектирования и проверки</h2>\n<ol><li><strong>Назовите изменение.</strong> Запишите старый путь, новый путь, безопасный fallback и доменную цель. Не смешивайте release flag, permission и эксперимент в одном ключе.</li><li><strong>Классифицируйте входы.</strong> Отделите protected facts от client-safe fields. Для каждого поля укажите, кто его видит и зачем он нужен.</li><li><strong>Выберите evaluator.</strong> Назначьте один слой владельцем eligibility. Второй слой может отображать результат, но не пересчитывает скрытое правило.</li><li><strong>Зафиксируйте targeting key.</strong> Опишите subject, переход логина, поведение после logout и политику активной сессии.</li><li><strong>Добавьте версию.</strong> Возвращайте effective config version вместе с решением. Сопоставляйте её с журналом и логами, не называя её глобальной версией системы.</li><li><strong>Разведите события.</strong> Отдельно назовите evaluation, exposure candidate, render confirmation и business effect. Для повторов задайте idempotency key.</li><li><strong>Проверьте отрицательный путь.</strong> Отключите provider, уберите targeting key и предъявите устаревшую конфигурацию. Убедитесь, что система выбирает ожидаемый fallback и не запускает скрытый второй evaluator.</li><li><strong>Откройте удаление.</strong> После выбора варианта удалите ветку, конфигурационный ключ, лишние тесты и документацию одной согласованной change. Продление срока должно иметь новую причину и новую дату review.</li></ol>\n<h2>Ограничения и критерий готовности</h2>\n<p>Эта модель не выбирает конкретный SDK, TTL кэша, транспорт событий или процент rollout. Она не доказывает, что provider доступен, токен клиента ограничен или эксперимент статистически значим. Актуальная документация OpenFeature описывает provider, evaluation context и provider events как части API и жизненного цикла. Она не задаёт вашей команде SLA свежести, политику персональных данных или семантику product exposure.</p>\n<p>Учебный код выше не является production-рецептом. В боевой системе отдельно проверяют авторизацию, валидацию context, обработку ошибок, наблюдаемость и совместимость версий. Не переносите строку <code>checkout-banner-v3</code> в рабочую конфигурацию без определения владельца и пути удаления.</p>\n<p>Критерий готовности проверяемый. Для одного реального flag возьмите один subject и один запрос. По логам и ответу назовите evaluator, targeting key, variant, config version и fallback. Затем покажите, где возникло evaluation, где произошёл render и как consumer обработал повтор события. Если любой ответ требует догадки или поиска правила в двух слоях, контракт ещё не готов.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://openfeature.dev/docs/reference/concepts/evaluation-context/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenFeature: Evaluation Context</a> — официальная документация описывает context, targeting key, слияние контекстов и осторожность с персональными данными. Она не задаёт универсальную политику хранения или свежести конфигурации.</li><li><a href=\"https://openfeature.dev/specification/sections/flag-evaluation/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenFeature Specification: Flag Evaluation API</a> — официальная спецификация разделяет provider и evaluation API, включая default value и resolution details. Учебная архитектура статьи добавляет командные границы поверх этого API.</li><li><a href=\"https://openfeature.dev/docs/reference/concepts/events/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenFeature: Events</a> — официальная документация описывает события готовности, ошибок и изменения состояния provider. Эти события не доказывают показ интерфейса и не дают exactly-once гарантию бизнес-события.</li></ul>"
}