control — your current behaviour — and one or more variants proposing a change.
Both ship in the same build, deploy at the same time, and run simultaneously. Different users
get different ones, and nobody sees more than one.
Assignment is a hash, not a lottery
When you callgetVariant(), no network request happens. The SDK computes:
getVariant() is synchronous and returns in
under a millisecond, so it’s safe in a render path.
It’s reproducible. Given a user id and an experiment key, you can compute which variant
they’ll get without asking Trevo anything — useful when debugging a report of “this user says
the button is missing”.
The hash is FNV-1a over UTF-16 code units, and it’s a frozen contract shared by every Trevo
SDK. The browser, Node, and any future client all bucket identically, which is what lets a
user get the same variant on the web and from your backend. See the
bucketing spec if you need the normative details.
Identity decides everything
Assignment is a function of identity, so the question “which variant does this user get?” reduces to “who does Trevo think this user is?”- Before
identify(), that’s an anonymous id stored in browser local storage and mirrored to atrevo_idcookie so the server can resolve the same variant during SSR - After
identify(userId), it’s your user id
identify() can move a user from
one variant to another, because the input to the hash changed. If they were mid-session and
looking at the treatment, they may see the control after logging in.
The fix is to identify early — before rendering anything under test. See
Variant mismatch.
Anonymous ids are also per browser. The same person on a laptop and a phone is two
participants until they sign in, and nothing can retroactively merge their pre-login history
on the second device.
Traffic splits
Splits are whole percentages that must sum to 100. The SDK warns in the console if a misconfigured experiment doesn’t add up, and assigns as best it can rather than failing. 50/50 is the default and usually right — equal groups reach significance fastest. Uneven splits make sense when you’re being cautious about a risky change, at the cost of needing more total traffic to reach the same confidence.More than two variants
Nothing stops you testing three or four arms, anddefineExperiment() gives you a typed
union so an unhandled arm is a compile error rather than a silent fallthrough:
Unknown keys return control
getVariant() returns 'control' for an experiment key that isn’t in the SDK’s config —
one that doesn’t exist yet, was rejected, or is paused — and it never throws. That includes
users who were already assigned: they return to control on the SDK’s next config refresh
(about a minute in the browser, up to five minutes in a mobile app), and get the same
variant back if the experiment resumes, as long as they keep the same identity and storage
(identify() switches bucketing to their user id, and reset() clears stored assignments).
Until the SDK has loaded a config, a returning visitor keeps their last assignment.
That’s deliberate: it means you can merge experiment code before the experiment exists in
Trevo, and a stopped experiment degrades to your current behaviour rather than breaking the
page.
One caveat: a shipped experiment stays in the config for 30 days after it ends, whether
or not a cleanup PR exists. New visitors get the winning variant, but in the browser and in
mobile apps a visitor already assigned a losing variant keeps it, because it stays listed at
0%. On day 30 the experiment leaves the config and every bucketed visitor reverts to control,
which is the variant you shipped against unless control won. Merge the cleanup PR, or remove
the getVariant() call yourself, before then.
Experiments end
When a winner is called, a cleanup PR promotes the winning code and deletes the losing branch along with thegetVariant() call. The flag is temporary by design — you end with plain code,
not an accumulating pile of dead conditionals.