feature-flag-experiment-validator
Validates the statistical significance of an A/B / feature-flag experiment result - computes per-metric effect size + p-value (chi-square for proportions, Welch's t-test for continuous metrics), applies a multiple-comparison correction (Bonferroni / Benjamini-Hochberg) when N>1 metric, surfaces practical-vs-statistical-significance distinction, and emits a ship/don't-ship verdict per metric. Use when an experiment has finished and someone is about to ship the winning variant off a dashboard readout, when a result rests on a small sample, or when more than one metric was compared - the rigorous version of "the variant looks better in the dashboard."
Install with skills.sh (any agent)
npx skills add testland/qa --skill feature-flag-experiment-validatorfeature-flag-experiment-validator
Overview
An experiment toggle A/B test routes each user into a cohort, measures behavior per cohort, and ships the winning variant. A/B tests are "sensitive to variance; they require a large sample size in order to reduce standard error and produce a statistically significant result" (ab-test-wiki (opens in new window); per-cohort toggle routing per feature-toggles (opens in new window)). Without proper analysis, teams ship variants that "look better" but aren't actually better. This skill validates the analysis.
When to use
Step 1 - Inputs
The validator needs, per variant:
# experiment-data.yml
experiment_id: checkout-promo-banner-v2
running_since: 2026-04-15
running_until: 2026-05-05 # 21 days
hypothesis: "Promo banner increases checkout completion."
variants:
- name: control
cohort_size: 12450
metrics:
checkout_completion_count: 8523
avg_session_duration_sec: [...samples...] # raw samples for continuous metrics
avg_revenue_per_user: [...samples...]
- name: treatment_a
cohort_size: 12380
metrics:
checkout_completion_count: 8755
avg_session_duration_sec: [...samples...]
avg_revenue_per_user: [...samples...]Per-metric, identify whether it's:
Different statistical tests apply.
Step 2 - Test per metric type
Proportions: chi-square or Fisher's exact
For "did the user convert? yes/no":
from scipy.stats import chi2_contingency
def proportion_test(c_success, c_total, t_success, t_total):
"""Returns (p_value, effect_size_pct)."""
table = [[c_success, c_total - c_success],
[t_success, t_total - t_success]]
chi2, p, dof, _ = chi2_contingency(table)
c_rate = c_success / c_total
t_rate = t_success / t_total
effect = (t_rate - c_rate) / c_rate * 100 # relative lift in %
return p, effectFor very small cells (< 5 expected per cell), Fisher's exact is more accurate; chi-square otherwise.
Continuous: Welch's t-test or Mann-Whitney U
For "what's the average revenue per user?":
from scipy.stats import ttest_ind, mannwhitneyu
def continuous_test(c_samples, t_samples, parametric=True):
"""Welch's t-test (parametric) or Mann-Whitney U (non-parametric)."""
if parametric:
t, p = ttest_ind(c_samples, t_samples, equal_var=False)
else:
u, p = mannwhitneyu(c_samples, t_samples, alternative='two-sided')
c_mean = sum(c_samples) / len(c_samples)
t_mean = sum(t_samples) / len(t_samples)
effect = (t_mean - c_mean) / c_mean * 100
return p, effectUse Mann-Whitney U when the metric isn't normally distributed (revenue per user - heavy right tail; latency - log-normal). Welch's t-test for approximately-normal metrics.
Step 3 - Multiple-comparisons correction
Per ab-test-wiki (opens in new window)'s "challenges" framing: testing many metrics inflates the false-positive rate. With α=0.05 and 10 independent metrics, P(at least one false positive) ≈ 1 - 0.95^10 = 40%.
Default: Benjamini-Hochberg FDR control - balances false-positive vs false-negative rates; controls the proportion of "wins" that are actually noise. Use Bonferroni when the cost of any false positive is catastrophic (regulatory / safety contexts) and over-conservatism is acceptable.
Benjamini-Hochberg (FDR control)
from statsmodels.stats.multitest import multipletests
reject, p_adj, _, _ = multipletests(p_values, alpha=0.05, method='fdr_bh')
# `reject[i]` is True when metric i is significant after FDR control.Bonferroni (escape hatch - conservative)
adjusted_alpha = alpha / n_metrics # e.g. 0.05 / 10 = 0.005
# Each metric must have p < 0.005 to be significant.Over-conservative - increases false negatives.
For pre-registered single-primary-metric experiments, no correction needed for the primary; correction applies to secondary metrics.
Step 4 - Power analysis (was the experiment big enough?)
A non-significant result might mean "no effect" or "experiment too small." Compute post-hoc power:
from statsmodels.stats.power import NormalIndPower
def required_sample(effect_size, alpha=0.05, power=0.8):
"""How many users per variant to detect this effect with this power?"""
analysis = NormalIndPower()
return analysis.solve_power(effect_size=effect_size, alpha=alpha, power=power)If the observed effect (e.g., 0.5% relative lift) requires N=50,000 users per variant for 80% power and the experiment had N=12,000, the experiment was under-powered. The verdict shouldn't be "no effect"; it should be "inconclusive - re-run at higher N or accept that we can't detect effects this small."
Step 5 - Practical vs statistical significance
A 0.1% lift can be statistically significant at N=10M; that doesn't mean the team should ship.
Define minimum detectable effect (MDE) per metric:
# mde.yml
checkout_completion_rate:
mde_relative: 1.0 # 1% relative lift to be worth shipping
mde_absolute: 0.5 # OR a 0.5pp absolute lift
avg_revenue_per_user:
mde_absolute: 0.50 # $0.50/user; below this, ship cost > revenueThe verdict requires both statistical significance AND practical significance (effect ≥ MDE).
Step 6 - Output
Emit a per-metric results table (type, control, treatment, relative effect, raw and adjusted p-value, MDE met, verdict), then a verdict explanation, a ship/pause recommendation, and a power-analysis note. Full worked report: references/output-example.md.
Step 7 - Recommended cadence
Validate the experiment:
If continuous monitoring is required (e.g. a regression-detection A/B test), use a sequential testing framework (statsmodels' sequential probability ratio test) instead of repeated significance tests.
Anti-patterns
Seven analysis mistakes and their fixes: references/anti-patterns.md.
Limitations
References
Anti-patterns - feature-flag-experiment-validator
View source (opens in new window)Anti-patterns - feature-flag-experiment-validator
Each row maps a common analysis mistake to the step that prevents it.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Peeking and stopping at first significance | Inflates false-positive rate dramatically. | Pre-register stop date OR use sequential testing (Step 7). |
| Single metric only | Misses regressions in secondary metrics (revenue down even though completion up). | 5-10 metrics including guardrails (Step 1). |
| No multiple-comparisons correction | 10 metrics × α=0.05 = 40% chance of false positive somewhere. | FDR / Bonferroni (Step 3). |
| Ship based on practical significance without statistical | Random variance gets shipped as "lift." | Both required (Step 5). |
| Ship based on statistical significance without practical | 0.1% lift at N=10M ships; not worth maintenance burden. | MDE per metric (Step 5). |
| Welch's t-test on heavy-tailed metrics (revenue) | Test invalid; conclusion wrong. | Mann-Whitney U for non-normal metrics (Step 2). |
| Ignoring guardrail metrics (support tickets, churn, refund rate) | Ship something that breaks downstream. | Always include guardrails (Step 6 example). |
Worked output example - experiment validation
View source (opens in new window)Worked output example - experiment validation
The report this skill emits, for a multi-metric experiment where the primary metric wins but a guardrail metric regresses.
## Experiment validation - `checkout-promo-banner-v2`
**Run period:** 2026-04-15 to 2026-05-05 (21 days)
**Hypothesis:** Promo banner increases checkout completion.
**Variants:** control (12,450 users), treatment_a (12,380 users)
**Multiple-comparisons correction:** Benjamini-Hochberg FDR, α=0.05
**Verdict:** ⚠ MIXED - primary metric significant; secondary regressed.
### Per-metric results
| Metric | Type | Control | Treatment | Effect (rel) | p-value (raw) | p-value (adj) | MDE met? | Verdict |
|---------------------------------|-------------|---------|-----------|--------------|--------------:|--------------:|----------|---------|
| **checkout_completion_rate** | proportion | 68.5% | 70.7% | +3.2% | 0.012 | 0.024 | ✅ (>1%) | ✅ ship |
| avg_session_duration_sec | continuous | 245 | 238 | -2.9% | 0.18 | 0.36 | n/a | ─ no signal |
| avg_revenue_per_user | continuous | $4.21 | $3.98 | -5.5% | 0.044 | 0.088 | ⚠ | ⚠ trend; not significant after FDR |
| signup_rate | proportion | 4.2% | 4.3% | +2.4% | 0.61 | 0.61 | no | ─ no signal |
| support_tickets_per_user | continuous | 0.12 | 0.14 | +16.7% | 0.008 | 0.024 | ✅ | ⚠ ship-blocker - investigate |
### Verdict explanation
The primary metric (checkout completion) shows a 3.2% relative lift
that's statistically significant after FDR correction (p_adj=0.024)
and meets the MDE (>1%). On its own, this is a ship signal.
However:
- support_tickets_per_user shows a +16.7% relative increase
(p_adj=0.024; significant). This is a ship-blocker; investigate
what about the promo banner is causing more tickets.
- avg_revenue_per_user trends down (-5.5%) but isn't significant
after correction (p_adj=0.088). Cautionary signal; investigate
whether the lift in completion comes at the cost of basket size.
### Recommendation
PAUSE the ship. Investigate:
1. Why support tickets increased (categorize the new tickets;
identify the issue type).
2. Whether revenue per user is genuinely down or artifact of
variance.
If both are addressed, re-run for additional 7 days to validate.
### Power analysis
The experiment had sufficient power (>80%) to detect a 1% relative
lift on the primary metric. For revenue (-5.5% observed but not
significant): would need ~22,000 users per variant for 80% power;
current 12,400 is under-powered.Related skills
prod-canary-validator
Builds a canary-validation workflow that compares a canary deploy's metrics against the baseline (current main) - picks the metric set (error rate, p50/p95/p99 latency, business KPIs like checkout-completion), defines per-metric thresholds (absolute + relative-to-baseline), runs a statistical-comparison check (effect size + significance) over the canary's observation window, and emits a promote/rollback verdict. Use as the gate between canary deploy and full rollout - the deterministic version of "the on-call eyeballs the dashboard for 30 min.
release-runbook-author
Turns one service's release into a written six-phase runbook: pre-flight checks, a smoke gate, a canary observation window, a named human promote gate, progressive rollout, and post-release verification. Fixes each phase's pass criteria as a delta against a recorded baseline rather than a bare absolute number, gives canary and rollout separate windows and separate thresholds, and emits a per-phase evidence table that becomes the release record. The multi-team cutover-sequence procedure - dependency-ordered gates with one named owner each, hard timeboxes, written rollback triggers, and the reverse-order rollback path - is worked in references for windows where several teams cut over interdependent services. Use when a single service is about to ship and its release steps exist only as tribal knowledge or a chat thread, or when a shared release window needs its cutover order, gate owners, and rollback path written down.
synthetic-monitor-author
Drafts a synthetic monitor configuration for one critical user journey - picks the platform (Datadog Synthetics, Pingdom, Checkly, New Relic, etc.), authors the scripted-transaction body (Playwright-style for browser checks; HTTP-step for API checks), wires the cadence (typical 1-15 min), defines per-step assertions (DOM presence, API status, response shape) and aggregate alert thresholds (consecutive-failure count + on-call routing). Includes the RUM-coverage gap method for deciding which journeys to monitor: score real-user journeys from RUM / CrUX data by session volume times business value, diff against the existing monitor inventory, and emit a ranked gap list. Use when a critical journey needs continuous-in-production verification per ISTQB-canonical shift-right ("a test approach to test a system continuously in production"), or when synthetic coverage was never systematically derived from real usage data.