> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trevosdk.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Experiments and variants

> How assignment works, why it's deterministic, and what that means for your users.

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:

```
bucket = hash(identity + ":" + experimentKey) % 10000
```

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](/reference/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](/troubleshooting/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:

```ts theme={null}
const emailTiming = defineExperiment('email-capture-timing', [
  'control',
  'before-checkout',
  'after-payment',
]);
```

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.
