Skip to main content

feature-flags

Use when adding feature flag support to a service, designing a percentage-based rollout, setting up A/B experiments or multivariate tests, choosing between LaunchDarkly, Unleash, and OpenFeature, writing tests for flag-gated code, or managing flag lifecycle and cleanup.

Informations de source

Dépôt
kid-sid/codex-spellbook
Dernière activité de la source
11 mai 2026 à 18:04
Langue détectée de SKILL.md
anglais
Étoiles
22
Forks
1

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
feature-flags
description
Use when adding feature flag support to a service, designing a percentage-based rollout, setting up A/B experiments or multivariate tests, choosing between LaunchDarkly, Unleash, and OpenFeature, writing tests for flag-gated code, or managing flag lifecycle and cleanup.
# Feature Flags & A/B Testing Tactical patterns for flag evaluation, progressive rollouts, controlled experiments, and flag lifecycle — the code-level companion to infrastructure-level canary deployments. ## When to Activate - Adding feature flag support to a new or existing service - Designing a percentage-based or ring-based rollout - Setting up A/B experiments or multivariate tests - Choosing between LaunchDarkly, Unleash, and OpenFeature - Implementing sticky bucketing or mutual exclusion across experiments - Writing unit or integration tests for flag-gated code paths - Auditing, cleaning up, or governing stale flags ## Flag Types & Evaluation Context Four flag types cover all use cases: | Type | SDK method | Use for | |---|---|---| | `boolean` | `getBooleanValue` | On/off gates, kill switches | | `string` | `getStringValue` | Variant selection (A/B/C), layout names | | `number` | `getNumberValue` | Numeric config: timeout, batch size, rate limit | | `object` | `getObjectValue` | Complex config blob, multi-key experiment payload | ### Evaluation Context The context is the set of attributes the flag platform uses to apply targeting rules. Always include a stable user identifier. ```typescript // TypeScript — OpenFeature import { OpenFeature, type EvaluationContext } from "@openfeature/server-sdk"; const ctx: EvaluationContext = { targetingKey: user.id, // required: stable, not session-scoped email: user.email, orgId: user.orgId, plan: user.plan, // "free" | "pro" | "enterprise" country: request.geo.country, appVersion: "2.4.1", betaTester: user.betaTester, // custom boolean attribute }; const client = OpenFeature.getClient("payments"); const enabled = await client.getBooleanValue("new-checkout", false, ctx); const variant = await client.getStringValue("checkout-layout", "control", ctx); const timeout = await client.getNumberValue("api-timeout-ms", 5000, ctx); const config = await client.getObjectValue("rate-limit-config", { rpm: 100 }, ctx); ``` ```python # Python — Unleash context = { "userId": str(user.id), "properties": { "orgId": str(user.org_id), "plan": user.plan, "country": request.geo.country, }, } enabled = client.is_enabled("new-checkout", context, fallback_function=lambda: False) variant = client.get_variant("checkout-layout", context) # variant = {"name": "control" | "v2-grid", "payload": {"type": "string", "value": "..."}} ``` ```go // Go — LaunchDarkly ctx := ldcontext.NewBuilder(user.ID). Kind("user"). SetString("email", user.Email). SetString("orgId", user.OrgID). SetString("plan", user.Plan). Build() enabled, _ := ldClient.BoolVariation("new-checkout", ctx, false) variant, _ := ldClient.StringVariation("checkout-layout", ctx, "control") timeout, _ := ldClient.IntVariation("api-timeout-ms", ctx, 5000) ``` ## SDK Setup Patterns ### LaunchDarkly ```python # Python import ldclient from ldclient.config import Config from datetime import timedelta ldclient.set_config(Config( sdk_key="sdk-your-key", stream=True, # streaming pushes changes; polling pulls on interval poll_interval=30, # seconds, only used when stream=False initial_reconnect_delay=timedelta(seconds=1), )) ld = ldclient.get() if not ld.is_initialized(): logger.warning("LaunchDarkly SDK not initialized — flag calls will return defaults") ``` ```go // Go import ( ld "github.com/launchdarkly/go-sdk/ldclient" ldconf "github.com/launchdarkly/go-sdk/ldconfig" ) config := ldconf.Config{ Events: ldconf.EventsConfig{Capacity: 10_000}, } ldClient, err := ld.MakeCustomClient("sdk-your-key", config, 5*time.Second) if err != nil { log.Warnf("LaunchDarkly init failed: %v — using fallback defaults", err) } defer ldClient.Close() ``` ### Unleash ```typescript // TypeScript import { initialize } from "unleash-client"; const unleash = initialize({ url: "https://unleash.example.com/api/", appName: "frontend-app", customHeaders: { Authorization: "Bearer <token>" }, refreshInterval: 15, // seconds between polls metricsInterval: 60, }); // Block until first sync so requests don't see stale state await new Promise<void>((resolve) => unleash.on("synchronized", resolve)); ``` ```python # Python from UnleashClient import UnleashClient client = UnleashClient( url="https://unleash.example.com/api", app_name="backend-service", custom_headers={"Authorization": "Bearer <token>"}, refresh_interval=15, metrics_interval=60, ) client.initialize_client() ``` ### OpenFeature — Provider Abstraction OpenFeature decouples evaluation code from the vendor SDK. Swap providers without touching call sites. ```typescript import { OpenFeature } from "@openfeature/server-sdk"; import { LaunchDarklyProvider } from "@openfeature/launchdarkly-provider"; // import { UnleashProvider } from "@openfeature/unleash-provider"; // Change this one line to migrate vendor: await OpenFeature.setProviderAndWait(new LaunchDarklyProvider("sdk-key")); const client = OpenFeature.getClient("payments"); // All getBooleanValue / getStringValue calls remain identical after the swap ``` ### Caching & Offline Fallbacks ```python # GOOD: evaluate once per request, pass result down async def handle_checkout(request): ctx = build_context(request.user) use_v2 = client.is_enabled("new-checkout", ctx) # one call return render_checkout(request, v2=use_v2) # BAD: evaluate inside a loop — one SDK call per item async def handle_cart(request): for item in request.cart.items: if client.is_enabled("new-pricing", ctx): # N calls item.price = calculate_new_price(item) ``` Always define safe defaults — the value returned when the SDK cannot reach the flag service: ```typescript // Default must be the safe/conservative behavior const enabled = await client.getBooleanValue("new-checkout", false, ctx); // ^^^^^ off by default const rateLimit = await client.getNumberValue("rpm-limit", 100, ctx); // ^^^ conservative default ``` ## Targeting & Rollout Patterns | Pattern | Use Case | Bucketing | Remove after | |---|---|---|---| | Dark launch | Code ships, invisible to users | — | Feature is stable | | Percentage rollout | Gradual ramp 1→5→25→100% | Sticky by user ID | 100% + 2-week soak | | Ring deployment | Internal → beta → enterprise → all | Group (org, plan) | All rings enabled | | Kill switch | Emergency disable without deploy | — | Keep indefinitely | | Permission flag | Role/plan-gated capability | Attribute exact match | Pricing restructure | | A/B experiment | Controlled hypothesis test | Sticky by user ID | Experiment concluded | ### Sticky Bucketing Sticky bucketing ensures a user always gets the same variant — across sessions, devices, and services. ``` # GOOD: stable user ID as bucketing key bucket = murmurhash(flag_key + user.id) % 100 # user 'alice' → always bucket 37 → always "treatment" # BAD: session ID as bucketing key bucket = murmurhash(flag_key + session_id) % 100 # 'alice' gets control on mobile, treatment on desktop, control after re-login ``` ### Multivariate Flags ```python variant = client.get_variant("checkout-layout", context) match variant["name"]: case "v2-grid": return render_grid_layout() case "v2-list": return render_list_layout() case _: return render_control() # always handle the default / unknown case ``` ### Mutual Exclusion Two experiments on the same surface contaminate each other's results. Use traffic partitioning: ``` Total traffic: 100% ├── Experiment A pool: 50% of users (checkout button color) └── Experiment B pool: 50% of users (checkout button size) No user is in both pools — interaction effects are eliminated. ``` Most flag platforms expose this as "experiment groups" or "mutex layers." For manual control, add an upstream assignment flag that routes users into pools before the experiment flags fire. ## A/B Testing & Experimentation ### Hypothesis Framing ``` Template: "If we [change], then [metric] will [direction] by [magnitude] for [audience] because [reason]." Good: "If we show inline address validation, then checkout completion will increase by 8% for mobile users because form errors are caught earlier." Bad: "The new checkout will be better." (no metric, no magnitude) Bad: "Conversion will improve." (no baseline, no MDE) ``` ### Sample Size Calculation ```python import math def min_sample_size(p_baseline: float, mde_absolute: float, alpha=0.05, power=0.80) -> int: """ Returns minimum users per variant. p_baseline: current conversion rate (e.g. 0.12 for 12%) mde_absolute: smallest effect worth detecting (e.g. 0.02 for +2pp) """ z_alpha = 1.96 # two-tailed, α=0.05 z_power = 0.84 # power=0.80 sigma = math.sqrt(p_baseline * (1 - p_baseline)) n = ((z_alpha + z_power) * sigma / mde_absolute) ** 2 return math.ceil(n) # Baseline: 12% conversion. Want to detect +2pp lift. n = min_sample_size(0.12, 0.02) # → ~4,100 per variant → ~8,200 total ``` **Rule of thumb by effect size (binary metric, 80% power, α=0.05):** | Relative MDE | Approx. n per variant | |---|---| | 10% | 1,500 | | 5% | 5,500 | | 2% | 34,000 | | 1% | 130,000 | ### Statistical Significance & the Peeking Problem ``` Target: p < 0.05 (two-tailed), power ≥ 0.80 Peeking problem: checking significance daily and stopping at first p < 0.05 inflates false positive rate from 5% to ~40% over 20 checks. Fix options: 1. Pre-register sample size → evaluate ONCE at that threshold (no peeking) 2. Sequential testing (SPRT) — designed for continuous monitoring 3. Bayesian testing — computes probability of being best, handles early stopping ``` ### Holdout Groups ```python HOLDOUT_PCT = 5 # 5% of users never see any experiment def in_holdout(user_id: str) -> bool: bucket = mmh3.hash(f"global-holdout:{user_id}", signed=False) % 100 return bucket < HOLDOUT_PCT # Check before any experiment assignment if in_holdout(user.id): serve_all_controls(user) else: assign_experiments(user) ``` Holdout groups measure the cumulative long-term effect of all experiments combined. Without one, you can't distinguish "we shipped 10 wins" from "we got lucky with 3 wins and 7 neutral experiments." ### Metrics Hierarchy ``` Primary: the one metric the experiment is designed to move → checkout_completion_rate Guardrail: metrics that must NOT degrade, regardless of primary movement → api_p95_latency ≤ 400ms, error_rate ≤ 0.5%, revenue_per_user Secondary: directional signals, inform future work, never the decision basis → add_to_cart_rate, session_duration ``` An experiment is a **win** only if: primary improved AND all guardrails held. A guardrail violation kills the experiment — even with a strong primary improvement. ### Metric Event Emission ```typescript // Emit metric events alongside flag evaluation const { value: inExperiment } = await client.getBooleanDetails( "checkout-v2-exp", false, ctx ); trackEvent("experiment.assigned", { experiment: "checkout-v2-exp", variant: inExperiment ? "treatment" : "control", userId: ctx.targetingKey, }); // On conversion — linked by userId to calculate per-variant rate trackEvent("checkout.completed", { userId: ctx.targetingKey, revenue: order.total }); ``` ## Observability for Flags ### Structured Logging ```python # Python — log every flag evaluation import structlog, time log = structlog.get_logger() def evaluate_flag(key: str, ctx: dict, default: bool) -> bool: t0 = time.monotonic() result = client.is_enabled(key, ctx, fallback_function=lambda: default) log.info( "flag.evaluated", flag_key=key, variant=str(result), user_id=ctx.get("userId"), org_id=ctx.get("properties", {}).get("orgId"), duration_ms=round((time.monotonic() - t0) * 1000, 2), ) return result ``` ```typescript // TypeScript — include evaluation reason from OpenFeature const details = await client.getBooleanDetails("new-checkout", false, ctx); logger.info("flag.evaluated", { flag_key: "new-checkout", variant: String(details.value), reason: details.reason, // TARGETING_MATCH | DEFAULT | STATIC | ERROR user_id: ctx.targetingKey, duration_ms: performance.now() - start, }); ``` ### Metrics ``` # Prometheus counters / histograms flag_evaluations_total{flag, variant, reason} flag_evaluation_duration_seconds{flag, quantile} flag_sdk_errors_total{flag, error_type} stale_flags_count{flag, days_since_last_change} # from audit log ``` ### Tracing Integration ```python # OpenTelemetry — attach flag decision to the active span from opentelemetry import trace
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub