bucketingId, experimentKey, and variant
list, a conforming implementation returns the same variant name as every other conforming
implementation.
You only need this page if you are implementing assignment in a language Trevo does not ship
an SDK for. If you are only recording conversions, use REST — no
assignment logic required.
Why it is frozen
Assignment is deterministic and stateless. Nothing is stored, so a change to the arithmetic silently re-buckets every user in flight. If two implementations disagree, the same visitor gets one variant in the browser and another from the backend, their exposures collide, and the statistics stop meaning anything. Nothing errors and nothing alerts — the numbers are just quietly wrong.Terminology
1. Identity resolution
Resolved in this order, first non-empty value wins:- The identified user id, if the host application has called
identify() - The anonymous id — a v4 UUID minted on first visit and persisted by the platform
bucketingId explicitly and are responsible for passing the same
value the browser would use, in practice the trevo_id cookie.
The bucketingId is used verbatim. Do not trim, case-fold, or Unicode-normalise it.
café (NFC) and café (NFD) are different identities and bucket differently.
2. Hash
FNV-1a, 32-bit, over UTF-16 code units — not over UTF-8 bytes.- The unit is a UTF-16 code unit, not a byte. Textbook FNV-1a consumes one byte at a time; this consumes one 16-bit unit, XORing all 16 bits. ASCII inputs agree with a byte-wise implementation by coincidence — every ASCII character is one code unit whose high byte is zero — and diverge for everything else. An implementation validated only against ASCII identifiers will pass its own tests and corrupt data for any customer with non-ASCII ids.
- Characters outside the BMP are two units.
🙂(U+1F642) is the surrogate pair0xD83D 0xDE42and contributes two iterations — not one code point, and not four UTF-8 bytes.
3. Bucket derivation
U+003A), and the operands are concatenated in that
order — identity first.
Known property: the concatenation is not injective when an operand contains a colon.
("a:b", "c") and ("a", "b:c") both hash "a:b:c" and share a bucket. This is unreachable
in practice — bucketing ids are UUIDs, experiment keys are slugs — and is frozen as-is rather
than fixed, because adding escaping would re-bucket every existing user.
4. Bucket walk
Variants are walked in list order, accumulating traffic in basis points:trafficSplitis a whole percent, scaled to basis points by* 100, giving a[0, 10000)space that matches the bucket space exactly. All arithmetic is exact in integers; no rounding is performed and none is permitted.- The comparison is strictly less than. A variant owns
[cumulative_before, cumulative_after). With a 50/50 split, bucket4999is the first variant and5000is the second. - List order is part of the contract. Reordering variants with identical splits reassigns users.
- The trailing return is reachable only when splits sum to less than 100. Configs are schema-validated to sum to exactly 100, so it is a guard, not a behaviour to rely on.
5. Degenerate configurations
An SDK must never throw out of variant resolution. Failing to assign degrades to control,
because a thrown error in a host application’s render path is a worse outcome than an
unbucketed user.
6. Override precedence
Overrides are a QA affordance. Resolution order, highest first:- Explicit override —
?trevo_force=<key>:<variant>in the browser, or a programmatic / environmentforceVariantsmap in a server SDK. An override applies only if the named variant exists in that experiment’s loaded config; an unknown variant name is ignored and resolution continues. This is what stops a crafted URL from makinggetVariant()return an arbitrary string. - Deterministic assignment — sections 2 to 4.
?trevo_force= persists for the tab session, and ?trevo_force=clear drops
it. Overrides are per experiment key; comma-separate to force several.
7. Conformance
Trevo publishes conformance vectors alongside this spec:hashVectors—{ input, hash }pairs exercising the hash directly, including ASCII, accented Latin in both NFC and NFD, Cyrillic, CJK, emoji, and astral-plane input.cases—{ name, experimentKey, bucketingId, variants, hash, bucket, expected }. Thehashandbucketfields are diagnostics: they tell a failing implementation whether it diverged in the hash or in the walk.