<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Sert</title>
    <description>The latest articles on DEV Community by David Sert (@david_sert).</description>
    <link>https://dev.to/david_sert</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4032860%2F1f02fb3e-9524-4845-b086-ffe7d7d0f77b.jpg</url>
      <title>DEV Community: David Sert</title>
      <link>https://dev.to/david_sert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/david_sert"/>
    <language>en</language>
    <item>
      <title>Building a Test-and-Learn Framework for Experimentation Teams</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Fri, 21 Aug 2026 05:06:02 +0000</pubDate>
      <link>https://dev.to/david_sert/building-a-test-and-learn-framework-for-experimentation-teams-3njo</link>
      <guid>https://dev.to/david_sert/building-a-test-and-learn-framework-for-experimentation-teams-3njo</guid>
      <description>&lt;p&gt;A "test and learn" culture is easy to declare in a strategy deck and hard to build in practice. Most teams that say it out loud already run some experiments — the gap is not activity, it's a framework that turns individual test results into a compounding body of knowledge instead of a pile of one-off wins and losses nobody revisits. This guide lays out what a test-and-learn framework actually consists of, how it differs from simply "running more tests," and how to wire it into the tools most experimentation teams already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a test-and-learn framework is not
&lt;/h2&gt;

&lt;p&gt;It is not a synonym for experiment velocity. A team that ships twenty tests a quarter but never looks back at test twelve when planning test twenty-one is not running a test-and-learn framework — it is running twenty disconnected trials. Velocity is an input to learning, not the same thing as it.&lt;/p&gt;

&lt;p&gt;It is also not a tagline for "we're data-driven." That phrase describes a value, not a process. A framework has to specify who reviews a result, where the learning is recorded, how it gets surfaced to the next person planning a related test, and what happens when a result contradicts something the team believed six months ago. Without those mechanics, "test and learn" is aspirational language sitting on top of the same ad hoc process it was meant to replace.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three loops a real framework closes
&lt;/h2&gt;

&lt;p&gt;A working test-and-learn framework closes three separate feedback loops, and most teams that stall out have built only the first one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The per-test loop.&lt;/strong&gt; Hypothesis, launch, result, decision. This is the loop most teams already have, because it's the one a testing platform's dashboard makes visible by default: did the metric move, was it significant, do we ship it. It answers "was this specific idea right," and stops there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The knowledge loop.&lt;/strong&gt; This is the loop that's usually missing. A single test result becomes a durable, searchable finding — "urgency messaging on the PDP increased add-to-cart by 6% for mobile visitors, no effect on desktop" — that gets tagged, stored somewhere other than the testing tool's own history, and surfaces automatically when someone drafts a related hypothesis later. Without this loop, the same idea gets re-tested by a different team eighteen months later because nobody remembers, or could find, that it was already answered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The prioritization loop.&lt;/strong&gt; Findings from the knowledge loop should change what gets scored highly in your next planning cycle. If urgency messaging worked on mobile PDP, that's a signal the mechanism generalizes — it should raise the confidence score of related ideas in your &lt;a href="https://optipilot.com/strategy/how-to-prioritize-ab-tests" rel="noopener noreferrer"&gt;prioritization framework&lt;/a&gt;, not just sit in an archive. A framework where confidence scores never update based on accumulated evidence is not actually learning from its own history.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Per-test loop:        hypothesis -&amp;gt; launch -&amp;gt; result -&amp;gt; ship/kill decision
Knowledge loop:        result -&amp;gt; tagged finding -&amp;gt; searchable archive -&amp;gt; surfaces on related hypotheses
Prioritization loop:   accumulated findings -&amp;gt; updated confidence scores -&amp;gt; next cycle's ranking
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most teams have built the first loop well and the other two barely at all. That imbalance is usually why "we've been testing for two years and don't feel any smarter about our users" is a common complaint even at teams with healthy experiment counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where wins and losses both have to count
&lt;/h2&gt;

&lt;p&gt;A framework that only stores winning tests is building a biased archive, and it will eventually mislead the team that relies on it. A losing test — the urgency messaging that moved nothing — is exactly as valuable a finding as a winning one, because it rules out a mechanism the team might otherwise retry with a different label six months later.&lt;/p&gt;

&lt;p&gt;The discipline this requires is uncomfortable in a way that pure velocity is not: it means writing up the null results with the same rigor as the wins, including what confidence interval the null result actually had — a wide interval spanning zero means "inconclusive, worth retesting with more power," while a narrow interval spanning zero means "the mechanism doesn't work, stop proposing variants of it." Conflating those two kinds of null result is one of the most common ways a knowledge archive quietly degrades into noise; a genuinely closed loop keeps the distinction visible in the finding itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the framework lives operationally
&lt;/h2&gt;

&lt;p&gt;The knowledge loop fails most often not because nobody wants to write things down, but because the writing happens somewhere nobody revisits — a slide in a quarterly deck, a Slack thread, a spreadsheet tab that gets abandoned after the person who made it changes teams.&lt;/p&gt;

&lt;p&gt;If your experimentation platform supports it, the finding belongs as close to the test record itself as possible. Optimizely Feature Experimentation's native &lt;strong&gt;Hypothesis&lt;/strong&gt; and &lt;strong&gt;Test plan&lt;/strong&gt; fields on a rule are one option — they keep the original claim and the eventual result in the same place a future team member will actually look when they're deciding whether to relaunch something similar, rather than in a document they'd need to know exists. See the &lt;a href="https://optipilot.com/strategy/ab-testing-hypothesis-template-optimizely" rel="noopener noreferrer"&gt;hypothesis template guide&lt;/a&gt; for how to structure the claim itself so the eventual finding is easy to compare against it.&lt;/p&gt;

&lt;p&gt;Whatever storage you use, the test for whether it's actually part of the framework — rather than a compliance step nobody reads — is simple: when someone drafts a new hypothesis, does anything surface the related prior finding automatically, or does it depend on that person remembering to go looking? If it depends on memory, the knowledge loop isn't closed yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the review cadence
&lt;/h2&gt;

&lt;p&gt;The prioritization loop needs a recurring point where accumulated findings actually change scores, not just a backlog grooming meeting that re-ranks the same list by gut feel. A simple, sustainable cadence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;After every test&lt;/strong&gt;, the finding gets tagged with the mechanism it tested (not just the metric it moved) and stored where the next planner will see it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Before every prioritization cycle&lt;/strong&gt;, whoever scores confidence on new ideas is expected to check for related prior findings first — not from memory, but by searching the archive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quarterly&lt;/strong&gt;, the team reviews which mechanisms have accumulated multiple confirming or disconfirming results, and retires ideas that keep failing the same way across different surfaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This cadence is intentionally light. A test-and-learn framework that requires a heavyweight ritual to maintain will get skipped the first time the team is busy, and a skipped framework teaches the same lesson as no framework at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measuring the framework by test count alone.&lt;/strong&gt; A high-velocity team with no knowledge loop is not learning faster than a slower team that closes the loop — it's just accumulating more untracked history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archiving only wins.&lt;/strong&gt; A knowledge base without documented losses will eventually recommend re-testing ideas that were already ruled out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storing findings somewhere disconnected from where hypotheses get written.&lt;/strong&gt; If the archive requires a separate lookup step nobody remembers to take, it will not get used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Letting confidence scores stay static regardless of accumulated evidence.&lt;/strong&gt; If a mechanism has been confirmed three times across different surfaces, related ideas should score higher in prioritization than they did before any of that evidence existed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A test-and-learn framework is not the same as experiment velocity — it requires a knowledge loop and a prioritization loop on top of the basic per-test loop most teams already have.&lt;/li&gt;
&lt;li&gt;Null results need the same documentation discipline as wins, including whether the confidence interval was wide (inconclusive) or narrow (the mechanism doesn't work).&lt;/li&gt;
&lt;li&gt;Findings belong as close to the original hypothesis as operationally possible, so the next planner encounters them without having to remember to look.&lt;/li&gt;
&lt;li&gt;The real test of a closed loop is whether accumulated findings change prioritization scores over time, not whether a slide exists documenting past results.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How is a test-and-learn framework different from just running more A/B tests?
&lt;/h3&gt;

&lt;p&gt;Running more tests closes only the per-test loop — hypothesis to result to ship/kill decision. A framework additionally closes a knowledge loop, where each result becomes a durable, searchable finding, and a prioritization loop, where accumulated findings change how future ideas get scored.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should losing tests be documented as thoroughly as winning tests?
&lt;/h3&gt;

&lt;p&gt;Yes, and with more care about the confidence interval specifically. A win is straightforward to record; a loss needs to distinguish between "inconclusive — the interval was wide" and "the mechanism doesn't work — the interval was narrow around zero," because those two outcomes should lead to different next steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where should test findings be stored so they actually get reused?
&lt;/h3&gt;

&lt;p&gt;As close to the point where the next hypothesis gets written as possible. If your platform supports native hypothesis and result fields on the experiment record itself, that beats a separate document, because it removes the step of remembering a separate archive exists.&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>productmanagement</category>
      <category>experimentation</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Post Hoc Power Analysis: Why Recalculating Power After a Null Result Fails</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Fri, 21 Aug 2026 05:05:26 +0000</pubDate>
      <link>https://dev.to/david_sert/post-hoc-power-analysis-why-recalculating-power-after-a-null-result-fails-1p50</link>
      <guid>https://dev.to/david_sert/post-hoc-power-analysis-why-recalculating-power-after-a-null-result-fails-1p50</guid>
      <description>&lt;p&gt;An A/B test comes back inconclusive — no significant difference between variation and control — and someone on the team asks a reasonable-sounding question: "was the test even powered enough to detect a real effect?" The instinct is to run a power calculation using the effect size the test actually observed, plug it back into a power formula, and report the number. That calculation has a name, post hoc power analysis, and a well-documented problem: it is mathematically redundant with the p-value you already have, and treating it as new evidence is one of the most common ways a null result gets quietly reinterpreted as "probably a win we couldn't detect." This guide covers why the calculation doesn't do what it appears to do, what to check instead, and how to size a test correctly the first time so you never need the retrospective version.&lt;/p&gt;

&lt;h2&gt;
  
  
  What post hoc power analysis actually calculates
&lt;/h2&gt;

&lt;p&gt;Prospective power — the kind you calculate before a test launches — asks: "if the true effect is X, and I collect N visitors, what is the probability I detect it as significant?" That calculation depends on three things you choose in advance: the minimum effect worth detecting, your sample size, and your significance threshold. It is a planning tool, and it is exactly what a &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;sample size calculation&lt;/a&gt; is for.&lt;/p&gt;

&lt;p&gt;Post hoc power analysis takes the same formula and swaps one input: instead of the minimum detectable effect you decided was worth chasing, it plugs in the effect size the experiment actually observed. Then it reports the probability that a test of this size would have detected an effect of &lt;em&gt;this exact magnitude&lt;/em&gt; — the one already sitting in your results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the observed-effect version is circular
&lt;/h2&gt;

&lt;p&gt;The problem is not the arithmetic; it is what the output means. Prospective power tells you something you did not already know — whether your planned sample size is big enough for an effect you care about. Post hoc power computed from the observed effect tells you nothing you cannot already read off the p-value, because the two are mathematically tied together: for a fixed sample size, post hoc power is a strictly decreasing function of the p-value. A high p-value (a null result) will always produce a low post hoc power calculation, and a low p-value (a significant result) will always produce a high one. The calculation cannot come back and say "actually, this null result had high power" — that combination is not mathematically possible.&lt;/p&gt;

&lt;p&gt;That means a stakeholder who asks for post hoc power on a null result and receives a low number has learned nothing beyond "the result was not significant," restated in a more technical-sounding unit. The number feels like independent confirmation that "we just needed more data," but it is an algebraic reflection of the p-value you already had, not a second measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retrospective-power fallacy in practice
&lt;/h2&gt;

&lt;p&gt;The fallacy shows up in a specific, recognizable pattern: a test ends without significance, the team is reluctant to call the idea a loss, and a post hoc power calculation comes back low — say, 40%. That 40% gets reported as "the test was underpowered," which sounds like a design flaw rather than a null result, and the conclusion drawn is "the effect is probably real, we just need to run it longer or on more traffic."&lt;/p&gt;

&lt;p&gt;That conclusion does not follow from the number. A low post hoc power figure is a restatement of "not significant," and it carries no information about whether the true effect exists at all. The actual data available for that judgment is the confidence interval around the observed effect, not a recomputed power figure — and the confidence interval is the tool that should be doing this work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check instead of retrospective power
&lt;/h2&gt;

&lt;p&gt;If a null result raises the question of whether the test just needed more data, the honest way to answer it is to look at the confidence interval around the effect estimate, not to recompute power.&lt;/p&gt;

&lt;p&gt;A wide confidence interval that spans zero — say, a 95% CI of −1.5% to +4.0% on relative lift — genuinely is consistent with "we don't know yet, and a bigger sample might resolve it." That is real information: the interval is telling you the experiment did not have enough precision to distinguish a meaningful effect from no effect. This is functionally the same conclusion a well-designed prospective power calculation would have flagged before you ever launched, which is why sizing the test properly the first time — using an honestly chosen &lt;a href="https://optipilot.com/data/minimum-detectable-effect" rel="noopener noreferrer"&gt;minimum detectable effect&lt;/a&gt; — avoids the whole situation.&lt;/p&gt;

&lt;p&gt;A narrow confidence interval that spans zero — say, −0.3% to +0.4% — tells a different story: the test had plenty of precision, and it precisely located the effect at approximately zero. Rerunning that test with more traffic will not turn up a real effect, because the data already ruled out anything but a trivial one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Wide CI spanning zero:    [-1.5%, +4.0%]  -&amp;gt; underpowered for this effect size; more data could resolve it
Narrow CI spanning zero:  [-0.3%, +0.4%]  -&amp;gt; well-powered; the effect is precisely estimated near zero
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both examples are "not significant." Only the confidence interval tells you which kind of "not significant" you are looking at, and that distinction is the actual answer to the question post hoc power analysis is being asked to provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this connects to peeking and effect size
&lt;/h2&gt;

&lt;p&gt;Post hoc power analysis is a close cousin of two other retrospective mistakes covered elsewhere on this site. &lt;a href="https://optipilot.com/data/effect-size-ab-testing" rel="noopener noreferrer"&gt;Effect size reporting&lt;/a&gt; already explains why comparing the observed lift against your original MDE — rather than recalculating anything — is the correct way to judge whether a result was meaningfully sized. And the temptation to keep collecting data until the picture improves is exactly the &lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;optional-stopping problem&lt;/a&gt;: if you extend a test because a retrospective power number felt low, you are running an unplanned, uncontrolled extension of an experiment whose original stopping rule you have already broken.&lt;/p&gt;

&lt;p&gt;The common thread across all three is the same: decisions made by looking backward at data you already have, dressed up as if they were new evidence, inflate your real error rate without showing up anywhere in the reported statistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a retrospective power calculation is legitimate
&lt;/h2&gt;

&lt;p&gt;There is one legitimate retrospective use of a power calculation, and it does not use the observed effect size at all. Before deciding whether to run a &lt;em&gt;follow-up&lt;/em&gt; experiment, you can calculate what sample size would be needed to detect a specific, externally chosen effect size — for example, the smallest lift that would be worth the engineering cost of shipping the change permanently. That is a forward-looking calculation about a new, hypothetical test, using an effect size you chose for a reason unrelated to what the last test happened to observe. It looks similar to post hoc power analysis on the surface, but the input is a decision, not a readout, which is what keeps it from being circular.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reporting post hoc power as if it were independent evidence.&lt;/strong&gt; It is a deterministic function of the p-value you already had; it does not add information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using the observed effect size as the input, then acting surprised the number is low on a null result.&lt;/strong&gt; That outcome is guaranteed by the math, not a discovery about the experiment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extending a test's runtime because a retrospective power figure "felt low."&lt;/strong&gt; This breaks your stopping rule and reintroduces the peeking problem under a different name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping a confidence-interval read entirely.&lt;/strong&gt; The CI is doing the real diagnostic work a post hoc power calculation is being asked to do — check it first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Post hoc power analysis recomputes power using the effect size a test actually observed, and that number is mathematically tied to the p-value you already have — it is not new evidence.&lt;/li&gt;
&lt;li&gt;A null result will always produce a low post hoc power figure; that is guaranteed by the algebra, not a finding about whether the effect is real.&lt;/li&gt;
&lt;li&gt;The confidence interval around the observed effect — not a recalculated power figure — is the correct tool for judging whether a null result reflects "no effect" or "not enough precision to tell yet."&lt;/li&gt;
&lt;li&gt;The fix that avoids the question entirely is sizing the test honestly before it launches, using a minimum detectable effect you actually intend to act on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is post hoc power analysis ever useful?
&lt;/h3&gt;

&lt;p&gt;Only when it uses an externally chosen effect size to plan a genuinely new test — for example, sizing a follow-up experiment around the smallest lift worth shipping. Using the just-observed effect size from the test you're analyzing is the circular version to avoid.&lt;/p&gt;

&lt;h3&gt;
  
  
  My test came back with low post hoc power. Does that mean I should run it longer?
&lt;/h3&gt;

&lt;p&gt;Not on its own. Look at the confidence interval around the observed effect instead. A wide interval spanning zero suggests genuine imprecision that more data could resolve; a narrow interval spanning zero means the effect is precisely estimated at approximately zero, and more data will not change that conclusion.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is this different from a normal, prospective power calculation?
&lt;/h3&gt;

&lt;p&gt;A prospective power calculation, done before the test launches, uses an effect size you chose because it is the smallest one worth detecting — that is what a &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;sample size calculator&lt;/a&gt; does. Post hoc power analysis instead plugs in the effect size the test happened to observe, which makes the output a restatement of the p-value rather than independent planning information.&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>abtesting</category>
      <category>datascience</category>
      <category>analytics</category>
    </item>
    <item>
      <title>One-Tailed vs Two-Tailed A/B Tests: Which Should You Run</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Fri, 21 Aug 2026 05:04:51 +0000</pubDate>
      <link>https://dev.to/david_sert/one-tailed-vs-two-tailed-ab-tests-which-should-you-run-4em2</link>
      <guid>https://dev.to/david_sert/one-tailed-vs-two-tailed-ab-tests-which-should-you-run-4em2</guid>
      <description>&lt;p&gt;Every hypothesis makes a prediction about direction — "this will increase revenue," not "this will change revenue somehow." That prediction has a statistical consequence most teams never examine: whether the test behind it should be one-tailed or two-tailed. Get this choice wrong, or worse, make it after the data arrives, and you can turn a 5% false-positive rate into something closer to 10% without changing a single line of test code. This guide covers what each test actually claims, why Optimizely's Stats Engine defaults to two-tailed, the narrow cases where one-tailed is legitimate, and the much more common case where it is p-hacking wearing a statistics textbook's clothes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a two-tailed test actually claims
&lt;/h2&gt;

&lt;p&gt;A two-tailed test's null hypothesis is that the variation and the control produce the same result. Its alternative hypothesis is simply that they differ — in either direction. The test's rejection region is split across both tails of the distribution: half the alpha budget covers "variation beats control," half covers "control beats variation." At a 95% confidence level, that means roughly 2.5% of the probability mass sits in each tail.&lt;/p&gt;

&lt;p&gt;This is the conservative, symmetric default, and it is conservative on purpose. It treats "the button color change made things worse" as just as real and just as reportable a finding as "the button color change made things better." A two-tailed test does not presuppose which way the world will move; it only asks whether it moved at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a one-tailed test actually claims
&lt;/h2&gt;

&lt;p&gt;A one-tailed test narrows the alternative hypothesis to a single direction — "the variation is better than control," full stop. The rejection region lives entirely in one tail, so the same 5% alpha budget that was split across two tails in the two-tailed case is now concentrated in one. Practically, that means a one-tailed test reaches significance with a smaller observed effect than a two-tailed test would need, for the identical data and the identical alpha.&lt;/p&gt;

&lt;p&gt;That extra sensitivity is the entire appeal, and it is also the entire danger. A one-tailed test is a bet: you are declaring, before you see any data, that a result in the "wrong" direction is not just unlikely but &lt;em&gt;uninteresting&lt;/em&gt; — that you would take no action on it either way. If that premise is false, the test is miscalibrated, because it is silently discarding evidence of harm as if it could not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Optimizely's Stats Engine defaults to two-tailed
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://optipilot.com/data/optimizely-stats-engine" rel="noopener noreferrer"&gt;Optimizely's Stats Engine&lt;/a&gt; runs two-tailed tests by design, and that is not an arbitrary default — it is required for the false discovery rate control the engine is built around. FDR control needs symmetric tails to keep its error-rate guarantee honest across every metric in a test, including the ones you did not expect to move. A one-tailed override would quietly break that guarantee for the metric it was applied to.&lt;/p&gt;

&lt;p&gt;There is a practical reason to like the default even if you ignore the FDR mechanics: most experiments are run precisely because you do not know which way a change will move the metric. If you already knew the direction with certainty, you would not need to test it. Two-tailed testing keeps the platform honest about that uncertainty instead of assuming it away.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a one-tailed test is legitimate
&lt;/h2&gt;

&lt;p&gt;There is a narrow, defensible case for a one-tailed test, and it has nothing to do with statistical power. It applies only when a result in the "wrong" direction is genuinely inactionable — when you would ship the same decision regardless of whether the metric moved down or stayed flat, because either way the answer is "do not ship."&lt;/p&gt;

&lt;p&gt;A migration or performance-safety check is the clearest example: you are testing whether a new checkout implementation is &lt;em&gt;at least as fast&lt;/em&gt; as the old one before a full rollout you already intend to do for other reasons. A result showing it is dramatically faster changes nothing about the decision; a result showing it is the same speed changes nothing either. Only "meaningfully slower" would change what you do next, so the interesting region really is one-sided.&lt;/p&gt;

&lt;p&gt;Outside cases like that, a one-tailed test is difficult to justify honestly, because most product and marketing decisions are exactly the kind where a surprising result in the "wrong" direction would change what you do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode: switching tails after you've seen the data
&lt;/h2&gt;

&lt;p&gt;The far more common way a one-tailed test shows up in practice is not principled design — it is a rescue attempt. A two-tailed test comes back with a p-value of 0.08, just short of significance. Someone notices the effect is in the "expected" direction and proposes re-running the analysis as one-tailed, which roughly halves the p-value and pushes it under 0.05.&lt;/p&gt;

&lt;p&gt;This is p-hacking with a statistical vocabulary. The test's design was chosen after seeing the outcome it was meant to be judged against, which means the "5% error rate" claimed for the result was never actually in effect — the true error rate for a decision rule of "run two-tailed, and switch to one-tailed if it doesn't reach significance" is close to double the stated alpha, because you get two chances to declare a win from the same data. The fix is procedural, not statistical: decide the tail before the test launches, write it into the &lt;a href="https://optipilot.com/strategy/ab-testing-hypothesis-template-optimizely" rel="noopener noreferrer"&gt;hypothesis&lt;/a&gt;, and do not revisit the decision once results are in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked example: the same data, two verdicts
&lt;/h2&gt;

&lt;p&gt;The table below shows why the distinction is not academic. Both rows use the identical observed data — a 4.2% relative lift with a two-sided p-value of 0.062 — and differ only in which test was declared &lt;em&gt;before&lt;/em&gt; the experiment ran.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Declared before launch:  Two-tailed
Observed lift:           +4.2%
Two-sided p-value:       0.062
Verdict at alpha=0.05:   Not significant

Declared before launch:  One-tailed ("variation &amp;gt; control")
Observed lift:           +4.2%
One-sided p-value:       0.031
Verdict at alpha=0.05:   Significant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing about the underlying data changed between the two rows — only the pre-registered claim did. That is exactly why the decision has to be made before launch and recorded somewhere a reviewer can check it, rather than chosen after the fact to produce the answer you wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this interacts with your stopping rule
&lt;/h2&gt;

&lt;p&gt;Tail choice and stopping rule are two separate decisions that compound if you get both wrong. A one-tailed test that is also peeked at repeatedly during the run — checked daily, stopped the moment it crosses significance — stacks a directional shortcut on top of the &lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;optional-stopping problem&lt;/a&gt;, and the two inflate the same underlying quantity: how often you declare a winner that is not real. Fix the stopping rule with a proper sequential method or a fixed horizon, and fix the tail by writing it into the hypothesis before you launch. Neither substitutes for the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deciding the tail after seeing a "close" result.&lt;/strong&gt; This is the single most common misuse, and it is invisible in a results dashboard — the platform cannot tell the difference between a principled one-tailed test and a rescued one, because the giveaway is &lt;em&gt;when&lt;/em&gt; the decision was made, not what number came out of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating "one-tailed" as a way to get more power for free.&lt;/strong&gt; It is not free. You are trading the ability to detect and act on harm in the opposite direction for a smaller required effect size. That trade is sometimes worth making, but it is a trade, not a discount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Applying a one-tailed override to only the primary metric.&lt;/strong&gt; If a guardrail metric moves sharply in the "wrong" direction, a one-tailed primary-metric test gives you no formal signal to catch it — you are relying entirely on eyeballing the guardrail dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming Stats Engine has a one-tailed mode to switch on.&lt;/strong&gt; It does not, by design; the FDR guarantee depends on the two-tailed default holding for every metric in the experiment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A two-tailed test asks whether variation and control differ at all; a one-tailed test asks only whether variation is better, and gives up the ability to detect "worse" as a formal result.&lt;/li&gt;
&lt;li&gt;Optimizely's Stats Engine runs two-tailed tests by design — it is a requirement of the false discovery rate control the engine provides, not an arbitrary setting.&lt;/li&gt;
&lt;li&gt;A one-tailed test is only legitimate when a result in the unexpected direction is genuinely inactionable, decided and documented before the experiment launches.&lt;/li&gt;
&lt;li&gt;Choosing the tail after seeing the data — even informally, even just noticing the direction matches your prediction — silently doubles your real false-positive rate relative to the one you're reporting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I run a one-tailed test in Optimizely Web Experimentation or Feature Experimentation?
&lt;/h3&gt;

&lt;p&gt;Stats Engine's significance calculation is two-tailed by design across both products, as part of its false discovery rate guarantee. You can still write a directional hypothesis and interpret a two-tailed result through that lens at the analysis stage — you are simply not changing the underlying test statistic to do it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a one-tailed test ever more "correct" than a two-tailed test?
&lt;/h3&gt;

&lt;p&gt;Neither is more correct in the abstract; they answer different questions. A one-tailed test is the right tool only when a result in the unexpected direction is truly inactionable and that has been decided in advance. Outside that narrow case, a two-tailed test is the safer default because it does not assume away the possibility of harm.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does a one-tailed test need a smaller effect to reach significance?
&lt;/h3&gt;

&lt;p&gt;Because the same alpha budget is concentrated in one tail of the distribution instead of split across two, so a smaller observed deviation is enough to cross the threshold. That is the source of the extra sensitivity — and also the source of the risk, since it comes from narrowing what counts as evidence, not from collecting more of it.&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>abtesting</category>
      <category>datascience</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Google Content Experiments vs Optimizely: What Actually Changed</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Fri, 21 Aug 2026 05:04:15 +0000</pubDate>
      <link>https://dev.to/david_sert/google-content-experiments-vs-optimizely-what-actually-changed-2o17</link>
      <guid>https://dev.to/david_sert/google-content-experiments-vs-optimizely-what-actually-changed-2o17</guid>
      <description>&lt;p&gt;Google Content Experiments is the testing tool most people searching for it are not actually thinking of anymore, and that confusion is worth clearing up before comparing anything else. It was Google's original multivariate and A/B testing feature, built into classic Google Analytics, and Google deprecated it back in 2017 — years before Google Optimize, its more famous successor, was retired in 2023. If your team's testing history goes back far enough to remember Content Experiments by name, or you've simply found old documentation referencing it while researching a move to Optimizely, this guide covers what it actually was, how it differs from Optimizely, and which of two entirely different migration paths applies to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google Content Experiments actually was
&lt;/h2&gt;

&lt;p&gt;Content Experiments shipped as a feature of classic Google Analytics, reachable from the Behavior section of the Analytics interface. It let you test up to ten full-page variations against a control, using a simple redirect-based mechanism: visitors were routed to one of several distinct URLs, and Analytics tracked which URL produced better performance against a chosen objective. It replaced an even earlier tool, Google Website Optimizer, and it was itself replaced by Google Optimize in 2017 — Optimize added the visual editor, more targeting options, and eventually a Google Analytics 4 integration that Content Experiments never had.&lt;/p&gt;

&lt;p&gt;That lineage matters because it explains why searches for "Content Experiments" and searches for "Google Optimize" turn up overlapping but not identical results, and why some migration guidance written for one does not cleanly apply to the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Content Experiments compares to Optimizely
&lt;/h2&gt;

&lt;p&gt;The comparison is less about feature parity and more about what generation of testing tool each one represents.&lt;/p&gt;

&lt;p&gt;Content Experiments was redirect-only: every "variation" was a distinct, separately hosted page, and Analytics split traffic across the URLs and reported which one won on the metric you configured. It had no visual editor, no client-side DOM manipulation, and no concept of audience targeting beyond what you could encode into which visitors received which redirect.&lt;/p&gt;

&lt;p&gt;Optimizely Web Experimentation supports that same redirect-based pattern for full-page and URL-based tests, but it is one option among several rather than the only one. It also supports client-side DOM changes through a visual editor without requiring a second hosted page, audience targeting rules that go well beyond a simple traffic split, and integration with Optimizely's &lt;a href="https://optipilot.com/data/optimizely-stats-engine" rel="noopener noreferrer"&gt;Stats Engine&lt;/a&gt; for significance testing that accounts for multiple metrics and repeated looks at the data — none of which Content Experiments offered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Google Content Experiments (2012-2017):  redirect-only, up to 10 variations, basic Analytics objective, no visual editor
Optimizely Web Experimentation (today):  redirect-based OR visual-editor DOM changes, audience targeting, Stats Engine significance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a team that only ever used Content Experiments for simple landing-page redirect tests, the redirect-based workflow in Optimizely — covered in the &lt;a href="https://optipilot.com/web-experimentation/landing-page-ab-testing" rel="noopener noreferrer"&gt;landing page A/B testing guide&lt;/a&gt; — is the closest like-for-like starting point, even though the platform underneath it is considerably more capable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two different migration paths, depending on where you're coming from
&lt;/h2&gt;

&lt;p&gt;If your team's most recent testing tool was Content Experiments itself — meaning you never adopted Google Optimize before it, too, was retired — you are migrating across two deprecations at once, and there is no automated importer for either leg of that journey. Google never published an export format for live Content Experiments configurations, and the platforms differ enough structurally that a mechanical translation would not produce working tests regardless. The realistic path is the same structured rebuild any migration off a legacy Google testing tool requires: inventory what you were testing, decide whether each experiment maps most naturally to a redirect-based test or a visual-editor DOM change in Optimizely, and rebuild from there.&lt;/p&gt;

&lt;p&gt;If instead you moved from Content Experiments to Google Optimize at some point before Optimize's 2023 sunset, you are migrating from Optimize, not from Content Experiments — and that is a more common, better-documented path. The &lt;a href="https://optipilot.com/strategy/migrate-google-optimize-to-optimizely" rel="noopener noreferrer"&gt;Google Optimize to Optimizely migration guide&lt;/a&gt; covers that transition in full: mapping Optimize's objectives, audiences, and visual-editor changes to their Optimizely equivalents, and what does not carry over automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why there's no automated importer for either tool
&lt;/h2&gt;

&lt;p&gt;The absence of a migration tool is not an Optimizely limitation specifically — it reflects a gap upstream. Google never published a machine-readable export format for either Content Experiments or Optimize configurations, so no third-party platform, Optimizely included, has anything to build an importer against. Even if one existed, the underlying models differ enough — Content Experiments' pure-redirect approach versus Optimizely's combination of visual-editor DOM changes and redirects, different audience-targeting concepts, different significance calculations — that a mechanical translation would likely produce broken or subtly wrong tests rather than a clean lift-and-shift.&lt;/p&gt;

&lt;p&gt;That makes the rebuild a deliberate, if mechanical, exercise rather than a data-migration problem: most concepts from either legacy tool have a clear Optimizely counterpart, so the work is mapping and recreating rather than inventing a new testing program from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually carries over conceptually
&lt;/h2&gt;

&lt;p&gt;Even without an automated path, the underlying testing logic from a Content Experiments era program is not wasted. A redirect-based page comparison — page A's URL against page B's URL, split by traffic — maps directly onto a URL-redirect test in Optimizely Web Experimentation, and any historical learnings about which page structure won are still valid evidence worth feeding into your next &lt;a href="https://optipilot.com/strategy/ab-testing-hypothesis-template-optimizely" rel="noopener noreferrer"&gt;hypothesis&lt;/a&gt;, even though the original test predates the platform you're rebuilding in.&lt;/p&gt;

&lt;p&gt;What does not carry over is the objective configuration itself — Content Experiments' Analytics-goal-based objectives have no direct equivalent field in Optimizely, since Optimizely defines primary and guardrail metrics natively rather than borrowing them from an Analytics goals configuration. Those need to be redefined as part of the rebuild, not imported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assuming "Google Optimize migration" guidance covers Content Experiments too.&lt;/strong&gt; The two tools have different underlying mechanics — Content Experiments is redirect-only with no visual editor — so guidance written for an Optimize migration will skip steps a Content Experiments migration actually needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Looking for an export file that does not exist.&lt;/strong&gt; Neither Content Experiments nor Optimize ever published one; budget time for a structured rebuild rather than searching for an importer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating old Content Experiments results as historical noise instead of evidence.&lt;/strong&gt; A redirect-test result from years ago is still a real finding about what worked on that page — it belongs in your knowledge base, not the trash, even if the tooling that produced it is long gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recreating every legacy test exactly as it ran.&lt;/strong&gt; Some Content Experiments-era tests existed because the tool had no visual-editor option; in Optimizely, several of those may be better redesigned as DOM-change tests rather than reproduced as redirects out of habit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Google Content Experiments (deprecated 2017) and Google Optimize (deprecated 2023) are two different, sequential Google testing tools — check which one your team actually used before following migration guidance.&lt;/li&gt;
&lt;li&gt;Content Experiments was redirect-only with no visual editor; Optimizely supports that same pattern plus client-side DOM changes, audience targeting, and Stats Engine significance testing.&lt;/li&gt;
&lt;li&gt;Neither Google tool ever published an export format, so the migration path for either is a structured rebuild — inventory, map concepts, recreate — not a data import.&lt;/li&gt;
&lt;li&gt;If your path ran through Google Optimize before Optimizely, the dedicated &lt;a href="https://optipilot.com/strategy/migrate-google-optimize-to-optimizely" rel="noopener noreferrer"&gt;Google Optimize migration guide&lt;/a&gt; is the more specific resource to follow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Google Content Experiments the same thing as Google Optimize?
&lt;/h3&gt;

&lt;p&gt;No. Content Experiments was Google's earlier testing tool, built into classic Google Analytics and deprecated in 2017. Google Optimize was its successor, with a visual editor and GA4 integration, and was itself retired in 2023. They are two different products in the same lineage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I import my old Content Experiments configuration directly into Optimizely?
&lt;/h3&gt;

&lt;p&gt;No automated importer exists, because Google never published an export format for either Content Experiments or Google Optimize. The migration is a structured rebuild: map each concept to its Optimizely equivalent and recreate the experiments you still want to run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which Optimizely product replaces what Content Experiments did?
&lt;/h3&gt;

&lt;p&gt;Optimizely Web Experimentation covers the same redirect-based, full-page comparison pattern Content Experiments used, and adds a visual editor, audience targeting, and Stats Engine significance testing on top of it. See the &lt;a href="https://optipilot.com/web-experimentation/landing-page-ab-testing" rel="noopener noreferrer"&gt;landing page A/B testing guide&lt;/a&gt; for the closest equivalent workflow.&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>optimizely</category>
      <category>analytics</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Champion/Challenger Testing vs A/B Testing: When the Older Method Still Wins</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Fri, 21 Aug 2026 05:03:39 +0000</pubDate>
      <link>https://dev.to/david_sert/championchallenger-testing-vs-ab-testing-when-the-older-method-still-wins-5ch3</link>
      <guid>https://dev.to/david_sert/championchallenger-testing-vs-ab-testing-when-the-older-method-still-wins-5ch3</guid>
      <description>&lt;p&gt;Champion/challenger testing predates A/B testing as most web teams practice it today, and it never went away — it just moved into places like fraud scoring, credit models, and recommendation systems, where it is still the default. It resurfaces in web and product experimentation whenever a team needs something an A/B test does not naturally give them: a live, defensible incumbent that keeps running while a challenger earns its way past it, one comparison at a time. This guide covers what champion/challenger actually is, how it differs from a standard A/B test and from a multi-armed bandit, and when reaching for it is the right call instead of a reflex from a different industry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What champion/challenger actually means
&lt;/h2&gt;

&lt;p&gt;In a champion/challenger setup, there is always a "champion" — the current best-known approach, running in production and serving the majority of traffic — and one or more "challengers," each getting a smaller, deliberately limited slice. A challenger that beats the champion by a pre-agreed margin gets promoted to be the new champion. One that doesn't gets retired, and a new challenger takes its slot.&lt;/p&gt;

&lt;p&gt;The defining feature is that there is no fixed end date and no single terminal readout. The champion is always the reigning answer to "what do we run right now," and the question the process answers is not "which of these two options is better," but "is there currently anything that beats what we're running, and if so, promote it." That framing is closer to a standing tournament than to a bounded scientific test.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this differs from a standard A/B test
&lt;/h2&gt;

&lt;p&gt;A standard A/B test is symmetric and time-bounded: control and variation split traffic roughly evenly, the test runs until it reaches a pre-computed sample size, and it ends with a single decision — ship the variation, keep the control, or call it inconclusive. Once the decision is made, the test is over. There is no "champion" that persists into the next test by default; each new test starts from a fresh 50/50 split unless you deliberately structure it as a sequel.&lt;/p&gt;

&lt;p&gt;Champion/challenger drops the symmetry and the fixed endpoint on purpose. The champion typically gets the large majority of traffic — commonly 80-90% or more — because it is a known quantity you don't want to put unnecessarily at risk, while the challenger gets just enough to accumulate evidence without meaningfully denting overall performance if it turns out to be worse. And rather than one comparison with a hard stop, the process continues indefinitely: today's promoted challenger becomes tomorrow's champion, facing the next challenger in line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A/B test:                control (50%) vs variation (50%) -&amp;gt; fixed sample size -&amp;gt; one decision -&amp;gt; test ends
Champion/challenger:     champion (80-90%) vs challenger (10-20%) -&amp;gt; ongoing -&amp;gt; promote or retire -&amp;gt; repeats indefinitely
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How this differs from a multi-armed bandit
&lt;/h2&gt;

&lt;p&gt;Champion/challenger and &lt;a href="https://optipilot.com/data/multi-armed-bandit" rel="noopener noreferrer"&gt;multi-armed bandit&lt;/a&gt; allocation look similar on the surface — both give the current best option more traffic than the alternatives — but the mechanism and the guarantee behind them are different.&lt;/p&gt;

&lt;p&gt;A bandit reallocates traffic continuously and automatically based on observed performance, adjusting the split many times over a run in pursuit of maximizing cumulative reward across the whole traffic stream. It optimizes for &lt;em&gt;earning&lt;/em&gt; during the test itself, and it explicitly trades away the clean significance machinery of a fixed-split test to do it — see the fuller comparison in &lt;a href="https://optipilot.com/data/stats-accelerator-vs-mab-vs-contextual-bandit" rel="noopener noreferrer"&gt;Stats Accelerator vs MAB vs contextual bandit&lt;/a&gt; for the mechanics.&lt;/p&gt;

&lt;p&gt;Champion/challenger's traffic split is typically set once by a person, not continuously re-optimized by an algorithm, and the promotion decision at the end is usually a discrete, human-reviewed comparison against a stated threshold rather than a live reallocation curve. It trades the bandit's automatic optimization for something a bandit does not offer: an easily explainable, auditable record of exactly when and why each promotion happened — which matters a great deal in domains like credit and fraud decisioning, where a regulator or an internal risk committee needs to be able to reconstruct the reasoning behind a model change.&lt;/p&gt;

&lt;h2&gt;
  
  
  When champion/challenger is the right choice
&lt;/h2&gt;

&lt;p&gt;The clearest case for champion/challenger over a standard A/B test is when there genuinely is no natural end date to the comparison — recommendation ranking, fraud-scoring thresholds, and pricing models are all domains where "the current best model" is a permanent, evolving role rather than a question with a final answer. In those settings, running a fresh symmetric A/B test for every candidate improvement means repeatedly re-litigating a decision that champion/challenger treats as continuous by design.&lt;/p&gt;

&lt;p&gt;It is also the better fit when the cost of a wrong answer at scale is asymmetric and high, and you want the safety of keeping the bulk of traffic on a known-good option while a challenger proves itself gradually rather than immediately splitting evenly. A fraud-scoring model that gets it wrong on 50% of transactions during a symmetric test is a materially worse outcome than one that gets it wrong on 10-20% while a challenger is evaluated.&lt;/p&gt;

&lt;p&gt;And it fits a governance requirement an A/B test's single readout does not naturally satisfy: an auditable trail of exactly which model was live when, and what threshold each promotion cleared. Web experimentation platforms report a result; champion/challenger produces a lineage.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it is the wrong choice
&lt;/h2&gt;

&lt;p&gt;For a typical web or product experiment — a landing page redesign, a checkout flow change, a new onboarding sequence — a standard A/B test is almost always the better tool, not the weaker one. These changes usually do have a natural end state: you either ship the winner or you don't, and there is no ongoing "model" that needs a permanent incumbent role. Reaching for champion/challenger here mostly adds complexity — an indefinite runtime, an ambiguous promotion threshold, traffic sitting on a "challenger" slice long after a fixed-horizon test would have already reached a clean answer — without adding anything the situation actually needed.&lt;/p&gt;

&lt;p&gt;It is also the wrong tool when you need the formal significance guarantees a platform like Optimizely's &lt;a href="https://optipilot.com/data/optimizely-stats-engine" rel="noopener noreferrer"&gt;Stats Engine&lt;/a&gt; provides. Because champion/challenger's traffic split and promotion threshold are typically set by policy rather than derived from a significance calculation, it does not carry the same false-discovery-rate guarantee a properly sized, symmetric test does. If your organization needs a defensible "we know this is real" answer for a single, bounded decision, that is what a standard test is built to provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision framework
&lt;/h2&gt;

&lt;p&gt;Ask three questions before choosing between the two:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is there a natural end date to this comparison, or is "the current best option" a permanent, evolving role?&lt;/strong&gt; A permanent role points toward champion/challenger; a bounded decision points toward a standard test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you need a rigorous significance guarantee for a single decision, or an ongoing, auditable lineage of incremental promotions?&lt;/strong&gt; The former points toward a fixed-horizon A/B test; the latter points toward champion/challenger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the downside of a bad challenger large enough that you want it capped at a small traffic share indefinitely, rather than accepted at 50% for a bounded window?&lt;/strong&gt; If yes, that argues for champion/challenger's asymmetric, ongoing split over a standard test's symmetric one.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Using champion/challenger for a one-off decision with a real end state.&lt;/strong&gt; This just adds indefinite runtime and an ambiguous stopping point to a question that a fixed-horizon test would answer more cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confusing it with a bandit and expecting automatic reallocation.&lt;/strong&gt; Champion/challenger's split is typically fixed by policy; if you want continuous, algorithmic reallocation toward the better-performing option, that is a bandit, not this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping a stated promotion threshold.&lt;/strong&gt; Without one, "is the challenger good enough" becomes a judgment call made after the fact, which reopens exactly the kind of post-hoc reasoning a designed comparison is supposed to prevent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Applying it to web page tests out of habit from a fraud or risk background.&lt;/strong&gt; The governance need that makes champion/challenger valuable in those domains usually isn't present for a landing page test, and the standard A/B test's cleaner significance guarantee is the better fit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Champion/challenger keeps an incumbent "champion" running on most traffic indefinitely, promoting a challenger only when it clears a pre-agreed margin — there is no fixed end date and no single terminal readout.&lt;/li&gt;
&lt;li&gt;It differs from a bandit in that the traffic split and promotion decision are typically set by policy rather than continuously optimized by an algorithm, trading automatic reallocation for an auditable lineage.&lt;/li&gt;
&lt;li&gt;It fits domains with a permanent "current best" role and asymmetric downside risk — fraud, credit, ranking — better than it fits a typical web experiment with a real end state.&lt;/li&gt;
&lt;li&gt;For a bounded product decision, a standard A/B test with a proper significance guarantee is almost always the better tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is champion/challenger testing the same as an A/B test with unequal traffic splits?
&lt;/h3&gt;

&lt;p&gt;Not quite. An unequal split alone doesn't capture the defining feature, which is the lack of a fixed end date — champion/challenger is an ongoing process where today's winner becomes tomorrow's incumbent, not a single bounded comparison with an unusual split.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Optimizely support champion/challenger testing directly?
&lt;/h3&gt;

&lt;p&gt;Optimizely's &lt;a href="https://optipilot.com/data/optimizely-stats-engine" rel="noopener noreferrer"&gt;Stats Engine&lt;/a&gt; is built around bounded, significance-driven comparisons rather than an indefinite promotion lineage. Teams that need champion/challenger's governance model typically build it as a repeated series of fixed-horizon tests with a documented promotion rule between them, rather than relying on a single ongoing experiment configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is champion/challenger different from a multi-armed bandit in practice?
&lt;/h3&gt;

&lt;p&gt;A bandit continuously reallocates traffic toward whichever arm is performing best, optimizing for cumulative reward during the run itself. Champion/challenger's split is typically fixed by policy, and the promotion decision is a discrete, auditable comparison against a stated threshold rather than a live reallocation curve.&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>statistics</category>
      <category>machinelearning</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Sample Ratio Mismatch: Is Your A/B Test Broken?</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:10:45 +0000</pubDate>
      <link>https://dev.to/david_sert/sample-ratio-mismatch-is-your-ab-test-broken-2fe8</link>
      <guid>https://dev.to/david_sert/sample-ratio-mismatch-is-your-ab-test-broken-2fe8</guid>
      <description>&lt;p&gt;Your experiment finished, the dashboard shows a winner, and something still feels off. The traffic split you set to 50/50 is sitting at 47/53. Before you write up the result or ship the change, you need to answer one question: is the split you observed close enough to the split you designed, or is the test itself broken? That question has a precise, statistical answer, and getting it wrong is one of the most common ways a team ships a change based on a number that was never real. This is sample ratio mismatch, and it is the first thing a senior experimenter checks before looking at any lift.&lt;/p&gt;

&lt;h2&gt;
  
  
  What sample ratio mismatch is
&lt;/h2&gt;

&lt;p&gt;A sample ratio mismatch (SRM) occurs when the number of visitors actually assigned to each variation differs from the split you intended by more than random chance can explain. You designed a 50/50 test; the engine delivered something that a fair coin, flipped that many times, would essentially never produce.&lt;/p&gt;

&lt;p&gt;The important word is &lt;em&gt;chance&lt;/em&gt;. You should never expect a perfectly even split. If you assign 20,000 visitors with a fair 50/50 rule, landing on exactly 10,000/10,000 is rare — the count wobbles around the target every time, the same way 10,000 coin flips almost never give exactly 5,000 heads. SRM is not "the split isn't perfect." SRM is "the split is &lt;em&gt;so&lt;/em&gt; far off that randomness is no longer a believable explanation."&lt;/p&gt;

&lt;p&gt;That distinction is why you cannot judge SRM by eye. Looking at 47/53 tells you the direction and size of the gap but nothing about how surprising it is, because surprise depends on how many visitors produced that gap. The same 47/53 is unremarkable at 400 visitors and a five-alarm fire at 400,000. You need a test that accounts for &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;sample size&lt;/a&gt;. The tool for that is the chi-square goodness-of-fit test, and OptiPilot's free &lt;a href="https://optipilot.com/tools/srm-checker" rel="noopener noreferrer"&gt;SRM Checker&lt;/a&gt; runs it for you — paste in your observed counts and expected split, and it returns the p-value and verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an SRM invalidates your results
&lt;/h2&gt;

&lt;p&gt;Randomized assignment is the entire reason an A/B test can claim causality. When you split traffic randomly, the two groups are, on average, identical in every way — same mix of devices, same proportion of loyal customers, same intent — &lt;em&gt;except&lt;/em&gt; the change you are testing. That balance is what lets you attribute any difference in outcomes to the variation rather than to a difference in who was in each group.&lt;/p&gt;

&lt;p&gt;An SRM is direct evidence that the randomization did not work as designed. If a mechanism pushed extra visitors into one variation, that same mechanism almost certainly pushed a &lt;em&gt;non-random&lt;/em&gt; kind of visitor. Redirects that time out drop the impatient and the slow-connection users. Bot filters that fire unevenly strip out a specific traffic source. In every case the groups are no longer comparable, so the lift you measured is a mix of the real treatment effect and a selection bias you cannot separate out.&lt;/p&gt;

&lt;p&gt;This is the point that trips up teams under deadline pressure: &lt;strong&gt;you cannot adjust your way out of an SRM.&lt;/strong&gt; There is no correction factor, no reweighting, no "the split was 45/55 so I'll scale the numbers." You do not know which visitors are missing or why, so you cannot model the bias. An SRM means the data is untrustworthy at the source. The only valid responses are to find and fix the cause, then rerun — or, if you can prove the cause is benign and non-differential, to document exactly why. Reading the &lt;a href="https://optipilot.com/data/optimizely-results-page" rel="noopener noreferrer"&gt;Optimizely results page&lt;/a&gt; is meaningless until the split checks out.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SRM is detected with a chi-square test
&lt;/h2&gt;

&lt;p&gt;The standard test is Pearson's chi-square goodness-of-fit. It compares the visitor counts you &lt;em&gt;observed&lt;/em&gt; in each variation against the counts you &lt;em&gt;expected&lt;/em&gt; under your intended split, and returns a p-value: the probability of seeing a gap at least this large if assignment were truly random.&lt;/p&gt;

&lt;p&gt;The statistic is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chi-square = sum over each variation of (observed - expected)^2 / expected

degrees of freedom (df) = number of variations - 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a two-arm test, df = 1. For a three-arm 34/33/33 test, df = 2, and so on. You compute the statistic, look up the p-value for that value and df, and compare it to a threshold.&lt;/p&gt;

&lt;p&gt;The industry alarm threshold is &lt;strong&gt;p &amp;lt; 0.001&lt;/strong&gt;. If the p-value falls below it, you treat the experiment as having an SRM and stop trusting the results until you find the cause. Some teams run stricter: Microsoft's experimentation platform uses &lt;strong&gt;0.0005&lt;/strong&gt;, and a few use 0.0001. The threshold is deliberately far tighter than the 0.05 you use for a metric result, for two reasons. First, you run this check on every experiment, so a loose threshold would drown you in false alarms. Second, SRM is a binary data-integrity gate, not an effect you are trying to measure — you want it to fire only when something is genuinely wrong. Note that a real SRM usually produces a p-value orders of magnitude below any of these thresholds, not a marginal miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked chi-square example
&lt;/h2&gt;

&lt;p&gt;Take a 50/50 test. You intended an even split; you observed 10,200 visitors in A and 9,800 in B, for 20,000 total. Under a fair split you expect 10,000 in each.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observed:  A = 10,200   B = 9,800     Total = 20,000
Expected:  A = 10,000   B = 10,000    (50% of 20,000 each)

chi-square = (10,200 - 10,000)^2 / 10,000 + (9,800 - 10,000)^2 / 10,000
           = (200)^2 / 10,000 + (-200)^2 / 10,000
           = 40,000 / 10,000 + 40,000 / 10,000
           = 4 + 4
           = 8.0

df = 2 variations - 1 = 1
p-value for chi-square = 8.0, df = 1  -&amp;gt;  p ~= 0.0047
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to read that: p ≈ 0.0047 means a gap this large would happen about 5 times in 1,000 by pure chance. That clears the conventional 0.05 bar, so a naive check would panic. But it is &lt;strong&gt;above&lt;/strong&gt; the 0.001 SRM threshold, so by the standard you do not declare an SRM. It sits in the uncomfortable middle — worth a glance, not worth halting for. This is exactly why the strict threshold exists: a 2% wobble at this sample size is not yet strong enough evidence of a broken split.&lt;/p&gt;

&lt;p&gt;Now keep the &lt;em&gt;same 51/49 ratio&lt;/em&gt; but run 10x the traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observed:  A = 102,000   B = 98,000    Total = 200,000
Expected:  A = 100,000   B = 100,000

chi-square = (2,000)^2 / 100,000 + (-2,000)^2 / 100,000
           = 40 + 40
           = 80.0

df = 1
p-value for chi-square = 80.0, df = 1  -&amp;gt;  p ~= 3.7e-19  (effectively zero)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identical proportions, wildly different verdict. At 20,000 visitors the 51/49 split is plausibly noise; at 200,000 it is a certainty that something is wrong. This is the single most important intuition about SRM: &lt;strong&gt;the ratio alone tells you nothing — the mismatch is a function of the ratio and the sample size together.&lt;/strong&gt; It is also why staring at the percentages on a dashboard will never substitute for the test.&lt;/p&gt;

&lt;h2&gt;
  
  
  What causes sample ratio mismatch
&lt;/h2&gt;

&lt;p&gt;An SRM is a symptom, like a fever — it tells you something is wrong without telling you what. The KDD 2019 taxonomy from Microsoft and Booking.com researchers organizes the causes by &lt;em&gt;where in the pipeline&lt;/em&gt; the visitor count gets distorted. Walking that pipeline is how you narrow the search.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Assignment] --&amp;gt; B[Exposure]
    B --&amp;gt; C[Logging]
    C --&amp;gt; D[Analysis]
    A -.-&amp;gt;|"faulty hash, bad IDs,&amp;lt;br/&amp;gt;uneven ramp, carryover"| A
    B -.-&amp;gt;|"redirect timeouts, flicker,&amp;lt;br/&amp;gt;variation errors, latency"| B
    C -.-&amp;gt;|"bot filters, dropped events,&amp;lt;br/&amp;gt;caching, ad blockers"| C
    D -.-&amp;gt;|"wrong trigger point,&amp;lt;br/&amp;gt;bad joins, segment filters"| D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Assignment-stage causes
&lt;/h3&gt;

&lt;p&gt;The split is wrong at the moment of bucketing. A faulty randomization or hash function, corrupted or reassigned visitor IDs, or carryover effects from a previous experiment that still tag returning users. Uneven &lt;em&gt;ramping&lt;/em&gt; belongs here too: if you launch variation B a day after A, or ramp them to different allocations, the counts diverge for a completely mundane reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exposure-stage causes
&lt;/h3&gt;

&lt;p&gt;Assignment was fair, but visitors are lost between being bucketed and being counted. &lt;strong&gt;Redirect tests are the classic offender&lt;/strong&gt; — a visitor sent to a separate URL can close the tab before the redirect completes, so the event never fires and that visitor is never counted. Because the delay is not symmetric across variations, the counts drift. Client-side flicker and load races drop a variation when the page renders before the experiment code runs, latency in one variation lets impatient users leave first, and a variation that errors for a subset of browsers silently loses those visitors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logging and analysis-stage causes
&lt;/h3&gt;

&lt;p&gt;The split was fine; the &lt;em&gt;measurement&lt;/em&gt; distorts it. Bot and spam filtering applied unevenly strips a traffic source from one arm. Ad blockers and tracking-prevention block the logging beacon for some visitors more than others. Caching or CDN behavior serves stale assignments. On the analysis side, choosing the wrong starting point — counting from a downstream event that already differs between variations rather than from first exposure — manufactures an SRM out of clean data. Bad table joins and delayed log arrival do the same.&lt;/p&gt;

&lt;p&gt;The most useful directional clue: excess visitors in the &lt;em&gt;treatment&lt;/em&gt; arm often points to logging or engagement changes (the new variant generates more events, so more one-time visitors get recorded), while missing visitors in treatment points to exposure failures like redirects, errors, or latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to diagnose an SRM
&lt;/h2&gt;

&lt;p&gt;Once the check fails, resist the urge to guess. Diagnosis is a process of localization: find the segment or stage where the mismatch lives, and the cause usually becomes obvious.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    S[SRM detected] --&amp;gt; Q1{Is it in every segment&amp;lt;br/&amp;gt;or just some?}
    Q1 --&amp;gt;|Isolated to one&amp;lt;br/&amp;gt;browser/device/source| Seg[Suspect that segment:&amp;lt;br/&amp;gt;rendering bug, bot source,&amp;lt;br/&amp;gt;ad blocker, redirect on that platform]
    Q1 --&amp;gt;|Spread evenly&amp;lt;br/&amp;gt;across all segments| Q2{Does the split break&amp;lt;br/&amp;gt;at assignment or later?}
    Q2 --&amp;gt;|Wrong at first exposure| Assign[Assignment layer:&amp;lt;br/&amp;gt;hashing, IDs, ramp timing,&amp;lt;br/&amp;gt;overlapping experiments]
    Q2 --&amp;gt;|Fine at exposure,&amp;lt;br/&amp;gt;off downstream| Q3{Redirect or heavy&amp;lt;br/&amp;gt;client-side render?}
    Q3 --&amp;gt;|Yes| Exp[Exposure loss:&amp;lt;br/&amp;gt;redirect timeouts, flicker,&amp;lt;br/&amp;gt;variation errors]
    Q3 --&amp;gt;|No| Log[Logging/analysis:&amp;lt;br/&amp;gt;bot filter, wrong trigger point,&amp;lt;br/&amp;gt;join error, caching]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Work through it in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Segment the mismatch.&lt;/strong&gt; Break the counts down by browser, device, operating system, day, traffic source, and new-vs-returning. If the SRM collapses into one segment — say, only Safari, or only mobile, or only the day of a deploy — you have found it. A rendering bug, a browser that rejects redirects, or a bot wave from one source will all show up as a single hot segment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the stage where the ratio breaks.&lt;/strong&gt; Compare the split at assignment, at exposure, and at analysis. If bucketing is even but exposure is skewed, the loss is happening after assignment (redirects, errors, latency). If assignment itself is off, the problem is upstream in the hashing or ID logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the analysis trigger.&lt;/strong&gt; Confirm you are counting from first exposure, not from a downstream event that the treatment itself changed. This alone explains a large share of "SRMs" that are really analysis artifacts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line up the timeline.&lt;/strong&gt; Map the onset of the mismatch against deploys, ramp changes, and marketing sends. SRM that starts precisely at a code push has a precise cause.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One caution specific to how detection works: run the SRM check on the &lt;em&gt;whole&lt;/em&gt; experiment population, not on filtered segments as a decision input. Segmenting is a diagnostic tool for finding the cause once an SRM is flagged — it is not a place to hunt for fresh imbalances to act on, because slicing enough ways will always turn up a low p-value by chance.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to prevent SRM
&lt;/h2&gt;

&lt;p&gt;Prevention is cheaper than diagnosis. The teams that rarely see SRM have built these habits in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assign server-side where you can.&lt;/strong&gt; Server-side or edge assignment removes the flicker, redirect, and client-render failure modes that cause most exposure-stage mismatches. Client-side tests are more exposed and need tighter QA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bucket at the point of exposure.&lt;/strong&gt; Assign the visitor at the moment they are actually eligible to see the change, not at some earlier step that a variation may reach at a different rate. Assign-at-exposure keeps the counted population aligned with the treated population.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run an &lt;strong&gt;[&lt;/strong&gt;A/A test**](&lt;a href="https://optipilot.com/data/aa-testing" rel="noopener noreferrer"&gt;https://optipilot.com/data/aa-testing&lt;/a&gt;)&lt;/strong&gt; first.** An A/A test — two identical variants — should show &lt;em&gt;no&lt;/em&gt; SRM. If it does, your instrumentation is broken before you have tested anything real. It is the cheapest possible way to catch a pipeline problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put an SRM check in CI and pre-analysis.&lt;/strong&gt; Make the chi-square check an automated gate that runs continuously, not a manual step someone remembers at the end. Add it to your pre-launch &lt;a href="https://optipilot.com/tools/experiment-qa-checklist" rel="noopener noreferrer"&gt;experiment QA checklist&lt;/a&gt; so it is verified every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be careful with redirects and uneven ramps.&lt;/strong&gt; Prefer single-page variation over redirects when you can; when you cannot, expect and monitor a small imbalance. Ramp all variations together, and never adjust traffic mid-flight to "rebalance" counts — that corrupts the results further.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Optimizely detects SRM automatically
&lt;/h2&gt;

&lt;p&gt;Optimizely does not make you run this check by hand. Its Stats Engine A/B tests include automatic SRM detection with an &lt;strong&gt;Experiment Health&lt;/strong&gt; indicator on the results page: a green &lt;em&gt;Good&lt;/em&gt; status means no imbalance, and an &lt;em&gt;SRM detected&lt;/em&gt; status means the test has failed a traffic check and needs investigation before you trust it.&lt;/p&gt;

&lt;p&gt;One design detail matters for practitioners. Optimizely's detector does &lt;strong&gt;not&lt;/strong&gt; use a one-shot chi-square test. It uses a &lt;em&gt;sequential&lt;/em&gt; SRM (SSRM) method that checks traffic counts continuously as data arrives, so it can flag a problem within the first days of a launch — when stopping the test actually limits the damage — rather than only at the end. This is the same reasoning behind &lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;sequential testing and the peeking problem&lt;/a&gt;: a fixed-horizon test is only valid if you look once, whereas a sequential test is built to be monitored continuously. A retroactive end-of-experiment chi-square check has the opposite problem — by the time the totals even out, a transient mid-experiment SRM can be hidden entirely.&lt;/p&gt;

&lt;p&gt;A few operational limits are worth knowing. Optimizely's automatic detection applies to Stats Engine A/B tests with &lt;strong&gt;Manual&lt;/strong&gt; traffic distribution (not Stats Accelerator), running 45 days or less, with at least 1,000 visitors, and it evaluates only first decisions. It does not run on segmented results, and it does not check paused or archived experiments. So the manual chi-square check and the &lt;a href="https://optipilot.com/tools/srm-checker" rel="noopener noreferrer"&gt;SRM Checker&lt;/a&gt; still earn their place: for Stats Accelerator tests, for segment-level diagnosis once an alarm fires, and for any experiment run on a platform without built-in detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust the split before you trust the lift
&lt;/h2&gt;

&lt;p&gt;Sample ratio mismatch is the smoke detector of experimentation. It does not tell you what is on fire, but it tells you — reliably, and before you make an expensive decision — that you should not trust the numbers yet. The discipline is simple and non-negotiable: check the split with a chi-square test against a strict threshold, and if it fails, find and fix the cause rather than reasoning around it. A test with an SRM is not a weaker result; it is not a result at all.&lt;/p&gt;

&lt;p&gt;Make it the first thing you look at, not the last. Before you read a single metric, run your observed counts through the &lt;a href="https://optipilot.com/tools/srm-checker" rel="noopener noreferrer"&gt;SRM Checker&lt;/a&gt;. If the split holds, everything downstream — your lift, your confidence interval, your &lt;a href="https://optipilot.com/data/cuped-variance-reduction" rel="noopener noreferrer"&gt;variance-reduced estimates&lt;/a&gt; — rests on solid ground. If it doesn't, you have saved yourself from shipping a decision built on a broken experiment.&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>statistics</category>
      <category>datascience</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Prevent Optimizely from being blocked by ad-blockers using AWS</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:10:10 +0000</pubDate>
      <link>https://dev.to/david_sert/prevent-optimizely-from-being-blocked-by-ad-blockers-using-aws-17jn</link>
      <guid>https://dev.to/david_sert/prevent-optimizely-from-being-blocked-by-ad-blockers-using-aws-17jn</guid>
      <description>&lt;p&gt;Category: Privacy&lt;/p&gt;

&lt;p&gt;Some ad-blockers might block Optimizely from running. In this tutorial, we’ll see how we can prevent this from happening.&lt;/p&gt;

&lt;p&gt;Ad-blockers will often look at the domain the request is originating from and if it happens to be from a list of known analytics tools (such as Optimizely.com), the ad blocker will block the network request from happening.&lt;/p&gt;

&lt;p&gt;The key solution is to proxy requests via an API gateway which will forward all requests back to Optimizely.com.&lt;/p&gt;

&lt;p&gt;Here’s how.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create an AWS API Gateway
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
    subgraph Browser
        A[Your Website] --&amp;gt; B[Proxy Request]
    end

    subgraph AWS["AWS CloudFront"]
        B --&amp;gt; C[Your Domain cdn.yourdomain.com]
        C --&amp;gt; D[CloudFront Distribution]
    end

    subgraph Optimizely
        D --&amp;gt; E[cdn.optimizely.com]
        E --&amp;gt; F[Optimizely Snippet]
    end

    F --&amp;gt; A

    style AWS fill:#ff9900,color:#fff
    style Optimizely fill:#0037ff,color:#fff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Head the &lt;a href="https://aws.amazon.com/" rel="noopener noreferrer"&gt;AWS Management Console&lt;/a&gt; then head to the API gateway section&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbuqye7y5b7t3u3tj2y00.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbuqye7y5b7t3u3tj2y00.png" alt="The AWS API Gateway Section" width="799" height="169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, click on &lt;em&gt;Create New API&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4crhmbceul2zh40pskj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4crhmbceul2zh40pskj.png" alt="Click to create a new API" width="799" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select HTTP API as the API type:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv27xww81gdbd0t1qvp22.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv27xww81gdbd0t1qvp22.png" alt="Select API type" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 2: Create the required routes&lt;/p&gt;

&lt;p&gt;Now we are going to create our API routes and where the API will forward the requests to.&lt;/p&gt;

&lt;p&gt;We’ll need 3 routes, depending on the Optimizely product you use:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One that allows us to retrieve the Optimizely snippet (if you are using Web). The snippet is fetched via a &lt;code&gt;GET&lt;/code&gt; request done to &lt;a href="https://cdn.optimizely.com" rel="noopener noreferrer"&gt;https://cdn.optimizely.com&lt;/a&gt;. You can find the full snippet URL inside your Project settings.&lt;/li&gt;
&lt;li&gt;One that allows us to send decision &amp;amp; conversion events (logx.optimizely.com). This route will be fetched via a &lt;code&gt;POST&lt;/code&gt; request to &lt;a href="https://logx.optimizely.com/v1/event" rel="noopener noreferrer"&gt;https://logx.optimizely.com/v1/event&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;One that allows us to retrieve the Optimizely datafile (if you are using Full Stack). This route will be fetched via a &lt;code&gt;GET&lt;/code&gt; request to &lt;a href="https://cdn.optimizely.com/datafiles" rel="noopener noreferrer"&gt;https://cdn.optimizely.com/datafiles&lt;/a&gt;. You can find your datafile URL inside your Project settings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2c6xyel81sesywntxifc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2c6xyel81sesywntxifc.png" alt="Add to these values your Optimizely snippet and/or datafile" width="799" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s how it should look like once properly filled out:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9rbq9pe958eeygjw7qkz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9rbq9pe958eeygjw7qkz.png" alt="With snippet and/or datafile filled out" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS will ask you for a confirmation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19fqgx969dr8lov63xym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19fqgx969dr8lov63xym.png" alt="Final review" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Change the ANY to be exactly the same as the method on the right-hand side. (POST, GET &amp;amp; GET)&lt;/p&gt;

&lt;p&gt;Congrats you now have a working API which will proxy requests to Optimizely.com.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Fetch Optimizely from your newly-created API gateway
&lt;/h3&gt;

&lt;p&gt;Now that we’ve got a working API, it’s time to update our website to start fetching from this API.&lt;/p&gt;

&lt;h4&gt;
  
  
  Optimizely Web
&lt;/h4&gt;

&lt;p&gt;Update your script tag that contains Optimizely to no longer fetch the file from cdn.optimizely.com but from your AWS API Gateway. You’ll find the invoke URL on the main API page, as such:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F52dtw7895p9vo9hawnrn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F52dtw7895p9vo9hawnrn.png" alt="The invoke URL for your API" width="800" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now to ensure the Optimizely snippet sends events to the API gateway instead of the default Optimizely endpoint, this is a custom snippet setting that can’t be configured by a customer. You’ll need to ask your account manager about it. They can amend your snippet to ensure the snippet dispatches events to your newly-created API.&lt;/p&gt;

&lt;h4&gt;
  
  
  Optimizely Full Stack
&lt;/h4&gt;

&lt;p&gt;For Full Stack, you’ll need to customize the SDK’s &lt;em&gt;createInstance&lt;/em&gt; method to include a new datafile URL pointing to your API, as such:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;optimizely&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@optimizely/optimizely-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;optimizelyClientInstance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optimizely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createInstance&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sdkKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;datafileOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;autoUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;urlTemplate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https:///datafiles/%s.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll also need to provide a custom event dispatcher which will dispatch the events back to your newly AWS API Gateway. You can use &lt;a href="https://github.com/optimizely/javascript-sdk/blob/master/lib/event_processor/event_dispatcher/default_dispatcher.browser.ts" rel="noopener noreferrer"&gt;this built-in event dispatcher&lt;/a&gt; and change line 39 to indicate the POST endpoint of your newly created API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/resolve-mau-overages-optimizely" rel="noopener noreferrer"&gt;How to resolve MAU overages in Optimizely Experimentation (step-by-step)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/events-full-stack-to-web" rel="noopener noreferrer"&gt;Send Events from Optimizely Full Stack to Optimizely Web&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/overriding-variation-assignments-optimizely-web" rel="noopener noreferrer"&gt;Overriding Variation Assignments in Optimizely: Complete Guide for Web and Feature Experimentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/optimizely-results-page" rel="noopener noreferrer"&gt;How to Read the Optimizely Results Page Correctly&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>abtesting</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Optimizely vs Amplitude: Experimentation Compared</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:09:34 +0000</pubDate>
      <link>https://dev.to/david_sert/optimizely-vs-amplitude-experimentation-compared-7on</link>
      <guid>https://dev.to/david_sert/optimizely-vs-amplitude-experimentation-compared-7on</guid>
      <description>&lt;p&gt;"Optimizely vs Amplitude" is a slightly unfair framing, because the two products start from opposite ends of the same problem. Optimizely is an experimentation platform that added analytics; Amplitude is a product analytics platform that added experimentation. Both can run an A/B test and tell you which variation won, but the path each takes — and the org each is built for — is different. This guide compares them honestly so you can pick the right tool for your situation, or decide to run both.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR of the difference
&lt;/h2&gt;

&lt;p&gt;Optimizely leads with experimentation. Its core products — Web Experimentation (visual, client-side testing) and Feature Experimentation (server-side feature flags and SDK-based tests) — are purpose-built to run a high volume of rigorous experiments across marketing pages, product surfaces, and backend logic, with the Stats Engine making continuous monitoring statistically safe.&lt;/p&gt;

&lt;p&gt;Amplitude leads with product analytics. Amplitude Experiment is an experimentation layer bolted onto a best-in-class behavioral analytics engine, so every test is automatically connected to deep funnel, retention, and cohort analysis on the same event data.&lt;/p&gt;

&lt;p&gt;If your primary question is "what should we test, where, and how do we govern a large testing program," Optimizely is the stronger center of gravity. If your primary question is "how do users behave, and can I experiment without leaving my analytics," Amplitude is compelling. They are not mutually exclusive — many teams run both.&lt;/p&gt;

&lt;p&gt;The decision comes down to two questions: what is the primary job, and who runs the tests?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A{What is the primary job?} --&amp;gt;|Run and govern experiments| B{Who builds most of the tests?}
  A --&amp;gt;|Deep behavioral analytics| C{Is Amplitude already your&amp;lt;br/&amp;gt;analytics system of record?}
  B --&amp;gt;|Marketers, in a visual editor| D[Optimizely Web Experimentation]
  B --&amp;gt;|Engineers, behind feature flags| E[Optimizely Feature Experimentation]
  C --&amp;gt;|Yes, flag-based product tests| F[Amplitude Experiment]
  C --&amp;gt;|No, and you test backend + marketing pages| G[Optimizely for experiments,&amp;lt;br/&amp;gt;Amplitude for analysis]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What each tool is built for
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Optimizely
&lt;/h3&gt;

&lt;p&gt;Optimizely splits experimentation into two complementary products. &lt;strong&gt;Web Experimentation&lt;/strong&gt; uses a WYSIWYG Visual Editor and a JavaScript snippet, letting marketers and optimization teams build A/B tests, redirect tests, and multivariate tests on a live site without engineering for most changes (custom HTML/JS is available when needed). &lt;strong&gt;Feature Experimentation&lt;/strong&gt; is developer-first: server-side and client-side SDKs (Java, Python, Go, C#, JavaScript/Node, Swift, Android, and more) wrap experiments in feature flags, so you can roll features out, roll them back instantly, and test backend logic such as algorithms, pricing, or APIs. Both share Optimizely's Stats Engine for results.&lt;/p&gt;

&lt;p&gt;The platform is built for &lt;strong&gt;experimentation breadth and program scale&lt;/strong&gt;: visual marketing tests and deep server-side tests under one roof, audience targeting, mutual exclusion groups, multi-armed bandits, and a free Rollouts tier for teams starting with feature flags.&lt;/p&gt;

&lt;h3&gt;
  
  
  Amplitude
&lt;/h3&gt;

&lt;p&gt;Amplitude's foundation is its analytics graph: every event a user fires is stored and queryable through funnels, retention curves, pathfinder, and behavioral cohorts. &lt;strong&gt;Amplitude Experiment&lt;/strong&gt; sits on top of that data, offering feature flags, server-side and client-side evaluation, and both feature-flag-based experiments and a web experiment capability. Its defining advantage is that experiment results read from the same event stream as the rest of your analytics — so a winning variation can immediately be sliced by any cohort or downstream behavior you already track.&lt;/p&gt;

&lt;p&gt;Amplitude is built for &lt;strong&gt;product teams who live in their analytics&lt;/strong&gt; and want experimentation to be a natural extension of behavioral analysis rather than a separate discipline.&lt;/p&gt;

&lt;p&gt;A note on the moving market: OpenAI acquired the experimentation vendor Statsig in September 2025, and in May 2026 Amplitude announced it was taking over the Statsig brand, platform, and customer base. That leaves Amplitude with two experimentation lineages (its native Experiment product and the Statsig platform), and some analysts have flagged near-term uncertainty about how the overlapping capabilities will consolidate. Treat any roadmap promises in this area as provisional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Head-to-head comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Optimizely&lt;/th&gt;
&lt;th&gt;Amplitude&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary use case&lt;/td&gt;
&lt;td&gt;Experimentation across web and product/backend&lt;/td&gt;
&lt;td&gt;Product analytics, with experimentation layered on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Experimentation model&lt;/td&gt;
&lt;td&gt;Visual (Web Experimentation) + feature flags / server-side SDKs (Feature Experimentation)&lt;/td&gt;
&lt;td&gt;Feature-flag-based experiments + web experiment capability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual/no-code testing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes — mature WYSIWYG Visual Editor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited; beyond simple changes, work tends to need engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side / SDK testing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Strong, broad SDK coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, via Experiment SDKs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Statistics engine&lt;/td&gt;
&lt;td&gt;Stats Engine (&lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;always-valid&lt;/a&gt;, sequential, controls false positives)&lt;/td&gt;
&lt;td&gt;Sequential testing, t-tests, &lt;a href="https://optipilot.com/data/cuped-variance-reduction" rel="noopener noreferrer"&gt;CUPED&lt;/a&gt;, multi-armed bandits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics depth&lt;/td&gt;
&lt;td&gt;Solid experiment analytics; warehouse-native analytics available&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Best-in-class behavioral analytics, cohorts, retention, funnels&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration model&lt;/td&gt;
&lt;td&gt;Connects to external analytics (including Amplitude); CMS/DXP ecosystem&lt;/td&gt;
&lt;td&gt;Experimentation tied directly to native event data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing model&lt;/td&gt;
&lt;td&gt;MAU-based (free Rollouts tier for flags)&lt;/td&gt;
&lt;td&gt;Event-volume based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ideal team&lt;/td&gt;
&lt;td&gt;Optimization, growth, and engineering teams running a structured testing program&lt;/td&gt;
&lt;td&gt;Product teams who already standardize on Amplitude analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Experimentation capabilities compared
&lt;/h2&gt;

&lt;p&gt;The clearest difference is &lt;strong&gt;where and how you can test&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Optimizely covers two distinct surfaces well. For marketing and front-end teams, the Web Experimentation Visual Editor makes it straightforward to change copy, layout, and styling and ship an A/B test without a deploy. For engineering and product teams, Feature Experimentation runs tests behind feature flags directly in application code — including server-side paths where there is no DOM to manipulate, such as recommendation algorithms, checkout logic, or API behavior. Flags double as a kill switch, so a bad variation can be turned off remotely without redeploying. Optimizely also supports targeted rollouts, mutual exclusion, and &lt;a href="https://optipilot.com/data/multi-armed-bandit" rel="noopener noreferrer"&gt;multi-armed bandit&lt;/a&gt; optimization.&lt;/p&gt;

&lt;p&gt;Amplitude Experiment is primarily &lt;strong&gt;flag-based&lt;/strong&gt;: you gate a feature behind a flag, evaluate it client- or server-side, and measure the result against your event data. This is a clean model for product experimentation and progressive rollouts. Its web experimentation capability exists, but for anything beyond simple changes, practitioners generally report that meaningful variations require engineering involvement rather than a marketer-friendly visual workflow. If a large share of your testing is on marketing pages built by non-engineers, that distinction matters.&lt;/p&gt;

&lt;p&gt;On the statistics, both are credible. Optimizely's &lt;strong&gt;Stats Engine&lt;/strong&gt; is designed for "always-valid" inference — you can monitor results continuously without inflating false-positive rates, which suits teams that watch dashboards daily. Amplitude likewise uses &lt;strong&gt;sequential testing&lt;/strong&gt; for valid-anytime results, and supports t-tests, CUPED for variance reduction, multi-armed bandits, mutual exclusion groups, and holdouts. Neither team should feel they are settling for weak statistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analytics and data compared
&lt;/h2&gt;

&lt;p&gt;This is where Amplitude's genuine strength shows. Because Experiment runs on the same event pipeline as Amplitude Analytics, a result is never a dead end: you can immediately ask "did the winning variation help retention at day 30," "how did it perform for this behavioral cohort," or "where in the funnel did the lift come from" without exporting data or stitching IDs across tools. For teams whose core competency is behavioral analysis, that tight loop is the main reason to consider Amplitude Experiment over a standalone testing tool.&lt;/p&gt;

&lt;p&gt;Optimizely's analytics are good for reading experiment results and, increasingly, offers warehouse-native analytics that let you analyze decision and event data alongside the rest of your data in Snowflake, BigQuery, or Databricks. But product analytics is not its historical center of gravity the way it is Amplitude's. If your team's daily home is rich cohort and retention analysis, Amplitude will feel more native; if your team's daily home is running and governing experiments, Optimizely will.&lt;/p&gt;

&lt;p&gt;A practical caveat on cost: Amplitude's &lt;strong&gt;event-volume-based pricing&lt;/strong&gt; can scale quickly for high-traffic products, whereas Optimizely prices experimentation on monthly active users and offers a free Rollouts tier for feature flags. Model both against your actual traffic before deciding — the cheaper option depends heavily on your event volume versus user count.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to choose Optimizely
&lt;/h2&gt;

&lt;p&gt;Optimizely is the stronger choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Experimentation is the primary job&lt;/strong&gt;, not a feature of your analytics tool. You want one platform that handles both marketing-page tests and deep server-side product tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-engineers run a meaningful share of tests.&lt;/strong&gt; The Visual Editor lets growth and marketing teams ship experiments without a deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You test in the backend&lt;/strong&gt; — algorithms, pricing, infrastructure changes — where flag-based, server-side experimentation and an instant kill switch are essential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You run a high-volume, governed program&lt;/strong&gt; and want mutual exclusion, bandits, audience targeting, and always-valid statistics in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are starting with feature flags&lt;/strong&gt; and want a free on-ramp via Rollouts before scaling up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Amplitude makes sense
&lt;/h2&gt;

&lt;p&gt;Amplitude Experiment is the better fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amplitude is already your analytics system of record&lt;/strong&gt; and most of your team's decisions start in its funnels, cohorts, and retention reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your experimentation is product-led and flag-based&lt;/strong&gt; — gating features, progressive rollouts, and measuring impact on behavioral metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The analysis loop matters more than test authoring breadth.&lt;/strong&gt; You value being able to slice any result by behavioral cohort instantly, on the same data, without integration work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are not relying on a visual, marketer-driven editor&lt;/strong&gt; for the bulk of your tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be deliberate here given the current consolidation around the acquired Statsig platform: confirm which experimentation product a vendor is steering you toward and what its supported roadmap looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  How they work together
&lt;/h2&gt;

&lt;p&gt;These tools are not an either/or for many organizations, and the most pragmatic answer is often "both." A common pattern is to &lt;strong&gt;run experiments in Optimizely and analyze them in Amplitude&lt;/strong&gt;: Optimizely decides which variation a user sees and provides the statistical results, while the variation a user was bucketed into is sent into Amplitude as a user property or event, so you can analyze experiment impact against your full behavioral dataset.&lt;/p&gt;

&lt;p&gt;This is a supported, documented integration. Optimizely Feature Experimentation can forward decision data to Amplitude via a decision notification listener (setting an &lt;code&gt;[Optimizely] &amp;lt;flagKey&amp;gt;&lt;/code&gt; user property and an optional impression event), and Optimizely Web Experimentation integrates through custom analytics extensions, with the connector built and maintained by Amplitude. If your team has standardized on Amplitude for analytics but wants Optimizely's experimentation breadth and visual testing, this combination gives you the best of both rather than forcing a single choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Pick based on where your center of gravity sits. If experimentation is the discipline you are investing in — across marketing, product, and backend — Optimizely is built for that job and gives non-engineers and engineers a shared platform with rigorous statistics. If best-in-class behavioral analytics is your foundation and you want experimentation as a tightly coupled extension of it, Amplitude Experiment earns its place. And if you have both needs, the supported integration means you do not have to choose: experiment in Optimizely, analyze in Amplitude.&lt;/p&gt;

&lt;p&gt;This site covers both Optimizely products in implementation depth. Browse the &lt;a href="https://optipilot.com/feature-experimentation" rel="noopener noreferrer"&gt;Feature Experimentation guides&lt;/a&gt; and the &lt;a href="https://optipilot.com/web-experimentation" rel="noopener noreferrer"&gt;Web Experimentation guides&lt;/a&gt;, or start from the &lt;a href="https://optipilot.com/feature-experimentation/integrations" rel="noopener noreferrer"&gt;integration catalog&lt;/a&gt;. Not sure which platform fits your stack? &lt;a href="https://optipilot.com/contact" rel="noopener noreferrer"&gt;Contact us&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/feature-experimentation/integrations/amplitude" rel="noopener noreferrer"&gt;Integrate Amplitude with Optimizely Feature Experimentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/web-experimentation/integrations/amplitude" rel="noopener noreferrer"&gt;Integrate Amplitude with Optimizely Web Experimentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/strategy/testing-ai-models-with-feature-flags" rel="noopener noreferrer"&gt;Testing AI Models with Feature Flags: LLM Prompt Optimization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>abtesting</category>
      <category>analytics</category>
      <category>optimizely</category>
      <category>saas</category>
    </item>
    <item>
      <title>How the Optimizely Stats Engine Works</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:08:58 +0000</pubDate>
      <link>https://dev.to/david_sert/how-the-optimizely-stats-engine-works-3j6a</link>
      <guid>https://dev.to/david_sert/how-the-optimizely-stats-engine-works-3j6a</guid>
      <description>&lt;p&gt;Optimizely's Stats Engine is the statistical framework behind every winner, loser, and "inconclusive" verdict on the Experiment Results page. It exists to solve a specific, expensive problem: practitioners watch experiments in real time and make decisions the moment a result "looks significant," and traditional statistics punishes that behavior with a flood of false positives. Understanding how Stats Engine works is the difference between trusting a result and acting on noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Stats Engine Is and the Problem It Solves
&lt;/h2&gt;

&lt;p&gt;Stats Engine is Optimizely's proprietary statistical methodology for evaluating A/B tests. It is a &lt;strong&gt;frequentist&lt;/strong&gt; engine built on &lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;&lt;strong&gt;sequential testing&lt;/strong&gt;&lt;/a&gt; combined with &lt;a href="https://optipilot.com/data/false-discovery-rate-control" rel="noopener noreferrer"&gt;&lt;strong&gt;false discovery rate&lt;/strong&gt;&lt;/a&gt;** (FDR) control**. Those two mechanisms are the whole story, and each addresses a distinct failure mode of classical A/B testing.&lt;/p&gt;

&lt;p&gt;The first failure mode is the &lt;strong&gt;peeking problem&lt;/strong&gt;. Traditional fixed-horizon statistics (the t-test being the canonical example) are only valid if you commit to a &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;sample size&lt;/a&gt; in advance, wait until you reach it, and look exactly once. Every time you check results early and react, you give random noise another chance to cross the significance threshold. A test that peeks repeatedly can report a false "winner" far more often than its stated error rate suggests. But waiting passively for a pre-computed sample size is exactly what real experimentation teams do not do.&lt;/p&gt;

&lt;p&gt;The second failure mode is the &lt;strong&gt;multiple comparisons problem&lt;/strong&gt;. Real experiments rarely track one metric on one variation. Add more variations and more metrics and the chance of at least one false positive climbs quickly, even though each individual test holds its error rate. Worse, the rate that actually matters to a decision-maker, the proportion of false positives &lt;em&gt;among the results you act on&lt;/em&gt;, is higher still.&lt;/p&gt;

&lt;p&gt;Stats Engine is engineered so that both problems are handled automatically. As Optimizely's documentation states, results are &lt;strong&gt;always valid&lt;/strong&gt;: you can monitor a test continuously and stop as soon as you have a clear winner, without invalidating it, because the engine controls the false discovery rate throughout.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Sequential Testing Solves the Peeking Problem
&lt;/h2&gt;

&lt;p&gt;Sequential testing is the mechanism that makes continuous monitoring safe. Rather than computing a single p-value at one fixed endpoint, Stats Engine evaluates the experiment as evidence accumulates over time and produces inferences that remain valid no matter when you look.&lt;/p&gt;

&lt;p&gt;Optimizely's own framing uses a baking analogy that is worth keeping in mind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fixed Horizon : Set a timer before baking. You may only open the oven
                when the timer ends. Open it early and the result is unreliable.
Sequential    : Put the cake in without committing to a time. Open the oven
                whenever you like to check; looking never ruins the result.
                When it looks done, it is done.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mechanically, Stats Engine does not compute one confidence interval. It computes a &lt;strong&gt;series of 100 successive confidence intervals&lt;/strong&gt; across the experiment's lifetime, each with its own significance value. The numbers you see on the Results page are deliberately conservative summaries of that series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://optipilot.com/tools/statistical-significance-calculator" rel="noopener noreferrer"&gt;&lt;strong&gt;statistical significance&lt;/strong&gt;&lt;/a&gt; shown is the &lt;em&gt;smallest&lt;/em&gt; significance value observed across those sequential intervals, not the average and not the latest.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;confidence interval&lt;/strong&gt; shown is the &lt;em&gt;running intersection&lt;/em&gt; of all prior intervals: it tracks the smallest upper limit and the largest lower limit seen during the run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because of this, the displayed significance and interval may not exactly match the currently observed conversion rates. That is intentional. It is what makes the result robust to having been observed many times.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Significance Climbs (and Occasionally Drops)
&lt;/h3&gt;

&lt;p&gt;In a stable environment, significance should rise in a stepwise, generally increasing fashion as evidence accumulates. Two forms of evidence move it: &lt;strong&gt;larger differences&lt;/strong&gt; between conversion rates, and &lt;strong&gt;differences that persist across more visitors&lt;/strong&gt;. Early on, when the sample is small, large swings are treated conservatively, so you often see a flat line that later rises sharply once real evidence accrues.&lt;/p&gt;

&lt;p&gt;Significance can also fall, though Optimizely's analysis indicates this happens in only about 4% of experiments. Small dips of a few percentage points come from &lt;strong&gt;time bucketing&lt;/strong&gt;: Optimizely divides the experiment's runtime into 100 equal buckets that grow as the test runs, reshuffling visitors among them and recomputing as it goes, which produces minor fluctuations. Larger drops, potentially all the way to 0%, come from a &lt;strong&gt;stats reset&lt;/strong&gt;, a protective mechanism that triggers when the engine detects that the underlying environment has changed (the assumption that data is identically distributed has been violated). A reset is the engine refusing to stand behind a conclusion the new evidence no longer supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  False Discovery Rate Control vs Traditional Significance
&lt;/h2&gt;

&lt;p&gt;The second pillar is what makes Stats Engine trustworthy when you run many metrics and variations. The naive error metric is the &lt;strong&gt;false positive rate&lt;/strong&gt;: out of all the comparisons where there is truly no effect, what fraction are wrongly flagged? Optimizely controls something more decision-relevant, the &lt;strong&gt;false discovery rate&lt;/strong&gt;: out of the results you would actually act on (the declared winners and losers), what fraction are wrong?&lt;/p&gt;

&lt;p&gt;The distinction matters enormously. Optimizely's worked example: an experiment with ten comparison opportunities reports two winners, one of which is a false winner. Measured as a false positive rate, that is 1 in 10, about 10%, which sounds acceptable. But you do not implement the eight inconclusive results; you implement the two winners. Among those, your error rate is 1 in 2, or &lt;strong&gt;50%&lt;/strong&gt;. The false discovery rate captures the risk that actually reaches your roadmap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;False positive rate = false positives / all null comparisons   = 1/10 = 10%
False discovery rate = false positives / declared discoveries   = 1/2  = 50%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To control FDR across many hypotheses, Stats Engine uses a &lt;strong&gt;tiered version of the Benjamini-Hochberg procedure&lt;/strong&gt;. The tiering reflects that not all metrics deserve equal weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary metric (rank 1)&lt;/strong&gt; — evaluated independently of all others, so it reaches significance as fast as possible and is unaffected by how many other metrics you track.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secondary metrics (ranks 2-5)&lt;/strong&gt; — their significance threshold is adjusted for the number of metrics and variations. Adding more secondary metrics can slow each of them to significance, but never slows the primary metric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring metrics (rank 6+)&lt;/strong&gt; — each given a fractional weight of 1/n, so they have minimal impact on secondary metrics and none on the primary metric.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical payoff: Optimizely keeps the false discovery rate low (approximately 10%) while still letting your most important metric reach significance quickly. This is also why Stats Engine uses &lt;strong&gt;two-tailed tests&lt;/strong&gt;, which are required for FDR control.&lt;/p&gt;

&lt;p&gt;One caveat the documentation is explicit about: FDR control is &lt;strong&gt;not&lt;/strong&gt; maintained when you &lt;strong&gt;segment&lt;/strong&gt; results. The deeper you slice, the higher your chance of finding a spurious "significant" segment. Use segments for exploration, not for decisions, and only inspect the most meaningful ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Read Significance and Confidence Intervals
&lt;/h2&gt;

&lt;p&gt;Statistical significance answers a precise question: how unusual would these results be if the variation and baseline truly performed identically? At 90% significance, you are accepting roughly a 10% false-positive risk on that call. The confidence interval is the estimated range that likely contains the true effect (the true uplift), and Optimizely sets its confidence level to match your project's significance threshold (90% by default).&lt;/p&gt;

&lt;p&gt;The single most useful rule for reading the Results page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A variation reaches significance exactly when its confidence interval &lt;strong&gt;stops crossing zero&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confidence interval entirely above 0%&lt;/strong&gt; means a winning variation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence interval includes 0%&lt;/strong&gt; means inconclusive (you cannot yet rule out "no effect").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence interval entirely below 0%&lt;/strong&gt; means a losing variation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before declaring anything, Stats Engine enforces minimum data thresholds. For &lt;strong&gt;binary metrics&lt;/strong&gt;, it requires at least 100 visitors or sessions and at least 25 conversions in both the baseline and the variation. For &lt;strong&gt;numeric metrics&lt;/strong&gt; such as revenue, it requires at least 100 visitors or sessions but no fixed conversion count. Until those are met, the page reports that more visitors are needed and estimates the wait.&lt;/p&gt;

&lt;p&gt;A useful judgment heuristic from the documentation: if the &lt;strong&gt;observed mean (the tick mark)&lt;/strong&gt; sits near the &lt;strong&gt;edge&lt;/strong&gt; of the confidence interval, the engine may be accumulating evidence against its own conclusion, so consider waiting. If the observed mean sits near the &lt;strong&gt;center&lt;/strong&gt;, you can be more confident the call will hold.&lt;/p&gt;

&lt;p&gt;For revenue-per-visitor goals, be aware that revenue distributions are heavily skewed, which reduces &lt;a href="https://optipilot.com/data/ab-test-sample-size-statistical-power" rel="noopener noreferrer"&gt;statistical power&lt;/a&gt;. Stats Engine applies &lt;strong&gt;skew correction&lt;/strong&gt; to recover some of that power and to shape the confidence interval correctly, but skewed metrics still generally need more data than binary ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stats Accelerator: A Separate Feature, Not the Engine
&lt;/h2&gt;

&lt;p&gt;Stats Accelerator is frequently conflated with Stats Engine. They are different things. Stats Engine is the statistical methodology that evaluates results. &lt;strong&gt;Stats Accelerator is a traffic-allocation feature&lt;/strong&gt; that sits on top of it and uses a multi-armed-bandit-family algorithm (a variation on the Upper Confidence Bound strategy) to shorten the &lt;em&gt;time&lt;/em&gt; to statistical significance.&lt;/p&gt;

&lt;p&gt;Stats Accelerator monitors a running experiment and routes more traffic toward the variation showing the &lt;strong&gt;most significant difference from the baseline&lt;/strong&gt;, regardless of whether that difference is positive or negative, because its goal is to minimize &lt;em&gt;time&lt;/em&gt;, not &lt;em&gt;regret&lt;/em&gt;. Once a variation reaches significance, it is removed from consideration and traffic is redistributed to the rest. It requires at least three variations (a baseline plus two). It still produces statistical significance, because the underlying engine is still doing the inference.&lt;/p&gt;

&lt;p&gt;This is distinct from a true &lt;a href="https://optipilot.com/data/multi-armed-bandit" rel="noopener noreferrer"&gt;&lt;strong&gt;multi-armed bandit&lt;/strong&gt;&lt;/a&gt;** (MAB) optimization** (formerly "Accelerate Impact"), which minimizes &lt;em&gt;regret&lt;/em&gt; by funneling traffic to whichever variation currently performs best on the primary metric. MABs are for temporary, value-maximizing scenarios such as a Black Friday promotion, and crucially &lt;strong&gt;MAB optimizations do not generate statistical significance at all&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Produces significance?&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stats Accelerator&lt;/td&gt;
&lt;td&gt;Minimize time to significance&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;You want a reliable winner faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-armed bandit&lt;/td&gt;
&lt;td&gt;Maximize reward / minimize regret&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Short-lived campaigns; no permanent decision needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Because Stats Accelerator changes traffic allocation mid-flight, it risks a sampling bias called &lt;strong&gt;Simpson's Paradox&lt;/strong&gt; when conversion rates vary over time (for example, weekday-vs-weekend seasonality). Optimizely addresses this with the &lt;strong&gt;Epoch Stats Engine&lt;/strong&gt;, which produces a stratified, &lt;strong&gt;weighted improvement&lt;/strong&gt; estimate, comparing baseline and variation &lt;em&gt;within each interval&lt;/em&gt; between allocation changes, then combining those intervals by visitor count. This is also why, with Stats Accelerator enabled, the Results page may report both absolute (in percentage points) and relative improvement. For Feature Experimentation, use a user profile service (sticky bucketing) so frequent reallocation does not expose one visitor to multiple variations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Misconceptions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is the Optimizely Stats Engine Bayesian?
&lt;/h3&gt;

&lt;p&gt;No. This is the most common misconception, and it is worth correcting precisely. Stats Engine is a &lt;strong&gt;frequentist sequential&lt;/strong&gt; method. Optimizely does offer a separate, explicitly &lt;strong&gt;Bayesian&lt;/strong&gt; A/B testing mode (which expresses results as direct probabilities like "90% chance B beats A"), and a separate &lt;strong&gt;Frequentist Fixed Horizon&lt;/strong&gt; mode. But the classic Stats Engine, the one that powers sequential testing, is frequentist. It reports statistical significance and frequentist confidence intervals, not posterior probabilities. The reason a search for "Optimizely Bayesian" surfaces Stats Engine at all is that both Bayesian and sequential methods let you peek and stop early. That shared &lt;em&gt;behavior&lt;/em&gt; does not make them the same &lt;em&gt;methodology&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stats Engine results disagree with my t-test, so they must be wrong
&lt;/h3&gt;

&lt;p&gt;They can legitimately disagree, and Stats Engine is the more trustworthy of the two when you have been monitoring continuously. A t-test uses only the &lt;em&gt;currently observed&lt;/em&gt; mean and difference, so if evidence looked strong early and weakened later, a t-test can still report the stale, strong result. Stats Engine's intersected intervals are more conservative, less likely to declare a false winner, and less likely to reverse a call later.&lt;/p&gt;

&lt;h3&gt;
  
  
  I can keep slicing segments until something is significant
&lt;/h3&gt;

&lt;p&gt;You can, but you will be manufacturing false discoveries. FDR control does not extend across segments. Repeated segment-hunting inflates false positives exactly like peeking does.&lt;/p&gt;

&lt;h3&gt;
  
  
  A stats reset means the tool is broken
&lt;/h3&gt;

&lt;p&gt;The opposite. A reset means the engine detected that the environment changed and is protecting you from standing behind a conclusion the new data no longer supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Guidance for Trusting Your Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rank your metrics deliberately.&lt;/strong&gt; Put the metric that defines success as the primary metric, ideally measured close to the change in the funnel. It gets independent, fastest-to-significance treatment; everything else is secondary or monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the test run to its planned duration even though you can peek.&lt;/strong&gt; Sequential validity means peeking will not break your stats, but a result that has barely cleared the threshold on thin data is fragile. Treat experimentation as a standardized process, not a dashboard you babysit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the confidence interval, not just the significance number.&lt;/strong&gt; Width tells you precision; position relative to zero tells you direction; the tick mark's position warns you whether a call is at risk of reversing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use segments to explore, never to decide.&lt;/strong&gt; If a segment looks interesting, treat it as a hypothesis for a new, properly powered experiment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the method to the intent.&lt;/strong&gt; Use a standard A/B test (sequential Stats Engine) when you need a trustworthy decision; add Stats Accelerator to reach that decision faster; use a multi-armed bandit only for temporary value-maximization where you do not need a statistically defensible winner.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For how the three inference families differ in practice, see &lt;a href="https://optipilot.com/data/bayesian-vs-frequentist-vs-sequential-optimizely" rel="noopener noreferrer"&gt;Bayesian vs frequentist vs sequential testing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/optimizely-results-page" rel="noopener noreferrer"&gt;How to Read the Optimizely Results Page Correctly&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/event-properties-vs-user-attributes" rel="noopener noreferrer"&gt;Event Properties vs User Attributes in Optimizely&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/resolve-mau-overages-optimizely" rel="noopener noreferrer"&gt;Optimizely MAUs: What Counts as a Monthly Active User &amp;amp; How to Reduce Overages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>statistics</category>
      <category>abtesting</category>
      <category>optimizely</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Feature Flag Best Practices for Production Systems</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:07:11 +0000</pubDate>
      <link>https://dev.to/david_sert/feature-flag-best-practices-for-production-systems-3238</link>
      <guid>https://dev.to/david_sert/feature-flag-best-practices-for-production-systems-3238</guid>
      <description>&lt;p&gt;Your flag count has quietly crossed a threshold. What started as a clean way to ship a risky feature behind a toggle is now a few hundred conditionals scattered across services, half of them permanently on, nobody quite sure which are safe to delete. Someone flips the wrong one during an incident. A test passes locally and fails in CI because a flag defaulted differently. This is the predictable midlife of feature flagging: the mechanism is trivial, but the discipline around it is what separates teams who ship faster from teams who have simply moved their risk somewhere less visible.&lt;/p&gt;

&lt;p&gt;The good news is that the disciplines are well understood and largely independent of any one vendor. This article lays them out, then shows how they map onto Optimizely Feature Experimentation specifically, so that the practices have a concrete home in tooling rather than living only in a wiki page nobody reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify every flag by type and lifespan
&lt;/h2&gt;

&lt;p&gt;The single most useful idea in feature flagging is that not all flags are the same thing. Martin Fowler's taxonomy splits them into four categories, and the reason it matters is that each has a different owner, a different lifespan, and a different failure mode when you ignore it (&lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;Fowler, Feature Toggles&lt;/a&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Release toggles&lt;/strong&gt; hide in-progress work so it can merge to trunk and ship dormant. They are the shortest-lived flags you own, meant to survive days or weeks, and they should be deleted the moment the feature is fully rolled out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experiment toggles&lt;/strong&gt; split traffic between variations to measure an outcome. They live exactly as long as the test needs to reach a decision, then collapse to the winning variation. This is the layer beneath A/B testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ops toggles&lt;/strong&gt; give operators control over system behavior: degrade an expensive feature under load, disable a flaky dependency, or trip a kill switch during an incident. Some are short-lived, but a genuine kill switch may live indefinitely as a safety control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissioning toggles&lt;/strong&gt; decide who gets access to what, such as gating a feature to premium accounts. These can legitimately live for years and are effectively part of your product, not temporary scaffolding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fowler frames these along two axes: &lt;strong&gt;longevity&lt;/strong&gt; (how long the flag should exist) and &lt;strong&gt;dynamism&lt;/strong&gt; (whether the decision is fixed per deployment or must be re-evaluated per request). The categories fall at different points, and conflating them is the root of most flag debt. A release toggle that gets treated like a permanent ops switch never gets cleaned up, because nobody remembers it was supposed to be temporary.&lt;/p&gt;

&lt;p&gt;The practical takeaway is to make the type explicit at creation. Decide, before you write the flag, whether it is a release, experiment, ops, or permissioning flag, and record that alongside an owner and an expected removal or review date. In Optimizely, flag rules make the distinction concrete: an A/B test rule is an experiment flag, while a targeted delivery is a release or ops flag. If you are weighing a fixed split against an adaptive one for an experiment flag, the trade-offs are covered in &lt;a href="https://optipilot.com/data/stats-accelerator-vs-mab-vs-contextual-bandit" rel="noopener noreferrer"&gt;Stats Accelerator vs multi-armed bandit vs contextual bandit&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[New flag] --&amp;gt; B{What is its job?}
    B --&amp;gt;|Hide in-progress work| C[Release toggle&amp;lt;br/&amp;gt;lifespan: days-weeks&amp;lt;br/&amp;gt;delete after rollout]
    B --&amp;gt;|Split traffic to measure| D[Experiment toggle&amp;lt;br/&amp;gt;lifespan: the test&amp;lt;br/&amp;gt;collapse to winner]
    B --&amp;gt;|Operator control / kill switch| E[Ops toggle&amp;lt;br/&amp;gt;lifespan: short, or&amp;lt;br/&amp;gt;indefinite for safety]
    B --&amp;gt;|Gate access by plan/role| F[Permissioning toggle&amp;lt;br/&amp;gt;lifespan: years&amp;lt;br/&amp;gt;part of the product]
    C --&amp;gt; G[Book its removal now]
    D --&amp;gt; G
    E --&amp;gt; H[Owner + review date]
    F --&amp;gt; H
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Name flags so they stay searchable at scale
&lt;/h2&gt;

&lt;p&gt;At ten flags, naming is a matter of taste. At three hundred, it is the difference between a flag you can find and a flag you are afraid to touch. A good flag key is greppable across your codebase, self-describing, and encodes enough context that someone who did not create it can guess its purpose and owner.&lt;/p&gt;

&lt;p&gt;A convention worth adopting encodes, in a consistent order, the owning team or domain, the flag's intent, and where it applies. Something like &lt;code&gt;checkout_new-address-form_release&lt;/code&gt; or &lt;code&gt;search_semantic-ranking_experiment&lt;/code&gt; reads cleanly, sorts sensibly, and tells a reader the type without opening a dashboard. Keep to one delimiter, avoid spaces and punctuation, and never reuse a retired key for a new purpose. OptiPilot's free &lt;a href="https://optipilot.com/tools/flag-naming-generator" rel="noopener noreferrer"&gt;feature flag naming convention generator&lt;/a&gt; turns a flag's type, team, and feature into a consistent key across dot, kebab, and snake styles, so a whole team names flags the same way.&lt;/p&gt;

&lt;p&gt;Naming discipline pays off precisely because flag keys are the join between your code and your configuration. Optimizely makes this contract literal: a rule's key cannot be changed after creation, and if the key referenced in your &lt;code&gt;decide&lt;/code&gt; call does not match a key in the platform, no traffic is served to that rule (&lt;a href="https://support.optimizely.com/hc/en-us/articles/38908008297741-Manage-rules" rel="noopener noreferrer"&gt;Manage rules&lt;/a&gt;). A typo is not a loud error; it is a silent no-op that quietly serves the fallback. Consistent, reviewed names make that class of mistake far easier to catch in code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll out gradually and keep a kill switch
&lt;/h2&gt;

&lt;p&gt;The reason flags earn their keep in production is that they decouple deploy from release. You ship code dark, then turn it on for a slice of traffic, watch, and widen the slice, or reverse instantly if something breaks. Two properties make this safe: a gradual ramp and a fast reversal.&lt;/p&gt;

&lt;p&gt;A gradual ramp means starting small. Optimizely's own guidance for feature rollouts recommends 10% as a starting allocation before widening (&lt;a href="https://support.optimizely.com/hc/en-us/articles/45552846481037-Run-Feature-Rollouts-in-Feature-Experimentation" rel="noopener noreferrer"&gt;Run Feature Rollouts&lt;/a&gt;). You can also target the ramp: deliver to internal users first, then beta customers, then the general population, using audience conditions rather than a blunt percentage. Optimizely's targeted delivery supports exactly this, letting you roll out to a percentage of a specific audience and increase it over time (&lt;a href="https://support.optimizely.com/hc/en-us/articles/38932063198989-Flag-delivery-targeted-delivery-overview" rel="noopener noreferrer"&gt;Flag delivery overview&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Fast reversal is the kill switch. Because flag changes take effect through configuration rather than a code deploy, you can roll a feature back in seconds. In Optimizely, deliveries take effect without a code deployment, and you can toggle a flag on and off without re-bucketing the users already assigned, as long as you do not change the traffic allocation at the same moment (&lt;a href="https://support.optimizely.com/hc/en-us/articles/38938396924941-Run-flag-deliveries-targeted-delivery" rel="noopener noreferrer"&gt;Run flag deliveries&lt;/a&gt;). That last caveat matters operationally: an incident kill switch should flip the flag off, not simultaneously re-slice traffic, or you will disturb bucketing for everyone else.&lt;/p&gt;

&lt;p&gt;There is a subtle trap in ramping that shows up most in experiment flags. Bucketing is deterministic: Optimizely hashes the user ID to assign a variation, so the same user gets the same experience as long as the distribution has not changed (&lt;a href="https://support.optimizely.com/hc/en-us/articles/38931713970189-Core-concepts-of-Feature-Experimentation" rel="noopener noreferrer"&gt;Core concepts&lt;/a&gt;). Change the traffic allocation on a running experiment and users can be re-bucketed, which both disturbs their experience and can skew results. If you need to change allocation mid-flight, use a user profile service to keep bucketing sticky, and watch for &lt;a href="https://optipilot.com/data/sample-ratio-mismatch" rel="noopener noreferrer"&gt;sample ratio mismatch&lt;/a&gt;, the canary that tells you your split is not landing the way you configured it.&lt;/p&gt;

&lt;p&gt;One decision to make deliberately: whether a given flag needs measurement. Optimizely separates a plain &lt;strong&gt;targeted delivery&lt;/strong&gt; (free, no analytics, dispatches no decision events) from a &lt;strong&gt;feature rollout&lt;/strong&gt; (full analytics, consumes monthly active users and impressions). Use a targeted delivery for a simple release or kill switch where you only need on/off control, and a rollout or A/B test when you actually want to measure impact. Reaching for the analytics-bearing rule on a flag you will never analyze is a common and needless cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environments and the datafile: where flags actually live
&lt;/h2&gt;

&lt;p&gt;Flags are configuration, and configuration needs somewhere to live per stage of your pipeline. In Optimizely, a project contains one or more &lt;strong&gt;environments&lt;/strong&gt;, typically staging and production, and each environment holds its own ruleset for a flag. You can run a flag at 100% in development and 10% in production, or configure entirely different rules per environment, with separate permissions for who can change each (&lt;a href="https://support.optimizely.com/hc/en-us/articles/38931713970189-Core-concepts-of-Feature-Experimentation" rel="noopener noreferrer"&gt;Core concepts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;At runtime the SDK does not call back to Optimizely on every decision. It downloads a &lt;strong&gt;datafile&lt;/strong&gt;, a JSON snapshot of your flags and rules for one environment, and evaluates decisions locally against it. When you edit a rule, the datafile updates within a few seconds, and how quickly that reaches production depends on how often your app fetches it; webhooks let you push updates in near real time (&lt;a href="https://support.optimizely.com/hc/en-us/articles/38908008297741-Manage-rules" rel="noopener noreferrer"&gt;Manage rules&lt;/a&gt;). This architecture is why flag evaluation is fast and why it keeps working during a brief network blip: decisions are computed from an in-memory config, not a synchronous network round trip.&lt;/p&gt;

&lt;p&gt;It also defines your failure mode, which is the next thing to get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test safely with flags and sensible fallbacks
&lt;/h2&gt;

&lt;p&gt;Flags multiply the states your system can be in, and untended they make tests flaky and behavior unpredictable. Three habits keep that under control.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;always have a safe default&lt;/strong&gt;. Design so that the flag being off, or the configuration being unavailable, yields a working, conservative experience. Optimizely's &lt;code&gt;decide&lt;/code&gt; method returns an &lt;code&gt;OptimizelyDecision&lt;/code&gt; object whose &lt;code&gt;enabled&lt;/code&gt; field is false until a rule turns the flag on, and on a critical error, such as the SDK not being ready or an unknown flag key, it returns a &lt;code&gt;null&lt;/code&gt;&lt;code&gt;variationKey&lt;/code&gt; and populates &lt;code&gt;reasons&lt;/code&gt; (&lt;a href="https://docs.developers.optimizely.com/feature-experimentation/docs/decide-methods-for-the-javascript-sdk" rel="noopener noreferrer"&gt;Decide methods&lt;/a&gt;). Your code should treat that state as "off" and fall back to a value you control, rather than assuming a variable will be present. For resilience against the SDK not having a datafile yet, you can also bundle a datafile with the application so the client can initialize synchronously from a known-good snapshot.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;decouple the decision from the decision point&lt;/strong&gt;. Fowler's advice is to avoid scattering raw flag checks throughout the codebase, because changing the logic then means hunting down every call site. Instead, funnel decisions through a single abstraction, a &lt;code&gt;FeatureDecisions&lt;/code&gt; object or equivalent, so the toggle point (where behavior branches) is separated from the toggle router (how the decision is made). This keeps business logic testable without the flagging system present, and confines flag removal to one place.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;test the states you actually ship&lt;/strong&gt;. At minimum, exercise the flag-on and flag-off paths, since both are real production states. Wrap decisions so tests can inject a decision directly rather than standing up the SDK, which removes network dependence and the flakiness that comes with it. For the human side of pre-launch verification, a structured pass like the &lt;a href="https://optipilot.com/tools/experiment-qa-checklist" rel="noopener noreferrer"&gt;experiment QA checklist&lt;/a&gt; catches the targeting, bucketing, and event-tracking mistakes that unit tests miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pay down flag debt before it compounds
&lt;/h2&gt;

&lt;p&gt;Every flag is inventory, and inventory has a carrying cost. Fowler's framing is blunt: "savvy teams view their feature toggles in their codebase as inventory which comes with a carrying cost and seek to keep that inventory as low as possible." Each live flag is an extra branch to reason about, an extra combination to test, and an extra thing that can be misconfigured. Stale flags are the single most common feature-flag problem, and they accumulate because deleting a flag is nobody's urgent job.&lt;/p&gt;

&lt;p&gt;The fix is to make cleanup a scheduled, owned activity rather than a good intention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book the removal when you create the flag.&lt;/strong&gt; For every release toggle, add a removal task to the backlog at the moment it is introduced. The work of retiring it is then already tracked, not rediscovered months later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give short-lived flags an expiry.&lt;/strong&gt; Record an expected removal date on release and experiment flags. Some teams go further and build "time bombs" that fail a test, or even refuse to start the application, once a flag outlives its expiration date, converting silent debt into a loud, actionable failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap the inventory.&lt;/strong&gt; A Lean-style limit on the total number of live flags forces a retirement before a new flag can be added, keeping the pile bounded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the whole set on a cadence.&lt;/strong&gt; Optimizely's flags dashboard gives you the raw material: every flag with its status, creator, and last-modified date, filterable by environment and audience (&lt;a href="https://support.optimizely.com/hc/en-us/articles/30715996139789-Custom-Flags-Dashboard" rel="noopener noreferrer"&gt;Custom Flags Dashboard&lt;/a&gt;). A monthly sweep for flags that are fully rolled out, concluded, or untouched for a quarter turns "someone should clean these up" into a concrete list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a flag is genuinely done, retire it in both places. Remove the code branch and the flag definition together, so you never leave a dangling key or an orphaned conditional. The code side of that sweep can be automated: &lt;a href="https://optipilot.com/feature-experimentation/implementation/find-unused-feature-flags" rel="noopener noreferrer"&gt;find unused feature flags&lt;/a&gt; walks through locating stale flag references and dangling keys in your codebase before they calcify. In Optimizely, archiving a flag removes it from the datafile and requires that it not be active in your highest-priority environment, usually production, which is a useful guardrail against archiving something still serving traffic; the flag's data is retained so you can unarchive later if needed (&lt;a href="https://support.optimizely.com/hc/en-us/articles/30715996139789-Custom-Flags-Dashboard" rel="noopener noreferrer"&gt;Custom Flags Dashboard&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  How Optimizely maps flags to your code
&lt;/h2&gt;

&lt;p&gt;Pulling the pieces together, here is the shape of the integration. A flag is, in Optimizely's own words, "a place in code where a decision is made." You create a user context from a user ID and any targeting attributes, then call &lt;code&gt;decide&lt;/code&gt; with the flag key. The returned decision tells you whether the flag is enabled and carries any configuration variables and the variation key (&lt;a href="https://docs.developers.optimizely.com/feature-experimentation/docs/key-concepts-and-differences-in-full-stack-and-feature-experimentation" rel="noopener noreferrer"&gt;Key concepts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The following is illustrative JavaScript, showing the decision routed through a single wrapper with an explicit fallback. Method names are verified against current SDK docs, but exact syntax varies by language and SDK major version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Illustrative: one place that owns the decision, with a safe default.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FALLBACK_SORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alphabetical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// conservative, always-works behavior&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProductSort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;optimizely&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Wait until the SDK has a datafile; on failure, fall back.&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;optimizely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onReady&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sortMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FALLBACK_SORT&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optimizely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createUserContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product_sort&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Critical error: variationKey is null and reasons are populated.&lt;/span&gt;
  &lt;span class="c1"&gt;// Treat as "off" and use our own fallback rather than trusting variables.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variationKey&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flag decision failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sortMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FALLBACK_SORT&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sortMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sort_method&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FALLBACK_SORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two SDK details are worth knowing for production hygiene. First, &lt;code&gt;decide&lt;/code&gt; dispatches a decision (impression) event by default so the exposure shows up in results; when you only want to read a flag's state without recording an exposure, the &lt;code&gt;OptimizelyDecideOption.DISABLE_DECISION_EVENT&lt;/code&gt; option suppresses it (&lt;a href="https://docs.developers.optimizely.com/feature-experimentation/docs/decide-methods-for-the-javascript-sdk" rel="noopener noreferrer"&gt;Decide methods&lt;/a&gt;). This is the right tool when you evaluate a flag early to pre-render content and will record the real exposure later at the point of interaction. Second, remember the silent-failure rule from earlier: a flag key in your code with no matching, running rule serves no traffic, so keep code and configuration in lockstep.&lt;/p&gt;

&lt;p&gt;The through-line across all of this is modest but powerful. Treat each flag as a small, owned, dated commitment rather than a permanent fixture. Name it so it can be found, ramp it so a mistake is cheap, keep a switch that reverses it fast, and schedule its removal the day you create it. Do that consistently and feature flags go back to being what they were supposed to be: a mechanism for shipping with confidence, not a second codebase you are quietly maintaining by accident.&lt;/p&gt;

&lt;p&gt;To see a flag decision run end to end, open the &lt;a href="https://optipilot.com/demo" rel="noopener noreferrer"&gt;demo lab&lt;/a&gt;. The &lt;a href="https://optipilot.com/demo/javascript" rel="noopener noreferrer"&gt;JavaScript v6+&lt;/a&gt;, &lt;a href="https://optipilot.com/demo/react" rel="noopener noreferrer"&gt;React&lt;/a&gt; and &lt;a href="https://optipilot.com/demo/nextjs" rel="noopener noreferrer"&gt;Next.js SSR&lt;/a&gt; harnesses each make a live decision and show its events. Teams still on v5 can use the &lt;a href="https://optipilot.com/demo/javascript-browser-v5" rel="noopener noreferrer"&gt;JavaScript Browser v5&lt;/a&gt; harness.&lt;/p&gt;

</description>
      <category>featureflags</category>
      <category>devops</category>
      <category>webdev</category>
      <category>abtesting</category>
    </item>
    <item>
      <title>False Discovery Rate Control in A/B Testing</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:06:35 +0000</pubDate>
      <link>https://dev.to/david_sert/false-discovery-rate-control-in-ab-testing-35fg</link>
      <guid>https://dev.to/david_sert/false-discovery-rate-control-in-ab-testing-35fg</guid>
      <description>&lt;p&gt;Track one metric on an A/B test and a false positive is a 1-in-20 event at the usual 95% confidence threshold. Track twenty metrics and, under the null hypothesis, you should &lt;em&gt;expect&lt;/em&gt; one of them to look like a winner purely by chance. Add a few variations and a segment breakdown, and the number of simultaneous comparisons climbs into the dozens. This is the multiple comparisons problem, and left uncontrolled it turns a rich results page into a machine for manufacturing phantom wins. The false discovery rate is the modern statistical answer to it — a way to keep testing many things at once without letting your rate of false claims run away from you.&lt;/p&gt;

&lt;p&gt;This guide explains what the false discovery rate is, how it differs from older corrections like family-wise error control and Bonferroni, how the Benjamini-Hochberg procedure works with a fully worked example, and how Optimizely's Stats Engine applies false discovery rate control automatically across every metric and variation on the results page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the False Discovery Rate?
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;false discovery rate (FDR)&lt;/strong&gt; is the expected proportion of your "discoveries" — the results you flag as statistically significant — that are actually false positives. If you declare 10 winning metrics and your FDR is controlled at 5%, then on average you expect about 0.5 of those 10 to be a mistake. FDR is a property of the &lt;em&gt;set&lt;/em&gt; of significant results, not of any single test.&lt;/p&gt;

&lt;p&gt;That framing is the whole point. A traditional p-value controls the error rate of one hypothesis in isolation: a 5% significance level means that &lt;em&gt;if this one variation has no real effect&lt;/em&gt;, there is a 5% chance you wrongly call it significant. But an experiment rarely tests one hypothesis. It tests a primary metric, several secondaries, a stack of guardrails, maybe two or three variations, and often a few segments. Every one of those cells is a hypothesis test. The false discovery rate asks a more useful question: across everything I flagged as a discovery, what fraction should I expect to be wrong?&lt;/p&gt;

&lt;p&gt;Controlling the FDR lets you keep a wide, informative results page — which you want, because catching harm requires watching many metrics — while bounding how often you chase something that was never really there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Multiple Comparisons Problem
&lt;/h2&gt;

&lt;p&gt;The mechanism is pure arithmetic. Suppose every metric on your experiment is truly null (no real difference between variation and control), and you test each at a 5% significance level. The probability that a single test does &lt;em&gt;not&lt;/em&gt; produce a false positive is 0.95. The probability that &lt;em&gt;none&lt;/em&gt; of &lt;code&gt;m&lt;/code&gt; independent tests produces one is &lt;code&gt;0.95^m&lt;/code&gt;, so the probability of at least one false positive is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(at least one false positive) = 1 - 0.95^m

m = 1   metric   --&amp;gt;  5.0%
m = 5   metrics  --&amp;gt;  22.6%
m = 10  metrics  --&amp;gt;  40.1%
m = 20  metrics  --&amp;gt;  64.2%
m = 50  metrics  --&amp;gt;  92.3%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At twenty metrics you are more likely than not to see at least one "significant" result even when nothing is happening. This is why a naive results page that runs every metric at a flat 95% threshold is untrustworthy the moment it carries more than a handful of metrics: the headline significance numbers no longer mean what they appear to mean. The problem compounds with variations and segments, because each comparison is another draw from the same lottery.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdb.devchild.me%2Fstorage%2Fv1%2Fobject%2Fpublic%2Foptipilot-docs-images%2Fstat-charts%2Ffdr-multiple-metrics.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdb.devchild.me%2Fstorage%2Fv1%2Fobject%2Fpublic%2Foptipilot-docs-images%2Fstat-charts%2Ffdr-multiple-metrics.svg" alt="Line chart of the probability that at least one of m independent null metrics reads significant at alpha equals 5%. Without correction the chance climbs from 5% at one metric to about 64% at twenty; with false discovery rate control it stays flat near 5%." width="720" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The instinct to fix this by simply "watching fewer metrics" is the wrong trade. You need guardrail metrics to catch changes that win on conversion but quietly raise refunds or slow the page — that is a deliberately wide net. The right fix is not to look at less, but to adjust the significance bar to account for how much you are looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Family-Wise Error Rate vs False Discovery Rate
&lt;/h2&gt;

&lt;p&gt;There are two different error rates you might try to control, and the choice shapes everything downstream.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;family-wise error rate (FWER)&lt;/strong&gt; is the probability of making &lt;em&gt;even one&lt;/em&gt; false positive across the entire family of tests. Controlling FWER at 5% means there is at most a 5% chance that &lt;em&gt;any&lt;/em&gt; of your significant results is wrong. This is a strict, conservative standard — appropriate when a single false claim is catastrophic, such as a drug-approval trial.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;false discovery rate (FDR)&lt;/strong&gt; is the &lt;em&gt;expected proportion&lt;/em&gt; of false positives among the results you called significant. Controlling FDR at 5% means you accept that some individual discoveries may be wrong, as long as, on average, no more than 5% of them are.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Family-wise error rate (FWER)&lt;/th&gt;
&lt;th&gt;False discovery rate (FDR)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Controls&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Probability of &lt;em&gt;any&lt;/em&gt; false positive&lt;/td&gt;
&lt;td&gt;Expected &lt;em&gt;fraction&lt;/em&gt; of false positives among discoveries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One mistake is unacceptable&lt;/td&gt;
&lt;td&gt;A bounded share of mistakes is tolerable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Power&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low — misses many real effects&lt;/td&gt;
&lt;td&gt;High — detects far more real effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typical procedure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bonferroni, Holm&lt;/td&gt;
&lt;td&gt;Benjamini-Hochberg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best fit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Small number of critical tests&lt;/td&gt;
&lt;td&gt;Many exploratory tests, as in A/B testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For online experimentation, FDR is almost always the better target. You run many tests, most effects are small, and the cost of missing a real winner (low power) is a genuine business cost. FDR control gives you dramatically more statistical power than FWER control while still keeping false claims on a leash. That trade — accept a known, bounded fraction of errors in exchange for detecting many more true effects — is why FDR has become the default for large-scale testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Benjamini-Hochberg Procedure
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Benjamini-Hochberg (BH) procedure&lt;/strong&gt;, introduced by Yoav Benjamini and Yosef Hochberg in 1995, is the standard method for controlling the false discovery rate. It is refreshingly mechanical.&lt;/p&gt;

&lt;p&gt;Given &lt;code&gt;m&lt;/code&gt; p-values and a target FDR level &lt;code&gt;q&lt;/code&gt; (say 0.10):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sort the p-values in ascending order, so &lt;code&gt;p(1) &amp;lt;= p(2) &amp;lt;= ... &amp;lt;= p(m)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For each rank &lt;code&gt;k&lt;/code&gt;, compute the BH critical value &lt;code&gt;(k / m) * q&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Find the &lt;strong&gt;largest&lt;/strong&gt; rank &lt;code&gt;k&lt;/code&gt; for which &lt;code&gt;p(k) &amp;lt;= (k / m) * q&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Declare significant every hypothesis from rank 1 up to and including that &lt;code&gt;k&lt;/code&gt; — even any whose raw p-value sits above the naive threshold.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key idea is the sloped threshold. Instead of comparing every p-value to one fixed cutoff, BH compares each ranked p-value to a line that rises with its rank. A p-value only needs to beat &lt;code&gt;(k / m) * q&lt;/code&gt;, which is more forgiving for higher ranks when there are many small p-values supporting them. The procedure adapts: when the data contain many strong effects, the effective cutoff loosens; when almost nothing is real, it tightens toward the strict Bonferroni bound.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Collect m p-values from every metric and variation] --&amp;gt; B[Sort ascending: p1 to pm]
    B --&amp;gt; C[For each rank k compute BH critical value k over m times q]
    C --&amp;gt; D{Find largest k where p_k is at or below its BH critical value}
    D --&amp;gt; E[Reject ranks 1 through k: declare significant]
    D --&amp;gt; F[Ranks above k: not significant]
    E --&amp;gt; G[Expected false positives among rejections stays at or below q]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A Worked Example
&lt;/h3&gt;

&lt;p&gt;Take 10 metrics on an experiment and a target FDR of &lt;code&gt;q = 0.10&lt;/code&gt;. Sort the p-values ascending, compute each BH critical value &lt;code&gt;(k / 10) * 0.10 = k * 0.01&lt;/code&gt;, and compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rank k   p-value    BH critical (k/m)*q    p &amp;lt;= critical?
  1      0.001         0.010                yes
  2      0.008         0.020                yes
  3      0.012         0.030                yes
  4      0.021         0.040                yes
  5      0.030         0.050                yes
  6      0.041         0.060                yes
  7      0.060         0.070                yes   &amp;lt;-- largest passing rank
  8      0.200         0.080                no
  9      0.350         0.090                no
 10      0.600         0.100                no

Largest k with p(k) &amp;lt;= (k/m)*q  is  k = 7.
Benjamini-Hochberg rejects ranks 1 through 7  -&amp;gt;  7 discoveries.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice rank 7: its p-value is 0.060, which is &lt;em&gt;above&lt;/em&gt; the naive 0.05 threshold, yet BH still declares it significant. That is the procedure working as designed — the weight of six even-smaller p-values below it earns rank 7 a place. Now compare the same data under a strict Bonferroni correction, which would test every metric against &lt;code&gt;0.10 / 10 = 0.01&lt;/code&gt;: only ranks 1 (0.001) and 2 (0.008) clear that bar, for &lt;strong&gt;2 discoveries&lt;/strong&gt; instead of 7. Same data, same 10% error budget — BH finds more than three times as many real effects because it spends that budget across the whole set instead of on each test in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  FDR Control vs Bonferroni Correction
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Bonferroni correction&lt;/strong&gt; is the best-known multiple-comparisons fix: divide your significance level by the number of tests, so with 20 metrics at 5% each metric must clear 0.05 / 20 = 0.0025. It controls the family-wise error rate, and it is trivially simple. It is also badly suited to A/B testing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Bonferroni&lt;/th&gt;
&lt;th&gt;Benjamini-Hochberg (FDR)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Controls&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Family-wise error rate&lt;/td&gt;
&lt;td&gt;False discovery rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Threshold&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fixed: &lt;code&gt;alpha / m&lt;/code&gt; for every test&lt;/td&gt;
&lt;td&gt;Sloped: &lt;code&gt;(k / m) * q&lt;/code&gt;, rank-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;As metrics grow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cutoff shrinks fast; power collapses&lt;/td&gt;
&lt;td&gt;Cutoff adapts; power preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;False negatives&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Many — misses real winners&lt;/td&gt;
&lt;td&gt;Far fewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Right context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A few make-or-break tests&lt;/td&gt;
&lt;td&gt;Many metrics, exploratory testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Bonferroni's flaw in this setting is that it treats every additional metric as equally threatening and pays for it with a cutoff that shrinks toward zero. With 50 metrics, each must beat 0.001 — so real, moderate effects get thrown out alongside the noise, and your false-negative rate soars. FDR control accepts a small, known fraction of false positives in exchange for keeping the power to detect the effects that matter. For an experimentation program that lives or dies by finding true wins across a broad metric set, that is the correct trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Optimizely's Stats Engine Controls the False Discovery Rate
&lt;/h2&gt;

&lt;p&gt;Optimizely's Stats Engine applies false discovery rate control automatically. Per Optimizely's documentation, the Stats Engine "provides a data-rich view of visitor interactions, includes confidence intervals, and applies &lt;a href="https://support.optimizely.com/hc/en-us/articles/39028383356813-Optimizely-Experiment-Results-page" rel="noopener noreferrer"&gt;false discovery rate control&lt;/a&gt;" across the metrics on the Experiment Results page. You do not configure it, sort p-values, or apply a correction by hand — it is built into how significance is reported. For the broader mechanics of how significance and always-valid inference are computed, see &lt;a href="https://optipilot.com/data/optimizely-stats-engine" rel="noopener noreferrer"&gt;How the Optimizely Stats Engine Works&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two documented properties are worth internalizing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Testing more does not raise your false-positive risk.&lt;/strong&gt; Optimizely states that the "Stats Engine adjusts statistical significance for every metric and variation, so testing more does not raise the risk of false positives." That is the FDR guarantee in plain language — the correction scales with how many comparisons the results page carries, so adding a guardrail does not silently inflate your chance of a phantom winner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The primary metric is protected.&lt;/strong&gt; On the results page, "the primary metric's statistical significance is calculated independently from all other metrics and monitoring goals." The tiering matters: your headline result reaches significance as fast as possible, while secondary and monitoring metrics absorb the multiple-comparisons adjustment. Adding many low-improvement secondary metrics slows &lt;em&gt;their&lt;/em&gt; time to significance, but never the primary's.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why the &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410289762189-Change-the-statistical-significance-setting-in-Optimizely-Experimentation" rel="noopener noreferrer"&gt;Change the statistical significance setting&lt;/a&gt; documentation can promise that carrying a wide metric set — including a full complement of &lt;a href="https://optipilot.com/data/guardrail-metrics" rel="noopener noreferrer"&gt;guardrail metrics&lt;/a&gt; — does not cost you in false alarms. FDR control is what makes a broad, honest results page statistically safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Not Change Metrics Mid-Experiment
&lt;/h3&gt;

&lt;p&gt;There is one operational rule that follows directly from how FDR control is computed. Optimizely controls the false discovery rate over "metrics currently appearing on the Experiment Results page." Its &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410283825421-Edit-a-metric" rel="noopener noreferrer"&gt;Edit a metric&lt;/a&gt; documentation warns that changing the metrics during a running experiment "alters the number and nature of the hypotheses being tested, which can affect the false discovery rate control calculations," and that "excessive metric changes after an experiment or campaign has started may invalidate Optimizely's guarantees regarding false detection."&lt;/p&gt;

&lt;p&gt;The reason is exactly the BH mechanism above: the critical values depend on &lt;code&gt;m&lt;/code&gt;, the number of hypotheses. Add or swap metrics after data has started arriving and you retroactively change &lt;code&gt;m&lt;/code&gt;, which shifts every threshold and reopens the door to the cherry-picking FDR control was meant to close. Decide your metric set before launch and freeze it — the same discipline that protects you from the &lt;a href="https://optipilot.com/data/winners-curse-regression-to-the-mean" rel="noopener noreferrer"&gt;winner's curse&lt;/a&gt; and from &lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;sequential testing&lt;/a&gt; peeking traps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Implications for Your Experiments
&lt;/h2&gt;

&lt;p&gt;Understanding FDR control changes a few concrete habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the metrics you need, not fewer.&lt;/strong&gt; Because the Stats Engine controls FDR across the results page, you can carry a healthy set of guardrails and secondaries without inflating false positives. Trim metrics for clarity, not out of statistical fear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock the metric set at launch.&lt;/strong&gt; Write your primary, secondary, and guardrail metrics into the experiment brief before traffic starts, and do not edit them mid-flight. Changing &lt;code&gt;m&lt;/code&gt; mid-experiment undermines the FDR guarantee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the primary metric first.&lt;/strong&gt; It is computed independently and reaches significance fastest. Treat secondary and monitoring metrics as supporting evidence whose significance the FDR adjustment deliberately makes more conservative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power the experiment for the metrics that matter.&lt;/strong&gt; FDR control makes false positives rare, but it cannot rescue an underpowered test from false negatives. Size the experiment properly — see &lt;a href="https://optipilot.com/data/ab-test-sample-size-statistical-power" rel="noopener noreferrer"&gt;A/B test sample size and statistical power&lt;/a&gt; — so you can actually detect the effects you care about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate the traffic split.&lt;/strong&gt; No error-rate control matters if the experiment is broken at the instrumentation level. Check for &lt;a href="https://optipilot.com/data/sample-ratio-mismatch" rel="noopener noreferrer"&gt;sample ratio mismatch&lt;/a&gt; before you trust any significance number on the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a good false discovery rate to target?
&lt;/h3&gt;

&lt;p&gt;For most online experimentation, controlling the FDR at 5% to 10% is standard — the same range as conventional significance levels. A 10% FDR means you accept that, on average, up to one in ten of your flagged discoveries may be a false positive, in exchange for substantially more power to detect real effects. Optimizely ties this to your project-wide statistical significance setting rather than asking you to specify an FDR directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the false discovery rate the same as the false positive rate?
&lt;/h3&gt;

&lt;p&gt;No, and the distinction is important. The false positive rate (or Type I error rate) is the probability of a false positive among all &lt;em&gt;truly null&lt;/em&gt; tests — it looks backward from the ground truth. The false discovery rate is the expected fraction of false positives among the results you &lt;em&gt;declared significant&lt;/em&gt; — it looks forward from your discoveries. FDR is the more actionable quantity because it describes the reliability of the conclusions you actually act on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not just use Bonferroni correction?
&lt;/h3&gt;

&lt;p&gt;Bonferroni controls the family-wise error rate by dividing your significance level across all tests, which makes the threshold shrink rapidly as you add metrics. With many metrics it becomes so conservative that it discards real effects along with the noise, driving up false negatives. Benjamini-Hochberg controls the false discovery rate instead, adapting the threshold to the data and preserving far more power — a better fit for the many-metric reality of A/B testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does tracking more metrics in Optimizely increase my false positive risk?
&lt;/h3&gt;

&lt;p&gt;No. Optimizely's documentation states that the Stats Engine "adjusts statistical significance for every metric and variation, so testing more does not raise the risk of false positives." The false discovery rate control scales with the number of comparisons on the results page. Adding metrics can lengthen the time for secondary metrics to reach significance, but it does not inflate your overall false-positive risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does changing metrics mid-experiment break FDR control?
&lt;/h3&gt;

&lt;p&gt;Benjamini-Hochberg critical values depend on the number of hypotheses being tested. When you add, remove, or edit metrics on a running experiment, you change that number retroactively, which shifts the thresholds the Stats Engine already applied and can invalidate Optimizely's guarantees against false detection. Decide your metric set before launch and keep it fixed for the life of the experiment.&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>abtesting</category>
      <category>datascience</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Experiment Design for A/B Testing: A Practical Framework</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:05:59 +0000</pubDate>
      <link>https://dev.to/david_sert/experiment-design-for-ab-testing-a-practical-framework-1b33</link>
      <guid>https://dev.to/david_sert/experiment-design-for-ab-testing-a-practical-framework-1b33</guid>
      <description>&lt;p&gt;An A/B test can be implemented perfectly and still answer the wrong question. Experiment design is the written contract that connects a business decision to a causal comparison: who is eligible, what is randomized, what changes, how outcomes are measured, and what evidence will trigger action.&lt;/p&gt;

&lt;p&gt;This guide turns those choices into a one-page specification, then maps the same design to Optimizely Web Experimentation and Feature Experimentation. It complements the deeper guides to an &lt;a href="https://optipilot.com/strategy/ab-testing-hypothesis-template-optimizely" rel="noopener noreferrer"&gt;A/B testing hypothesis&lt;/a&gt;, &lt;a href="https://optipilot.com/data/ab-testing-metrics-framework-oec-optimizely" rel="noopener noreferrer"&gt;experiment metrics&lt;/a&gt;, &lt;a href="https://optipilot.com/data/ab-test-sample-size-statistical-power" rel="noopener noreferrer"&gt;sample size and statistical power&lt;/a&gt;, &lt;a href="https://optipilot.com/data/how-long-to-run-ab-test" rel="noopener noreferrer"&gt;test duration&lt;/a&gt;, and &lt;a href="https://optipilot.com/strategy/optimizely-experiment-qa-checklist" rel="noopener noreferrer"&gt;pre-launch QA&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment design is the blueprint for a causal answer
&lt;/h2&gt;

&lt;p&gt;The output of experiment design is not a list of variation ideas. It is a precise description of the comparison that will support a decision. Optimizely's own &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410282997005-Create-a-basic-experiment-plan" rel="noopener noreferrer"&gt;basic experiment plan&lt;/a&gt; asks why the experiment runs, where and when variations run, who sees them, what changes, and how success is measured. A rigorous design adds the unit of randomization, estimand, power assumptions, and decision rule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Business decision] --&amp;gt; B[Causal question]
  B --&amp;gt; C[Unit and eligibility]
  C --&amp;gt; D[Control and treatment]
  D --&amp;gt; E[Metrics and estimand]
  E --&amp;gt; F[MDE, sample, and duration]
  F --&amp;gt; G[Optimizely configuration]
  G --&amp;gt; H[QA and launch]
  H --&amp;gt; I[Ship, iterate, stop, or conclude]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Start with the decision the result must support
&lt;/h3&gt;

&lt;p&gt;Write the action before the hypothesis. A useful decision is specific enough that a result can change it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ship the checkout recommendation module to all eligible customers if it produces a practically valuable increase in completed orders without materially increasing refund rate; otherwise iterate or stop.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Turn that decision into one falsifiable causal question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Among eligible signed-in shoppers, what is the effect of showing the recommendation module during checkout, compared with the current checkout, on completed orders within seven days of assignment?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This order prevents a common failure: designing an attractive variation, choosing an available metric, and only later deciding what the test was meant to prove.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define the estimand before the dashboard metric
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;estimand&lt;/strong&gt; is the effect the team wants to estimate. Define five elements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Checkout recommendation example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Population&lt;/td&gt;
&lt;td&gt;Signed-in shoppers who begin checkout in supported markets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treatment&lt;/td&gt;
&lt;td&gt;Checkout with the recommendation module enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comparator&lt;/td&gt;
&lt;td&gt;Current production checkout with the module absent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outcome&lt;/td&gt;
&lt;td&gt;Orders completed within seven days of assignment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time window&lt;/td&gt;
&lt;td&gt;Seven days from the first eligible checkout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use an intention-to-treat estimand by default: analyze people according to the variation to which they were assigned, even if rendering fails or they do not engage with the module. This preserves the benefit of random assignment and estimates the effect of the policy as deployed.&lt;/p&gt;

&lt;p&gt;An exposure-only analysis answers a different question because it conditions on behavior or technical events that may happen after assignment. It can be useful as a diagnostic, but it should not silently replace the primary analysis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Know when an experiment is the wrong tool
&lt;/h3&gt;

&lt;p&gt;Do not randomize merely because the platform makes it possible. An experiment is a poor choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No plausible outcome would change the decision.&lt;/li&gt;
&lt;li&gt;Available traffic cannot detect an effect worth acting on within a useful timeframe.&lt;/li&gt;
&lt;li&gt;The treatment cannot be assigned independently enough to create a credible control.&lt;/li&gt;
&lt;li&gt;Random assignment would be unethical, unlawful, or operationally unacceptable.&lt;/li&gt;
&lt;li&gt;Instrumentation cannot distinguish assignment, exposure, and outcomes reliably.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When assignment cannot be randomized but a decision still needs evidence, use a quasi-experimental design and state its assumptions explicitly. That design should not be presented as equivalent to a randomized test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the experimental unit and assignment mechanism
&lt;/h2&gt;

&lt;p&gt;The experimental unit is the entity independently assigned to control or treatment. It determines what must remain stable, where interference can occur, and which observations are independent in the analysis.&lt;/p&gt;

&lt;h3&gt;
  
  
  User, session, account, device, or cluster
&lt;/h3&gt;

&lt;p&gt;Match the unit to how the treatment is delivered and how people can influence one another:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Unit&lt;/th&gt;
&lt;th&gt;Appropriate when&lt;/th&gt;
&lt;th&gt;Main risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;A durable identity receives an individual experience&lt;/td&gt;
&lt;td&gt;The same person appears under multiple IDs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session&lt;/td&gt;
&lt;td&gt;The treatment is intentionally temporary and session-contained&lt;/td&gt;
&lt;td&gt;Repeat sessions receive inconsistent experiences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Account&lt;/td&gt;
&lt;td&gt;Members share settings, workflows, or outcomes&lt;/td&gt;
&lt;td&gt;Too few accounts for adequate power&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;The experience is device-specific and identity is unavailable&lt;/td&gt;
&lt;td&gt;Cross-device contamination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster&lt;/td&gt;
&lt;td&gt;Locations, teams, or networks can affect one another&lt;/td&gt;
&lt;td&gt;Correlated outcomes reduce effective &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;sample size&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a B2B account-level recommendation feature, randomizing individual members can contaminate the comparison: one member may configure the experience for colleagues, or team members may discuss it. Assign the account, then analyze with methods that respect account-level assignment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep assignment stable
&lt;/h3&gt;

&lt;p&gt;Use a durable identifier for the unit of randomization. If the treatment must persist across browser sessions and services, a transient session ID is not sufficient. Document how anonymous identities merge with authenticated identities and what happens when a user changes devices.&lt;/p&gt;

&lt;p&gt;Optimizely Web Experimentation uses deterministic bucketing: it hashes a user ID or cookie with MurmurHash, so the same identifier maps to the same variation while the relevant configuration remains stable. Optimizely also warns that some traffic changes can rebucket visitors; the official &lt;a href="https://docs.developers.optimizely.com/web-experimentation/docs/how-bucketing-works" rel="noopener noreferrer"&gt;bucketing guide&lt;/a&gt; recommends avoiding allocation changes after results accumulate.&lt;/p&gt;

&lt;p&gt;The randomization unit is not necessarily the metric denominator. A user-randomized experiment may measure revenue per eligible user, orders per user, or average order value among orders. Each denominator answers a different question and must be fixed in the measurement plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detect spillovers and network effects
&lt;/h3&gt;

&lt;p&gt;Random assignment does not prevent treated units from changing control units' outcomes. Before launch, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can users communicate about or share the treatment?&lt;/li&gt;
&lt;li&gt;Does treatment change inventory, marketplace liquidity, queue time, or prices for everyone?&lt;/li&gt;
&lt;li&gt;Can one account member alter settings seen by other members?&lt;/li&gt;
&lt;li&gt;Does a recommender learn from behavior pooled across variations?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If interference is plausible, assign clusters that contain the spillover, use a switchback or marketplace-specific design, or redefine the estimand. Record the remaining interference risk rather than assuming it away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define control, treatment, and eligibility precisely
&lt;/h2&gt;

&lt;p&gt;A reproducible comparison needs versioned experiences and frozen entry rules. “Business as usual” and “all users” are not adequate specifications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make the control a real production baseline
&lt;/h3&gt;

&lt;p&gt;Describe the control as thoroughly as the treatment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User interface and copy.&lt;/li&gt;
&lt;li&gt;Code path and service dependencies.&lt;/li&gt;
&lt;li&gt;Default feature-flag values.&lt;/li&gt;
&lt;li&gt;Existing personalization or recommendation logic.&lt;/li&gt;
&lt;li&gt;Analytics events expected from the baseline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the checkout example, the control is “the current checkout with no recommendation component and no recommendation API call,” not simply “original.” This matters if production changes while the experiment is being prepared.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change one causal idea at a time
&lt;/h3&gt;

&lt;p&gt;One hypothesis can require several implementation edits. Adding a recommendation module may change layout, make an API request, add loading behavior, and introduce new tracking. Those edits express one causal idea if the intended decision is whether to deploy the module as a package.&lt;/p&gt;

&lt;p&gt;If the decision is which component causes the effect—placement, copy, image, or algorithm—use separate experiments or a &lt;a href="https://optipilot.com/data/multivariate-testing-vs-ab-testing" rel="noopener noreferrer"&gt;multivariate test&lt;/a&gt;. A bundled treatment cannot identify the effect of each ingredient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Freeze inclusion and exclusion rules
&lt;/h3&gt;

&lt;p&gt;Define eligibility before anyone inspects results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Target pages, routes, or flag call sites.&lt;/li&gt;
&lt;li&gt;Geography, language, device, browser, and app version.&lt;/li&gt;
&lt;li&gt;Account tier, authentication state, and product entitlement.&lt;/li&gt;
&lt;li&gt;Prior exposure and concurrent-experiment exclusions.&lt;/li&gt;
&lt;li&gt;Employee, bot, fraud, and QA-traffic handling.&lt;/li&gt;
&lt;li&gt;The exact moment a unit enters the experiment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Changing eligibility after viewing outcomes creates a new analysis population. If a safety issue forces a change, record the time and configuration, then decide whether to restart rather than blending incompatible cohorts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the measurement plan
&lt;/h2&gt;

&lt;p&gt;Metrics operationalize the estimand. They should be specified as formulas and event rules, not only selected by name in a dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choose one primary metric
&lt;/h3&gt;

&lt;p&gt;The primary metric must be affected by the treatment, interpretable by stakeholders, and tied to the ship decision. Define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Numerator and denominator.&lt;/li&gt;
&lt;li&gt;Aggregation unit.&lt;/li&gt;
&lt;li&gt;Attribution or conversion window.&lt;/li&gt;
&lt;li&gt;Event source and deduplication key.&lt;/li&gt;
&lt;li&gt;Direction of improvement.&lt;/li&gt;
&lt;li&gt;Practical effect threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the checkout example, “conversion” is ambiguous. A better definition is “distinct eligible users with at least one completed order within seven days divided by distinct assigned eligible users.”&lt;/p&gt;

&lt;p&gt;Optimizely Web Experimentation treats the first metric added as the primary metric; its &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410289104013-Steps-to-create-an-experiment" rel="noopener noreferrer"&gt;experiment setup guide&lt;/a&gt; recommends choosing an event directly affected by the change. Put the decision metric first deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add restrained secondary and guardrail metrics
&lt;/h3&gt;

&lt;p&gt;Secondary metrics explain mechanism or downstream impact. For the recommendation module, useful secondary measures might include recommendation clicks, units per order, and revenue per eligible user.&lt;/p&gt;

&lt;p&gt;Guardrails describe harms that can veto a nominal win: refund rate, checkout latency, payment errors, support contacts, or gross margin. State each veto rule in advance. A dashboard containing dozens of metrics invites selective interpretation; include only metrics with a defined role.&lt;/p&gt;

&lt;h3&gt;
  
  
  Instrument assignment and outcomes separately
&lt;/h3&gt;

&lt;p&gt;An outcome event is not proof that assignment or exposure worked. Validate three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assignment:&lt;/strong&gt; the unit was bucketed into a variation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exposure:&lt;/strong&gt; the intended experience was actually delivered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome:&lt;/strong&gt; the conversion or guardrail event occurred and was attributed correctly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Specify how the pipeline handles duplicate events, bots, missing identifiers, delayed events, identity merges, and events arriving after the experiment stops. Run an A/A or staged validation when the cost of a broken decision pipeline is high.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design for enough information, not merely significance
&lt;/h2&gt;

&lt;p&gt;A design should collect enough information to make the business decision. Statistical significance alone does not establish that an effect is large enough, safe enough, or measured without bias.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pick an MDE worth acting on
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://optipilot.com/data/minimum-detectable-effect" rel="noopener noreferrer"&gt;minimum detectable effect&lt;/a&gt; (MDE) should reflect the smallest effect that would justify implementation and opportunity cost. Derive it from baseline performance, expected value, engineering cost, risk, and competing roadmap items.&lt;/p&gt;

&lt;p&gt;Do not inflate the MDE simply to make the required sample fit available traffic. That creates a faster test by declaring smaller—but potentially valuable—effects invisible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calculate sample size and calendar duration
&lt;/h3&gt;

&lt;p&gt;Use the baseline rate, MDE, significance threshold, desired power, and number of variations to estimate the required sample. Then convert eligible sample into calendar time using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The share of traffic satisfying eligibility.&lt;/li&gt;
&lt;li&gt;Total experiment allocation.&lt;/li&gt;
&lt;li&gt;Variation traffic distribution.&lt;/li&gt;
&lt;li&gt;Weekly and seasonal cycles.&lt;/li&gt;
&lt;li&gt;Ramp time and planned outages.&lt;/li&gt;
&lt;li&gt;Conversion delay and data-processing latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://optipilot.com/data/ab-test-sample-size-statistical-power" rel="noopener noreferrer"&gt;sample-size guide&lt;/a&gt; covers the calculation; the design document should retain the inputs and result. The &lt;a href="https://optipilot.com/data/how-long-to-run-ab-test" rel="noopener noreferrer"&gt;duration guide&lt;/a&gt; explains why reaching a nominal sample early does not erase calendar-cycle or delayed-conversion requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Predefine the stopping and decision rules
&lt;/h3&gt;

&lt;p&gt;Write the complete rule before launch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Statistical method and confidence policy.&lt;/li&gt;
&lt;li&gt;Minimum runtime and information requirement.&lt;/li&gt;
&lt;li&gt;Practical-effect threshold or acceptable interval.&lt;/li&gt;
&lt;li&gt;Guardrail vetoes.&lt;/li&gt;
&lt;li&gt;Safety-stop conditions.&lt;/li&gt;
&lt;li&gt;Treatment of inconclusive results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not stop because a preferred variation briefly looks significant, change the primary metric after seeing results, or segment repeatedly until a favorable result appears. Optimizely Stats Engine supports ongoing results monitoring, but operational visibility is not permission to rewrite the design after outcomes are known.&lt;/p&gt;

&lt;h2&gt;
  
  
  Map the experiment design into Optimizely
&lt;/h2&gt;

&lt;p&gt;The same causal design can be implemented in either Web Experimentation or Feature Experimentation. The platform configuration enforces parts of the design; it does not choose the question, unit, estimand, MDE, or decision threshold for the team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start from one design card
&lt;/h3&gt;

&lt;p&gt;Use a one-page design card as the source of truth:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Checkout recommendation design&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Decision&lt;/td&gt;
&lt;td&gt;Ship, iterate, or stop the module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Causal question&lt;/td&gt;
&lt;td&gt;Effect of offering recommendations on seven-day order completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Population&lt;/td&gt;
&lt;td&gt;Signed-in checkout starters in supported markets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Randomization unit&lt;/td&gt;
&lt;td&gt;Durable user ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control&lt;/td&gt;
&lt;td&gt;Current checkout without recommendation call or component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treatment&lt;/td&gt;
&lt;td&gt;Checkout with recommendation module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary metric&lt;/td&gt;
&lt;td&gt;Seven-day completed-order users per assigned user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secondary metrics&lt;/td&gt;
&lt;td&gt;Recommendation clicks, units per order, revenue per user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guardrails&lt;/td&gt;
&lt;td&gt;Refund rate, checkout errors, p95 latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allocation&lt;/td&gt;
&lt;td&gt;100% of eligible traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distribution&lt;/td&gt;
&lt;td&gt;50% control, 50% treatment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Precomputed minimum sample plus complete weekly cycles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision rule&lt;/td&gt;
&lt;td&gt;Practical lift and statistical evidence with no guardrail veto&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
  D[Shared experiment design card]
  D --&amp;gt; W1[Web: URL or saved page]
  D --&amp;gt; W2[Web: audience]
  D --&amp;gt; W3[Web: original and variation]
  D --&amp;gt; W4[Web: allocation, distribution, metrics]
  D --&amp;gt; F1[Feature: flag and variables]
  D --&amp;gt; F2[Feature: attributes and audience]
  D --&amp;gt; F3[Feature: A/B test rule]
  D --&amp;gt; F4[Feature: decide, events, metrics]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Web Experimentation mapping
&lt;/h3&gt;

&lt;p&gt;The current official &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410289104013-Steps-to-create-an-experiment" rel="noopener noreferrer"&gt;Web Experimentation workflow&lt;/a&gt; maps the design as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Design field&lt;/th&gt;
&lt;th&gt;Web Experimentation object&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where treatment can occur&lt;/td&gt;
&lt;td&gt;Target By URL or Target By Saved Pages; page triggers and conditions where needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eligible population&lt;/td&gt;
&lt;td&gt;Audience conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control and treatment&lt;/td&gt;
&lt;td&gt;Original and variation under Design &amp;gt; Variations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random assignment&lt;/td&gt;
&lt;td&gt;Design &amp;gt; Traffic Allocation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary and supporting outcomes&lt;/td&gt;
&lt;td&gt;Track &amp;gt; Metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-launch evidence&lt;/td&gt;
&lt;td&gt;Preview, testing, Summary, and pre-launch review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set total traffic allocation to 100% when every eligible visitor should enter the experiment, then distribute that traffic 50/50 between original and treatment. Optimizely distinguishes the eligible share entering the test (&lt;strong&gt;allocation&lt;/strong&gt;) from the share assigned to each variation (&lt;strong&gt;distribution&lt;/strong&gt;) in its &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410289350029-Update-traffic-allocation-and-distribution" rel="noopener noreferrer"&gt;traffic documentation&lt;/a&gt;. Freeze both before launch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature Experimentation mapping
&lt;/h3&gt;

&lt;p&gt;Feature Experimentation implements the same design in application code and a flag ruleset:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Design field&lt;/th&gt;
&lt;th&gt;Feature Experimentation object&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where treatment can occur&lt;/td&gt;
&lt;td&gt;Flag evaluation call site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control and treatment&lt;/td&gt;
&lt;td&gt;Flag variations and variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eligible population&lt;/td&gt;
&lt;td&gt;User-context attributes and audience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random assignment&lt;/td&gt;
&lt;td&gt;A/B Test flag rule, allocation, and distribution mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exposure&lt;/td&gt;
&lt;td&gt;SDK &lt;code&gt;decide&lt;/code&gt; call and its decision event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outcomes&lt;/td&gt;
&lt;td&gt;Tracked events configured as metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Optimizely's official &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410289860493-Get-started-with-Optimizely-Feature-Experimentation" rel="noopener noreferrer"&gt;Feature Experimentation getting-started guide&lt;/a&gt; describes the sequence: create a flag, implement it with &lt;code&gt;decide&lt;/code&gt;, define an audience and flag rule, then analyze the Experiment Results page. The newer &lt;a href="https://support.optimizely.com/hc/en-us/articles/38937573368589-Run-A-B-tests-in-Feature-Experimentation" rel="noopener noreferrer"&gt;A/B test configuration guide&lt;/a&gt; adds the hypothesis, test plan, traffic allocation, metrics, distribution mode, and flag variations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optimizelyClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createUserContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;durableUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;market&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;account_tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountTier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout_recommendations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variationKey&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;treatment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;renderCheckout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;recommendations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;renderCheckout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;recommendations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The durable user ID implements user-level assignment. Attributes implement declared eligibility. The flag key defines the decision point, while the returned variation selects the control or treatment code path. Track the completed-order event with the same user context so the result can be attributed to the decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preserve the analysis contract in Collaboration
&lt;/h3&gt;

&lt;p&gt;If the organization uses Optimizely Collaboration, store the structured design alongside the work. The official &lt;a href="https://support.optimizely.com/hc/en-us/articles/16504341933069-Manage-hypotheses" rel="noopener noreferrer"&gt;hypothesis documentation&lt;/a&gt; provides a Brief tab for a structured plan, a Variations tab for images or links, an Experiment tab for linking the implementation, and History for an activity trail.&lt;/p&gt;

&lt;p&gt;Attach the design card, metric definitions, MDE inputs, QA evidence, risks, and assumptions. Collaboration can preserve the contract and make changes visible; it does not repair a design that was never specified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a pre-launch design review
&lt;/h2&gt;

&lt;p&gt;Review the design before launch, when weaknesses can still be fixed without contaminating results. Separate validity from feasibility so “we can build it” is not mistaken for “it will answer the question.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Validity review
&lt;/h3&gt;

&lt;p&gt;Challenge the causal comparison:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Selection:&lt;/strong&gt; Are eligibility rules evaluated before treatment, and are exclusions symmetrical?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spillover:&lt;/strong&gt; Can treated units alter control outcomes?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attrition:&lt;/strong&gt; Can missing outcomes differ by variation?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Novelty:&lt;/strong&gt; Could a temporary reaction dominate the intended long-run effect?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrumentation:&lt;/strong&gt; Can assignment, exposure, and outcomes be reconciled?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent tests:&lt;/strong&gt; Can another experiment interact with this treatment or metric?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity:&lt;/strong&gt; Can one randomization unit receive multiple IDs or variations?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Record mitigations and residual risks. Randomization balances expected pre-treatment differences; it does not fix differential loss, broken event collection, or post-assignment filtering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feasibility review
&lt;/h3&gt;

&lt;p&gt;Confirm the operating plan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Required sample and realistic calendar duration.&lt;/li&gt;
&lt;li&gt;Engineering, design, analytics, and QA ownership.&lt;/li&gt;
&lt;li&gt;Event latency and conversion window.&lt;/li&gt;
&lt;li&gt;Rollout and rollback procedure.&lt;/li&gt;
&lt;li&gt;Safety monitoring and incident owner.&lt;/li&gt;
&lt;li&gt;Browser, device, and environment coverage.&lt;/li&gt;
&lt;li&gt;Production baseline freeze and change control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the design cannot be executed as written, revise it before exposure rather than improvising after launch.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-page experiment-design checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Decision, causal question, and hypothesis are explicit.&lt;/li&gt;
&lt;li&gt;Population, randomization unit, and analysis unit are distinct.&lt;/li&gt;
&lt;li&gt;Control and treatment are versioned production specifications.&lt;/li&gt;
&lt;li&gt;Eligibility, exclusions, and prior-exposure rules are frozen.&lt;/li&gt;
&lt;li&gt;One primary metric has a complete formula and conversion window.&lt;/li&gt;
&lt;li&gt;Secondary metrics explain mechanism; guardrails have veto rules.&lt;/li&gt;
&lt;li&gt;Assignment, exposure, and outcome events can be reconciled.&lt;/li&gt;
&lt;li&gt;MDE, sample, duration, and stopping rule are recorded.&lt;/li&gt;
&lt;li&gt;Optimizely pages or flags, audiences, traffic, variations, and metrics match the design.&lt;/li&gt;
&lt;li&gt;QA evidence, rollback owner, and launch approval are documented.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the &lt;a href="https://optipilot.com/strategy/optimizely-experiment-qa-checklist" rel="noopener noreferrer"&gt;Optimizely experiment QA checklist&lt;/a&gt; to turn the approved design into a production launch review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is experiment design in A/B testing?
&lt;/h3&gt;

&lt;p&gt;Experiment design is the specification that connects a decision to a causal comparison. It defines the population, randomization unit, eligibility, control, treatment, metrics, power assumptions, duration, and decision rule before traffic is exposed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the randomization unit the same as the analysis unit?
&lt;/h3&gt;

&lt;p&gt;Not always. A test can randomize accounts and observe users or transactions, but the analysis must account for dependence within each randomized account. Treating correlated observations as independently randomized overstates the amount of information in the test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should an A/B test change only one page element?
&lt;/h3&gt;

&lt;p&gt;It should test one causal idea, which may require several coordinated implementation changes. If the decision requires isolating the contribution of individual elements or their interactions, use separate experiments or a multivariate design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can traffic allocation change after an experiment starts?
&lt;/h3&gt;

&lt;p&gt;Operationally, Optimizely permits traffic changes, but they can affect bucketing and interpretation. Predefine allocation and distribution, avoid changing them after results accumulate, and document any safety-driven intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Optimizely replace an experiment design document?
&lt;/h3&gt;

&lt;p&gt;No. Optimizely implements pages or flags, audiences, variations, traffic rules, and metrics. The team must still define the causal question, unit, estimand, MDE, duration, risks, and action threshold. The configuration should be a faithful implementation of that prior design.&lt;/p&gt;

&lt;p&gt;Use a &lt;a href="https://optipilot.com/data/multivariate-testing-vs-ab-testing" rel="noopener noreferrer"&gt;multivariate test&lt;/a&gt; when one decision depends on the separate effect of multiple components.&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>statistics</category>
      <category>productmanagement</category>
      <category>datascience</category>
    </item>
  </channel>
</rss>
