Skip to content
FlagPilot
Menu

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

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

  2. Step 2

    Flag off

    A flag disabled in this environment serves its off variation, reason OFF.

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

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

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

packages/eval/src/hash.ts
input  = UTF-8 bytes of `${salt}.${bucketValue}`
hash   = 32-bit FNV-1a, then a murmur3-style finalizer
bucket = hash % 10000        // basis points, 0..9999

Run it next to your app

Sign in to the dashboard, or clone the repository and start the whole stack locally with Docker and pnpm.