identify() was called after the variant rendered
The most common cause by far.
Before identify(), a visitor is bucketed on an anonymous id. After it, they’re bucketed on
your user id. Different input, different hash, potentially a different variant — so a user who
logs in mid-session can watch the page change.
identify() as early as you can, before rendering anything under test. In React,
that means in an effect that runs as soon as auth resolves, not deep in the component tree.
It also skews data: the anonymous exposure and the identified conversion can end up attributed
to different arms. Identifying early is a correctness fix, not just a cosmetic one.
The same person on two devices
Anonymous ids live in browser local storage, so they’re per browser. The same human on a laptop and a phone is two participants until they sign in, and can legitimately be in different arms. Once both sessions callidentify() with the same user id, both bucket on that id and agree.
Nothing can retroactively merge the pre-login history on the second device.
Expected behaviour, worth explaining to whoever reported it.
Server and browser disagree
Both sides must bucket on the same identity at the same moment:userIdwhen the visitor is identified- otherwise the
trevo_idcookie value
userId while the browser is still anonymous will assign a different
variant to the same visit.
middleware + getTrevoBootstrap() handle this for you. If you’re hand-rolling with
resolveExperiments(), it’s your responsibility.
The config changed and one side is on the old copy
Assignment is deterministic in identity + experiment key + the variant list. Change a traffic split or reorder variants and the sides disagree until caches turn over: the API caches config for ~55s, the browser SDK polls every 60s and serves a localStorage copy up to 24h old before its first fetch lands, and Next.js SSR revalidates on its own ~55s window. Expect a couple of minutes of skew after any edit; a returning visitor can be up to a day behind.Local storage was cleared
Clearing storage discards the anonymous id, and a fresh one is minted on the next visit. That’s a new identity, so a new assignment. Incognito windows start fresh every time — which is why testing in incognito can show you a different variant each session. Note this is why AsyncStorage is a required peer for React Native: without persistence the id is re-minted every launch, re-bucketing every user continuously.Someone is using a forced variant
QA overrides pin a variant for testing:Everyone sees control
Different problem — that’s usually not a mismatch at all:- The experiment key in your code doesn’t match the one in Trevo. An unrecognised key
returns the identity’s last cached assignment if one exists,
'control'otherwise — and logs no warning. UsedefineExperiment()to get a console warning when your declared variants drift from the live config - The experiment is paused or ended, so it left the config — new visitors get control, already-bucketed visitors keep their cached variant (and stop recording exposures)
- The experiment is running but its linked PRs haven’t merged — Trevo holds 100% on control until they do
- The experiment shipped more than 30 days ago and aged out of the config
- Config hasn’t loaded yet — check
trevo.isReady() - The SDK never initialised — see No events arriving