Все статьи

Реализация Feature Flags в Node.js: от Enterprise-решений до кастомных тогглов

·MAGMA

Современный подход к безопасным релизам в production подразумевает, что новый код должен прибыть на сервер, но оставаться неактивным. Именно эту парадигму реализуют feature flags — механизмы, которые разделяют развертывание кода и активацию функциональности. Они позволяют выпускать обновления для ограниченной группы пользователей, проводить A/B-тестирование и мгновенно отключать проблемные функции без необходимости повторного деплоя.

Принципиальное отличие в процессе деплоя

В классическом сценарии инцидент в production развивается по шаблону: выкатывается новая версия, что-то ломается, команда откатывается к предыдущему стабильному билду. Этот процесс отката занимает от 5 до 15 минут, в течение которых пользователи сталкиваются с ошибками.

Feature flags кардинально меняют эту ситуацию. При возникновении проблемы вы просто отключаете флаг — функциональность становится недоступной мгновенно. Откат происходит без передеплоя, без ожидания выполнения пайплайна, без паники в рабочих чатах. Сам код остается на серверах, но перестает выполняться.

Помимо быстрых откатов, система флагов открывает несколько стратегически важных возможностей:

  • Канареечные релизы: Активация функции для 1% пользователей с последующим мониторингом метрик и постепенным расширением аудитории.
  • A/B-тестирование: Направление 50% трафика на каждую из вариантов реализации с измерением конверсии.
  • Бета-программы: Включение функциональности для конкретных пользовательских ID или аккаунтов.
  • Аварийные выключатели: Отключение проблемных интеграций без изменений кода.
  • Операционные тогглы: Деактивация ресурсоемких функций при высокой нагрузке.
  • Разработка в основной ветке: Возможность помещать незавершенные функции за флагом, избегая создания долгоживущих веток.

LaunchDarkly: комплексное Enterprise-решение

LaunchDarkly представляет собой наиболее функционально насыщенный управляемый сервис для работы с feature flags. Платформа предоставляет расширенные возможности: правила таргетирования, процентные роуллауты, мультивариативные флаги и обновления в реальном времени без необходимости постоянного опроса сервера.

Установка SDK выполняется стандартным способом:

npm install @launchdarkly/node-server-sdk

Инициализация клиента требует минимальной настройки:

// flags/launchdarkly.js
const { init } = require('@launchdarkly/node-server-sdk');
let ldClient;

async function getClient() {
  if (ldClient) return ldClient;
  ldClient = init(process.env.LAUNCHDARKLY_SDK_KEY);
  await ldClient.waitForInitialization();
  return ldClient;
}

После инициализации проверка состояния флага становится тривиальной операцией:

async function isFeatureEnabled(featureKey, userContext) {
  const client = await getClient();
  return client.variation(featureKey, userContext, false);
}

// Использование в обработчике запроса
app.get('/new-feature', async (req, res) => {
  const user = { key: req.user.id };
  if (await isFeatureEnabled('new-ui', user)) {
    // Рендер новой версии интерфейса
  } else {
    // Рендер старой версии
  }
});

Основное преимущество LaunchDarkly — централизованное управление флагами через веб-интерфейс с возможностью тонкой настройки правил активации для разных сегментов пользователей. Изменения применяются мгновенно без перезапуска приложения.

Unleash: самодостаточная open-source альтернатива

Для команд, предпочитающих полный контроль над инфраструктурой, Unleash предлагает мощную open-source платформу для управления feature flags. Решение можно развернуть на собственных серверах, сохраняя при этом богатый функционал, сравнимый с коммерческими аналогами.

Установка клиентской библиотеки:

npm install unleash-client

Настройка подключения к серверу Unleash:

// flags/unleash.js
const { initialize } = require('unleash-client');
const unleash = initialize({
  url: process.env.UNLEASH_URL,
  appName: 'my-node-app',
  instanceId: process.env.INSTANCE_ID || 'default',
});

unleash.on('ready', () => {
  console.log('Unleash client ready');
});

module.exports = unleash;

Проверка активации функции с контекстом пользователя:

const unleash = require('./flags/unleash');

function shouldShowNewDashboard(userId) {
  const context = {
    userId: userId,
    // Дополнительные атрибуты для сегментации
  };
  return unleash.isEnabled('new-dashboard', context);
}

Unleash поддерживает продвинутые стратегии активации, включая постепенный роуллаут, таргетирование по пользовательским атрибутам и комбинирование нескольких условий. Архитектура системы предполагает наличие отдельного сервера Unleash, который может быть развернут как в виде Docker-контейнера, так и на Kubernetes.

Кастомная реализация: минималистичный подход

Для проектов, где внедрение сторонних решений неоправданно или требуется максимальная простота, можно реализовать собственную систему feature flags буквально за несколько часов. Такой подход исключает внешние зависимости и дает полный контроль над логикой работы.

Базовая реализация может использовать in-memory хранилище с возможностью горячей перезагрузки конфигурации:

// flags/custom.js
class FeatureFlagManager {
  constructor() {
    this.flags = new Map();
    this.listeners = new Set();
    this.loadFlags();
  }

  loadFlags() {
    // Загрузка флагов из БД, файла конфигурации или удаленного источника
    this.flags.set('new-checkout', {
      enabled: true,
      percentage: 30, // Включено для 30% пользователей
      userIds: ['beta-user-1', 'beta-user-2'],
    });
  }

  isEnabled(flagName, userId = null) {
    const flag = this.flags.get(flagName);
    if (!flag) return false;
    
    if (!flag.enabled) return false;
    
    // Проверка явного списка пользователей
    if (userId && flag.userIds && flag.userIds.includes(userId)) {
      return true;
    }
    
    // Процентный роуллаут на основе хеша userId
    if (flag.percentage && userId) {
      const hash = this.stringHash(userId);
      return (hash % 100) < flag.percentage;
    }
    
    return flag.enabled;
  }

  stringHash(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      hash = ((hash << 5) - hash) + str.charCodeAt(i);
      hash |= 0; // Преобразование в 32-битное целое
    }
    return Math.abs(hash);
  }

  updateFlag(flagName, config) {
    this.flags.set(flagName, config);
    this.notifyListeners(flagName);
  }

  onUpdate(listener) {
    this.listeners.add(listener);
  }

  notifyListeners(flagName) {
    this.listeners.forEach(listener => listener(flagName));
  }
}

module.exports = new FeatureFlagManager();

Интеграция с приложением становится предельно простой:

const featureFlags = require('./flags/custom');

// В обработчике маршрута
app.get('/experimental-feature', (req, res) => {
  const userId = req.user?.id || req.ip;
  if (featureFlags.isEnabled('experimental-flow', userId)) {
    // Новая экспериментальная логика
  } else {
    // Стандартная логика
  }
});

Для production-использования кастомную реализацию стоит расширить персистентным хранением конфигурации, механизмом кэширования и API для удаленного управления флагами. Простейшим вариантом может стать хранение настроек в Redis с подпиской на обновления через Pub/Sub.

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

Вернуться к блогу