feat: Full Stack v12 — safe traffic ramp-up, variation preview & experiment exclusion - #418
Conversation
…face signatures Beads: ai-driven-product-dev-p5at Agent: sdk-js Foundation only — no algorithm/behavior change. Build+lint green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-n7x9 18 Mocha/Chai tests, spec-derived expected numbers as independent oracle. AC2/AC4/AC5 coverage. Fail cleanly (methods not implemented yet). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-n7x9 getBucketRanges/selectBucketAnchored/getBucketForVisitorAnchored per normative spec. Packed path frozen. 28/28 bucketing tests pass (18 anchored + 10 packed), TS2420 self-resolved on default yarn test:mocha. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-pm33 9 Mocha/Chai tests: AC1 gate branching, AC4 stops/ta:0, AC8 guard precedence, AC9 no schema drift. 4 fail (gate not implemented), 5 pass (packed-path + invariant locks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-pm33 Number(experience.version) > 8 -> anchored (via _buildVariationAllocations + getBucketForVisitorAnchored); else packed unchanged (_buildPackedBuckets extracted verbatim). Guard/hash/seed/event payloads untouched. 37/37 data suite pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + runner Beads: ai-driven-product-dev-jnz9 59 vectors (all spec categories: incident-flip, raise-superset, per-sliver, lower-eject, stopped-arm, ta:0, NaN, boundary, v8===v9 coincidence). Expected values computed then frozen; every spec-given band matched. Runner 60 passing. Full monorepo suite green (480 Mocha + 36 Playwright). Cross-SDK contract for the 5 sibling SDKs; fixture at packages/bucketing/tests/cross-sdk-bucketing-vectors.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d test files Beads: ai-driven-product-dev-kz6x Review R1 (conf 95): header comments claimed methods/gate not-yet-implemented (RED); now shipped + passing. Rewrote to describe shipped behavior for sibling-SDK porters; fixed stale data-manager.ts:620-637 -> :685 gate ref. Comment-only. Suites unchanged (bucketing 28, data 97). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…SonarCloud S2871 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ent stamp is 11) + browser golden-vector test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erience version Introduces an anchored bucketing layout chosen per experience by experience.version: experiences at version > 11 bucket via the anchored layout, while version <= 11, missing, or non-numeric keep the existing packed walk unchanged. Anchored ranges stay fixed to each variation's position, so raising traffic allocation grows every arm in place instead of relocating ranges and re-bucketing already-assigned visitors. Adds public API to @convertcom/js-sdk-bucketing (getBucketRanges, selectBucketAnchored, getBucketForVisitorAnchored) and a VariationAllocation type in @convertcom/js-sdk-types. Hash, seed, stored-decision guard, forced-variation path, and event payloads are unchanged. BREAKING CHANGE: bucketing layout is now selected by experience.version; experiences at version > 11 use the new anchored layout instead of the packed cumulative walk.
Pure exported helper parsePreviewParam(value) -> {experienceId,variationId}|null,
dot-separated numeric ids, exported from js-sdk public entry. 12-row case-table test.
Beads: ai-driven-product-dev-hooy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ygiene (AC1,AC2) Add debugToken?:string to ConfigBase; ApiManager appends debug_token=<value> and forces _conv_low_cache=1 to the config-fetch URL when set (proper &-joined query). Token never sent to track endpoint nor logged. Config-matrix + hygiene tests (6) + api suite (16) green. Beads: ai-driven-product-dev-m568 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…emo (AC3-fetch,AC8) ApiManager.getConfigByExperience(id) fetches /config/<sdkKey>?exp=<id>&_conv_low_cache=1 (+debug_token when configured), memoized process-wide in a module-level Map keyed sdkKey:experienceId, 60s TTL (live Date.now), in-flight promise cached to collapse concurrent calls to one fetch. 7 tests + 23 api suite green. Beads: ai-driven-product-dev-e0aw Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ger (AC4,AC7) Pure DataManager.getPreviewDecision(experience, variationId) resolves the variation directly from the passed experience.variations (not shared config), returns a BucketedVariation-shaped decision bypassing rules/env/status/traffic/stored/hash, with no putData and no enqueue. Unknown variation -> null. 7 tests + 104 data suite green. Beads: ai-driven-product-dev-rr3l Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…isolation (AC3,AC5,AC6,AC7)
Add enableStorage flag (gates putData in DataManager fresh-bucketing branch). Context.setPreview
resolves via shared config or getConfigByExperience, validates variation, stores per-context
_preview. Preview context: runExperience forces getPreviewDecision for the target key, all run*
run with enableTracking/enableStorage false + BUCKETING fires suppressed, trackConversion
short-circuits. Zero /track (fetch+beacon) and zero store writes; concurrent non-preview context
unaffected. Fixed latent getData() {}-vs-null bug. Node 4 preview tests + 159 js-sdk suite +
Playwright browser gate (zero-trace on real transport) all green.
Beads: ai-driven-product-dev-t5ir
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dit R1 fix) Thread enableStorage through matchRulesByField -> selectLocations and gate its putData (data-manager.ts) so a preview context running a location-targeted non-target experience writes nothing. Guard Context runCustomSegments/setDefaultSegments/updateVisitorProperties on _preview (no visitor-state persistence on a preview context). Fix JSDoc @returns parse error in parse-preview-param.ts. 4 new storage tests + browser store-write spy green; js-sdk 163 + data 104 + segments 7 + experience 12 suites green. Beads: ai-driven-product-dev-y5gj Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…review (decision-audit R2) selectLocations obtained the visitor's stored locations array by reference from getData (live _bucketedVisitors on the memory path) and push/spliced it in place BEFORE the enableStorage-gated putData -- mutating shared visitor state on a preview context. Now operates on a copy when enableStorage is false (zero change to the normal persist path). Regression test (seed locations, run under preview, assert array unchanged) + 9 preview tests + 164 js-sdk + 104 data (incl golden-vector) + browser gate all green. Beads: ai-driven-product-dev-ehrh Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stale RED comments getConfigByExperience now evicts its cache entry on fetch rejection (guarded against clobbering a newer entry) so a transient failure no longer blocks preview retry for 60s; regression test added. Removed stale RED-phase comments + now-false 'as any' casts from committed preview tests (methods exist on impl+interface). 8 config + 24 api + 104 data + 9 context-preview + 164 js-sdk + browser gate 39 all green. Beads: ai-driven-product-dev-4qqt Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pired preview-config cache entries Adds localeCompare comparators to the two bare string .sort() calls flagged by SonarCloud rule S2871 in api-manager-config-by-experience.tests.ts, and sweeps expired entries from the process-wide configByExperienceCache map on every getConfigByExperience() call so resolved-but-expired entries for distinct experienceIds don't accumulate unbounded in long-running servers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(bucketing): anchored layout (contract v12) — JS SDK reference impl
Beads: ai-driven-product-dev-xypk Agent: sdk-js Files: 1 changed Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nRuleType literal Beads: ai-driven-product-dev-rv02 Agent: sdk-js Files: 3 changed Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…owser gate Beads: ai-driven-product-dev-hpul Agent: sdk-js RED: data-unit 4 pass / 5 fail (seam missing); integration 2 pass / 4 fail Files: 3 test files Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ted_into_experience_key seam Beads: ai-driven-product-dev-hpul Agent: sdk-js GREEN: data-unit 9/9, full data pkg 90/90, integration 5/6 (AC3 = test-timing bug, fix next) RuleManager untouched (AC7). package.json build-churn deliberately excluded. Files: packages/data/src/data-manager.ts Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cross-instance read Beads: ai-driven-product-dev-hpul Agent: sdk-js Integration now 6/6. Test-timing bug (async DataStore flush), seam unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bit-identical
Beads: ai-driven-product-dev-uosk
Agent: sdk-js
Rules suite 47/47 (38 existing + 9 new). All 8 rows assert RuleManager.isRuleMatched({})=false (Compatibility contract). RuleManager src untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
qs-02: Experiment preview (debug token + preview links)
feat(rules): mutual-exclusion audience rule (bucketed_into_experience_key) — qs-03
There was a problem hiding this comment.
Code Review
This pull request introduces several major features, including an anchored bucketing layout to prevent visitor reshuffling when traffic allocations are raised, a zero-trace preview mode for forcing specific experience variations, and a mutual-exclusion audience rule (bucketed_into_experience_key). It also adds support for a debugToken to force low-cache config fetches. The code review identifies critical issues that need to be addressed: rule composition is broken within audiences containing the mutual-exclusion rule because it bypasses the rule manager; the preview override is missing in runExperiences (plural); feature previewing is broken because the preview state is not propagated to FeatureManager; and _resolveBucketingExclusion lacks defensive handling for a missing visitorId.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…ument how to test
Reads the ?convert_preview={experienceId}.{variationId} param at each demo's
context-creation seam via the already-public parsePreviewParam helper, and
awaits context.setPreview() before the experience renders. Adds a "Testing
preview links" section to each demo README with a concrete example URL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…arity) runExperiences() now returns the forced preview decision for the previewed experience (replace-in-place, else append), matching runExperience() and the PHP/Android/Python/iOS SDKs; other experiences still decide normally and zero-trace holds. Extends the debug-token AC2 hygiene test to the preview ?exp= fetch seam, and corrects a stale test comment that claimed the (now passing) zero-trace storage regression locks must fail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L42GvyvKc4t8AXG7nnhq3f
Cross-SDK release review — javascript-sdk #418Reviewed at head Context: this SDK is the reference implementation, so several items below aren't "JS bugs" so much as decisions the other three either didn't inherit or inherited differently. Those are marked [parity] and need one owner deciding which side is normative. 1. Preview is not zero-trace — location lifecycle events still fire
Both location fires pass Ruby, iOS, and Android never fire location lifecycle events at all, so this is JS-only. 2. Anchored anchors and bucket values live in different coordinate spaces
Any project that sets the public 3. Preview config cache key omits the fields that change the URL
Two SDK instances sharing an ruby-sdk#43 ( 4. Query parameters are interpolated without encoding
A 5. [parity] Exclusion audiences resolve from the leaf alone, ignoring siblings
iOS matches this deliberately ( For an audience like 6. [parity] Explicit-null
|
…rams Cross-SDK release-review parity fixes (JS oracle): URL-encode environment/exp/debug_token in both config-fetch builders; suppress LOCATION_ACTIVATED/DEACTIVATED events while a preview is active via a preview-scoped suppressEvents flag (location matching still runs; normal enableTracking:false runs still fire events). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
Thanks for the thorough cross-repo pass, Joseph — genuinely useful. I went through every item against the code and the qs specs. JS dispositions below; fixes pushed in Fixed (
|



Full Stack v12 — next major SDK release
This branch delivers three major capabilities, shipped consistently across all Full Stack SDKs. All three are inert until activated on the backend/UI side, so this deploys safely in any order.
1. Anchored (ramping) bucketing layout
Makes changing an experiment's total traffic allocation safe on a running experiment.
versionproperty:> 11→ anchored pass (contract v12);≤ 11/ missing → packed pass, bit-identical to today. No schema change, zero migration.2. Experiment preview
Lets users preview any variation — including drafts and paused ones — in their own environment, with zero trace.
debugTokenconfig option (24h QA token) unlocks the full config, including draft/paused experiences.convert_preview={expId}.{varId}force a single variation with no side effects: no tracking events, no visitor-state writes, no impact on reporting.3. Mutual exclusion / inclusion audience rule
Lets teams control which experiments a visitor can be in at the same time, through the normal audience-targeting rules.
bucketed_into_experience_key, resolved against SDK-stored bucketing state.negated: true→ "NOT in experiment X" (mutual exclusion); default → "in experiment X" (inclusion).Notes
version12 stamp, plus the related convert-ui work (preview link/QR button, rule picker) and shared docs, are tracked and coordinated separately.