Skip to main content
@traffical/openfeature-server and @traffical/openfeature-web are official OpenFeature providers that wrap the native Traffical SDKs (@traffical/node and @traffical/js-client). If your team already evaluates flags through the OpenFeature API, you can point it at Traffical without rewriting your call sites — you keep getBooleanValue, getObjectValue, and friends, and Traffical handles bucketing, decisions, and measurement underneath. The providers are thin translation layers over the native clients, so they inherit the same SHA-256 v2 bucketing, layered resolution, and contextual-bandit scoring as every other Traffical SDK — a given unit buckets identically whether you resolve through OpenFeature or the native client.
Resolve is a decision, not an exposure. Reading a flag value records an intent-to-treat (ITT) decision. It does not record that the user actually saw the treatment. You must signal exposure explicitly (or opt into exposureOnResolve) — otherwise your primary metrics, SRM health, and optimization stay empty. See Signalling exposure.

Installation

Install the provider for your paradigm plus the matching OpenFeature SDK (the OpenFeature SDK is a peer dependency).
Both providers depend on the corresponding native client (@traffical/node / @traffical/js-client) transitively — you don’t install it separately, but you do construct it and hand it to the provider.

Registering the provider

The provider takes an already-constructed Traffical client. You own the client’s lifecycle (one instance per process/page); the provider is a membrane over it.

Server

The server provider requires OpenFeature transaction context (backed by AsyncLocalStorage) so that per-request identity and the decision the user saw can be stitched to later exposure and reward events. See Request scoping.

Web

On web the evaluation context is static — it’s the bound user. When identity changes (login/logout), update it with OpenFeature.setContext(...); the provider clears its current-decision memo and reconciles, and the SDK emits PROVIDER_CONTEXT_CHANGED.

Provider options

Both provider constructors accept an optional second argument:

Resolving parameters

Each Traffical parameter maps to one OpenFeature flag. The four value types line up directly:
Use getStringDetails / getBooleanDetails / etc. when you need the full ResolutionDetails — the variant, reason, and flagMetadata (including the decisionId you’ll echo back on exposure):
Types are checked strictly — there is no silent coercion ("true" never becomes true). A type mismatch returns your default with reason: ERROR and errorCode: TYPE_MISMATCH.

Targeting key → unit key

OpenFeature identifies the subject with targetingKey. Traffical buckets on the project’s configured unit-key field (e.g. userId), not a field literally named targetingKey. The provider maps them for you: it writes the targetingKey into the client’s unit-key field before deciding.
Every other context field is available for policy targeting conditions, exactly as with the native SDK. If targetingKey is missing the provider throws TargetingKeyMissingError (the OpenFeature SDK catches it and returns your default with reason: ERROR) — bucketing has no valid identity, so it fails loud rather than silently mis-bucketing to an anonymous id.
Multi-entity per-layer unit keys (e.g. bucketing one layer on accountId and another on userId) aren’t expressed through targetingKey. Pass the additional identifiers as ordinary context attributes.

Signalling exposure

This is the one thing an OpenFeature integration must not skip. A resolve records a decision (the intent-to-treat / ITT event) — it means “this unit would get this variant.” A true exposure means “this unit actually saw the treatment,” and it’s a separate, explicit signal. The provider never turns a resolve into an exposure automatically (doing so would erase the ITT-vs-treated distinction), so you fire exposure at render time via the reserved tracking event:
The provider looks up the decision by decisionId (in the request store, then the client’s decision cache), filters it to the flag’s owning layer, and calls the native trackExposure. It never re-decides — a decision it can’t find is dropped with a one-time warning rather than synthesized (a re-decide would mint an orphan decision and re-sample propensity against a possibly-refreshed bundle).
Missing exposures leave your measurement empty. Traffical’s default primary metric source is Exposures (treated / ToT), and exposures also gate the SRM health check and bandit/optimization training. A drop-in integration that resolves flags but never fires $traffical.exposure will silently record decisions with:
  • empty ToT primary metrics — nothing to compare,
  • a blind SRM gate — imbalance goes undetected,
  • a non-learning optimizer — bandits get no training signal.
The provider watches for this: if it sees decisions but no exposures past a threshold, it emits a one-time warning and a PROVIDER_ERROR telling you to fire $traffical.exposure or set exposureOnResolve.

exposureOnResolve — the zero-friction escape hatch

If you can’t (or don’t want to) thread an explicit render-time signal, set exposureOnResolve: true. The resolver then fires the exposure on the decision it just made — same decisionId, correct propensity and config version, filtered to the owning layer:
This is safe (denominators are per-unit and trackExposure dedups), but it collapses ToT ≈ ITT: you lose the ability to distinguish “would have been treated” from “was treated.” Use it when resolve is the render, and prefer the explicit signal when it isn’t.

Request scoping

On the server, exposure and reward events must attach to the same decision the request actually served. The provider does this through OpenFeature’s transaction context — wrap each request so the provider has a per-request identity and decision store:
Everything resolved inside the transaction shares one decision, so:
  • one request → one decisionId,
  • exposure and reward events find the decision without re-deriving identity from their own call’s context,
  • the ITT population for co-resolved flags matches a native batched decide().
The store is discarded at request end — no cross-request bleed, no unbounded growth.
The server provider is designed for evaluationMode: "bundle" clients (local, per-request context). With a "server"-mode client, resolution is asynchronous and best-effort — a synchronous decide() returns the last cached snapshot rather than a fresh per-request resolve, so the provider can’t reliably do per-request targetingKey targeting. It warns if you construct it with one.

Flag metadata

Every resolution carries Traffical internals in flagMetadata under the traffical.* prefix. All values are scalars (string | number | boolean): Metadata reflects the flag’s owning layer — never a sibling experiment’s layer that the unit happens to be bucketed into. On web, bandit internals (propensity, modelVersion) are gated out because flagMetadata is visible in browser devtools.

Tracking rewards

Business events (conversions, revenue, engagement) go through OpenFeature’s track. Pass a numeric reward under the standard value field; put everything else in the metadata object:
The provider always sets the unitKey from the request’s targetingKey (server) or bound identity (web) — if it can’t derive one it fails loud rather than emitting an unjoinable event. Reward metrics join to first exposure temporally on unit_key, so a correct unit key is all reward correctness requires — carry flagKey and other fields as metadata, never inside the numeric value.

Co-measured parameters: the object-flag pattern

By default, one parameter is one flag, each with its own exposure. Deterministic bucketing keeps a unit consistent across flags, so this is the natural model. When several parameters make up one variant that must be measured together, model them as a single json object-flag instead. One flag → one variant → one exposure, and it’s the ITT-parity option (the whole bundle is decided and exposed as a unit):
Reach for the object-flag when parameters are interdependent (they only make sense evaluated together) or when experiments must share one ITT population. Keep separate flags when parameters vary independently.

Measurement fidelity

A few honest properties worth knowing:
  • ITT is reshaped, not inflated. Denominators are per-unit, so resolve-without-render and repeat resolves don’t inflate ITT. But because sibling attributionOnly layers are dropped from the ITT source, per-key resolution records a co-running experiment’s ITT only when its own flag is resolved. Batched per-request decisions (server) and the object-flag pattern restore native parity when you need co-measured populations.
  • Exposures gate the primary path (ToT, SRM, optimization) — see the warning above.
  • Reward metrics need only unit_key; the attribution array is auxiliary (dashboard breakdowns).
  • Propensity is captured faithfully on the exposure event and is future-enabling (off-policy evaluation), not a present correctness requirement. Its capture depends on exposures firing and on never re-deciding.

Next steps

Node.js SDK

The native server client the provider wraps.

Events & metrics

How exposures and rewards become metrics.

Decisions & attribution

What decisionId stitches together.

Optimization

How exposures train the bandit.