setupAB: correct isControl and carry edge fields on multi-arm tests - #332
Closed
MO-Thibault wants to merge 1 commit into
Closed
setupAB: correct isControl and carry edge fields on multi-arm tests#332MO-Thibault wants to merge 1 commit into
MO-Thibault wants to merge 1 commit into
Conversation
setupAB could not express a split test with more than two arms, in three
separate ways. All three surface together the moment a third arm exists.
isControl was `selected.id !== treatmentId`, so any arm that is neither the
named treatment nor the control was reported as a holdout, and had
OPTABLE_RESOLVED and the SDK targeting cache cleared. For an arm that still
resolves EIDs that is wrong twice over: it corrupts the measurement, and
because the session guard survives the wipe the user serves an empty cache
for the rest of the session. It is now `selected.id === controlId`, which is
the same predicate for two arms and the intended one for more.
fillTrafficPercentages rebuilt each variant as {id, trafficPercentage},
dropping skipMatchers, skipResolvers and matcher_override. Those fields are
the reason an arm would differ at the edge at all, and ABTestConfig already
declares them. Variants are now spread through, so the selected variant can
be handed straight to InitConfig.abTests.
The sticky read returned the object stored in localStorage. Editing an arm's
skipMatchers or its weight then applied only to newly assigned users. It now
resolves the stored id against the current variant list and returns that
entry, so config is the single source of truth for everything but the id.
Adds optableSplitTest, which forces a variant by id. optableControlGroup only
reaches the two arms named by controlId and treatmentId, so it cannot hold a
middle arm for QA. An unknown id falls through to normal assignment.
Behaviour is unchanged for every two-arm caller: with variants production and
test, `id === 'test'` and `id !== 'production'` are the same test.
Tests: 12 new cases covering three-arm assignment, cache retention for a
middle arm, skipMatchers passthrough, config re-read over the stored copy,
and the optableSplitTest precedence chain. 387 pass.
Contributor
Author
|
Unblocking note: Optable/optable-solutions#775 no longer waits on this. Chess now layers its third arm inside the This is still worth landing on its own merits — it is what makes a genuine third variant work, stops |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
setupABcannot express a split test with more than two arms. Three separate defects, all of which surface the moment a third variant exists. TheMore than two variantssection ofabTestAssignment.mdalready documents this as a supported shape, so today that documentation is wrong.The concrete case: chess is running a three-arm test —
productionat 50%,skip1pat 45% carryingskipMatchers: ['1p'], and a 5% holdout — and the wrapper has to hand-roll the whole assignment becausesetupABcannot hold it.isControltreated every non-treatment arm as a holdoutA middle arm that still resolves EIDs was classified as control and had its cache cleared on every page load. That is wrong twice over: it corrupts the measurement, and because the session guard (
OPTABLE_TARGETING_DONE, sessionStorage) survives the wipe, the user serves an empty cache for the rest of the session rather than re-resolving.Now
selected.id === controlId.This is not a behaviour change for any current caller. With two variants the two predicates are the same test. All 17 templated bundles in optable-solutions pass exactly
[{id:'production'},{id:'test',trafficPercentage:5}], and the documented custom-name example already passescontrolIdandtreatmentIdtogether.skipMatcherswas stripped off the variantfillTrafficPercentagesrebuilt each variant as{id, trafficPercentage}, discardingskipMatchers,skipResolversandmatcher_override. Those fields are the reason an arm would differ at the edge at all, andABTestConfigalready declares them.Variants are now spread through, so the assigned variant can go straight into
InitConfig.abTests— which is the only wayskip_matchersreaches the edge, sincetargeting.tsreads it offdetermineABTest(config.abTests)and a top-levelskipMatchersis inert.The sticky read returned the stored copy, not the current config
Editing an arm's
skipMatchers, or its weight, applied only to newly assigned users; anyone already holding that arm kept whatever was serialized at assignment time. The read now resolves the stored id against the current variant list and returns that entry, so config is the single source of truth for everything except the id. Unknown ids are still discarded.New
optableSplitTestflagoptableControlGrouponly reaches the two arms named bycontrolIdandtreatmentId, so there was no way to hold a middle arm for QA.?optableSplitTest=<id>forces a variant by id and takes precedence overoptableControlGroup. An id absent fromvariantsis ignored and assignment proceeds normally, so a stale flag cannot invent an arm.Test plan
abTestAssignment.test.ts: three-arm assignment, cache retention for a middle arm, control still clearing its cache,skipMatcherspassthrough and absence, config re-read over the stored copy, and the fulloptableSplitTestprecedence chain. Existing cases are unmodified.pnpm test— 387 pass, 24 suites.pnpm build-libandprettier --check lib/clean.setupABwith the chess config: 50.01% / 44.98% / 5.01%, zero null selections;skipMatcherspresent only onskip1p,isControltrue only on the holdout.skip_matchers=1p.Docs updated:
abTestAssignment.mdgains a section on variants that change what the edge resolves, states theisControlrule for multi-arm tests, and documentsoptableSplitTest;flags.mdgains the new flag.Release
Consumers need this on npm before they can use a third arm. Optable/optable-solutions#775 is waiting on it.