Evaluation
How evaluation works
One implementation (packages/eval) is shared by the SDK, the delivery service’s client endpoint and the dashboard debugger, so all three give the same answer for the same context.
Precedence: first match wins
- Step 1
Validate
No snapshot: NOT_INITIALIZED. A context without a non-empty string key: INVALID_CONTEXT. Unknown flag: FLAG_NOT_FOUND. A flag that fails validation (unknown variation ids, rollout weights not summing to 10000): MALFORMED_FLAG.
- Step 2
Flag off
A flag disabled in this environment serves its off variation, reason OFF.
- Step 3
Overrides
First a user override whose key equals the context key, then an organization override for the context’s organizationId. Within a kind the first listed wins. Reason OVERRIDE.
- Step 4
Rules, in order
An enabled rule matches when its conditions match (all = AND, any = OR). It serves a fixed variation or a weighted rollout. Reason RULE_MATCH, with the rule id and index.
- Step 5
Default
Nothing matched: serve the default (“on”) variation, reason FALLTHROUGH.
Every failure path returns the caller’s default value with reason ERROR and an error kind. Evaluation never throws. The typed helpers also return the default with WRONG_TYPE when the flag’s type differs from the one asked for.
Defined behavior for edge cases
- Missing attribute
- The condition does not match, for every operator including the negated ones: an unknown context never gains access through “does not equal”.
- Array attribute
- Positive operators match if any element matches; negated operators if none do.
- Numeric operators
- Both sides are coerced with Number(); non-numeric or empty strings never match.
- eq, neq, in, not_in
- Strict type equality: the string "1" does not equal the number 1.
- Segments
- Excluded list wins, then included list, then the segment’s conditions. Unknown segment keys count as not a member.
- Rollout without its attribute
- The rule is skipped and evaluation continues with the next rule.
- Rule with no conditions
- Never matches.
Operators
- eq
- neq
- contains
- not_contains
- starts_with
- ends_with
- in
- not_in
- gt
- lt
- gte
- lte
- in_segment
- not_in_segment
The canonical bucket
Percentage rollouts must give the same answer in every SDK and on the server, forever, so the hash is a documented contract (packages/eval/src/hash.ts).
The salt is fixed per flag and environment when the flag is created, so assignments survive rule edits and differ between flags. Weights are cumulative basis points in listed order: raising a rollout from 10% to 25% only adds users, and nobody already in the 10% leaves it.
input = UTF-8 bytes of `${salt}.${bucketValue}`
hash = 32-bit FNV-1a, then a murmur3-style finalizer
bucket = hash % 10000 // basis points, 0..9999Run it next to your app
Sign in to the dashboard, or clone the repository and start the whole stack locally with Docker and pnpm.