OpenFeature Providers

OpenFeature Web Provider

ConfigDirector Provider for the OpenFeature Web SDK

Introduction

The OpenFeature Web Provider is intended to be used in combination with the OpenFeature Web SDK. All modern web browsers on popular platforms should be supported.

The ConfigDirector OpenFeature Web Provider is tested on the latest versions of Chrome, Firefox, Safari, and Edge.

Telemetry collection within browser SDKs uses Web Workers to aggregate and send telemetry data. If the provider runs on older browsers without Web Workers support, config evaluation will continue to work but no telemetry data will be collected for that session.

Installation

The provider can be installed from NPM: https://www.npmjs.com/package/@configdirector/openfeature-web-provider

npm install --save @openfeature/web-sdk @configdirector/openfeature-web-provider

Configure and initialize the client

  1. Create an instance of the provider using your client SDK key. You can retrieve a client SDK key for each environment under SDK keys in the dashboard's navigation panel.
  2. Set the OpenFeature provider, together with the context you already know, so the first connection carries it.
  3. Get a client instance from OpenFeature.
import { OpenFeature } from "@openfeature/web-sdk";
import { ConfigDirectorProvider } from "@configdirector/openfeature-web-provider";

const provider = new ConfigDirectorProvider("YOUR-CLIENT-SDK-KEY");
try {
  await OpenFeature.setProviderAndWait(provider, {
    targetingKey: "12345", // In OpenFeature, the `targetingKey` represents the context's identifier
    name: "Example User",
    traits: {
      region: "North America", // Any arbitrary traits which can be referenced in targeting rules
    },
  });
} catch (error) {
  console.warn("Continuing until ConfigDirector sends config:", error);
}
const client = OpenFeature.getClient();

The provider connects with the context it has when it is registered. Pass the context you already know to setProviderAndWait, or set it with OpenFeature.setContext before registering the provider, so the first connection carries it.

Additional configuration options

Additional configuration options can be passed into the provider in the optional second argument of the constructor.

For example, the metadata can be provided like this:

import { ConfigDirectorProvider } from "@configdirector/openfeature-web-provider";

const provider = new ConfigDirectorProvider("YOUR-CLIENT-SDK-KEY", {
  metadata: {
    appName: "YOUR-APP-NAME",
    appVersion: "1.0.2",
  },
});

The provider accepts the same configuration options as the Javascript SDK client, refer to the additional configuration options section of the Javascript SDK for a full list.

Values kept on the device

By default, the provider keeps the last values it received from ConfigDirector in the browser's localStorage, and serves them on the next launch of the same context until live values arrive. An application opened without a network, after at least one successful connection, resolves flags to the values ConfigDirector last sent instead of the default values passed to the OpenFeature client.

Registering the provider still waits for live values or the connection timeout: setProviderAndWait rejects with ProviderNotReadyError on a timeout as it does without kept values, and the provider then reports the STALE status, with a Stale event, while it serves the kept values. Flags resolve to the kept values in that status. Once live values arrive, the provider reports READY with a Ready event and the kept values are replaced. A context change that cannot connect reports an error and then STALE again, and keeps serving the current values. Flags resolved from the kept values carry the CACHED reason; see Evaluation details and hooks.

The values are kept for the context they were evaluated for, and served on the next launch only when the evaluation context has the same kind and targetingKey. A context without a targetingKey matches on the same site, as long as the site's data has not been cleared, because ConfigDirector derives its identifier from the instance ID the SDK stores; such a context keeps its percentage rollout bucket for as long as that instance ID lasts. Other attributes take no part in the match: live values correct them within a second when there is a network. A context change keeps serving the current values until the new context's values arrive, as it does with live values.

Turn it off, or supply your own store, with the caching option, which takes the same object the JavaScript SDK takes: enabled, defaulting to true, and provider, a CachingProvider with read(), write(value), remove() and sizeLimit() in bytes, defaulting to localStorage under a key derived from the client SDK key with a size limit of 256 KB. The entry is one string whose format belongs to the SDK and may change between SDK versions. An entry above the size limit is removed with one warning, a provider that fails is logged once and never stops the provider, and nothing is kept in offline mode. The entry, and the localStorage key the provider uses by default, carry a SHA-256 digest of the client SDK key, never the key itself. Browsers compute that digest only on pages served over https or from localhost, so on a plain http page on another host nothing is kept and a provider of your own is not called.

import { ConfigDirectorProvider } from "@configdirector/openfeature-web-provider";

const provider = new ConfigDirectorProvider("YOUR-CLIENT-SDK-KEY", {
  caching: { enabled: false },
});

Retrieve config values

To retrieve config values, use the OpenFeature client:

const booleanValue = client.getBooleanValue("my-config-key", false);

const stringValue = client.getStringValue("my-string-config-key", "Default");

For additional information regarding the OpenFeature client refer to the OpenFeature Web SDK documentation.

Evaluation details and hooks

The detailed getters, such as getBooleanDetails, say why a value was returned:

OutcomeReasonError codeVariant
A value was foundTARGETING_MATCHThe value ID
A value was served from the values kept on the deviceCACHEDThe value ID
The config carries no valueDEFAULT
The config key is unknownERRORFLAG_NOT_FOUND
No config values have arrived yetERRORPROVIDER_NOT_READY
The value cannot be read as the requested typeERRORTYPE_MISMATCH

The variant is ConfigDirector's ID for the value served, a stable hash you can send to analytics in place of the value itself; values served in offline mode have none. In the three error cases the getters return the in-code default value you pass, and OpenFeature runs the error stage of your hooks, so an error-monitoring hook sees a missing config or a value of another type. getObjectValue with an object as its in-code default value reads only a JSON object, and with an array only a JSON array; JSON of the other shape gives TYPE_MISMATCH.

Before version 1.10.0 of the provider, the details carried no reason, variant or error code, and error hooks did not run.

Update the context

The provider connects with the context it has when it is registered, as shown in Configure and initialize the client. To change the context afterwards, for example when a user signs in, call OpenFeature.setContext. Each change opens a new connection with the new context:

await OpenFeature.setContext({
  targetingKey: "654321",
  name: "Another User",
  traits: {
    region: "Australia",
  },
});

To evaluate flags for an entity other than a user, such as a tenant, set the context kind with a top-level kind attribute:

await OpenFeature.setContext({
  targetingKey: "acme",
  kind: "tenant",
  name: "Acme Inc.",
  traits: {
    plan: "enterprise",
  },
});

Leaving kind out means user. A kind inside traits is an ordinary trait, not the context kind. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and flags still resolve successfully, as if no context had been given. Context kinds are available from version 1.7.0 of the provider.

In the OpenFeature Web SDK with the ConfigDirector provider, the context's targetingKey will map to ConfigDirector's context id, and its kind to the context kind.Register the provider with the context you know when the page loads, so the first connection carries it. A context set only afterwards replaces a first connection that had none, and that first connection counts as a separate monthly active user.

For additional information regarding the OpenFeature client refer to the OpenFeature Web SDK documentation.

Test your code

Code that reads flags through OpenFeature is tested by swapping the provider, so it never has to change for a test and nothing from ConfigDirector is involved. The OpenFeature Web SDK ships an in-memory provider for that: TypedInMemoryProvider, in @openfeature/web-sdk 1.8 and newer, which serves the values your test sets. Earlier versions ship the same provider as InMemoryProvider, now deprecated.

Each flag is a set of named variants and the defaultVariant it resolves to:

checkout.test.ts
import { OpenFeature, TypedInMemoryProvider } from "@openfeature/web-sdk";

const provider = new TypedInMemoryProvider({
  "new-checkout": { variants: { on: true, off: false }, defaultVariant: "on" },
  "max-items": { variants: { default: 20 }, defaultVariant: "default" },
});

afterEach(() => OpenFeature.close());

test("shows the new checkout", async () => {
  await OpenFeature.setProviderAndWait(provider);
  const client = OpenFeature.getClient();

  expect(client.getBooleanValue("new-checkout", false)).toBe(true);
  expect(client.getNumberValue("max-items", 0)).toBe(20);

  await provider.putConfiguration({
    "new-checkout": { variants: { on: true, off: false }, defaultVariant: "off" },
  });
  expect(client.getBooleanValue("new-checkout", true)).toBe(false);
});

putConfiguration replaces every flag and emits ConfigurationChanged with the keys it was given, so a component that re-reads its flags on that event follows the change. A flag can also carry a contextEvaluator, a function from the evaluation context to the name of the variant to serve, for a test of code that targets specific users. OpenFeature is one global, so shut it down after each test with OpenFeature.close(), which lets the next test register its own provider.

A React application that reads flags through @openfeature/react-sdk has OpenFeatureTestProvider from that package as well, a component that serves the values it is given to the hooks beneath it.