@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).@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
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
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:getStringDetails / getBooleanDetails / etc. when you need the full ResolutionDetails — the variant, reason, and flagMetadata (including the decisionId you’ll echo back on exposure):
"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 withtargetingKey. 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.
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: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).
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:
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:- 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 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 inflagMetadata 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’strack. Pass a numeric reward under the standard value field; put everything else in the metadata object:
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 singlejson 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):
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
attributionOnlylayers 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.