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
cutover-sequence-author
Sequences a multi-team release cutover into dependency-ordered gates: builds the cross-service dependency graph, converts it into a numbered gate list where every gate carries exactly one named owner, a hard timebox, and a written rollback trigger, then derives the reverse-order rollback path and the window hard-stop rule. Emits one cutover plan document with an authority table and a runtime log. Use when two or more teams must cut over interdependent services inside one shared release window and nobody has yet written down the order, who calls each gate, or what reverses it.
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. Use when a single service is about to ship and its release steps exist only as tribal knowledge or a chat thread, so nobody can say in advance what evidence promotes it, what evidence halts it, or who decides.
rum-to-synthetic-gap-analyzer
Reads Real User Monitoring data (Datadog RUM, Sentry Performance, GA4 Core Web Vitals / CrUX) to identify high-traffic user journeys that have no synthetic monitor coverage: ranks journeys by session volume times business value, diffs the ranked list against existing synthetic monitors, and emits a prioritized gap list ready to feed into synthetic-monitor-author. Use when an observability stack has RUM instrumented but the team suspects synthetic coverage is sparse, biased toward low-traffic paths, or was never systematically derived from real usage data.
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). 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").