Skip to main content
An experiment is one question. A variant is one answer being tested. Every experiment has a 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 call getVariant(), no network request happens. The SDK computes:
then walks the variants, accumulating their traffic splits as basis points, and returns the one whose range contains the bucket. Three consequences follow, and they’re the reason it works this way: It’s stable. The same identity and key always produce the same variant — on every page load, every device, every session, forever. A user never sees the interface change under them. It’s fast. No network call on the hot path. 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
  • After identify(userId), it’s your user id
Which leads to the single most common surprise: calling 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, and defineExperiment() gives you a typed union so an unhandled arm is a compile error rather than a silent fallthrough:
But be deliberate. Every extra arm splits your traffic, so four arms at equal weight means each gets 25% and you wait roughly twice as long as a two-arm test to reach the same certainty. Most mature teams run two or three arms and more experiments, rather than fewer experiments with more arms.

Unknown keys return control

getVariant() returns 'control' for an experiment key it doesn’t recognise — one that doesn’t exist yet, is finished, or is paused. It logs a warning the first time, but never throws. 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.

Experiments end

When a winner is called, a cleanup PR promotes the winning code and deletes the losing branch along with the getVariant() call. The flag is temporary by design — you end with plain code, not an accumulating pile of dead conditionals.