FlagBeta

Flag evaluation, version 1

This is the language-neutral definition of how flag evaluates a flag for a context. The Rust crate crates/flag-eval is the reference implementation; every SDK implements this document and must pass the conformance vectors in spec/vectors/ exactly:

  • evaluation.json: bucketing (salt, key, unit → bucket) and suites, each a snapshot plus cases (flag key, context JSON → expected result).
  • redaction.json: private-attribute redaction.
  • envelope.json: opening an encrypted server snapshot (see sdk-protocol.md).

All JSON is UTF-8. “String” means a JSON string; “number” a JSON number, compared as an IEEE 754 double.

Snapshot

{
  "envId": "env_...",
  "version": 42,
  "hashedTargets": false,
  "flags": { "<flag key>": Flag },
  "segments": { "<segment key>": Segment }
}

Unknown fields anywhere must be ignored, so the server can add fields without breaking older SDKs.

Flag

Field Type Meaning
key string The flag key.
version integer Flag version, reported in events.
on boolean Targeting on. Default false.
variations array of JSON values The possible values. Results name them by index.
offVariation integer, optional Served when the flag is off or a prerequisite fails.
fallthrough Serve Served when no target or rule matches.
targets array of Target Individual targets, checked in order.
rules array of Rule Checked in order after targets.
prerequisites array of {key, variation} Flags that must be on and serve that variation.
salt string Bucketing salt.
clientSide boolean Visible to client-side keys.
trackEvents boolean SDKs send a full feature event per evaluation.

Serve is {"variation": i} or {"rollout": Rollout}. If both are present the rollout wins. If neither is, the flag is malformed.

Target: {"contextKind": "user", "variation": i, "values": ["key", ...]}. contextKind defaults to "user".

Rule: {"id": "...", "clauses": [Clause, ...], ...Serve, "trackEvents": false}. A rule matches when every clause matches; a rule without clauses matches every context. The Serve fields (variation, rollout) sit directly in the rule object.

Rollout:

{
  "variations": [{"variation": 0, "weight": 50000}, {"variation": 1, "weight": 50000}],
  "bucketBy": "key",
  "contextKind": "user",
  "seed": "optional; replaces the flag's salt",
  "experiment": {
    "id": "exp_...",
    "defaultVariation": 0,
    "layer":   {"key": "...", "salt": "...", "start": 0, "end": 50000},
    "holdout": {"key": "...", "salt": "...", "start": 0, "end": 5000}
  }
}

Weights are integers in units of 1/100,000. bucketBy defaults to "key", contextKind to "user".

Segment

Field Type Meaning
key, version, salt As for flags.
included, excluded array of string User-kind keys always in / always out.
includedContexts, excludedContexts array of {contextKind, values} The same for any kind.
rules array of SegmentRule {"id", "clauses", "weight"?, "bucketBy": "key", "rolloutContextKind": "user"}

Contexts

A context is a JSON object.

  • Single kind: {"kind": "user", "key": "u1", "<attribute>": value, ..., "_meta": {...}}. kind defaults to "user".
  • Multi kind: {"kind": "multi", "<kind>": {single context without kind}, ...}, at least one kind.

A context is invalid, and evaluation returns the error INVALID_CONTEXT, if it is not an object, kind is not a string, a kind name is empty, is "kind" or "multi", or has characters other than ASCII letters, digits, ., _ and -, or any kind lacks a non-empty string key. The attributes of a kind are its fields except kind and _meta; key is an attribute.

Attribute references. A name that does not start with / names a top-level attribute literally (so a/b is the attribute called a/b). A name that starts with / is a path: split the rest on /, replace ~1 with / and then ~0 with ~ in each part, take the first part as a top-level attribute and each further part as a field of a JSON object (anything else: missing). A JSON null value counts as missing.

Private attributes

Before a context leaves an SDK in an event, the SDK removes private attributes: the names configured globally on the SDK, then those in the context’s _meta.privateAttributes, each a reference as above. key and kind are never removed. The context’s _meta is replaced by {"redactedAttributes": [references actually removed, in that order]}, or dropped if nothing was removed. In a multi context each kind is redacted on its own.

Evaluation

evaluate(snapshot, flagKey, context) returns {value, variation, reason, experimentId?}. value is the variation’s value, or null with variation: null when the caller’s default must be served.

  1. If the snapshot has no flag flagKey: error FLAG_NOT_FOUND.
  2. If the context is invalid: error INVALID_CONTEXT.
  3. Evaluate the flag:
    1. If the flag is on the stack of flags being evaluated (a prerequisite cycle): error MALFORMED_FLAG.
    2. If on is false: serve offVariation with reason OFF (or null and no variation, reason OFF).
    3. For each prerequisite in order: if the snapshot lacks the flag, or evaluating it (recursively, with this context) does not give the required variation, or that flag is not on, serve the off variation with reason PREREQUISITE_FAILED and prerequisiteKey. If the prerequisite’s evaluation is a MALFORMED_FLAG error, return that error.
    4. For each target in order: if the context has the target’s kind and that kind’s key is in values, serve the target’s variation, reason TARGET_MATCH.
    5. For each rule in order (index i): if it matches, serve it, reason RULE_MATCH with ruleIndex: i and ruleId.
    6. Serve fallthrough, reason FALLTHROUGH.
  4. Serving a variation index that is not in variations (including offVariation): error MALFORMED_FLAG.

An error result is {"value": null, "variation": null, "reason": {"kind": "ERROR", "errorKind": "..."}}. SDKs add WRONG_TYPE when a typed accessor (boolean, string, number, object) gets a value of another JSON type; they return the caller’s default for every error.

Clauses

{"contextKind": "user", "attribute": "email", "op": "endsWith", "values": ["@acme.com"], "negate": false}
  • segmentMatch: matches if the context is in any segment listed in values (non-strings and unknown segments are skipped). negate inverts the result. attribute and contextKind are ignored.
  • An operator not listed below never matches, whatever negate says.
  • Attribute kind: the actual value is the array of the context’s kind names (any contextKind).
  • Otherwise, if the context lacks contextKind or the attribute is missing, the clause does not match, whatever negate says.
  • If the actual value is an array, the clause matches when any element matches; otherwise when the value matches. A value matches when the operator holds for it and any of values. Then negate inverts the result.
op Holds when
in JSON equality; numbers compare by value (30 equals 30.0); arrays and objects deeply.
startsWith, endsWith, contains Both are strings and the string test holds (case-sensitive, by code point).
matches The actual value is a string and the clause value, a regular expression, is found anywhere in it.
lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual Both are numbers (no string coercion).
before, after Both are dates and actual is strictly earlier / later.
semVerEqual, semVerLessThan, semVerGreaterThan Both are strings that parse as versions, compared by precedence.
segmentMatch See above.

Regular expressions use the syntax common to RE2, ECMAScript and Python (no look-around, no backreferences) with RE2’s meaning: the shorthand classes are ASCII (\d is [0-9], \w is [0-9A-Za-z_], \s is [\t\n\f\r ], \b and \B are ASCII word boundaries, \D \W \S their negations, inside and outside character classes), . matches one Unicode code point other than \n, $ matches only at the very end, and escaped punctuation (\-, \:) is that character. Engines that differ rewrite the pattern first (the Rust implementation, ECMAScript’s \s, Python’s $ and \s). A value that is not a string or not a valid pattern never matches.

Dates are numbers (Unix milliseconds, floored) or strings in exactly the form YYYY-MM-DDTHH:MM:SS[.fraction](Z|+HH:MM|-HH:MM), where T and Z may be lowercase. Fractions of any length are truncated to milliseconds. Field ranges are checked (days per month with leap years, hours 0-23, minutes and seconds 0-59, offsets up to 23:59). Anything else is not a date.

Versions follow Semantic Versioning 2.0.0, except that the minor and patch numbers may be left out (2 is 2.0.0, 2.1 is 2.1.0). No leading v, no leading zeros in numbers or numeric pre-release identifiers. Build metadata (+...) must be well-formed and is ignored. Precedence: major, minor, patch numerically; a version with a pre-release is lower than the same version without; pre-release identifiers compare left to right, numeric ones numerically and below alphanumeric ones, alphanumeric ones by ASCII order; a shorter list that is a prefix of a longer one is lower.

Segments

A context is in a segment when, in order:

  1. its user-kind key is in included, or for some includedContexts entry the context’s key of that kind is in values: in;
  2. its user-kind key is in excluded, or the same for excludedContexts: out;
  3. some rule’s clauses all match and, if the rule has a weight, the bucket of the context’s rolloutContextKind/bucketBy unit under (segment salt, segment key) is below weight: in;
  4. otherwise out.

Segments may test other segments (segmentMatch); a segment already being evaluated does not match.

Hashed targets

When the snapshot has hashedTargets: true (client-safe snapshots), every key in target values, segment included/excluded and segment context values is the lowercase hex SHA-256 of the UTF-8 key. Compare the hash of the context’s key instead of the key. Clause values are never hashed.

Bucketing

bucket(salt, key, unit) = floor(H * 100000 / 2^64)
H = the first 8 bytes of SHA-256(UTF-8 of salt + "." + key + "." + unit), as a big-endian unsigned 64-bit integer

Compute it with exact integer arithmetic (128-bit, big integers or mulhi): doubles lose bits.

The unit is the context’s attribute bucketBy of kind contextKind: a string as is, or a number with no fractional part and magnitude at most 2^53 written in decimal without exponent or fraction (12345.0 is "12345"). Any other value, a missing attribute or a missing kind leaves no unit.

Rollouts (no experiment): with salt = the rollout’s seed if present, else the flag’s salt, take b = bucket(salt, flagKey, unit), or 0 when there is no unit. Walk variations adding up weights; serve the first whose running sum exceeds b. If none does (weights short of 100,000), serve the last one. An empty list is MALFORMED_FLAG.

Experiments (the rollout has experiment). The context is in the experiment only if all hold, else it gets experiment.defaultVariation and is not in the experiment:

  1. it has a unit;
  2. if holdout is set: bucket(holdout.salt, holdout.key, unit) is not in [start, end);
  3. if layer is set: bucket(layer.salt, layer.key, unit) is in [start, end);
  4. walking variations as above with b = bucket(salt, flagKey, unit), some running sum exceeds b (weights may add up to less than 100,000: the rest is outside the traffic allocation).

In the experiment, the reason gets "inExperiment": true and the result "experimentId". SDKs send an exposure event for such results.

Reasons

{"kind": "OFF"}
{"kind": "TARGET_MATCH"}
{"kind": "RULE_MATCH", "ruleIndex": 0, "ruleId": "r1", "inExperiment": true}
{"kind": "FALLTHROUGH", "inExperiment": true}
{"kind": "PREREQUISITE_FAILED", "prerequisiteKey": "other-flag"}
{"kind": "ERROR", "errorKind": "FLAG_NOT_FOUND | MALFORMED_FLAG | INVALID_CONTEXT | WRONG_TYPE"}

inExperiment is present only when true. OpenFeature providers map them to resolution reasons: OFF → DISABLED; TARGET_MATCH and RULE_MATCH → TARGETING_MATCH; FALLTHROUGH → SPLIT when the fallthrough is a rollout, else DEFAULT; PREREQUISITE_FAILED → PREREQUISITE_FAILED; ERROR → ERROR with the OpenFeature error code (FLAG_NOT_FOUND, PARSE_ERROR for malformed flags, INVALID_CONTEXT or TARGETING_KEY_MISSING, TYPE_MISMATCH). The OpenFeature variant is the variation index as a decimal string.

OpenFeature contexts

An OpenFeature evaluation context maps to a flag context like this: targetingKey becomes key (unless key is set); an attribute kind sets the kind; with kind: "multi", each object-valued attribute is one kind’s context, whose targetingKey likewise becomes its key, and top-level attributes that aren’t objects (including targetingKey) are dropped. Everything else is an attribute. A missing key maps to the OpenFeature error TARGETING_KEY_MISSING, any other invalid context to INVALID_CONTEXT.

This page as Markdown: /docs/evaluation.md · All docs for agents: /llms.txt