{ "index": 123, "slug": "editorial-2024-08-practice-feature-flags", "title": "Фича-флаг как контракт выпуска: включить, проверить, удалить", "excerpt": "Release-флаг снижает риск только тогда, когда у него есть ограниченная аудитория, безопасный fallback, владелец, срок пересмотра и заранее описанное удаление.", "contentHtml": "

В пятницу новый checkout включают для пяти процентов пользователей. В понедельник команда видит ошибки только на части запросов и выключает флаг. Но старый и новый код остаются в репозитории, тесты продолжают покрывать два пути, а конфигурация живёт без владельца. Через месяц никто не знает, можно ли удалить условие: новый путь мог записать данные в другом формате, а клиент мог закешировать старый вариант.

\n

Цена такой ошибки растёт не вместе с процентом rollout. Она растёт с каждым новым условием, исключением и сервисом, который читает тот же ключ. Выключенный флаг уменьшает аудиторию, но не убирает ветки, не возвращает изменённые данные и не объясняет, почему система выбрала вариант. Тезис статьи простой: release-флаг — это временный контракт выпуска. Он должен описывать границу решения, безопасное значение, наблюдение и момент, когда код исчезнет.

\n

Механизм: boolean скрывает решение

\n

Значение true или false отвечает только на один вопрос: какой путь выбрать сейчас. Выпуск требует ответов на другие вопросы. Для кого действует правило? Где его вычисляют? Что произойдёт при ошибке провайдера или устаревшей конфигурации? Кто остановит rollout? Как проверить новый путь? Что удалить после выбора варианта?

\n

Удобно разделить флаг на пять частей. Owner принимает решение на review. Audience задаёт стабильную когорту и окружение. Fallback определяет путь при недоступном или сомнительном решении. Signals показывают, что именно произошло. Cleanup связывает итоговый вариант с удалением условий, настроек и тестовых исключений.

\n

Эти части не заменяют flag management system. Они задают контракт вокруг неё. Провайдер может вернуть default value, сообщить об ошибке или показать, что его состояние устарело. Приложение всё равно должно решить, какое значение безопасно для конкретной операции. Для цены, права доступа и другого защищённого решения fallback выбирает сервер. Клиент получает уже вычисленный результат, а не правило с закрытым контекстом.

\n

Симптом → причина → проверка → действие

\n
СимптомПричинаПроверкаДействие
«Временный» флаг появляется без даты удаленияBoolean приняли за весь контракт выпускаНайдите owner, review date и список ветокОформите карточку и назначьте review до rollout
После выключения ошибки исчезли, но код не меняетсяDisablement перепутали с cleanupСравните ветки, config keys, тесты и формат записанных данныхОткройте отдельное удаление и оставьте fallback до его завершения
Один пользователь видит разные вариантыСервисы используют разные targeting keys или версии конфигурацииЗапишите key, effective version и границу сессии в evaluation detailsНазначьте один authoritative evaluator или явно опишите split decision
Событие exposure есть, а интерфейс не показалсяEvaluation назвали эффектомРазделите evaluation, response, render и user actionПереименуйте сигнал и не делайте вывод о результате по одной записи
Провайдер недоступенFallback не определён для ошибки или stale stateВоспроизведите provider error в изолированной проверкеВерните объявленный safe value и поднимите сигнал оператору
\n

Карточка до первой строки кода

\n

Сначала сформулируйте один вопрос выпуска. «Включить новый checkout» слишком широко. Лучше: «Показать новую форму checkout синтетической внутренней когорте, оставив прежнюю форму доступной при любом сбое оценки». Такая формулировка задаёт аудиторию, вариант и отрицательный путь.

\n
const releaseFlagCard = {\n  key: 'synthetic-checkout-copy-v1',\n  class: 'release',\n  owner: 'synthetic-checkout-owner',\n  audience: 'synthetic-internal-beta-cohort',\n  targetingKey: 'synthetic-subject-id',\n  fallback: 'existing-checkout-copy',\n  reviewAt: 'synthetic-2026-09-30',\n  signals: ['evaluation-outcome', 'client-render-error'],\n  cleanup: ['freeze-variant', 'remove-branches', 'remove-config'],\n};\n\n// Учебная запись. Она не создаёт флаг, не читает provider\n// и не отправляет telemetry или данные пользователя.
\n

В примере все значения synthetic. Дата не запускает таймер, ключ не определяет настоящую когорту, а массив cleanup не удаляет файлы. Он показывает форму review. В реальной системе карточка должна ссылаться на конкретные владельца, окружение, конфигурацию и проверку, но не должна содержать секреты.

\n

Где принимать решение

\n

Сервер должен владеть решением, если оно зависит от entitlement, цены, account state, fraud signal или другого защищённого входа. Сервер возвращает клиенту presentation value и, при необходимости, effective configuration version. Браузер не должен повторять правило по урезанному контексту: такое дублирование создаёт два источника истины.

\n

Клиент может вычислять presentation-флаг, если все входы уже публичны: например, доступная локаль, опубликованный вариант оформления или локальная настройка layout. Это сокращает сетевой путь, но не отменяет проверку token, CORS, cache и refresh semantics конкретного провайдера.

\n

Разделяйте evaluation и exposure. Evaluation означает, что evaluator вернул вариант для заданного контекста. Exposure можно использовать для записи попытки показать вариант, но запись не доказывает успешный render, пользовательское действие или бизнес-эффект. Если событие доставляется повторно или теряется, это нужно учитывать в consumer и в выводах из метрик.

\n
\"Дерево
Сначала проверяется контракт, затем выбирается граница решения. Схема учебная: она не показывает состояние реального flag-сервиса, rollout или production-метрики.
\n

Порядок внедрения

\n
  1. Назовите старый и новый путь. Запишите, какая функция меняется, что остаётся fallback и какие данные каждый путь читает или записывает.
  2. Выберите класс флага. Не смешивайте release, эксперимент, миграцию данных и постоянную policy в одном ключе. Для каждого класса нужны разные сроки и проверки.
  3. Назначьте owner и review date. Owner должен выбрать итоговый вариант, продлить контракт с причиной или начать cleanup. Продление без новой причины не является нейтральным действием.
  4. Опишите audience boundary. Зафиксируйте environment, targeting key, способ удержания когорты и данные, которые не покидают сервер. Проверьте поведение после логина, смены сессии и обновления конфигурации.
  5. Определите fallback и отрицательный путь. Проверьте default value, provider error, stale state, таймаут и недоступность сети. Убедитесь, что возврат не скрывает несовместимые данные.
  6. Назовите сигналы и их пределы. Разделите evaluation result, render error, API failure и user action. Для каждого напишите, какой вывод он разрешает и какой запрещает.
  7. Расширяйте аудиторию постепенно. На каждом шаге проверяйте ошибку, latency, целостность ответа и работу старой ветки. Если сигнал нарушает stop condition, верните объявленный fallback и зафиксируйте причину.
  8. Зафиксируйте выбранный вариант. После решения не добавляйте новые rules в старый release-флаг. Создайте cleanup change для кода, конфигурации, тестов, документации и лишних сигналов.
\n

Как проверить удаление

\n

Выключение и удаление проходят разными проверками. После выключения убедитесь, что новый путь не получает аудиторию и старый путь отвечает за заявленный контракт. Затем проверьте, не остались ли записи нового формата, client cache, server cache, отдельные разрешения и тестовые обходы. Только после этого удаляйте ветки. Иначе «cleanup» может убрать защитное условие раньше, чем система перестанет читать его последствия.

\n

Простой критерий для pull request: поиск по ключу флага не находит production-условий после удаления, тесты больше не выбирают вариант через старую конфигурацию, а единственный оставшийся путь не зависит от временного default. Если данные менялись, добавьте отдельную проверку совместимости. Не объявляйте cleanup завершённым по одному зелёному unit-тесту.

\n

Ограничения

\n

Карточка не выбирает безопасный процент rollout и не заменяет threat model, approval, миграцию схемы, SLA или план отката данных. OpenFeature задаёт API evaluation context, targeting key, providers и provider events, но не навязывает поля owner, reviewAt или cleanup. Эти поля — локальная инженерная policy.

\n

Разные провайдеры по-разному обновляют конфигурацию, кэшируют правила и обрабатывают ошибки. Нельзя переносить интервал refresh или семантику токена одного SDK в другой без проверки его документации. Нельзя считать targeting key доказательством единой когорты во всех сервисах, если они не используют общую версию конфигурации и один authoritative evaluator.

\n

Учебный объект выше не является результатом production-эксперимента. Он не доказывает latency, delivery rate, безопасность данных или полезный эффект новой формы. Production-вывод требует реальных наблюдений, определённого окна измерения и заранее согласованного stop condition.

\n

Проверяемые источники

\n

Проверяемая готовность

\n

Флаг готов к включению, если другой инженер без устного контекста может назвать его вопрос, owner, audience boundary, authoritative evaluator, safe fallback, signals, stop condition и review date. Флаг готов к удалению, если выбран вариант зафиксирован, поиск не находит временных веток, тесты не зависят от старого ключа, а изменения данных проверены отдельно. Если хотя бы один ответ отсутствует, это не повод добавить ещё один boolean. Это сигнал вернуть контракт на review.

" }