@application-kit/feature-flags
@application-kit/feature-flags
Фичефлаги из окружения: набор флагов объявляет приложение, пакет даёт порт чтения с адаптером окружения, SSR-состояние для Vue и фейк для спек. Флагов продукта в пакете нет.
Два правила держат всё остальное:
- Имя флага объявляется ровно один раз — в наборе приложения. Из него выводится тип имени, из него же адаптер узнаёт, какие переменные окружения существуют.
- Состояния «по умолчанию» у флага нет. Значение каждого флага приходит из окружения, а пропущенная переменная роняет старт процесса. Дефолт в коде сделал бы забытую настройку окружения неотличимой от сознательного выбора — а ищут как раз её, когда стенд ведёт себя не так, как договаривались.
Установка
yarn add @application-kit/feature-flags @libs-for-dev/nestjs-config-module
Peer: @libs-for-dev/nestjs-config-module (подпуть /parse-boolean — разбор булевых переменных, без
зависимостей); vue — только для входа /vue.
Входы
| Вход | Что внутри | Зависимости |
|---|---|---|
@application-kit/feature-flags |
defineFeatureFlags, реестр FeatureFlagRegistryInterface, порт FeatureFlagsInterface, токен FEATURE_FLAGS, адаптер EnvironmentFeatureFlags, ошибки |
изоморфно, без Nest и DOM |
@application-kit/feature-flags/vue |
useFeatureFlag, ключ FEATURE_FLAG_STATE, parseFeatureFlagState |
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 { EnvironmentFeatureFlags } from '@application-kit/feature-flags'
export class FeatureFlagsEnvironment {
@Expose()
@IsObject()
public FEATURE_FLAGS: FeatureFlagState
// Конфиг-модуль конструирует класс без аргументов: переменных столько, сколько флагов, и объявлять их
// по одной `@Expose()` значило бы держать вторую копию набора.
public constructor(source: Record<string, unknown> = process.env) {
this.FEATURE_FLAGS = featureFlags.readEnvironment(source)
}
}
export class FeatureFlagsConfig extends EnvironmentFeatureFlags {
public constructor(@Inject(FeatureFlagsEnvironment) environment: FeatureFlagsEnvironment) {
super(featureFlags, environment.FEATURE_FLAGS)
}
}
// провайдер модуля
{ provide: FEATURE_FLAGS, useExisting: FeatureFlagsConfig }
Чтение на сервере
Через порт по токену, а не чтением 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-состоянием:
// сервер, перед 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 управления флагами.
Related Packages
Backend-agnostic feature flags for Vue 3 — boolean and multivariate flags, conte...
Client-side feature flag composables and components for Nuxt/Vue 3
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.1.0 | ^3.5.0 | MIT | |
| 0.0.0 | ^3.5.0 | MIT |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |