Skip to content

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

Свод рекомендаций по работе с фиче-флагами в можно.: от именования до стратегии очистки и управления флаговым долгом.

Именование флагов

Хорошее имя флага — самодокументированное и однозначное. Плохое — требует телепатии.

Конвенция именования

<функциональность>-<действие/статус>
ШаблонПримерХорошо/Плохо
новый-компонентnew-checkoutХорошо
фича-enabledai-search-enabledХорошо
kill-компонентkill-payment-gwХорошо
эксперимент-описаниеexp-cta-colorХорошо
flag1Плохо
testПлохо
feature_flag_newПлохо (неинформативно)

Рекомендации

ПравилоПример
kebab-casenew-checkout, dark-mode-rollout
Только латиница, цифры и дефисыapi-v2, search-v3
Не используйте flag или feature в ключеfeature-new-checkout-flag → ✅ new-checkout
Kill switch — префикс kill-kill-payment-gateway, kill-third-party-api
Эксперименты — префикс exp-exp-pricing-layout, exp-cta-placement
Временные фичи — префикс tmp-tmp-holiday-banner-2026
Перманентные конфигурации — префикс cfg-cfg-rate-limit, cfg-max-upload-size

Именование флаговых ключей

ПравилоХорошоПлохо
Короткие осмысленные идентификаторыA, B, control, treatmentvariant1, variant2
Контрольная группа — control или Acontrolold, current
В описании флага — расшифровка вариантовA = старый дизайн, B = новый

Когда архивировать, а когда удалять

КритерийАрхивУдаление
Флаг отработал, старый код удалён✅ Да❌ Нет
Нужна история изменений для аудита✅ Да❌ Нет
Экспериментальный флаг, не пошёл в прод❌ Нет✅ Да
Флаг создан по ошибке (опечатка в ключе)❌ Нет✅ Да
Тестовый флаг для локальной разработки❌ Нет✅ Да
Флаг-дубликат❌ Нет✅ Да

Совет: Правило по умолчанию — архивируйте. Удаление необратимо. Если сомневаетесь, отправьте в архив и удалите через месяц, если флаг точно не понадобится.

Модель разрешений (Permission Model)

можно. использует ролевую модель доступа с иерархией ADMINDEVELOPERVIEWER (каждая роль включает права нижестоящих):

ДействиеAdminDeveloperViewer
Просмотр флагов и сегментов
Просмотр и экспорт аудит-лога
Создание флагов
Изменение стратегий и таргетинга
Архивация флагов
Управление сегментами
Удаление флагов
Управление окружениями
Управление API-ключами
Управление пользователями
Управление интеграциями (вебхуками)

Рекомендации по ролям

ПринципОписание
Принцип наименьших привилегийDeveloper не имеет доступа к управлению ключами, пользователями и окружениями
Управление инфраструктурой — только AdminAPI-ключи, пользователи, окружения и интеграции доступны только роли Admin
Viewer для стороннихАудиторы, менеджеры продукта — только просмотр
Регулярный аудитРаз в квартал проверяйте список пользователей и их роли

Стратегия очистки флагов

Флаги, оставленные в коде после полного роллаута, создают флаговый долг (flag debt) — технический долг, характерный для систем с фиче-флагами.

Признаки флагового долга

  • Условные конструкции if (flag) с мёртвой веткой старого кода
  • Флаги, включённые на 100% больше месяца
  • Сложные цепочки зависимостей между флагами
  • Код, который невозможно понять без знания состояния флагов

Процесс очистки

graph TD
    A[Флаг на 100%] --> B{Прошло > 2 недель?}
    B -->|Нет| C[Ждём]
    B -->|Да| D[Удаляем старый код]
    D --> E[Мержим PR]
    E --> F[Архивируем флаг]
    F --> G{Прошёл месяц?}
    G -->|Да| H[Удаляем флаг]
    G -->|Нет| F

Чек-лист очистки флага

  1. Флаг на 100% минимум 2 недели
  2. Метрики стабильны — нет регрессий
  3. Старый код не нужен — никто не планирует откат
  4. Удалить if (flag): оставить только новый код, удалить старый
  5. Удалить импорт/зависимость SDK, если флаг был последним
  6. Заархивировать флаг в веб-панели
  7. Документировать удаление в описании флага: дата, причина

Автоматизация очистки

Добавьте в CI проверку «просроченных» флагов:

yaml
# .github/workflows/flag-cleanup-check.yml
name: Flag Cleanup Check
on:
  schedule:
    - cron: '0 8 * * 1'  # Каждый понедельник

jobs:
  stale-flags:
    runs-on: ubuntu-latest
    steps:
      - name: Поиск просроченных флагов
        run: |
          curl "${{ secrets.MOZHNO_URL }}/api/v1/flags?includeArchived=false" \
            -H "Authorization: Bearer ${{ secrets.MOZHNO_TOKEN }}" | \
            jq -r '.items[] | select(.enabled == true) | "\(.key): активен \(.createdAt)"'

      - name: Поиск флагов без описания
        run: |
          curl "${{ secrets.MOZHNO_URL }}/api/v1/flags" \
            -H "Authorization: Bearer ${{ secrets.MOZHNO_TOKEN }}" | \
            jq -r '.items[] | select(.description == null or .description == "") | "⚠ Нет описания: \(.key)"'

Календарь очистки

ПериодичностьДействие
ЕженедельноПроверить флаги на 100% дольше 2 недель
Раз в спринтЗаархивировать флаги после удаления старого кода
Раз в месяцУдалить архивные флаги старше месяца
Раз в кварталПолный аудит активных флагов, обновление описаний

Архитектурные паттерны

Паттерны организации флагов в коде

ПаттернКодКогда применять
Инлайнif (client.isEnabled("flag", ctx)) { ... }Единичные флаги, быстрый старт
Feature WrapperfeatureService.ifEnabled("flag", ctx, () -> newCode())Много флагов в одном сервисе — убирает повторяющийся if
Фабрика контекстаMozhnoContexFactory.forUser(user)Один и тот же набор атрибутов передаётся в десятках мест
MiddlewareПерехватчик HTTP/gRPC, добавляющий атрибуты в контекстАтрибуты из заголовков запроса (userId, tenantId, country)

Пример: Feature Wrapper на Java

java
@Service
public class FeatureService {
    private final MozhnoClient client;

    public <T> T ifEnabled(String flag, MozhnoContext ctx,
                           Supplier<T> newCode, Supplier<T> oldCode) {
        return client.isEnabled(flag, ctx) ? newCode.get() : oldCode.get();
    }
}

// Использование:
var result = featureService.ifEnabled("new-checkout", ctx,
    () -> processNew(order),   // новый код
    () -> processOld(order)    // старый код
);

Пример: Middleware на Express

Пример: Фабрика контекста на Java

java
public class MozhnoContextFactory {
    public static MozhnoContext forRequest(HttpServletRequest req) {
        return MozhnoContext.builder()
            .userId(req.getHeader("X-User-Id"))
            .addProperty("tenantId", req.getHeader("X-Tenant-Id"))
            .addProperty("country", req.getHeader("X-Country"))
            .addProperty("device", req.getHeader("X-Device"))
            .build();
    }
}

Тестирование с фиче-флагами

Модульное тестирование

Тестируйте обе ветки кода — с флагом и без флага:

java
@Test
void testNewCheckoutFlow() {
    var ctx = MozhnoContext.builder().userId("test-user").build();
    when(client.isEnabled("new-checkout", ctx)).thenReturn(true);

    var result = checkoutService.process(order, ctx);

    assertThat(result.getFlow()).isEqualTo("new");
}

@Test
void testOldCheckoutFlow() {
    var ctx = MozhnoContext.builder().userId("test-user").build();
    when(client.isEnabled("new-checkout", ctx)).thenReturn(false);

    var result = checkoutService.process(order, ctx);

    assertThat(result.getFlow()).isEqualTo("old");
}

Совет: Мокайте SDK-клиент в тестах, а не сервер можно.. Тесты должны быть быстрыми и не зависеть от сети.

Интеграционное тестирование

Для интеграционных тестов поднимите реальный сервер можно. в тестовом окружении:

java
@SpringBootTest
@AutoConfigureMockMvc
class CheckoutIntegrationTest {

    @Autowired
    private MozhnoClient client;

    @Test
    void testWithRealFlagEvaluation() {
        var ctx = MozhnoContext.builder().userId("test-user").build();
        boolean enabled = client.isEnabled("new-checkout", ctx);
        // Поведение зависит от конфигурации флага в тестовом окружении
    }
}

Тестирование стратегий роллаута

СценарийКак тестировать
0% роллаутУбедиться, что ни один пользователь не получает фичу
100% роллаутУбедиться, что все пользователи получают фичу
50% роллаутПроверить детерминированность: один userId всегда даёт один результат
Правило таргетингаПроверить, что правило срабатывает для подходящих пользователей и не срабатывает для остальных
Kill SwitchУбедиться, что фича мгновенно отключается для всех

Тестирование с разными окружениями

Используйте разные API-ключи для разных окружений в тестах:

java
// application-test.properties
mozhno.url=http://localhost:8080
mozhno.api-key=test-env-api-key

Флаги в тестовом окружении можно менять без влияния на production.

Управление флаговым долгом

Метрики флагового долга

МетрикаЦелевое значениеПочему важно
Средний возраст флага< 30 днейСтарые флаги — кандидаты на удаление
Флагов на 100% старше 7 дней0Они должны быть удалены
Флагов без описания0Неописанный флаг = неизвестный риск
Активных постоянных флагов< 10% от общего числаЕсли флагов > 50, пора чистить
Глубина вложенности флагов≤ 2Глубокая вложенность усложняет отладку

Категории флагов по жизненному циклу

pie title Распределение флагов по стадиям
    "Активные (Rollout)" : 15
    "Полностью включены (100%)" : 25
    "Выключены" : 10
    "В архиве" : 30
    "Удалены" : 20

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

Антипаттерны

АнтипаттернПочему плохоКак исправить
Флаг на флагеif (flagA && flagB) — невозможно отлаживатьОбъединить в сегмент или один флаг
Флаги в циклахПроверка флага на каждой итерации — накладные расходыПроверить флаг до цикла
Флаг как конфигif (flag) timeout = 30 else timeout = 60Использовать настоящий конфиг, не фиче-флаг
Вечные флагиФлаг существует 6+ месяцевЗапланировать удаление или пометить как перманентный
Флаги без владельцаНикто не отвечает за очисткуНазначить ответственного в описании флага
Копипаста контекстаДублирование MozhnoContext.builder()...Вынести в фабричный метод или middleware

Здоровые паттерны

ПаттернПример
Фабрика контекстаMozhnoContextFactory.forUser(user) — создаёт контекст с нужными атрибутами
Middleware для контекстаПерехватчик HTTP-запросов, автоматически добавляющий атрибуты в контекст
Feature WrapperfeatureService.executeIfEnabled("flag", ctx, () -> newCode()) — обёртка, убирающая повторяющийся if
Флаги как dependencyИнжектить MozhnoClient как бин, а не создавать в каждом методе

Документирование флагов

Что должно быть в описании флага

markdown
# Описание флага: new-checkout

**Зачем:** Переработка процесса оформления заказа для увеличения конверсии.

**Кто создал:** @dev-team-lead

**План роллаута:**
- 2026-06-21: 1% (канареечный)
- 2026-06-22: 10%
- 2026-06-23: 50%
- 2026-06-24: 100%

**План очистки:** Удалить старый код до 2026-07-08

**Зависимости:** Нет

**Связанные задачи:** JIRA-1234, PR #567

Шаблон описания

Зачем: [краткое описание]
Кто создал: [email или GitHub username]
План роллаута: [даты и проценты]
План очистки: [дата или условие]
Зависимости: [другие флаги или системы]
Связанные задачи: [ссылки на тикеты/PR]

Что дальше?

Released under the AGPL v3.0 License.