Skip to content

qs-02: Experiment preview (debug token + preview links) - #415

Merged
abbaseya merged 9 commits into
feat/fullstack-v12from
feat/experiment-preview
Jul 15, 2026
Merged

qs-02: Experiment preview (debug token + preview links)#415
abbaseya merged 9 commits into
feat/fullstack-v12from
feat/experiment-preview

Conversation

@abbaseya

@abbaseya abbaseya commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements qs-02 Experiment Preview (debug token + preview links) for the Convert JS SDK — an additive feature (no bucketing-contract change, no API/OpenAPI work; the serving primitives ?exp= and ?debug_token= already exist server-side).

Stacked PR: targets feat/anchored-bucketing-layout (qs-01), NOT main.

Two capabilities:

(A) debugToken config optionconfig.debugToken (string, optional). When set, every config-fetch URL carries debug_token=<value> and a forced _conv_low_cache=1 (regardless of network.cacheLevel), so the SDK receives the full QA config with server caches bypassed. The token is never sent to the track endpoint and never logged. No change to experience-running semantics.

(B) Per-context experiment previewcontext.setPreview({experienceId, variationId}) force-decides one variation, bypassing rules/audiences/segments/locations/environment/status/traffic/stored-decisions/bucketing-hash. Fetches the experience via ?exp= when absent from config (memoized process-wide, 60 s TTL). Zero-trace: on a preview context ALL tracking is suppressed at the enqueue source (fetch AND sendBeacon) and ALL visitor-state writes are disabled; other experiences still evaluate/decide normally. Inert on bad input (warn + normal behavior). Plus a pure exported helper parsePreviewParam(value).

Acceptance criteria — all covered by tests

AC Coverage
AC1 debugToken transport config-matrix tests (debug_token + forced _conv_low_cache)
AC2 token hygiene token absent from track request + logs
AC3 forced decision draft delivered only via mocked ?exp= renders the requested variation
AC4 full bypass draft/paused/mismatched-env/non-RUNNING/traffic-0/different-stored-decision
AC5 zero trace Node transport+store spies and Playwright browser gate (zero /track via sendBeacon+fetch, zero DataStore writes on the real UMD bundle)
AC6 isolation concurrent non-preview context buckets/persists/tracks normally
AC7 inert on bad input unknown experience id / variation id → warn + normal behavior
AC8 memoization two preview contexts, same experience, 60 s → exactly one ?exp= fetch; TTL refetch
AC9 parse helper parsePreviewParam('123.456'){experienceId:'123',variationId:'456'}; malformed → null
AC10 regression lock full monorepo suite green; no behavior change when neither feature is used

Test results

  • Full monorepo Node suite: green (api 24, data 104, js-sdk 164, + all other packages).
  • Playwright browser gate: 39 passed (incl. AC5 zero-trace + zero-store on the real browser transport).

Notable engineering notes

  • New per-call enableStorage flag (mirrors enableTracking) threaded through the bucketing + rule-match + location paths so a preview context performs zero visitor-state writes — chosen over instance-wide suppression to preserve isolation (AC6).
  • The ?exp= response is never merged into shared config (per-context scratch) — no leak across contexts.
  • A 3-round adversarial decision audit found and fixed two zero-trace storage defects (an ungated selectLocations write, and an in-place locations-array mutation via a by-reference getData), both with regression tests.

Open decisions for reviewer (non-blocking, deliberate)

  • Preview forcing is applied on the by-key runExperience only; the plural runExperiences/runFeature(s) run suppressed (zero-trace) but do not force the target. Safe for the canonical draft-only-via-?exp= target (omitted from shared config). Matters only if the previewed experience is also a shared-config experience rendered via the batch API. Flagging for an explicit call.
  • Constructor putSegments (context.ts) seeds segments at construction if visitorProperties are supplied — before setPreview. Outside the preview lifecycle; document that a trace-free preview context should be created without initial visitorProperties.

Test plan

  • Review the zero-trace suppression seams (context.ts run*/trackConversion guards; data-manager enableStorage gating).
  • Confirm token hygiene (AC2).
  • Verify the stacked base is correct (feat/anchored-bucketing-layout).

🤖 Generated with Claude Code

abbaseya and others added 8 commits July 5, 2026 17:45
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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a preview mode feature (qs-02 / SDK-4) to the JS SDK, allowing a Context to be put into a zero-trace preview state for a specific experience and variation. It adds getConfigByExperience to ApiManager with process-wide memoization, supports a debugToken option to force low-cache config fetches, and implements getPreviewDecision in DataManager to bypass standard bucketing and tracking. The review feedback highlights three key improvements: evicting expired entries from the configByExperienceCache to prevent memory leaks, URL-encoding query parameters in getConfigByExperience to avoid query corruption, and shallow-copying storedLocations in DataManager.selectLocations to prevent unintended in-place mutations of stored objects.

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.

Comment thread packages/api/src/api-manager.ts
Comment thread packages/api/src/api-manager.ts
Comment thread packages/data/src/data-manager.ts
@abbaseya abbaseya self-assigned this Jul 5, 2026
@abbaseya
abbaseya requested a review from a team July 5, 2026 18:36
…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>
@sonarqubecloud

sonarqubecloud Bot commented Jul 5, 2026

Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
0 New issues
0 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
1.9% Duplication on New Code

See analysis details on SonarQube Cloud

Base automatically changed from feat/anchored-bucketing-layout to feat/fullstack-v12 July 7, 2026 03:20
@abbaseya
abbaseya merged commit 9aa0d1a into feat/fullstack-v12 Jul 15, 2026
1 check passed
@abbaseya
abbaseya deleted the feat/experiment-preview branch July 15, 2026 23:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants