
Релизы «всё и сразу» — главный источник нервов, ночных выкладок и откатов. Фича‑флаги (переключатели фич) позволяют отделить выкладку кода от включения фичи для пользователей. Вы можете:
В числах это обычно означает: меньше откатов и инцидентов, быстрее Time‑to‑Market, выше конверсия благодаря безопасным экспериментам.
Совет: не смешивайте роле‑базовые разрешения и разруливание инцидентов в одном флаге — иначе получите путаницу и риск ошибочного включения.
Антипаттерн: «включили на 100% и пошли спать». Между шагами давайте системе «устояться», наблюдайте графики: ошибки, 95/99‑перцентили задержек, целевые метрики бизнеса.
Принцип надёжности: «при отказе всё остаётся работать в безопасном состоянии». Значение по умолчанию и локальный кэш — обязательны.
Ниже — минимальная, но рабочая реализация: сервер с флагами, локальная оценка правил, процентная раскатка по стабильному хэшу userId, API для управления и пример использования в маршруте. Код можно запускать как обычный Node‑сервис.
// file: server.ts
import express, { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
// --- Модель флага ---
interface Condition {
attr: string;
op: 'eq' | 'in';
value: string | number | boolean | Array<string | number | boolean>;
}
interface Rule {
match?: Condition[]; // условия применения правила
percentage?: number; // 0..100 — доля аудитории
value: boolean; // значение флага, если правило сработало
}
interface Flag {
key: string;
enabled: boolean; // глобальный рубильник флага
defaultValue: boolean; // значение по умолчанию
rules: Rule[]; // правила, применяются сверху вниз
description?: string;
owner?: string;
}
// --- Простое хранилище флагов в памяти ---
const flags = new Map<string, Flag>();
// Пример флага: поэтапный чекаут
flags.set('checkout.new', {
key: 'checkout.new',
enabled: true,
defaultValue: false,
description: 'Новый чекаут: сначала сотрудники и 10% пользователей PRO',
owner: 'team-payments',
rules: [
// Внутренние пользователи (e-mail домена) — всегда включено
{
match: [{ attr: 'email', op: 'in', value: ['@company.com'] }],
value: true,
},
// Для тарифа PRO — 10% по стабильному хэшу userId
{
match: [{ attr: 'plan', op: 'eq', value: 'pro' }],
percentage: 10,
value: true,
},
],
});
// --- Оценка флага ---
function hashToPercent(seed: string): number {
const h = crypto.createHash('sha1').update(seed).digest('hex').slice(0, 8);
const n = parseInt(h, 16); // 0..2^32
return (n % 100) + 1; // 1..100
}
function matchesCondition(ctx: Record<string, any>, c: Condition): boolean {
const v = ctx[c.attr];
if (c.op === 'eq') return v === c.value;
if (c.op === 'in') {
if (Array.isArray(c.value)) {
return c.value.includes(v) || (typeof v === 'string' && c.value.some((x) => typeof x === 'string' && v.toString().includes(x as string)));
}
return typeof v === 'string' && typeof c.value === 'string' && v.includes(c.value);
}
return false;
}
function evaluateFlag(flag: Flag | undefined, ctx: Record<string, any>): boolean {
if (!flag) return false; // безопасный дефолт
if (!flag.enabled) return false; // глобально выключен
for (const r of flag.rules) {
if (r.match && !r.match.every((c) => matchesCondition(ctx, c))) {
continue; // условия не подходят — к следующему правилу
}
if (r.percentage !== undefined) {
const id = ctx.userId ?? ctx.sessionId ?? 'anonymous';
const p = hashToPercent(String(id) + ':' + flag.key);
if (p <= Math.max(0, Math.min(100, r.percentage))) {
return r.value;
}
// процент не попал — продолжаем искать правило дальше
continue;
}
return r.value; // правило сработало без процента
}
return flag.defaultValue;
}
// --- Расширяем Request, чтобы было удобно проверять флаги ---
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express {
interface Request {
feature: (key: string, ctx?: Record<string, any>) => boolean;
user?: { id: string; email?: string; plan?: string };
}
}
}
const app = express();
app.use(express.json());
// Простая аутентификация-заглушка: вытаскиваем user из заголовков для примера
app.use((req: Request, _res: Response, next: NextFunction) => {
const id = req.header('x-user-id') || 'anon';
const email = req.header('x-user-email') || undefined;
const plan = req.header('x-user-plan') || 'free';
req.user = { id, email, plan };
req.feature = (key: string, extraCtx: Record<string, any> = {}) => {
const flag = flags.get(key);
const ctx = {
userId: req.user?.id,
email: req.user?.email || '',
plan: req.user?.plan || 'free',
...extraCtx,
};
return evaluateFlag(flag, ctx);
};
next();
});
// --- API для чтения и управления флагами ---
app.get('/flags', (_req, res) => {
res.json(Array.from(flags.values()));
});
app.put('/flags/:key', (req, res) => {
const key = req.params.key;
const incoming = req.body as Partial<Flag> & { key?: string };
const current = flags.get(key);
const updated: Flag = {
key,
enabled: incoming.enabled ?? current?.enabled ?? true,
defaultValue: incoming.defaultValue ?? current?.defaultValue ?? false,
rules: incoming.rules ?? current?.rules ?? [],
description: incoming.description ?? current?.description,
owner: incoming.owner ?? current?.owner,
};
flags.set(key, updated);
res.json(updated);
});
// --- Пример бизнес-маршрута: новый чекаут ---
app.get('/checkout', (req, res) => {
const useNew = req.feature('checkout.new');
if (useNew) {
return res.json({ version: 'new', message: 'Новый чекаут активен' });
}
return res.json({ version: 'old', message: 'Старый чекаут' });
});
app.listen(3000, () => {
console.log('Feature Flags server running on http://localhost:3000');
});
Как попробовать:
Производственный уровень потребует: хранить флаги в базе, вести аудит, раздавать обновления через Pub/Sub, а SDK — кэшировать с TTL и экспозициями.
Минимальный протокол: GET /flags отдаёт JSON и ETag. Клиенты запрашивают периодически, получая 304 Not Modified, и обновляют локальный кэш только при изменениях.
Флаги собирают «долг», если их не убирать. Чёткий цикл решает проблему:
Автоматизация помогает: линтер, который ругается на «просроченные» флаги; отчёты о флагах, не меняющих значение более N дней; кодмоды для массового удаления.
Простой расчёт: если команда тратит 10 часов в месяц на откаты/инциденты в релизах, а флаги режут это хотя бы наполовину, экономится ~5 часов × ставка × 12 месяцев — обычно это больше, чем стоимость внедрения.
Фича‑флаги — это дисциплина управления рисками. Простая система с локальной оценкой, понятными правилами и строгим процессом удаления флагов даёт быстрые, безопасные выкаты и реальное ускорение бизнеса. Начните с одного сервиса и одного флага, добавьте процентную раскатку и аудит — уже через месяц вы увидите меньше инцидентов и больше уверенности в релизах.