Files
zeekayandHanzo Dev 99724b95b2 go: the flag evaluator, ported from the Rust staticlib to pure Go
cloud linked a Rust staticlib (native/flags) over cgo to decide flags. That
made every consumer of the flags path a cgo build, needed a prebuilt
libhanzo_flags.a on the link line, and put a second language in the release
pipeline for one pure function.

This is that function in Go, with no dependencies at all — the rollout hash,
the full property-operator set, semver ranges, the date operators, and the
serde-compatible value rendering the operators compare through.

The rollout hash decides which identities fall inside a percentage, so drift
would split cohorts differently here than in the rest of the fleet. The port is
therefore pinned to the Rust implementation's own answers: testdata/cases.json
is a 621-case table covering every operator, eight rollout percentages against
forty identities each, multivariate bucketing, group aggregation and the
numeric/escaping edges; testdata/oracle.json is what the Rust evaluator
returned for all of them; parity_test.go asserts this package still agrees.

Four deliberate mutations confirm the table has teeth: shifting the hash by one
bit (27 cases), changing the variant salt (27), dropping case folding from
exact matching (2), and dropping the '+' from positive float exponents (4).

Two divergences the port found and had to reproduce rather than fix: an integer
JSON literal and a float one render differently forever (1e3 is 1000.0, never
1000), and distinct_id must be present but may be empty.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 12:15:36 -07:00

6.3 KiB

hanzoai/flags

Feature flags, percentage rollouts, and A/B experiments for the Hanzo platform — one engine, a client in every language.

The engine is native to Hanzo Cloud: flag definitions live in per-org/project SQLite (encrypted at rest), and evaluation runs in-process with PostHog-compatible semantics — identical rollout hash, property operators, variants, and payload shapes. No external flags service, no KV, no network hop on the hot path.

This repo is the canonical home of the evaluator and the client SDKs.

The evaluator itself is go/ — pure Go, zero dependencies, importable by anyone who wants to decide flags locally instead of over HTTP. It replaced a Rust staticlib that cloud linked over cgo; go/testdata/oracle.json holds that implementation's answer to all 621 cases in the parity table, and go/parity_test.go is the standing proof the two still agree — identity by identity on the rollout hash, and character for character on every rendered value.

Every client speaks the same two calls:

Call What
POST /v1/flags Evaluate all flags for one identity → {featureFlags, featureFlagPayloads}
PUT/GET/DELETE /v1/flags/defs[/:key] Manage definitions (audited, versioned)

Auth: Authorization: Bearer <HANZO_API_KEY> server-side; browsers ride the gateway session. Scope to a project with X-Project-Id (omit for the org default). Full docs: https://docs.hanzo.ai/docs/flags


TypeScript / JavaScript — @hanzo/flags

npm install @hanzo/flags
import { HanzoFlags } from "@hanzo/flags"

const flags = new HanzoFlags({
  host: "https://api.hanzo.ai",
  token: process.env.HANZO_API_KEY, // omit in-browser
  project: "web",                    // optional
})

await flags.load({ distinctId: user.id, personProperties: { plan: "pro" } })

flags.isEnabled("new-nav")          // boolean
flags.getVariant("checkout-exp")    // "control" | "treatment" | null
flags.getPayload("checkout-exp")    // { cta: "Buy now" }

Results cache for a 15s TTL per identity and degrade fail-safe (network error → previous values). One-shot server-side: evaluateFlags(config, ctx). Source: typescript/.

Go — github.com/hanzoai/flags/go

go get github.com/hanzoai/flags/go
import flags "github.com/hanzoai/flags/go"

c := flags.New("https://api.hanzo.ai", flags.WithToken(os.Getenv("HANZO_API_KEY")))

res, err := c.Evaluate(ctx, flags.Context{
    DistinctID:       "user-42",
    PersonProperties: map[string]any{"plan": "pro"},
})

res.IsEnabled("new-nav")          // bool
res.Variant("checkout-exp")       // "treatment" | ""
var cfg struct{ CTA string `json:"cta"` }
res.Payload("checkout-exp", &cfg) // decode the payload

The same package evaluates locally, with no server involved — this is the engine cloud runs:

res := flags.Evaluate(defs, flags.Context{
    DistinctID:       "user-42",
    PersonProperties: map[string]any{"plan": "pro"},
})

Evaluate is pure: same definitions and same context, same answer, on every host and in every process. EvaluateJSON(defs, ctx []byte) ([]byte, error) is the wire-shaped equivalent.

Inside hanzoai/cloud itself, don't use the HTTP client — call the in-process seam directly: flags.Bool/Int/String(key) (zero-copy, no HTTP). Source: go/.

Python — hanzo-flags

pip install hanzo-flags   # or: uv add hanzo-flags
from hanzo_flags import Flags

flags = Flags("https://api.hanzo.ai", token=os.environ["HANZO_API_KEY"])

res = flags.evaluate("user-42", person_properties={"plan": "pro"})

res.is_enabled("new-nav")         # bool
res.variant("checkout-exp")       # "treatment" | None
res.payload("checkout-exp")       # {"cta": "Buy now"}

Zero dependencies (stdlib urllib). Source: python/.

Rust — hanzo-flags-client

[dependencies]
hanzo-flags-client = "0.1"
use hanzo_flags_client::{Flags, Context};

let flags = Flags::new("https://api.hanzo.ai")
    .with_token(std::env::var("HANZO_API_KEY")?);

let res = flags.evaluate(Context::new("user-42").prop("plan", "pro")).await?;

res.is_enabled("new-nav");          // bool
res.variant("checkout-exp");        // Option<&str>
res.payload("checkout-exp");        // Option<&serde_json::Value>

For in-process evaluation (no HTTP at all — embed the evaluator itself), depend on the hanzo-flags evaluator crate in cloud/native/flags: evaluate(&[FlagDef], &EvalContext) -> EvalResponse, or over C FFI as hanzo_flags_evaluate(defs_json, ctx_json). Source: rust/.

C / C++ — FFI

The evaluator ships a C ABI (link libhanzo_flags.a from cloud/native/flags):

extern char* hanzo_flags_evaluate(const char* defs_json, const char* ctx_json);
extern void  hanzo_flags_free(char* result);

char* out = hanzo_flags_evaluate(defs, "{\"distinct_id\":\"user-42\"}");
/* out = {"featureFlags":{...},"featureFlagPayloads":{...}} — parse, then: */
hanzo_flags_free(out);

Pure function, panic-guarded, errors returned as {"error": "..."}.


Curl (any language)

# Evaluate
curl -X POST https://api.hanzo.ai/v1/flags \
  -H "Authorization: Bearer $HANZO_API_KEY" -H "Content-Type: application/json" \
  -d '{"distinct_id":"user-42","person_properties":{"plan":"pro"}}'

# Define / roll out / A-B
curl -X PUT https://api.hanzo.ai/v1/flags/defs/checkout-exp \
  -H "Authorization: Bearer $HANZO_API_KEY" -H "Content-Type: application/json" \
  -d '{"active":true,"filters":{
        "groups":[{"properties":[],"rollout_percentage":50}],
        "multivariate":{"variants":[
          {"key":"control","rollout_percentage":50},
          {"key":"treatment","rollout_percentage":50}]},
        "payloads":{"treatment":{"cta":"Buy now"}}}}'

Platform switches (operators)

Waitlist intake, public signup, invite gating, gateway limits — the same engine evaluated from a reserved platform scope, managed at admin.hanzo.ai: GET /v1/admin/flags (switchboard) · PUT /v1/admin/flags/:key (flip; SuperAdmin). Flips apply hot within one TTL — no redeploy.

License

MIT