Skip to main content

Evaluation record

The EvaluationRecord is the single normative artifact produced by every flag evaluation in every exd SDK. It is the only contract between SDKs and any downstream consumer. There is no second record shape, no per-sink shape variation, and no transformation layer that changes its semantics in transit.

This invariant is the telemetry analogue of the manifest spec's "single validator" rule: any property the analysis surface relies on MUST be enforced by the SDK at record-emission time.


Wire format

JSON is the canonical wire format. Streaming-bus encodings (Avro, Protobuf) are derived from the JSON schema field-for-field; their schema files are tracked in crates/exd-client/schemas/ and versioned with schema_version.

Records are emitted one per evaluation. Batching is a sink concern (see sinks) and never changes the record shape.

JSON encoding invariants

These rules apply to every record on every sink. Sink implementers and warehouse consumers can rely on them.

AspectRule
EncodingUTF-8.
WhitespaceInsignificant. The reference SDK emits compact JSON (no insignificant whitespace) for records; pretty-printing is reserved for CLI output.
NumbersAll integer fields fit in signed 64-bit. Floating-point fields use IEEE 754 double precision. Producers MUST NOT emit NaN, +Infinity, or -Infinity; consumers MAY reject records that contain them.
TimestampsRFC 3339 UTC strings with millisecond precision (2026-05-08T14:22:13.123Z). Producers MUST emit UTC (Z suffix) and SHOULD include millisecond precision; consumers MUST accept any RFC 3339 timestamp.
Object key orderingInsignificant. Consumers MUST NOT depend on key order.

Schema

{
"schema_version": 1,
"evaluation_id": "01HXYZW3TQK4GVNB...",

"timestamp": "2026-05-08T14:22:13.123Z",
"ingested_at": null,

"namespace": "checkout",
"environment": "production",
"flag_key": "new-checkout-flow",

"variant_key": "variant-b",
"variant_value": { "type": "bool", "value": true },
"evaluation_reason": "matched_rule",
"matched_rule_id": "rule-2",

"manifest_version": 43,
"manifest_etag": "abc123def",

"unit_id_hash": "9f86d081884c7d65...",
"unit_id_type": "user",
"secondary_unit_ids": {},

"context_attributes": { "country": "US", "plan": "pro" },

"sdk_name": "exd-client-rust",
"sdk_version": "0.4.2",

"request_id": null,
"trace_id": null,
"span_id": null
}

Field reference

FieldRequiredTypeNotes
schema_versionyesintegerCurrently 1. Producers MUST emit this.
evaluation_idyesstringULID or UUIDv7. Time-orderable, unique within a producing SDK process.
timestampyesRFC 3339 stringWall-clock time of the evaluation. UTC with millisecond precision RECOMMENDED.
ingested_atnoRFC 3339 string | nullWhen a downstream loader received the record. SDK MUST emit null.
namespaceyesslugThe flag-namespace slug from the manifest.
environmentyesslugThe environment key resolved at evaluation time.
flag_keyyeskeyThe flag key.
variant_keyyeskeyThe variant key returned to the caller.
variant_valueyestyped variant (see below)The typed value of the variant.
evaluation_reasonyesenumOne of matched_rule, fallthrough, off, prerequisite_failed, error, sdk_default. See § Evaluation reasons.
matched_rule_idconditionalstring | nullRequired when evaluation_reason = matched_rule; null otherwise.
manifest_versionyesintegerThe monotonic manifest version active at evaluation time. Same value the manifest spec calls manifest_version; the SDK type Namespace::flag_version() returns this.
manifest_etagnostring | nullETag from the loader if available. Useful for joining records to the exact archive bytes.
unit_id_hashconditionalstring | nullSHA-256 hex digest of the entity identifier used for bucketing. See § Privacy filtering. Required if and only if a bucketing identifier was present in the evaluation context.
unit_id_typeconditionalslugThe type tag of the bucketing identifier (user, account, device, session, …). Required when unit_id_hash is present.
secondary_unit_idsnomap<string, string>Map of additional ID types to their SHA-256 hex digests, for cross-unit analysis. Empty object permitted.
context_attributesyesmap<string, scalar | string-list>Privacy-filtered context attributes. Values are typed per the AttrValue enum: string, integer, float, boolean, or StrList (a JSON array of strings). See § Context attribute encoding. Empty object permitted.
sdk_nameyesstringE.g. "exd-client-rust", "exd-client-typescript". The value "exd-server" identifies records produced by the server-side evaluation path (POST /evaluate{,/all}).
sdk_versionyesstringSemver.
request_idnostring | nullApplication-level request identifier when known.
trace_idnostring | nullOTel trace ID hex string when an OTel context is active.
span_idnostring | nullOTel span ID hex string when an OTel context is active.

Context attribute encoding

context_attributes carries the (privacy-filtered) typed evaluation context. Each value is encoded per its AttrValue variant:

AttrValue variantJSON encoding
String(s)JSON string
Int(i)JSON integer
Float(f)JSON number
Bool(b)JSON boolean
StrList(v)JSON array of strings

StrList is the only collection type in schema version 1. Mixed-type arrays, nested objects, and arrays of non-string scalars are NOT part of the contract; producers MUST NOT emit them and consumers MAY reject records that contain them.

Variant value encoding

variant_value is an inline-table object with a type discriminator and a value payload. The five types correspond to the manifest spec's flag types:

typevalue JSON encoding
"bool"JSON boolean
"string"JSON string
"int"JSON integer
"float"JSON number
"json"any JSON value

A producer MUST emit type matching the flag's declared flag.type. A flag.type mismatch is an SDK bug.

Evaluation reasons

ReasonWhen it appliesmatched_rule_id
matched_ruleA rule's predicate matched the context and that rule's variant was returned.The rule's stable identifier ("rule-<index>" or rule's declared id).
fallthroughNo rule matched and the env's variant was returned.null
offThe flag is in the kill-switch shape (a variant declared on the env block with no rules, or _-default returned).null
prerequisite_failedA prerequisite flag returned a value that failed the dependency check.null
errorThe SDK encountered an internal error and fell back to a default. The record is still emitted for observability.null
sdk_defaultThe caller passed a default to the eval call site and the SDK returned it because the flag is unknown or the SDK is uninitialized.null

This vocabulary is stable per § Stability and evolution. Earlier versions of exd used a single rule_matched string field; producers SHOULD emit evaluation_reason and matched_rule_id separately as of schema version 1.


Privacy filtering

exd records MUST NOT contain raw personally identifiable information at the record-emission boundary. Two filtering rules apply unconditionally:

1. Entity identifier hashing

The bucketing identifier (the value referenced by [segment.bucket].entity_id_attribute) is hashed with SHA-256 and emitted as unit_id_hash in lowercase hex. The raw identifier MUST NOT appear anywhere in the record.

A flag namespace MAY opt out of hashing by setting raw_entity_ids = true in namespace.toml (see namespace § raw_entity_ids). When set, the SDK emits the raw identifier in unit_id_hash (the field name is preserved for schema stability; the contents are no longer a hash). This option is intended for non-personal identifiers (internal account IDs, B2B tenant slugs) and SHOULD NOT be used for any field that could constitute PII.

2. Private attribute filtering

Attributes named in namespace.private_attributes (or per-flag flag.private_attributes) MUST be removed from context_attributes before record emission. Producers MUST also remove these attributes from secondary_unit_ids keys and values. The two lists are unioned at filter time; declaring an attribute on one does not unset the other. See namespace § private_attributes and flag § private_attributes.

Size cap

Records SHOULD NOT contain attributes whose values exceed 1 KiB after JSON serialization. Producers MAY truncate long values to a documented length and emit a T011 diagnostic when reading such records.


Stability and evolution

The schema is versioned by the integer schema_version field. Compatibility rules:

  • Backward compatibility. Readers of version N MUST accept version N-1 records. Producers of version N MUST NOT remove fields that existed in N-1.
  • Field addition. New optional fields MAY be added at any version increment. Consumers MUST ignore unknown fields.
  • Field removal or repurposing. Requires a major version bump. Producers and consumers from different majors are not interoperable.
  • Enum value addition. New evaluation_reason values MAY be added at minor increments; consumers MUST tolerate unknown values by treating them as error for analysis purposes (and emitting T012 if they implement that diagnostic).

Schema version 1 is stable as of the spec acceptance date.


See also