> ## 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.

# Bucketing spec

> The normative definition of how an identity maps to a variant. Frozen.

Status: **frozen**. Normative for every Trevo SDK.

This defines how an identity is mapped to a variant. It is the one piece of logic that must be
byte-identical on every platform: given the same `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](/install/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

| Term            | Meaning                                                        |
| --------------- | -------------------------------------------------------------- |
| `bucketingId`   | The identity assignment is keyed on. A stable opaque string    |
| `experimentKey` | The experiment's stable key                                    |
| `variants`      | Ordered list of `{ name, trafficSplit }`. Order is significant |
| `trafficSplit`  | Whole percent, `0`–`100`. The list sums to `100`               |
| bucket          | Integer in `[0, 10000)` derived from the identity and key      |

## 1. Identity resolution

Resolved in this order, first non-empty value wins:

1. The identified user id, if the host application has called `identify()`
2. The anonymous id — a v4 UUID minted on first visit and persisted by the platform

If neither is available, do **not** assign: return the control variant and record no exposure.

Switching from an anonymous id to a user id changes the bucketing basis and may therefore
change the assigned variant. That is intended — it is the point at which a visitor becomes a
known user.

Server-side callers supply `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.

```
FNV_OFFSET_BASIS_32 = 0x811C9DC5
FNV_PRIME_32        = 0x01000193

fnv1a32(s):
    hash = FNV_OFFSET_BASIS_32
    for each UTF-16 code unit u in s:      # 0..0xFFFF
        hash = hash XOR u                  # 16-bit XOR, not 8-bit
        hash = (hash * FNV_PRIME_32) mod 2^32
    return hash as unsigned 32-bit
```

Two properties differ from textbook FNV-1a and are the most likely source of a divergent port:

* **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 pair
  `0xD83D 0xDE42` and contributes two iterations — not one code point, and not four UTF-8
  bytes.

Non-JavaScript implementations must encode to UTF-16 and iterate 16-bit units. Unpaired
surrogates are hashed as-is; the algorithm is defined over code units and is total over any
UTF-16 sequence.

Arithmetic is unsigned 32-bit with wraparound.

## 3. Bucket derivation

```
bucket = fnv1a32(bucketingId + ":" + experimentKey) mod 10000
```

The separator is a single ASCII colon (`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:

```
cumulative = 0
for variant in variants:
    cumulative = cumulative + variant.trafficSplit * 100
    if bucket < cumulative:
        return variant.name
return last variant's name
```

* `trafficSplit` is 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, bucket `4999` is the first variant and `5000` is 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

| Condition                                | Required behaviour                                                                                                                           |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `variants` is empty                      | Return `"control"`. Warn once. Do not throw                                                                                                  |
| Splits do not sum to 100                 | Assign anyway, using the walk above. Warn once                                                                                               |
| No resolvable identity                   | Return `"control"`, record no exposure                                                                                                       |
| Experiment key absent from loaded config | Return the last cached assignment for that identity if one exists, otherwise `"control"`. Never assign against a config the SDK has not seen |

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:

1. **Explicit override** — `?trevo_force=<key>:<variant>` in the browser, or a programmatic /
   environment `forceVariants` map 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 making `getVariant()` return an
   arbitrary string.
2. **Deterministic assignment** — sections 2 to 4.

A forced variant **must not** record an exposure and **must not** be written to the assignment
cache. Preview traffic that reaches the results pipeline is indistinguishable from real traffic
and biases the experiment it was previewing.

In the browser, `?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 }`. The
  `hash` and `bucket` fields are diagnostics: they tell a failing implementation whether it
  diverged in the hash or in the walk.

Every Trevo SDK runs these vectors in its own test suite, and CI runs every SDK's suite. **A
new language binding is not conformant until it does.** If you are writing one, pass every
vector before sending a single exposure.
