DarkRiDDeR14 мин

Где принимать решение флага: server, client и журнал exposure

АрхитектураРелизы

Флаг меняет не только интерфейс. Он решает, где живёт правило: на сервере, в браузере или одновременно в двух местах. Ошибка обычно проявляется после первого успешного rollout. Клиент получил достаточно входов, чтобы сам вычислить eligibility; сервер вернул один вариант, а браузер при следующем запросе — другой; аналитика назвала один event exposure, хотя пользователь не видел результат из-за ошибки рендера. Цена — не только утечка лишнего контекста. Команда теряет возможность объяснить, почему конкретный запрос увидел именно этот путь.

Противоположная крайность тоже плоха: каждый косметический элемент требует round trip к серверу, хотя правило зависит только от уже публичной настройки приложения. Здесь стоимость — задержка и лишняя связность. Вопрос звучит не «server или client лучше». Он звучит так: какие входы защищены, какой компонент владеет окончательным решением, какой ключ удерживает когорту и что именно означает запись exposure. Без этих четырёх ответов флаг превращается в распределённое угадывание.

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

  1. Симптом. Для одного account UI иногда показывает новый вариант, а API продолжает выполнять старую ветку, либо браузер получил entitlement, который ему не нужен.
  2. Причина. Входы, evaluator, cohort key, config version и event semantics распределены по разным слоям без единого contract.
  3. Проверка. Для каждого флага выпишите protected input, public presentation input, authoritative evaluator, stable targeting key, effective config version и тип события.
  4. Действие. Оставьте решение там, где доступен минимальный набор допустимых входов. Клиенту передавайте только результат и данные, нужные для отображения; exposure делайте идемпотентным на стороне потребителя, не обещая exactly-once.

Три слоя решения

Первый слой — input. Здесь важно различить публичный параметр интерфейса и защищённый факт. Локаль, объявленная тема или заранее опубликованный вариант текста могут быть client-safe, если они действительно уже доступны клиенту. Право доступа, индивидуальная скидка, fraud signal, внутренний account state и правило, раскрывающее их, не должны становиться материалом для browser evaluator только ради удобства rollout. Если клиенту нужен ответ «можно ли показать кнопку», сервер может вернуть boolean и version, не раскрывая причину.

Второй слой — evaluator. Он отвечает за вычисление варианта для конкретного context. OpenFeature v0.6.0 описывает typed evaluation с key, default value и optional context; это полезная форма, но не указание, где запускать provider. Источник позволяет разделить contract вызова и implementation: server-side evaluator может иметь защищённый context, client-side evaluator — только разрешённый поднабор. Решение о placement остаётся архитектурным: оно зависит от данных, latency, availability и доверенной границы.

Третий слой — presentation. Результат evaluation ещё не равен показанному эффекту. Между ними может быть network response, client cache, rendering, feature branch и пользовательское действие. Поэтому слово exposure нужно договорить. В этой статье exposure — попытка зафиксировать, что evaluator вернул конкретный вариант для заданного context и configuration version. Такая запись не доказывает показ интерфейса, не измеряет результат и не обязана быть доставлена ровно один раз.

Точка вычисления и её цена
ВариантДопустимые входыСильная сторонаГлавный рискПроверка перед выбором
Server decisionprotected entitlement, pricing rule, account state, server-only policyклиент получает минимум данных и готовый verdictлишний сетевой путь и рассинхрон при повторной client evaluationвернуть только presentation value и config version; не дублировать правило в браузере
Client presentation decisionуже публичная конфигурация, locale, layout preference, разрешённый anonymous contextбыстрый UI без дополнительного endpointв клиент уезжает лишний context или меняется cohort keyпроверить token, CORS, cache and refresh boundary конкретного SDK
Split decisionserver считает eligibility, client выбирает только presentation within returned contractможно отделить policy от UIдва слоя могут незаметно менять один и тот же смыслназвать single source of truth и запретить клиенту переоценивать protected rule
Два независимых evaluatorразные наборы данных без согласованной версиинет технического преимущества само по себеодин subject получает разные варианты и спор о виновникене использовать без явного consistency contract
Схема потока решения: server получает защищённый context, вычисляет eligibility и возвращает клиенту presentation value вместе с configuration version; client фиксирует отдельный exposure candidate. Пунктиром показано, что доставку события нельзя считать exactly-once.
Диаграмма отделяет decision, rendering и event delivery. Она не показывает SDK traffic, реальную когорту, latency или результаты аналитики.

Когорта начинается с ключа, а не с процента

Процентный rollout устойчив только относительно выбранного ключа и версии правила. Если сервер выбирает cohort по account ID, а браузер — по anonymous ID, один человек может попасть в разные варианты на соседних экранах. Если key меняется после логина, нужен отдельный переход: какая версия сохраняется в сессии, как объединяются pre-login и post-login события, какой вариант имеет приоритет. Молчаливо считать, что hash сам решит эту задачу, нельзя.

OpenFeature context v0.6.0 называет targeting key идентификатором subject и допускает custom fields; provider может требовать его для fractional evaluation. Это полезное ограничение: ключ нужен сделать явным. Но спецификация не обещает distributed snapshot. Когда effective configuration version меняется, когорту может корректно пересчитать следующий запрос. Система должна решить, допустимо ли это во время активной сессии, или нужна pinning policy. Такой выбор лучше записать рядом с флагом, чем искать его в cache invalidation после жалобы.

Минимальный воспроизводимый пример

Следующий пример не подключает provider и не отправляет event. Он берёт только фиксированную synthetic карточку из этого overlay. Она помогает проверить форму решения: protected input остаётся за server boundary, client получает boolean и version, а exposure boundary прямо содержит запрет на exactly-once обещание. Никакого account, токена, endpoint, event broker или production config здесь нет.

const input = createFixedSyntheticFeatureFlagInput('fixed-server-decision-v1');
const report = inspectSyntheticFeatureFlag(input);
const draft = planSyntheticFeatureFlagReview(report);

if (report.decision.code !== 'keep-protected-decision-server-side') {
  throw new Error('unexpected teaching decision');
}

console.log(report.flagCard.clientPayload);
// already-decided-boolean-and-config-version
console.log(draft.actions[2]);
// treat-exposure-delivery-as-not-exactly-once

Эта fixture строит report только из object literal, заранее записанного в модуле. Любой field, похожий на flag store, URL, clock, CI, telemetry или production marker, отклоняется до анализа. Если подменить decision, cohort key, config version question или разрежить массив stages, plan не будет принят. Такой тест не проверяет реальный SDK. Он защищает пример от ложного вывода, что запись document уже стала вызовом production-системы.

Evaluation, exposure и effect — разные факты

Evaluation — evaluator получил key и context и вернул вариант или default. Exposure candidate — приложение подготовило запись, что этот вариант был вычислен в определённой версии конфигурации. Rendered exposure — клиент действительно дошёл до точки показа; он всё ещё не равен успешному действию. Business effect требует отдельного доменного события и отдельной методологии сравнения. Склеивать эти уровни в одно слово «показали фичу» опасно: при сбое невозможно понять, что именно не произошло.

У event delivery есть собственная семантика. Provider events OpenFeature v0.6.0 относятся к готовности, ошибке, изменению configuration или stale state provider. Они не определяют бизнес-event ingestion. Даже если ваш transport повторяет сообщение при сбое, это не превращается в global exactly-once: retry, timeout, consumer crash и переигранная сессия остаются отдельными случаями. Если downstream требует дедупликацию, он должен иметь идемпотентный key и объяснение окна хранения. Текст события должен называть attempt или delivered record, а не «единственный факт показа».

Факт, который можно записать, и факт, который нельзя подменить
ЗаписьЧто содержитЧего не обещает
Evaluation recordflag key, returned variant, context boundary, effective config versionчто UI успешно отрендерился и событие доставлено
Exposure candidateidempotency key, request or session boundary, chosen variantexactly-once delivery, уникального пользователя или полезного действия
Render confirmationдостижение конкретного client-side display pointчто пользователь прочитал или применил результат
Domain eventотдельное действие после показа и свой business contractчто изменение вызвано только флагом без методики контроля

Изменение конфигурации не обязано быть мгновенным

Immutable commit Front-end API Unleash от 24 мая 2024 описывает отдельный продуктовый путь: FRONTEND token, CORS boundary и refresh interval с random offset. Из этого следует практический вопрос, а не универсальный тайминг: когда именно ваш клиент узнаёт о новой версии и как он ведёт себя до этого. Server evaluator может получить configuration раньше браузера; Edge или proxy — иметь другой cache; локальный provider — другой lifecycle. В production contract стоит говорить «version returned by this evaluator», а не «все пользователи уже на новом проценте».

Когда нужны несколько evaluator, полезнее передавать result than rule. Server возвращает selected variant и effective config version. Client может кешировать presentation в допустимой границе, но не должен заново применять hidden targeting. Если UI обязан обновиться после config change, отдельно определите event, refresh, invalidation и user-visible transition. Это делает некрасивый случай видимым: пользователь может увидеть старый вариант до следующего refresh, и это не ошибка именно флага, пока contract говорит, что такое поведение разрешено.

Порядок проектирования

  1. Классифицируйте inputs. Отделите protected facts от client-safe presentation fields. Список должен быть короче реального context, а не равен ему.
  2. Выберите authoritative evaluator. Один слой владеет eligibility. Второй не пересчитывает то же правило с другим key или другой config version.
  3. Зафиксируйте cohort key. Назовите subject, переход login/logout, version boundary и поведение активной сессии.
  4. Опишите payload. В client response положите только verdict, variant и нужную version; не передавайте скрытый rule ради удобной отладки.
  5. Разделите события. Evaluation, exposure candidate, render и domain effect должны иметь разные названия, schema и ограничения.
  6. Проверьте delivery честно. Если нужен dedupe, определите idempotency key и consumer behavior. Не заменяйте это словом exactly-once.
  7. Проведите один controlled check. В разрешённой среде сравните один subject, один key и одну config version на двух границах. Не меняйте одновременно key, cache и rollout percent.

Ограничения и следующий проверяемый шаг

Этот механизм не выбирает SDK, cache TTL, payload schema, event transport или срок хранения дедупликационных ключей. Он не доказывает, что client token действительно ограничен, CORS правильно настроен или provider последовательно выдаёт один вариант. OpenFeature разделы про context и events на v0.6.0 были experimental; их нельзя читать как SLA. Immutable commit Front-end API Unleash от 24 мая 2024 описывает Unleash 4.18+ и не даёт переносимой гарантии для другой платформы.

Следующий проверяемый шаг — выбрать один существующий флаг, зависящий от account-level данных, и заполнить таблицу из шести строк: protected input, public payload, evaluator, cohort key, config version and event type. Затем найдите одно дублирование правила между server и client. Уберите дублирование или запишите контракт, почему оно необходимо. Ожидаемый результат — конкретный: по одному event и одному response можно объяснить, кто принял решение и чего это событие не доказывает.

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

  • OpenFeature Specification v0.6.0: release, 16.05.2023 — Первичный versioned release OpenFeature, опубликованный 16 мая 2023 и уже доступный к августу 2024. В выпуск вошли provider events, initialization и shutdown; это не доказательство одинаковой реализации у каждого provider или exactly-once доставки какого-либо события.
  • OpenFeature v0.6.0: Flag Evaluation API — Первичная спецификация typed evaluation: flag key, default value и optional evaluation context; при abnormal execution возвращается default value. Раздел помечен hardening и задаёт API-контракт, а не policy хранения секретов, TTL или жизненный цикл конкретного флага.
  • OpenFeature v0.6.0: Evaluation Context — Первичная спецификация context: targeting key идентифицирует subject, а global, client и invocation context имеют порядок merge. Раздел experimental; он не обещает непрерывную консистентность когорты между сервисами, браузерами или версиями конфигурации.
  • OpenFeature v0.6.0: Events — Первичная experimental-спецификация provider events: readiness, error, configuration changed и stale. Она описывает обработчики состояния provider, но не является семантикой бизнес-exposure, не определяет delivery transport и не даёт exactly-once гарантию.
  • Unleash: Feature Toggles, immutable Git commit 3417039 (21.08.2024) — Первичный неизменяемый Git-снимок Unleash от 21 августа 2024: флаг имеет type и description, а activation strategies настраиваются по environment; disabled toggle в environment evaluates false. Этот commit не утверждает, что у произвольной команды есть owner, срок удаления, метрика или единая стратегия cleanup.
  • Unleash: Front-end API access, immutable Git commit 4ae65d6 (24.05.2024) — Первичный неизменяемый Git-снимок Unleash от 24 мая 2024: отдельный front-end API, FRONTEND token, CORS boundary и refresh interval с random offset. Это конкретный механизм Unleash 4.18+, а не универсальная модель client-side evaluation или обещание моментальной смены всех клиентов.