Тестирование в продакшене с помощью feature-флагов
Updated Jul 2026
Основная идея
Feature-флаги отделяют деплой от релиза. Вы деплоите код в продакшен, но контролируете, кто его видит. Это превращает каждый деплой в тестируемый эксперимент с измеримыми результатами и автоматическим откатом.
Традиционный подход — деплоить всё для всех одновременно — это бинарная ставка. Feature-флаги превращают эту ставку в серию небольших обратимых экспериментов.
Платформы feature-флагов
| Платформа | Тип | Таргетирование | Аналитика | Стоимость |
|---|---|---|---|---|
| LaunchDarkly | SaaS | Пользователь, сегмент, %-базированное, кастомные правила | Встроенное экспериментирование | SaaS за пользователя |
| Unleash | Open source / SaaS | На основе стратегий (постепенное, по ID пользователя, по IP) | Базовые метрики | Бесплатно (self-hosted) |
| Flagsmith | Open source / SaaS | Сегмент, %-базированное, мультивариантное | Встроенная аналитика | Бесплатный тариф |
| Split | SaaS | Таргетирование на основе атрибутов | Полный набор экспериментирования | SaaS за пользователя |
| OpenFeature | Стандарт/SDK | Независимая от провайдера спецификация | Зависит от провайдера | Бесплатно (спецификация) |
Выбор платформы
- LaunchDarkly для корпоративных команд, которым нужны наиболее зрелые возможности таргетирования и экспериментирования
- Unleash для команд, которым нужен open-source, self-hosted контроль с поэтапными стратегиями выпуска
- Flagsmith для команд, которым нужен хороший баланс функций и цены с open-source вариантом
- OpenFeature как абстрактный слой, если вы хотите избежать привязки к вендору или использовать несколько провайдеров
Стратегия тестирования feature-флагов
Паттерн выпуска с контролем качества
# Feature-flagged AI summarization with quality gates and fallback
import ldclient
from ldclient.config import Config
from ldclient import Context
ldclient.set_config(Config("sdk-key-production"))
client = ldclient.get()
def get_ai_summary(document, user_context):
"""Feature-flagged AI summarization with automatic quality gate."""
context = Context.builder(user_context["user_id"]) \
.set("plan", user_context["plan"]) \
.set("region", user_context["region"]) \
.build()
# Check if this user should get the new AI summary feature
if client.variation("ai-summary-v2", context, False):
try:
summary = call_new_ai_summary_endpoint(document)
# Quality gate: verify the summary meets minimum quality bar
quality_score = evaluate_summary_quality(summary, document)
if quality_score < 0.7:
# Track degraded quality as a metric
client.track("ai-summary-quality-degraded", context,
metric_value=quality_score)
# Fall back to old implementation
return call_legacy_summary_endpoint(document)
client.track("ai-summary-v2-success", context,
metric_value=quality_score)
return summary
except Exception as e:
client.track("ai-summary-v2-error", context)
return call_legacy_summary_endpoint(document)
else:
return call_legacy_summary_endpoint(document)
План поэтапного выпуска
Дисциплинированный выпуск проходит через шлюзы, каждый с конкретными сигналами качества:
# progressive-rollout-plan.yaml
feature: ai-summary-v2
rollout_stages:
- name: internal_dogfood
percentage: 0%
targeting: "email ends with @ourcompany.com"
duration: 3 days
quality_gates:
- error_rate < 1%
- p95_latency < 3s
- quality_score_avg > 0.8
rollback_trigger: "any gate fails for 15 minutes"
- name: beta_users
percentage: 5%
targeting: "plan == 'beta'"
duration: 5 days
quality_gates:
- error_rate < 0.5%
- p95_latency < 2.5s
- quality_score_avg > 0.82
- user_satisfaction_score > 4.0
rollback_trigger: "any gate fails for 30 minutes"
- name: gradual_rollout
percentage_stages: [10%, 25%, 50%, 75%, 100%]
advance_interval: 24 hours
quality_gates:
- error_rate < 0.3%
- p95_latency < 2s
- quality_score_avg > 0.85
- no_pager_incidents
rollback_trigger: "any gate fails for 1 hour OR pager fires"
- name: general_availability
percentage: 100%
cleanup: "remove feature flag, delete old code path"
Тестирование поведения feature-флагов
Feature-флаги сами по себе нуждаются в тестировании. Неправильно настроенный флаг может вызвать частичный сбой или несогласованный пользовательский опыт.
Модульное тестирование логики флагов
# test_feature_flags.py
import pytest
from unittest.mock import patch
class TestFeatureFlagBehavior:
def test_flag_on_uses_new_implementation(self, mock_ld_client):
"""When flag is ON, the new implementation should be used."""
mock_ld_client.variation.return_value = True
result = get_ai_summary("test document", {"user_id": "u1", "plan": "beta"})
assert result.source == "ai-summary-v2"
mock_ld_client.track.assert_called_with(
"ai-summary-v2-success", pytest.ANY, metric_value=pytest.ANY
)
def test_flag_off_uses_legacy_implementation(self, mock_ld_client):
"""When flag is OFF, the legacy implementation should be used."""
mock_ld_client.variation.return_value = False
result = get_ai_summary("test document", {"user_id": "u1", "plan": "free"})
assert result.source == "legacy-summary"
def test_flag_on_with_quality_degradation_falls_back(self, mock_ld_client):
"""When flag is ON but quality is poor, should fall back to legacy."""
mock_ld_client.variation.return_value = True
with patch("evaluate_summary_quality", return_value=0.4):
result = get_ai_summary("test document", {"user_id": "u1", "plan": "beta"})
assert result.source == "legacy-summary"
mock_ld_client.track.assert_called_with(
"ai-summary-quality-degraded", pytest.ANY, metric_value=0.4
)
def test_flag_on_with_exception_falls_back(self, mock_ld_client):
"""When flag is ON but the new implementation throws, should fall back."""
mock_ld_client.variation.return_value = True
with patch("call_new_ai_summary_endpoint", side_effect=TimeoutError):
result = get_ai_summary("test document", {"user_id": "u1", "plan": "beta"})
assert result.source == "legacy-summary"
mock_ld_client.track.assert_called_with("ai-summary-v2-error", pytest.ANY)
Интеграционное тестирование: оба пути
Каждый путь кода с feature-флагом должен быть покрыт интеграционными тестами:
@pytest.mark.parametrize("flag_state", [True, False])
def test_summary_endpoint_works_in_both_states(flag_state, test_client, mock_flags):
"""Both code paths must produce valid responses."""
mock_flags.set("ai-summary-v2", flag_state)
response = test_client.post("/api/summarize", json={"document": "Test content..."})
assert response.status_code == 200
assert "summary" in response.json()
assert len(response.json()["summary"]) > 0
Гигиена feature-флагов
Управление техническим долгом
Feature-флаги, оставшиеся в коде навсегда, становятся техническим долгом. Внедрите процесс очистки:
| Возраст флага | Действие |
|---|---|
| < 7 дней | Активный выпуск — оставить как есть |
| 7-30 дней | Должен быть на 100% или откачен |
| 30-90 дней | Запланировать задачу на очистку |
| > 90 дней | Флаг является техническим долгом — приоритизировать удаление |
Соглашения об именовании флагов
Последовательное именование делает флаги обнаруживаемыми и их назначение понятным:
{team}-{feature}-{version}
Examples:
checkout-ai-summary-v2
search-semantic-ranking-v1
onboarding-new-flow-q1-2026
Мониторинг воздействия feature-флагов
Каждый feature-флаг должен иметь связанные метрики, отвечающие на вопросы:
- Работает ли новый путь кода? (Доля ошибок по состоянию флага)
- Производителен ли он? (Задержка по состоянию флага)
- Лучше ли он для пользователей? (Бизнес-метрики по состоянию флага)
- Стабилен ли он? (Нет тренда деградации со временем)
# Prometheus metrics for feature flag monitoring
from prometheus_client import Counter, Histogram
flag_requests = Counter(
'feature_flag_requests_total',
'Total requests per feature flag state',
['flag_name', 'flag_state', 'outcome']
)
flag_latency = Histogram(
'feature_flag_latency_seconds',
'Latency by feature flag state',
['flag_name', 'flag_state'],
buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
)
Feature-флаги — это фундамент тестирования в продакшене. Они превращают деплой из рискового события в контролируемый эксперимент с измеримыми результатами качества.