
Релиз — не обязан быть «всё или ничего». Фича‑флаги позволяют:
Это экономит деньги на откатах, снижает время простоя и ускоряет проверку гипотез.
Минимальная схема:
Типы флагов:
Где хранить:
Ниже — рабочий пример: лёгкий сервис, который оценивает флаги. Он поддерживает булевые, процентные и многовариантные флаги, сегменты и kill switch. Для простоты — хранение в памяти с REST‑управлением. В проде замените на БД/Redis, принцип остаётся тем же.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install "fastapi>=0.115" "uvicorn[standard]>=0.23" "pydantic>=2.0"
uvicorn main:app --reload
# main.py
from typing import List, Dict, Optional
from fastapi import FastAPI, HTTPException, Header
from pydantic import BaseModel, Field
import hashlib
import os
app = FastAPI(title="Feature Flags Service", version="1.0.0")
ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", "dev")
class UserContext(BaseModel):
id: str = Field(..., description="Уникальный идентификатор пользователя")
country: Optional[str] = None
plan: Optional[str] = None
class Variant(BaseModel):
name: str
weight: float = Field(..., ge=0.0, le=100.0)
class Segment(BaseModel):
# Пример: {"country": ["RU", "KZ"], "plan": ["pro"]}
attributes: Dict[str, List[str]]
class FlagDefinition(BaseModel):
key: str
enabled: bool = False
rollout_percentage: float = Field(0.0, ge=0.0, le=100.0)
salt: str = Field(..., description="Соль для детерминированного распределения")
variants: Optional[List[Variant]] = None
segments: Optional[List[Segment]] = None
kill_switch: bool = False
class EvaluateRequest(BaseModel):
flags: List[str]
user: UserContext
class EvaluateResult(BaseModel):
on: bool
variant: Optional[str] = None
reason: Optional[str] = None
# Простейшее хранилище в памяти. В проде: БД + кеш.
FLAGS: Dict[str, FlagDefinition] = {
"new_checkout": FlagDefinition(
key="new_checkout",
enabled=True,
rollout_percentage=10.0,
salt="checkout-v1",
segments=[Segment(attributes={"country": ["RU", "KZ"], "plan": ["pro", "business"]})],
variants=None,
kill_switch=False,
),
"price_test": FlagDefinition(
key="price_test",
enabled=True,
rollout_percentage=100.0,
salt="pricing-2024-12",
variants=[
Variant(name="off", weight=50.0),
Variant(name="v1", weight=25.0),
Variant(name="v2", weight=25.0),
],
segments=None,
kill_switch=False,
),
}
# Детерминированное распределение по бакетам 0..100.
# При одинаковых (salt, user.id) выдаёт одну и ту же долю.
def bucket_percent(seed: str, salt: str) -> float:
h = hashlib.sha256((salt + ":" + seed).encode("utf-8")).hexdigest()
# Берём 32 бита из начала хэша, нормируем в проценты 0..100.
n = int(h[:8], 16)
return (n / 0xFFFFFFFF) * 100.0
def matches_segments(user: UserContext, segments: Optional[List[Segment]]) -> bool:
if not segments:
return True
for seg in segments:
ok = True
for attr, allowed in seg.attributes.items():
val = getattr(user, attr, None)
if val is None or val not in allowed:
ok = False
break
if ok:
return True
return False
def choose_variant(variants: List[Variant], p: float) -> str:
# p — процент 0..100; идём по кумулятивной сумме весов
total = 0.0
for v in variants:
total += v.weight
if p < total:
return v.name
# На случай неточной суммы весов — последний вариант
return variants[-1].name
def evaluate_flag(flag: FlagDefinition, user: UserContext) -> EvaluateResult:
if flag.kill_switch:
return EvaluateResult(on=False, reason="kill_switch")
if not flag.enabled:
return EvaluateResult(on=False, reason="disabled")
if not matches_segments(user, flag.segments):
return EvaluateResult(on=False, reason="segment_miss")
p = bucket_percent(user.id, flag.salt)
if flag.variants and len(flag.variants) > 0:
chosen = choose_variant(flag.variants, p)
if chosen == "off":
return EvaluateResult(on=False, variant=None, reason="variant_off")
return EvaluateResult(on=True, variant=chosen, reason="variant")
# Булевый/процентный
if p < flag.rollout_percentage:
return EvaluateResult(on=True, reason="percent_rollout")
return EvaluateResult(on=False, reason="percent_rollout")
@app.get("/flags", response_model=List[FlagDefinition])
async def list_flags():
return list(FLAGS.values())
@app.put("/flags/{key}", response_model=FlagDefinition)
async def upsert_flag(key: str, flag: FlagDefinition, x_admin_token: str = Header("")):
if x_admin_token != ADMIN_TOKEN:
raise HTTPException(status_code=403, detail="forbidden")
if key != flag.key:
raise HTTPException(status_code=400, detail="key mismatch")
FLAGS[key] = flag
return flag
@app.post("/evaluate", response_model=Dict[str, EvaluateResult])
async def evaluate(req: EvaluateRequest):
result: Dict[str, EvaluateResult] = {}
for k in req.flags:
flag = FLAGS.get(k)
if not flag:
result[k] = EvaluateResult(on=False, reason="not_found")
continue
result[k] = evaluate_flag(flag, req.user)
return result
Примеры запросов:
# Получить список флагов
curl -s http://localhost:8000/flags | jq .
# Оценить флаг для пользователя
curl -s -X POST http://localhost:8000/evaluate \
-H 'Content-Type: application/json' \
-d '{
"flags": ["new_checkout", "price_test"],
"user": {"id": "user_123", "country": "RU", "plan": "pro"}
}' | jq .
# Обновить флаг (админ)
curl -s -X PUT http://localhost:8000/flags/new_checkout \
-H 'Content-Type: application/json' \
-H 'X-Admin-Token: dev' \
-d '{
"key": "new_checkout",
"enabled": true,
"rollout_percentage": 25,
"salt": "checkout-v1",
"segments": [{"attributes": {"country": ["RU"], "plan": ["pro", "business"]}}],
"variants": null,
"kill_switch": false
}' | jq .
Пошаговый выкат:
A/B‑тест:
Анти‑паттерны:
Что собирать:
Быстрый старт без сложных систем:
CREATE TABLE feature_flags (
key text PRIMARY KEY,
enabled boolean NOT NULL DEFAULT false,
rollout_percentage real NOT NULL DEFAULT 0,
salt text NOT NULL,
variants jsonb, -- [{"name": "off", "weight": 50}, ...]
segments jsonb, -- [{"attributes": {"country": ["RU"], "plan": ["pro"]}}]
kill_switch boolean NOT NULL DEFAULT false,
updated_at timestamptz NOT NULL DEFAULT now(),
updated_by text NOT NULL DEFAULT 'system'
);
Если поверх использовать Redis‑кеш и pub/sub для моментальной доставки обновлений в приложения — вы получите почти «вендорский» опыт без подписки.
Итог: фича‑флаги — простой инструмент, который быстро окупается. Он снижает риски выката, ускоряет эксперименты и делает продукт управляемым — без бесконечных откатов и ночных релизов.