Когда деплой — это боль: почему разработчики обожают feature flags
Представьте: вы неделю пилили новую фичу. Покрыли тестами, прогнали код-ревью, нажали кнопку деплоя. И тут — бабах! — в продакшене всё ломается. Пользователи в ярости, менеджер дёргается, вы лихорадочно откатываете версию. Знакомо?
А теперь представьте другой сценарий: вы включаете новую фичу только для 5% пользователей. Смотрите метрики. Видите, что всё окей. Постепенно увеличиваете процент до 100%. Никакого стресса, никаких откатов, только спокойная уверенность.
Это магия feature flags (они же feature toggles, фича-флаги, флаги функций). В TypeScript проектах эта техника особенно хороша благодаря статической типизации и строгому контролю типов. Сегодня разберёмся, как внедрить feature flags в ваш проект, чтобы вы спали спокойно, а фичи включались без боли.
Что такое feature flags простыми словами
Feature flag — это условный переключатель в коде, который позволяет включить или выключить функциональность без изменения кода и повторного деплоя. Выглядит это примерно так:
if (featureFlags.isEnabled('new-checkout')) {
// новый код
} else {
// старый код
}
Всё гениальное просто, правда? Но за этой простотой скрывается мощный инструмент, который используют Google, Netflix, Facebook и другие гиганты. Они не деплоят каждую мелочь — они управляют поведением приложения через централизованную панель управления.
Когда feature flags реально нужны
- Canary release — выкатка новой фичи на малую часть пользователей
- Dark launch — скрытое тестирование, когда фича есть, но пользователи её не видят
- A/B тестирование — сравнение двух версий функциональности
- Kill switch — аварийное отключение проблемного кода без деплоя
- Permit testing — тестирование на внутренних пользователях или по ролям
В TypeScript проектах feature flags приобретают особый шарм: компилятор проверяет, что вы правильно используете флаги, не путаете типы и не забываете обработать все состояния.
Архитектура системы feature flags
Прежде чем писать код, давайте разберёмся, из чего состоит грамотная система управления функциями.
Базовые компоненты
- Хранилище флагов — где лежат значения (JSON, база данных, внешний сервис)
- Провайдер — модуль, который загружает и кэширует флаги
- Клиентская часть — код, который проверяет состояние флагов
- Панель управления — интерфейс для изменения флагов в реальном времени
Простейшая реализация на TypeScript
Начнём с самого базового примера. Создадим тип для флагов и простой сервис:
// types/feature-flags.ts
export type FeatureFlag =
| 'new-checkout'
| 'dark-mode'
| 'recommendations-v2'
| 'beta-chat';
export interface FeatureFlagConfig {
flag: FeatureFlag;
enabled: boolean;
rolloutPercentage?: number;
userIds?: string[];
}
Теперь напишем сам сервис:
// services/feature-flag.service.ts
export class FeatureFlagService {
private flags: Map<FeatureFlag, FeatureFlagConfig> = new Map();
constructor(initialFlags: FeatureFlagConfig[]) {
initialFlags.forEach(flag => {
this.flags.set(flag.flag, flag);
});
}
isEnabled(flag: FeatureFlag, userId?: string): boolean {
const config = this.flags.get(flag);
if (!config) return false;
if (!config.enabled) return false;
// Проверка по конкретным пользователям
if (config.userIds && userId) {
return config.userIds.includes(userId);
}
// Rollout по проценту пользователей
if (config.rolloutPercentage !== undefined && userId) {
const hash = this.hashCode(userId);
return (hash % 100) < config.rolloutPercentage;
}
return true;
}
private hashCode(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash |= 0;
}
return Math.abs(hash);
}
}
Это минимальная рабочая система. Но в реальном проекте вы, скорее всего, захотите использовать готовые решения, чтобы не изобретать велосипед.
Готовые библиотеки для TypeScript
На рынке есть несколько зрелых решений, которые отлично работают с TypeScript.
1. Unleash
Open-source решение с собственным сервером и отличным TypeScript SDK. Позволяет управлять флагами через веб-интерфейс, поддерживает стратегии активации (по проценту, по IP, по роли).
import { UnleashClient } from 'unleash-client';
const unleash = new UnleashClient({
url: 'https://your-unleash-instance.com/api',
appName: 'my-typescript-app',
environment: 'production',
});
if (unleash.isEnabled('new-checkout', { userId: '123' })) {
// новый код
}
2. LaunchDarkly
Корпоративный стандарт. Мощный, быстрый, с кучей интеграций. Для TypeScript есть отличный SDK с полной поддержкой типов.
import { init } from 'launchdarkly-node-server-sdk';
const client = init('sdk-key-12345');
const flagValue = await client.variation(
'new-checkout',
{ key: 'user-123', name: 'John' },
false // значение по умолчанию
);
if (flagValue) {
// новый код
}
3. Микро-решение на файлах
Если не хотите поднимать сервер, можно хранить флаги в JSON-файлах и перезагружать их при изменении. Отлично подходит для небольших проектов.
// flags.json
{
"new-checkout": {
"enabled": true,
"rolloutPercentage": 30
},
"dark-mode": {
"enabled": false
}
}
// services/file-based-flags.ts
import * as fs from 'fs';
export class FileBasedFlags {
private flags: Record<string, any>;
constructor(private filePath: string) {
this.flags = this.loadFlags();
fs.watchFile(filePath, () => {
this.flags = this.loadFlags();
});
}
isEnabled(flag: string): boolean {
return this.flags[flag]?.enabled ?? false;
}
private loadFlags(): Record<string, any> {
const raw = fs.readFileSync(this.filePath, 'utf-8');
return JSON.parse(raw);
}
}
Продвинутые техники работы с feature flags
Когда базовая система