feat: server-side A/B experiment assignment (Phase 1)#699
Conversation
| // so neither server-router nor ClientRouter merges an empty `experiments` into every | ||
| // app's appProps, and nothing extra is inlined into the page. | ||
| if (!experimentsEnabled()) { | ||
| return; |
There was a problem hiding this comment.
Shouldn't we expire stale cookies when experiments are disabled? eg. users who already have x-ab-* cookies keep readable variant cookies for up to 90 days. Clients can continue seeing/applying a variant even while the kill-switch is expected to put everyone in control.
| protocol: 'ILC_CLIENT_PROTOCOL', | ||
| }, | ||
| experiments: { | ||
| enabled: 'EXPERIMENTS_ENABLED', |
There was a problem hiding this comment.
prefix with ILC_? Not sure, but it looks like half of the variables follow this convention.
| const { experiments } = this.#ilcState; | ||
| const experimentsProps = experiments ? { appProps: { experiments } } : {}; | ||
|
|
||
| return deepmerge.all([appConfigProps, routeProps, experimentsProps]); |
There was a problem hiding this comment.
a bit expensive operation. I do not see alternative now but it could be a bottleneck potentially
| * | ||
| * @param options.secure emit cookies with `Secure` (set when the site is https). | ||
| */ | ||
| export function assignExperiments( |
There was a problem hiding this comment.
Do you use function by purpose ? I could be complex to scale functions maybe it make sense to create responsible class ?
There was a problem hiding this comment.
since it's a pure resolver without any internal mutable state to encapsulate and safe to call once per request with no lifecycle. constructor would add additional step without owning any state
| import { createHash } from 'node:crypto'; | ||
| import type { ExperimentId, ExperimentVariant, VariantName } from './interfaces'; | ||
|
|
||
| const BUCKET_COUNT = 100; |
There was a problem hiding this comment.
Does it make sense to put it into configuration ?
There was a problem hiding this comment.
I don't think to be honest at the moment it's valuable enough, it's actually should be considered in percentage mass - that's why it's like not any number is fit here
| // ILC mints its own session id (gateway-agnostic, ILC-public like `ilc-i18n`), so the | ||
| // feature works in OSS ILC without depending on any deployment's identity headers. | ||
| /** Stable per-visitor session id minted by ILC; seeds deterministic bucketing. */ | ||
| export const SESSION_COOKIE = 'ilc-sid'; |
There was a problem hiding this comment.
Who is responsible for establish the cookie ?
There was a problem hiding this comment.
added additional comments regarding this
- kill-switch now expires stale x-ab-* cookies so disabling puts mid-experiment visitors back on control instead of leaving a readable variant cookie for up to 90 days - rename env var EXPERIMENTS_ENABLED -> ILC_EXPERIMENTS_ENABLED to match the ILC_ convention used by the other ILC-specific settings - clarify in cookies.ts that it only defines cookie names/options; index.ts is what actually writes them - fill docs/ab-testing.md: a Phase 1 guide for product (what you can/can't do) and developers (defining/reading experiments)
Coverage ReportIlc/serverCommit SHA:61d34c18a7cc506825097d72a844f3ca81bc5726 Test coverage results 🧪File details
Ilc/clientCommit SHA:61d34c18a7cc506825097d72a844f3ca81bc5726 Test coverage results 🧪File details
RegistryCommit SHA:61d34c18a7cc506825097d72a844f3ca81bc5726 Test coverage results 🧪File details
|
Introduces server-side A/B experiment assignment in ILC (Phase 1).
ILC resolves a single experiment variant per visitor in the
onRequesthook, before any fragment renders, and propagates it to every fragment viaappProps.experimentsand to the browser via inlinedilcState+ sticky cookies. Assignment is deterministic (hash-based), cookie-sticky, config-driven, and fail-safe — any failure falls back to the baseline variant. A global kill-switch turns the whole layer off without a deploy.See
docs/ab-testing.mdfor the full design: what Phase 1 supports (and what it intentionally does not do yet), how to define experiments in configuration, and how apps read the assigned variant.The consuming side — a
useExperimenthook plus genericappPropsdelivery — ships as a companion change in the React application library.