Feature Flags: практическое руководство для разработчиков

Вы создали новую функцию. Она протестирована, проверена и готова к релизу. Но вы нервничаете — что если в продакшене что-то пойдёт не так? Что если пользователям не понравится?

Feature flags позволяют деплоить код, не показывая его пользователям, а затем постепенно раскатывать, отслеживая проблемы. В этом руководстве мы научимся эффективно использовать feature flags.

Интересное
Последние новости в мире программирования
Самые свежие новости и полезные материалы в моем telegram канале.
go

Что такое feature flags?

Feature flag (он же feature toggle) — это условие, которое контролирует, выполняется ли часть кода:

if (featureFlags.get('new-checkout')) {
  showNewCheckout()
} else {
  showOldCheckout()
}

Значение флага хранится внешне (в файле конфигурации, базе данных или удалённом сервисе), поэтому вы можете менять его без деплоя кода.

Эта простая концепция открывает мощные возможности:

  • Dark launches: деплой кода, скрытого от пользователей
  • Постепенные раскатки: включение функции для 1%, затем 10%, затем 100%
  • Kill switches: мгновенное отключение сломанных функций
  • A/B тестирование: показ разных вариантов разным пользователям

Типы feature flags

Булевые флаги

Простейший тип — включено или выключено:

if (flags.get('dark-mode-enabled')) {
  applyDarkTheme()
}
Интересное
Последние новости в мире программирования
Самые свежие новости и полезные материалы в моем telegram канале.
go

Процентные раскатки

Включение функции для процента пользователей:

// 10% пользователей видят новую функцию
if (flags.get('new-pricing', { context: { userId: user.id } })) {
  showNewPricing()
}

Система флагов использует ID пользователя для детерминированного определения, входит ли этот пользователь в 10%. Один и тот же пользователь всегда получает один и тот же результат.

Таргетинг пользователей

Включение функций для определённых пользователей или групп:

// Только premium-пользователи получают эту функцию
if (flags.get('advanced-analytics', { context: { plan: user.plan } })) {
  showAdvancedAnalytics()
}

Или таргетинг по ID пользователя:

// Только бета-тестеры
if (flags.get('experimental-feature', { context: { userId: user.id } })) {
  showExperimentalFeature()
}

Какой тип feature flag вы бы использовали для тестирования нового checkout-процесса с 5% пользователей?

Булевый флаг
Процентная раскатка
Таргетинг по ID пользователя
Переменная окружения

Реализация feature flags

Интересное
Последние новости в мире программирования
Самые свежие новости и полезные материалы в моем telegram канале.
go

Простая реализация в памяти

Для небольших проектов можно начать с простого объекта:

const FEATURE_FLAGS = {
  'new-checkout': false,
  'dark-mode': true,
  'experimental-api': false,
}

function isEnabled(flagName) {
  return FEATURE_FLAGS[flagName] ?? false
}

Это работает, но изменения требуют изменений кода и редеплоя.

Конфигурация на основе файлов

Храните флаги в JSON-файле:

import { readFileSync } from 'fs'

function loadFlags() {
  const content = readFileSync('flags.json', 'utf-8')
  return JSON.parse(content)
}

function isEnabled(flagName) {
  const flags = loadFlags()
  return flags[flagName] ?? false
}

Лучше — можно менять флаги без изменения кода. Но всё ещё нужно перезапускать приложение для применения изменений.

Удалённая конфигурация

Для production-систем используйте сервис удалённой конфигурации с обновлениями в реальном времени:

import { Replane } from '@replanejs/sdk'

interface Configs {
  'new-checkout': boolean
  'advanced-analytics': boolean
}

const replane = new Replane<Configs>()

await replane.connect({
  sdkKey: process.env.REPLANE_SDK_KEY!,
  baseUrl: 'https://cloud.replane.dev',
})

// Флаги обновляются в реальном времени, перезапуск не нужен
function isEnabled(flagName: keyof Configs, context?: Record<string, unknown>): boolean {
  if (context) {
    return replane.get(flagName, { context })
  }
  return replane.get(flagName)
}

С удалённой конфигурацией вы можете менять флаги в панели управления, и изменения применятся на всех серверах за секунды.

Интересное
Последние новости в мире программирования
Самые свежие новости и полезные материалы в моем telegram канале.
go

Процентные раскатки в деталях

Процентные раскатки должны быть детерминированными — один и тот же пользователь всегда должен получать один и тот же результат. Вот как это реализовать:

import { createHash } from 'crypto'

function isInRollout(userId: string, flagName: string, percentage: number): boolean {
  // Создаём хеш от userId + flagName
  const key = `${userId}:${flagName}`
  const hash = createHash('md5').update(key).digest('hex')

  // Преобразуем первые 8 hex-символов в число, затем в диапазон 0-100
  const bucket = parseInt(hash.substring(0, 8), 16) % 100

  return bucket < percentage
}

Использование:

// 10% раскатка new-checkout
if (isInRollout(user.id, 'new-checkout', 10)) {
  showNewCheckout()
} else {
  showOldCheckout()
}

Хеш обеспечивает:

  • Один и тот же пользователь + флаг = один и тот же результат (детерминированность)
  • Разные флаги дают разные результаты (независимость)
  • Распределение примерно равномерное

Большинство сервисов feature flags обрабатывают это автоматически. Например, с Replane вы настраиваете процентные раскатки в панели управления, а SDK обрабатывает математику.

Почему важно, чтобы процентные раскатки были детерминированными?

Для экономии CPU
Для уменьшения сетевых вызовов
Чтобы пользователи получали консистентный опыт
Для соответствия нормативам

Лучшие практики

Называйте флаги понятно

Используйте описательные имена, объясняющие, что делает флаг:

// Хорошо
'enable-new-checkout-flow'
'show-premium-features'
'use-v2-recommendation-engine'

// Плохо
'flag1'
'test'
'new_thing'
Интересное
Последние новости в мире программирования
Самые свежие новости и полезные материалы в моем telegram канале.
go

Устанавливайте сроки истечения

Feature flags временные. Когда функция полностью раскатана, удаляйте флаг:

// Этот флаг на 100% уже 3 месяца
// Пора удалить его и почистить код
if (flags.get('new-checkout')) {  // УДАЛИТЬ ЭТО
  showNewCheckout()
} else {
  showOldCheckout()  // УДАЛИТЬ ЭТО
}

// Становится просто:
showNewCheckout()

Отслеживайте даты создания флагов и устанавливайте напоминания о чистке.

Используйте значения по умолчанию разумно

Всегда имейте безопасное значение по умолчанию на случай недоступности сервиса флагов:

// Хорошо: безопасное значение по умолчанию
const newFeatureEnabled = flags.get('risky-new-feature', { default: false })

// Плохо: нет значения по умолчанию, может выбросить ошибку
const newFeatureEnabled = flags.get('risky-new-feature')

Для рискованных функций значение по умолчанию должно быть false. Для устоявшихся функций за флагами значение по умолчанию может быть true.

Логируйте вычисления флагов

При отладке нужно знать, какие флаги были активны:

function getFlag(name, context) {
  const value = flags.get(name, { context })
  logger.debug(`Flag ${name} = ${value} for context ${JSON.stringify(context)}`)
  return value
}
Интересное
Последние новости в мире программирования
Самые свежие новости и полезные материалы в моем telegram канале.
go

Держите проверки флагов простыми

Избегайте сложной логики вокруг флагов:

// Плохо: вложенные флаги запутывают
if (flags.get('feature-a')) {
  if (flags.get('feature-b')) {
    doSomething()
  } else if (flags.get('feature-c')) {
    doSomethingElse()
  }
}

// Лучше: один флаг, понятное поведение
if (flags.get('feature-a-with-b')) {
  doSomething()
}

Распространённые паттерны

Kill switch

Флаг, который обычно true, но может быть переключён в false для отключения функции:

if (flags.get('payments-enabled', { default: true })) {
  processPayment()
} else {
  showMaintenanceMessage()
}

Если платежи сломаются в 2 часа ночи, переключите флаг, и пользователи увидят дружелюбное сообщение вместо ошибок.

Интересное
Последние новости в мире программирования
Самые свежие новости и полезные материалы в моем telegram канале.
go

Ops-флаги vs release-флаги

Разделяйте операционные флаги и флаги релизов:

// Ops-флаг: контролирует поведение, долгоживущий
const rateLimit = flags.get('api-rate-limit')

// Release-флаг: временный, удалить после раскатки
if (flags.get('new-ui-enabled')) {
  showNewUI()
}

Ops-флаги остаются навсегда. Release-флаги должны удаляться после полной раскатки функции.

Trunk-based development

Feature flags позволяют trunk-based development — все коммитят в main, но незавершённые функции скрыты за флагами:

main ──●──●──●──●──●──●──●──▶
       │  │  │  │  │  │  │
       Feature A (выключен)
          └──┴──┴──┴──┘
             Feature B (выключен)
                   └──┘

Это избавляет от долгоживущих feature-веток и конфликтов слияния.

Инструменты для feature flags

Можно построить свою систему feature flags, но существующие инструменты экономят время:

  • Replane — Open-source, self-hosted или облако, поддержка JavaScript/Python/.NET
  • LaunchDarkly — Enterprise feature management
  • Unleash — Open-source feature toggles
  • Flagsmith — Open-source с облачной опцией

Для open-source и self-hosting Replane — надёжный выбор с обновлениями в реальном времени через SSE и встроенной историей версий.

Интересное
Последние новости в мире программирования
Самые свежие новости и полезные материалы в моем telegram канале.
go

Итоги

Feature flags позволяют:

  • Деплоить код, не показывая его пользователям
  • Постепенно раскатывать функции на проценты пользователей
  • Таргетировать определённых пользователей или группы
  • Мгновенно отключать сломанные функции

Ключевые практики:

  • Называйте флаги понятно
  • Чистите старые флаги
  • Используйте безопасные значения по умолчанию
  • Логируйте вычисления флагов

Упражнения

  1. Реализуйте систему флагов: Создайте простой класс feature flag с поддержкой булевых флагов и процентных раскаток.

  2. Добавьте правила таргетинга: Расширьте систему флагов поддержкой правил типа “включить для premium-пользователей” или “включить для пользователей из Европы”.

  3. Создайте панель управления флагами: Создайте простой веб-интерфейс для просмотра и переключения feature flags.

Если хотите всегда быть в курсе последних новостей в мире программирования и IT, подписываетесь на мой Telegram-канал, где я делюсь свежими статьями, новостями и полезными советами. Буду рад видеть вас среди подписчиков!

Обсуждение