Лучшие практики
Свод рекомендаций по работе с фиче-флагами в можно.: от именования до стратегии очистки и управления флаговым долгом.
Именование флагов
Хорошее имя флага — самодокументированное и однозначное. Плохое — требует телепатии.
Конвенция именования
<функциональность>-<действие/статус>| Шаблон | Пример | Хорошо/Плохо |
|---|---|---|
новый-компонент | new-checkout | Хорошо |
фича-enabled | ai-search-enabled | Хорошо |
kill-компонент | kill-payment-gw | Хорошо |
эксперимент-описание | exp-cta-color | Хорошо |
flag1 | — | Плохо |
test | — | Плохо |
feature_flag_new | — | Плохо (неинформативно) |
Рекомендации
| Правило | Пример |
|---|---|
| kebab-case | new-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, treatment | variant1, variant2 |
Контрольная группа — control или A | control | old, current |
| В описании флага — расшифровка вариантов | A = старый дизайн, B = новый | — |
Когда архивировать, а когда удалять
| Критерий | Архив | Удаление |
|---|---|---|
| Флаг отработал, старый код удалён | ✅ Да | ❌ Нет |
| Нужна история изменений для аудита | ✅ Да | ❌ Нет |
| Экспериментальный флаг, не пошёл в прод | ❌ Нет | ✅ Да |
| Флаг создан по ошибке (опечатка в ключе) | ❌ Нет | ✅ Да |
| Тестовый флаг для локальной разработки | ❌ Нет | ✅ Да |
| Флаг-дубликат | ❌ Нет | ✅ Да |
Совет: Правило по умолчанию — архивируйте. Удаление необратимо. Если сомневаетесь, отправьте в архив и удалите через месяц, если флаг точно не понадобится.
Модель разрешений (Permission Model)
можно. использует ролевую модель доступа с иерархией ADMIN → DEVELOPER → VIEWER (каждая роль включает права нижестоящих):
| Действие | Admin | Developer | Viewer |
|---|---|---|---|
| Просмотр флагов и сегментов | ✅ | ✅ | ✅ |
| Просмотр и экспорт аудит-лога | ✅ | ✅ | ✅ |
| Создание флагов | ✅ | ✅ | ❌ |
| Изменение стратегий и таргетинга | ✅ | ✅ | ❌ |
| Архивация флагов | ✅ | ✅ | ❌ |
| Управление сегментами | ✅ | ✅ | ❌ |
| Удаление флагов | ✅ | ✅ | ❌ |
| Управление окружениями | ✅ | ❌ | ❌ |
| Управление API-ключами | ✅ | ❌ | ❌ |
| Управление пользователями | ✅ | ❌ | ❌ |
| Управление интеграциями (вебхуками) | ✅ | ❌ | ❌ |
Рекомендации по ролям
| Принцип | Описание |
|---|---|
| Принцип наименьших привилегий | Developer не имеет доступа к управлению ключами, пользователями и окружениями |
| Управление инфраструктурой — только Admin | API-ключи, пользователи, окружения и интеграции доступны только роли 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
Чек-лист очистки флага
- Флаг на 100% минимум 2 недели
- Метрики стабильны — нет регрессий
- Старый код не нужен — никто не планирует откат
- Удалить
if (flag): оставить только новый код, удалить старый - Удалить импорт/зависимость SDK, если флаг был последним
- Заархивировать флаг в веб-панели
- Документировать удаление в описании флага: дата, причина
Автоматизация очистки
Добавьте в CI проверку «просроченных» флагов:
# .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 Wrapper | featureService.ifEnabled("flag", ctx, () -> newCode()) | Много флагов в одном сервисе — убирает повторяющийся if |
| Фабрика контекста | MozhnoContexFactory.forUser(user) | Один и тот же набор атрибутов передаётся в десятках мест |
| Middleware | Перехватчик HTTP/gRPC, добавляющий атрибуты в контекст | Атрибуты из заголовков запроса (userId, tenantId, country) |
Пример: Feature Wrapper на 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
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();
}
}Тестирование с фиче-флагами
Модульное тестирование
Тестируйте обе ветки кода — с флагом и без флага:
@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-клиент в тестах, а не сервер можно.. Тесты должны быть быстрыми и не зависеть от сети.
Интеграционное тестирование
Для интеграционных тестов поднимите реальный сервер можно. в тестовом окружении:
@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-ключи для разных окружений в тестах:
// 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 Wrapper | featureService.executeIfEnabled("flag", ctx, () -> newCode()) — обёртка, убирающая повторяющийся if |
| Флаги как dependency | Инжектить MozhnoClient как бин, а не создавать в каждом методе |
Документирование флагов
Что должно быть в описании флага
# Описание флага: 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]Что дальше?
- Работа с флагами — жизненный цикл и командный процесс
- Таргетинг — правила и сегменты
- Аудит — отслеживание изменений
- SDK: Обзор — архитектура SDK и интеграция в код