feat(blocks): generic sticky variant-resolution primitive (experiments) - #494
Open
hugo-ccabral wants to merge 1 commit into
Open
feat(blocks): generic sticky variant-resolution primitive (experiments)#494hugo-ccabral wants to merge 1 commit into
hugo-ccabral wants to merge 1 commit into
Conversation
Rollout item B of the PLP ranking experiments design (04_engineering/platform/experiments/README.md in `context`). Extracts the sticky-assignment / self-healing re-roll core out of `cms/resolve.ts`'s `evaluateVariantRule` into `sdk/experiments.ts` as `stickyDecide`, so it can be driven by a variant list from any source. The CMS multivariate path now calls the same function and keeps its own `trafficToPct` fingerprint, so live visitors are not re-rolled. On top of it, `resolveExperimentVariant` resolves an N-way weighted experiment from control-plane config in KV (frozen contract 1), returning contract 2's `ResolvedVariant`. Two gaps the design assumed were already solved, and are solved here: - The CMS engine is not N-way weighted. It is first-match-wins over independent boolean matchers, each with its own `traffic`. The weighted draw (`pickWeightedVariant`) and the weight-vector fingerprint (`weightsFingerprint`) are new. - `deco_segment` could not hold a variant id — `active`/`inactiveDrawn` are boolean buckets. String-valued decisions now ride in an `exp` map, which is deliberately the shape Deco Analytics consumes. The map is omitted when empty, so cookies stay byte-identical for sites with no experiment, and `segmentCacheToken` folds it into `__abf` so two variants never share cached HTML. `persistFlags` moves to after the section loaders: an experiment is resolved inside a loader, so persisting before them dropped the assignment and re-rolled the visitor on every request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nicacioliveira
approved these changes
Aug 20, 2026
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.
Rollout item B of the PLP ranking experiments design
(
04_engineering/platform/experiments/README.mdincontext).Companion doc PR publishing the signature for item C: decocms/context#683
What this does
Extracts the sticky-assignment / self-healing re-roll core out of
cms/resolve.ts'sevaluateVariantRuleintosdk/experiments.tsasstickyDecide, so it can be driven by a variant list from any source. TheCMS multivariate path now calls the same function, so the two assignment paths
cannot drift.
On top of it,
resolveExperimentVariantresolves an N-way weighted experimentfrom control-plane config in KV (frozen contract 1), returning contract 2's
ResolvedVariant.The signature item C consumes
One argument in a loader. The KV binding (
EXPERIMENTS_KV), requesthostname, and
deco_segmentcookie all come from the ambientRequestContextthat
workerEntry.tsalready installs; the KV read is memoised per request, soN loaders cost one
kv.get.ctxexists for tests —config,kv,hostname,segmentCookie,assignments,random, all optional.Every failure mode (binding absent, key unset, malformed document, KV outage)
returns
null, never a thrown request. Callers behave exactly as today onnull.Two things the design assumed were already solved
The design's "Key insight #2" claimed the CMS multivariate engine already had
N-way weighted variants and a reusable cookie. Reading the code, neither held.
Both are solved here; both are corrected in the doc PR.
The CMS engine was never N-way weighted. It lists N variants each gated
by its own independent boolean matcher, evaluated first-match-wins — there
is no weighted draw over a variant list, and
StoredFlag.valuewas aboolean. The sticky/re-roll policy did extract cleanly; the weighted draw(
pickWeightedVariant) and the weight-vector fingerprint(
weightsFingerprint) are net-new.deco_segmentcould not physically hold a variant id.active/inactiveDrawnare boolean buckets. String-valued decisions now ride in anexpmap — deliberately the shape Deco Analytics consumes, making thatmigration a read rather than a translation. The map is omitted when empty,
so cookies stay byte-identical for sites with no experiment (an added empty
map would re-issue every visitor's cookie on deploy and, via
segmentCacheToken, cold-start every edge cache entry).The non-obvious fix
persistFlagsran before the section loaders. An experiment is resolvedinside a loader (that is where item C calls it), so the assignment would never
have reached the cookie and every visitor would re-roll on every request. Moved
to after the loaders, and it now merges experiment assignments through the same
writer — one
deco_segment, oneSet-Cookie.Item C would have hit this as "stickiness doesn't work" with no obvious cause.
Cache safety
segmentCacheTokenfolds experiment assignments in on the same footing asboolean flags, so the existing
__abfcache-key split inworkerEntry.tscovers them with no change. Without that, two visitors on different variants
would share cached HTML. There is a test for exactly that.
CMS multivariate keeps its own
trafficToPctfingerprint — swapping it for theexperiment weight hash would re-roll every live visitor on the next deploy.
Tests
26 new tests in
packages/blocks/src/sdk/experiments.test.ts:isFresh, weight-proportional routing)the other arm, and legacy fingerprint-less
deco_segmentcookiesfingerprint is written so the re-roll cannot repeat
readExperimentConfigfailure modesBlocks suite: baseline on
origin/mainis27 failed | 667 passed; withthis branch
27 failed | 693 passed. Same 27 failures, allthis runtime has no URLPattern Web API(needs Node 24+, environmental).The 2
workerEntry.test.tsdraft-preview failures also reproduce on cleanorigin/main— both verified by stashing, neither is from this branch.tsc --noEmitclean.biome checkclean on all touched files.Not in scope
sdk/abTesting.tsis untouched — it is a binary migration-period proxy and adead end for this. Only its
kv.get<T>(key, "json")shape is borrowed.Nothing in
farmrio-storefront,control-plane, orai-farmrio.🤖 Generated with Claude Code
Summary by cubic
Introduces a generic sticky variant resolver and an N-way weighted experiment API. Aligns CMS boolean matcher stickiness with experiments and fixes cookie persistence so assignments reach
deco_segmentand stick across requests.Review notes
@decocms/blocks/sdk/experimentsexport withstickyDecide(extracted fromcms/resolve.ts) andresolveExperimentVariant(experimentKey, ctx?) -> Promise<ResolvedVariant | null>.cmsRoutenow writesdeco_segmentafter loaders and mergestakeExperimentAssignments(); prevents per-request re-rolls.deco_segmentschema insdk/flags.tswithexpmap{ experimentKey: variantId }(omitted when empty).segmentCacheTokenincludes variant ids; existing analytics remain unchanged.stickyDecidewith its owntrafficToPctfingerprint; behavior for binary flags is unchanged.EXPERIMENTS_KV, unset/malformed config, KV errors) returnnull; callers follow the current path onnull.Rollout
resolveExperimentVariantfrom a loader and branch onvariantId/payload. Do not set cookies; the framework persists assignments.EXPERIMENTS_KVis bound and serves per-hostname config; changing weights will re-roll each visitor once, then stick.Written for commit efbcccc. Summary will update on new commits.