
Фича‑флаги — это управляемые переключатели в коде, которые позволяют включать/выключать функциональность без нового деплоя. Бизнес‑выгоды:
Нужны:
Не нужны:
Минимальный вариант:
Более надёжный вариант:
Доставлять флаги можно двумя способами:
Ключевой принцип — «оценка на краю»: решения по флагам принимаются локально и очень быстро (микросекунды), а не требуют сетевой поездки.
У каждого флага должны быть владелец, цель, план запуска, критерии успеха/отката и срок удаления.
Чтобы один и тот же пользователь стабильно попадал в одну группу, используйте детерминированное хэширование (salt + userId → число 0..100). Тогда при 10% включения одни и те же люди будут видеть функцию до тех пор, пока процент не изменится.
Лучшие практики:
Ниже — полностью рабочий пример. Он:
// file: featureFlags.ts
// Требуется Node.js 18+ (для fs/promises и встроенного fetch, хотя тут сеть не используется)
import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { watch } from 'node:fs'
// Типы конфигурации флагов
export type Context = {
userId?: string
country?: string
platform?: 'ios' | 'android' | 'web' | string
[key: string]: unknown
}
type PercentRule = { userIdPercent: { salt: string; percent: number } }
type CountryInRule = { countryIn: string[] }
type PlatformInRule = { platformIn: string[] }
type UserInRule = { userIdIn: string[] }
type Condition = PercentRule | CountryInRule | PlatformInRule | UserInRule
export type FlagRule = {
if: Condition
value: boolean | string | number
}
export type FlagDefinition = {
type: 'boolean' | 'string' | 'number'
default: boolean | string | number
rules?: FlagRule[]
description?: string
}
export type FlagsConfig = {
flags: Record<string, FlagDefinition>
version?: string
}
// Хэш → процент 0..100 (детерминированно и стабильно)
function percentForKey(key: string): number {
const h = createHash('sha256').update(key).digest()
// Берём первые 4 байта как беззнаковое 32‑битное число
const value = h.readUInt32BE(0)
return (value / 0xffffffff) * 100
}
function matches(cond: Condition, ctx: Context): boolean {
if ('userIdPercent' in cond) {
const { salt, percent } = cond.userIdPercent
const id = ctx.userId ?? ''
const p = percentForKey(`${salt}:${id}`)
return p < percent
}
if ('countryIn' in cond) {
if (!ctx.country) return false
return cond.countryIn.includes(String(ctx.country).toUpperCase())
}
if ('platformIn' in cond) {
if (!ctx.platform) return false
return cond.platformIn.includes(String(ctx.platform).toLowerCase())
}
if ('userIdIn' in cond) {
if (!ctx.userId) return false
return cond.userIdIn.includes(ctx.userId)
}
return false
}
export class FlagStore {
private config: FlagsConfig = { flags: {} }
private readonly filePath: string
private lastLoadedAt: number = 0
constructor(filePath: string) {
this.filePath = filePath
}
async load(): Promise<void> {
const raw = await readFile(this.filePath, 'utf8')
const parsed = JSON.parse(raw) as FlagsConfig
this.validate(parsed)
this.config = parsed
this.lastLoadedAt = Date.now()
}
watch(): void {
// Перезагружаем конфиг при изменениях файла (с защитой от дребезга)
let timer: NodeJS.Timeout | null = null
watch(this.filePath, { persistent: false }, () => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => this.load().catch(console.error), 100)
})
}
getConfig(): FlagsConfig {
return this.config
}
private validate(cfg: FlagsConfig) {
for (const [key, def] of Object.entries(cfg.flags)) {
if (!['boolean', 'string', 'number'].includes(def.type)) {
throw new Error(`Флаг ${key}: неизвестный тип ${String(def.type)}`)
}
if (def.rules) {
for (const r of def.rules) {
if (!('if' in r) || typeof r.if !== 'object') {
throw new Error(`Флаг ${key}: некорректное правило`)
}
}
}
}
}
}
export class FlagEvaluator {
constructor(private readonly store: FlagStore) {}
// Универсальная оценка
eval<T extends boolean | string | number>(flagKey: string, ctx: Context): T | undefined {
const def = this.store.getConfig().flags[flagKey]
if (!def) return undefined
if (!def.rules || def.rules.length === 0) return def.default as T
for (const rule of def.rules) {
if (matches(rule.if, ctx)) return rule.value as T
}
return def.default as T
}
// Удобные шорткаты
isOn(flagKey: string, ctx: Context): boolean {
const v = this.eval<boolean>(flagKey, ctx)
return Boolean(v)
}
}
// Пример использования
// 1) Создайте файл flags.json и положите туда конфиг (см. ниже)
// 2) Запустите этот модуль как скрипт: `node --loader ts-node/esm featureFlags.ts` или соберите с tsc
if (require.main === module) {
;(async () => {
const store = new FlagStore('./flags.json')
await store.load()
store.watch()
const evalr = new FlagEvaluator(store)
const ctxIvan: Context = { userId: 'u123', country: 'RU', platform: 'web' }
const ctxAnna: Context = { userId: 'u777', country: 'KZ', platform: 'ios' }
console.log('new_checkout for Ivan:', evalr.isOn('new_checkout', ctxIvan))
console.log('new_checkout for Anna:', evalr.isOn('new_checkout', ctxAnna))
})().catch((e) => {
console.error(e)
process.exit(1)
})
}
Пример конфигурации флагов (файл flags.json):
{
"version": "2026-02-01",
"flags": {
"new_checkout": {
"type": "boolean",
"default": false,
"description": "Новый процесс оплаты",
"rules": [
{ "if": { "userIdIn": ["admin", "qa1", "qa2"] }, "value": true },
{ "if": { "countryIn": ["RU", "KZ"] }, "value": true },
{ "if": { "userIdPercent": { "salt": "checkout_v1", "percent": 10 } }, "value": true }
]
},
"recommendation_model": {
"type": "string",
"default": "v1",
"description": "Модель рекомендаций",
"rules": [
{ "if": { "platformIn": ["ios"] }, "value": "v2" }
]
},
"rating_weight": {
"type": "number",
"default": 1.0,
"description": "Вес рейтинга для сортировки",
"rules": [
{ "if": { "userIdPercent": { "salt": "rank_weight", "percent": 5 } }, "value": 1.2 }
]
}
}
}
Как это использовать в веб‑приложении:
Минимальный набор метрик:
Алёрты:
День 1–2:
День 3–5:
День 6–8:
День 9–10:
День 11–14:
Фича‑флаги — простой способ ускорить релизы и снизить стоимость ошибок. Они работают, только если у вас есть:
Начните с маленького: один флаг, один сервис, одна фича. Через пару недель вы увидите, что релизы стали спокойнее, а эксперименты — дешевле. Дальше можно добавлять пуш‑обновления, подписи конфигов, интерфейс для продукта и автоматические «красные кнопки».