@application-kit/feature-flags
v0.3.0
Published
Фичефлаги из окружения: набор флагов параметром, порт с адаптером окружения, SSR-состояние для Vue и фейк для спек
Maintainers
Readme
@application-kit/feature-flags
Фичефлаги из окружения: набор флагов объявляет приложение, пакет даёт порт чтения с адаптером окружения, SSR-состояние для Vue и фейк для спек. Флагов продукта в пакете нет.
Два правила держат всё остальное:
- Имя флага объявляется ровно один раз — в наборе приложения. Из него выводится тип имени, из него же адаптер узнаёт, какие переменные окружения существуют.
- Состояния «по умолчанию» у флага нет. Значение каждого флага приходит из окружения, а пропущенная переменная роняет старт процесса. Дефолт в коде сделал бы забытую настройку окружения неотличимой от сознательного выбора — а ищут как раз её, когда стенд ведёт себя не так, как договаривались.
Установка
yarn add @application-kit/feature-flags @libs-for-dev/nestjs-config-modulePeer: @libs-for-dev/nestjs-config-module (подпуть /parse-boolean — разбор булевых переменных, без
зависимостей); vue — только для входа /vue, @nestjs/common — только для входа /nest.
Входы
| Вход | Что внутри | Зависимости |
|---|---|---|
| @application-kit/feature-flags | defineFeatureFlags, реестр FeatureFlagRegistryInterface, порт FeatureFlagsInterface, токен FEATURE_FLAGS, адаптер EnvironmentFeatureFlags, ошибки | изоморфно, без Nest и DOM |
| @application-kit/feature-flags/nest | FeatureFlagsRootModule | @nestjs/common |
| @application-kit/feature-flags/vue | useFeatureFlag, ключ FEATURE_FLAG_STATE, parseFeatureFlagState, createFeatureFlagChannel | vue |
| @application-kit/feature-flags/testing | FakeFeatureFlags | — |
Корень грузится и в клиентском бандле (набор нужен разбору SSR-состояния), поэтому связка с Nest — отдельный вход.
Набор флагов
Тип и значение объявляются рядом, в одном файле приложения:
import { defineFeatureFlags } from '@application-kit/feature-flags'
declare module '@application-kit/feature-flags' {
interface FeatureFlagRegistryInterface {
NEW_CHECKOUT: true
}
}
export const featureFlags = defineFeatureFlags({
NEW_CHECKOUT: { isPublic: true },
})Ключи записи проверяются против реестра в обе стороны: флаг реестра без записи и запись без флага в реестре не компилируются.
Единственное решение в записи — isPublic: уезжает ли флаг на клиент в SSR-состоянии. Наружу идёт явный
список, а не всё состояние: серверный флаг (доступ к интеграции, аварийный рубильник) в разметке страницы — это
утечка плана работ.
Префикс переменных — опция, по умолчанию FEATURE_: defineFeatureFlags({ … }, { environmentPrefix: 'FLAG_' }).
Пустой префикс — RangeError.
Окружение
Имя переменной — префикс плюс имя флага: FEATURE_NEW_CHECKOUT. featureFlags.readEnvironment(source) разбирает
окружение в состояние:
| Значение | Результат |
|---|---|
| false, 0, пустая строка (регистр и пробелы по краям не важны) | выключен |
| любая другая непустая строка — true, 1, on, yes | включён |
| переменной нет | отказ — MissingFeatureFlagError со списком недостающих переменных |
| переменная с префиксом, которой нет в наборе | отказ — UnknownFeatureFlagError со списком |
Разбор значения — parseBoolean конфиг-пакета: список выключающих строк один на все булевы переменные. Обе
проверки собирают полный список до броска, чтобы отказ назвал все проблемы разом; незнакомая переменная
проверяется первой — опечатка даёт сразу и лишнее имя, и недостающее.
Связка с Nest
Корневой модуль процесса подключает порт одной строкой:
import { FeatureFlagsRootModule } from '@application-kit/feature-flags/nest'
@Module({ imports: [FeatureFlagsRootModule.forRoot(featureFlags)] })
export class CommonModule {}forRoot разбирает process.env сразу, на импорте модуля-носителя: отказ приходит до сборки графа, а не на первом
запросе. Поэтому окружение обязано быть готово до импорта: правка из beforeAll спеки опоздает. Модуль глобальный:
FEATURE_FLAGS виден всему графу без своей регистрации.
Чтение на сервере
Через порт по токену, а не чтением process.env:
import type { FeatureFlagsInterface } from '@application-kit/feature-flags'
import { FEATURE_FLAGS } from '@application-kit/feature-flags'
export class SomeHandler {
public constructor(@Inject(FEATURE_FLAGS) private readonly featureFlags: FeatureFlagsInterface) {}
public execute(): void {
if (this.featureFlags.isEnabled('NEW_CHECKOUT')) { /* … */ }
}
}Чтение синхронное, и это решение: асинхронный isEnabled заставил бы ждать каждого потребителя, включая шаблон
Vue, где await в разметке невозможен. Адаптер с хранением в БД остаётся возможным — он держит обновляемый
снимок в памяти и встаёт под тот же токен без правок потребителей.
Клиент (Vue)
Публичный срез уезжает на страницу SSR-состоянием:
С @application-kit/vue-ssr состояние едет каналом — ставится до рендера и уезжает на страницу одним значением:
const featureFlagChannel = createFeatureFlagChannel(featureFlags)
// сервер, опция createSsrEntry
channels: () => [featureFlagChannel.withValue(featureFlagsPort.publicState())],
// клиент, опция hydrateSsrApplication
channels: [featureFlagChannel],Без него — те же шаги руками:
// сервер, перед renderToString
const state = featureFlagsPort.publicState()
app.provide(FEATURE_FLAG_STATE, state)
// … состояние сериализуется в страницу строкой (JSON.stringify)
// клиент, до app.mount()
app.provide(FEATURE_FLAG_STATE, parseFeatureFlagState(featureFlags, window.ssrFeatureFlags))В компоненте:
<script setup lang="ts">
import { useFeatureFlag } from '@application-kit/feature-flags/vue'
const isNewCheckoutEnabled = useFeatureFlag('NEW_CHECKOUT')
</script>
<template>
<NewCheckout v-if="isNewCheckoutEnabled" />
</template>- Без
provideвсё выключено: до прихода SSR-состояния помеченного фрагмента на странице быть не должно — показать лишнее и убрать при гидрации хуже, чем не показать. - Имя, которого нет в предоставленном состоянии, —
UnknownFeatureFlagNameErrorсо списком известных: имя из данных без проверки дало быundefined, и фрагмент молча исчез бы со страницы. parseFeatureFlagStateне бросает ни на какой форме испорченного провода: разбор стоит доapp.mount(), и бросок убил бы весь бутстрап клиента. Незнакомое имя на проводе игнорируется, пропущенное и не-булево — выключено: страница старой версии не роняет клиент новой во время выкатки.
Тесты потребителя
Через фейк порта, без process.env и vi.stubEnv. База фейка — «всё выключено», поэтому спека, которой флаг
нужен включённым, говорит это явно:
import { FakeFeatureFlags } from '@application-kit/feature-flags/testing'
const port = new FakeFeatureFlags(featureFlags, { NEW_CHECKOUT: true })Снимок publicState() фейка строится как у адаптера: непубличные флаги в нём выключены. Для компонента —
provide состояния:
mount(SomeComponent, {
global: { provide: { [FEATURE_FLAG_STATE]: featureFlags.stateOf((name) => name === 'NEW_CHECKOUT') } },
})Типовые отказы
| Симптом | Причина | Что делать |
|---|---|---|
| MissingFeatureFlagError: Missing feature flags in environment: FEATURE_X на старте | флаг есть в наборе, переменная в окружении не задана | дописать переменную в окружение каждого процесса |
| UnknownFeatureFlagError: Unknown feature flags in environment: FEATURE_X на старте | переменная есть, флага в наборе нет — опечатка или флаг снят из кода и забыт в окружении | поправить имя или убрать переменную |
| UnknownFeatureFlagNameError: Unknown feature flag requested: X. Known flags: … при рендере | опечатка в имени у useFeatureFlag | поправить имя по списку в тексте ошибки |
| Переменная выставлена, а на клиенте флаг выключен | флаг непубличный (isPublic: false) | проверить isPublic в наборе |
Чего здесь нет
Адаптера с хранением в БД, процента раскатки, таргетинга по пользователю, UI управления флагами.
