Javascript Browser SDK
Introduction
The Javascript browser SDK is intended to be used by web applications running on modern web browsers. All modern web browsers on popular platforms should be supported.
Browser SDKs are 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 SDK 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 SDK can be installed from NPM: https://www.npmjs.com/package/@configdirector/client-sdk
npm install --save @configdirector/client-sdk
yarn add @configdirector/client-sdk
pnpm add @configdirector/client-sdk
bun add @configdirector/client-sdk
Configure and initialize the client
- Create an instance of the client providing your client SDK key. You can retrieve a client SDK key for each environment under
SDK keysin the dashboard's navigation panel. - Initialize the client to initiate its connection lifecycle.
import { createClient } from "@configdirector/client-sdk";
export const client = createClient("YOUR-CLIENT-SDK-KEY");
await client.initialize();
Additional configuration options
metadata
The metadata option allows you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application.
import { createClient } from "@configdirector/client-sdk";
export const client = createClient("YOUR-CLIENT-SDK-KEY", {
metadata: {
appName: "YOUR-APP-NAME",
appVersion: "1.0.2",
},
});
await client.initialize();
logger
By default, the SDK logs to the console and it is set to log warnings and errors only. You can configure a logger by either creating a ConfigDirector console logger with a different log level, or by implementing the ConfigDirectorLogger interface to provide your own logger. The interface can be used to create an adapter to another logging library.
Configure the ConfigDirector console logger to a different level:
import { createClient, createConsoleLogger } from "@configdirector/client-sdk";
export const client = createClient("YOUR-CLIENT-SDK-KEY", {
logger: createConsoleLogger("debug"),
});
await client.initialize();
Implement your own logger adapter:
import { createClient, ConfigDirectorLogger } from "@configdirector/client-sdk";
const myLogger: ConfigDirectorLogger = {
debug: function (message: string, ...args: any): void {
// your specific logging library implementation here
},
info: function (message: string, ...args: any): void {
// your specific logging library implementation here
},
warn: function (message: string, ...args: any): void {
// your specific logging library implementation here
},
error: function (message: string, ...args: any): void {
// your specific logging library implementation here
},
};
export const client = createClient("YOUR-CLIENT-SDK-KEY", {
logger: myLogger,
});
await client.initialize();
connection
The connection object accepts the following optional values:
mode- The connection mode, one of
streaming,pollingoroffline. It is recommended to use the default ofstreamingunless you have a specific need to usepollinginstead. Inofflinemode the client never connects to ConfigDirector and serves the values you give it; see Run without a connection. - Defaults to
streaming
- The connection mode, one of
values- The values to serve in
offlinemode, keyed by config key. It can be given only inofflinemode. See Run without a connection.
- The values to serve in
pollingInterval- The interval, in seconds, between requests for config updates when
modeispolling. It has no effect in the other modes. A value below the minimum of 30 seconds is raised to 30 seconds and a warning is logged. See Polling intervals. - Defaults to
60seconds
- The interval, in seconds, between requests for config updates when
timeout- The timeout, in milliseconds, to be used in initialization and when updating the context. This is how long the
initializemethod will wait for data from ConfigDirector services before resolving its Promise. If the timeout is reached,initializewill return but the client will still be in an unready status and returning default values. The client will continue to attempt to connect and retrieve config values in the background. It has no effect inofflinemode. - If your application is used in environments where the users frequently have poor or no connection, you may want to use a lower timeout, or simply don't await the
initializemethod call - Defaults to
3000milliseconds
- The timeout, in milliseconds, to be used in initialization and when updating the context. This is how long the
url- The base URL used to connect to ConfigDirector services
- This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services. It has no effect in
offlinemode.
import { createClient } from "@configdirector/client-sdk";
export const client = createClient("YOUR-CLIENT-SDK-KEY", {
connection: {
mode: "streaming",
timeout: 2_000, // 2,000 milliseconds initialization timeout
},
});
await client.initialize();
Hooks
Hooks are events you can subscribe to in order to be notified of some key actions from the client. In order to subscribe to hooks you can provide a handler (or an array of handlers) in the options:
import { createClient } from "@configdirector/client-sdk";
export const client = createClient("YOUR-CLIENT-SDK-KEY", {
hooks: {
configEvaluated: (event) => { console.log("Received configEvaluated:", event); },
},
});
await client.initialize();
The following hooks are available:
clientReady: { action: ClientConnectAction }- Emitted when the client is initialized, or after reconnection due to a context update. When this event is emitted it means the client has received a payload from the ConfigDirector servers and is ready to evaluate configs. The event payload is an object with a single property:
action- The action that triggered the client to connect/reconnect, can be eitherinitializationorcontext update.
contextUpdated: { context: ConfigDirectorContext | undefined }- Emitted whenever the context is updated, including during initialization.
contextUpdatedis emitted beforeclientReady, therefore should not be relied upon as a lifecycle event but rather as an informational event to track updates to the context. context- The context the client is being updated to. It can beundefined.
- Emitted whenever the context is updated, including during initialization.
configsUpdated: { keys: string }- Emitted when a payload is received from the ConfigDirector servers with config data. It is emitted during initialization or context update when an entire config payload is received. After initialization or context update, it is emitted when updates are pushed from the server (or discovered via polling if that connection mode is used).
keys- A string array listing the config keys that were included in the payload from the server.removedKeys- A string array listing the keys of configs the payload removed. A config is removed when a full payload no longer includes it. Empty when nothing was removed.
cachedValuesLoaded: {}- Emitted when the values kept on the device from the last payload are loaded and served, before live values arrive. See Values kept on the device. The event carries nothing.
configEvaluated: { evaluation: ConfigEvaluation }- Emitted whenever a config is evaluated. This includes calls to
getValueandevaluate, and evaluations delivered to listeners viawatch. evaluation- AConfigEvaluationobject containing the details of the evaluation:key: string- The config key that was evaluatedvalue: string | number | boolean | object | ConfigEnumLikeType- The value the config evaluated to. It can be the default value provided togetValueorwatch. For example, if the config was evaluated before the client was initialized.valueId: string | undefined- The value ID, a stable hash of the value that you can send to a third party rather than thevalue. This can be useful for large values, like a JSON config, or to avoid disclosing the values themselves. It may beundefinedif the evaluation had to fall back to the in-code default value. To report an experiment to an analytics tool, send the variation instead: the pages for Amplitude, Mixpanel, Google Analytics 4, Segment and PostHog name the event each one reads.variation: Variation | undefined- The variation that was served, as{ id, name }. Theidstays the same when the variation is renamed, andnameis left out for a variation without one. It isundefinedwhen the config has no variations or the in-code default value is returned. A value served from the values kept on the device carries it too. Versions of the SDK before 1.11.0 do not report it.isDefaultValue: boolean- Whether or not the evaluation fell back to the default value provided in code.reason: EvaluationReason- The reason for the evaluation resolution. In the case the config successfully evaluated based on server-provided targeting rules, it will befound-match. A value served from the values kept on the device, before live values arrive, reportscached; see Values kept on the device. If the evaluation had to fall back to the default value, thereasonwill encode why the fallback was required:client-not-ready- The evaluation happened before the client finished initializationconfig-state-missing- The requested config key was not present in the payload received from the server. This could be due to an incorrect config key, or a config that was not enabled to be available to client SDKs.value-missing- The config key was present in the payload received from the server, but it carried no value or an empty string.invalid-number- The config was requested as a number but the value received from the server could not be converted to a number.invalid-boolean- The config was requested as a boolean but the value received from the server could not be converted to a boolean.invalid-json- The config was requested as a JSON object but the value received from the server could not be converted to a JSON object.type-mismatch- The config was requested with a data type that did not match the type of the config and no reasonable type conversion was possible.
TheEvaluationReasontype also listsrollout-skipped-no-identifier. It comes from server SDKs and the browser never reports it, so code that handles every reason needs a case for it and nothing more.
- Emitted whenever a config is evaluated. This includes calls to
Run without a connection
Set the connection mode to offline to run your application without connecting to ConfigDirector, for example in a suite of UI tests where each run pins every flag to a known value. An offline client needs no client SDK key, opens no connection and sends nothing, so the application is invisible to the ConfigDirector dashboard: no evaluations are counted and no lifecycle state changes. Every context receives the same value and targeting rules are not applied.
Give the values inline, with one value per config key. Your application decides where they come from, for example a module bundled with the test build or a document it fetched before creating the client:
import { createClient } from "@configdirector/client-sdk";
export const client = createClient({
connection: { mode: "offline", values: { "new-checkout": true, "max-items": 20 } },
});
await client.initialize();
The config type of each value follows from the value itself: a boolean, an integral number (integer), any other number (float), a string, or an object or array (json). A value read with an in-code default value of another type returns the in-code default value, as it does against ConfigDirector. watch callbacks run once with the offline value, when initialize loads it, and never again. updateContext stores the new context and changes no value. pollingInterval, timeout and url have no effect.
With mode: "offline" and no values, the client is ready at once and every config returns its in-code default value.
initialize rejects with a ConfigDirectorInitializationError, which the SDK exports, when the values cannot be loaded: a value such as null, or values given in streaming or polling mode. The client stays not ready, so a test run with a wrong value fails instead of passing on in-code default values.
If you come from LaunchDarkly, a map of values is what LaunchDarkly calls "flags from files", and offline with no values is its "offline mode".
Values kept on the device
By default, the client 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, shows the values ConfigDirector last sent instead of the default values provided in code. Every evaluation served this way reports the reason cached, with isDefaultValue set to false.
The client is not ready while it serves values kept on the device: isReady stays false, initialize still waits for live values or its timeout, and clientReady fires once live values arrive and replace them. The cachedValuesLoaded hook fires when the kept values are loaded, together with configsUpdated listing their keys, so watchers and hooks pick them up.
The values are kept for the context they were evaluated for, and served on the next launch only when the launch context has the same kind and id. A launch with another context serves the in-code default values until live values arrive, which then replace the kept values. A context without an id 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; a context without an id keeps its percentage rollout bucket for as long as that instance ID lasts. Name, traits, app name and app version take no part in the match: live values correct them within a second when there is a network. Once the client is running, updating the context 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:
enabled- Whether to keep the last values received. With
false, nothing is read or written, even when aprovideris given. - Defaults to
true
- Whether to keep the last values received. With
provider- A store of your own, for example an encrypted one, implementing
CachingProvider:read()returns the stored entry orundefined,write(value)replaces it,remove()deletes it, andsizeLimit()returns the largest entry, in bytes, the store accepts. Each ofread,writeandremovemay return a promise. One provider instance serves one client. - Defaults to the browser's
localStorage, under a key derived from the client SDK key, with a size limit of 256 KB
- A store of your own, for example an encrypted one, implementing
import { createClient } from "@configdirector/client-sdk";
export const client = createClient("YOUR-CLIENT-SDK-KEY", {
caching: { enabled: true, provider: myEncryptedStore },
});
await client.initialize();
The entry is one string whose format belongs to the SDK and may change between SDK versions; an entry a newer SDK cannot read is removed. Before every write the SDK measures the entry against sizeLimit(); above it, the entry is removed and a warning is logged once, so the device never serves values that no longer reflect the set. A provider that fails to read, write or remove is logged once and never stops the client: the worst outcome is the in-code default values. Keeping values on the device is ignored in offline mode.
The entry, and the localStorage key the SDK 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.
If you come from Firebase Remote Config, this is what activated values persisting across restarts gives you; if you come from LaunchDarkly, it is the browser SDK's bootstrapping from localStorage, without the bootstrap option.
Retrieve config values
To synchronously retrieve config values, use the client's getValue method. It requires two arguments. The first argument is the config key, and the second is the default value to be returned if the client has not yet received config values from ConfigDirector services.
You can also subscribe to config value changes via the watch method. It requires three arguments, the config key, the default value, and a callback function to be called when the config value is updated. When the config is removed, the callback is called with the default value. The watch method itself returns a function to remove that specific listener. Alternatively, you may also call the unwatch method to remove all listeners for a specific config key.
import { client } from "./config-director-setup"
// Retrieve the current value
const value = client.getValue("my-config-key", false);
// Subscribe to value updates
const unwatchMyKey = client.watch(
"my-config-key",
false,
(newValue) => {
console.log("Value updated:", newValue)
},
);
unwatchMyKey(); // Call the unwatch function returned to remove the listener
client.unwatch("my-config-key"); // Removes all listeners for that key
Both, getValue and watch accept default values for any type supported by that specific config (boolean, string, number, enums), and the default value decides the type the config is read as. A string config requested as a boolean or a number is converted when its value spells one. A boolean or number config requested as a string, or any config requested as a type its value cannot be converted to, evaluates to the default value with the type-mismatch reason. Mismatched types at runtime are captured by the telemetry collector and will surface as warnings in the ConfigDirector dashboard.
A JSON config read with an object or an array as its in-code default value takes its shape from that default: an object reads only a JSON object, and an array only a JSON array. A JSON array read with an object, or a JSON object read with an array, evaluates to the in-code default value with the type-mismatch reason.
client.getValue("my-string-config-key", "Default");
client.getValue("my-integer-config-key", 100);
client.getValue("my-boolean-config-key", false);
client.getValue<MyEnum>("my-enum-config-key", MyEnum.SomeDefaultValue);
Update the context
A context can be provided when initializing the client:
import { createClient } from "@configdirector/client-sdk";
export const client = createClient("YOUR-CLIENT-SDK-KEY");
await client.initialize({
id: "12345",
name: "Example User",
traits: {
region: "North America", // Any arbitrary traits which can be referenced in targeting rules
}
});
The context can also be updated via updateContext:
import { client } from "./config-director-setup"
await client.updateContext({
id: "654321",
name: "Another User",
traits: {
region: "Australia",
},
});
// Update it to `undefined` (anonymous user context) when a user signs out
await client.updateContext();
To evaluate configs for an entity other than a user, such as a tenant, set the context kind with kind:
await client.updateContext({
id: "acme",
kind: "tenant",
name: "Acme Inc.",
traits: {
plan: "enterprise",
},
});
Leaving kind out means user. 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 configs are evaluated as if no context had been given. Context kinds are available from version 1.7.0 of the SDK.
Awaiting
updateContext will wait until the new config values are downloaded or the connection times out. In the case of a timeout, the client will continue to attempt to connect with the new context in the background.The SDK keeps an instance ID in the browser's localStorage until the site's data is cleared, and ConfigDirector derives the identifier of a context without an id from it. A visitor who has not signed in keeps that identifier, and their percentage rollout bucket, from one visit to the next, and counts as one monthly active user however often they visit.
Track events
track reports to ConfigDirector that something happened in your application, such as a completed checkout. ConfigDirector shows tracked events on the Live feed and counts them for each variation of a running experiment; see Tracked events.
track accepts the name of the event and an optional object with a value and details, and reports the event for the client's current context:
client.track("checkout-completed", { value: 49.9, details: { plan: "pro", seats: 3 } });
client.track("tutorial-skipped");
valueis a number the event measures, such as an amount of money, a count or a duration.detailsis an object describing the event. Its values are strings, numbers, booleans, or nested objects and arrays.
An event counts for an experiment only when the current context has an id, the same one the experiment's config was evaluated for. Before the client has a context, or for a context without an id, the event is still reported and counts for no experiment. For a context marked anonymous, the event is reported without the id, so it counts for no experiment either.
track returns nothing and never throws. The event is sent with the SDK's next telemetry report, every 30 seconds and when the page is hidden. In offline mode, track does nothing. The SDK applies these limits, and logs one warning for each of them per telemetry report:
| Limit | Beyond it |
|---|---|
| An event name of 1 to 150 characters | The event is not reported. |
| 1,000 tracked events in one telemetry report | Further events are not reported until that report is sent. |
| Details of up to 20 top-level keys and 1 KB as JSON | The details are left out and the event is still reported. |
A value that is a finite number | The value is left out and the event is still reported. |
ConfigDirector applies limits of its own to the events it stores; see Limits.
From version 1.11.0 of the SDK, each evaluation it reports also says which variation was served and which percentage rollout and share served it. That is how ConfigDirector counts the contexts each variation of an experiment reached.
track is available from version 1.11.0 of the SDK, which exports the TrackOptions, TrackedEventDetails and TrackedEventDetailValue types.
Other useful client features
The client provides additional properties and methods.
evaluate
Evaluates a config the way getValue does and returns the ConfigEvaluation behind it — the value, whether it is the in-code default value, the reason, the valueId, and the variation — instead of the value alone. It is the same object the configEvaluated hook reports, so use it where the reason matters at the point of the read, such as when reporting an evaluation to another system; getValue is enough everywhere else. Like every read, it is reported to configEvaluated and counted in telemetry. The SDK exports the ConfigEvaluation, EvaluationReason and Variation types.
const evaluation = client.evaluate("my-config-key", false);
if (evaluation.reason === "client-not-ready") {
// Config values have not arrived yet.
}
evaluate is available from version 1.10.0 of the SDK, and its variation from version 1.11.0.
context
Returns the current context (which may be undefined). When calling updateContext, the context is not immediately updated. The context is updated after the new connection for the new context succeeds (or times out and goes on retry).
isReady
Returns a boolean indicating if the client has been successfully initialized and is ready to evaluate configs for the given context. It is initially false and becomes true after initialize succeeds.
Upon calling updateContext it becomes false again and it is set to true once updateContext succeeds. It stays false while the client serves the values kept on the device.
unwatchAll
Removes all listeners for all config keys that were previously created via watch.
dispose
Removes all listeners and observers, and closes all connections to ConfigDirector services. Only call dispose when your application shuts down and it will no longer make use of the client instance.
It is generally not needed for applications to call dispose explicitly.
Test your code
Use the SDK's testing entry point, @configdirector/client-sdk/testing, to test the code that reads your configs and flags. It creates a test client: the SDK's real client connected to an in-memory server that your test controls. Only the connection to ConfigDirector is replaced. Everything else is the same code that runs in production, so the code under test behaves exactly as it does against ConfigDirector, and no network connection is opened and no telemetry is sent.
import { createTestClient } from "@configdirector/client-sdk/testing";
const testClient = createTestClient({ values: { "new-checkout": true, "max-items": 20 } });
await testClient.client.initialize();
const checkout = new Checkout(testClient.client);
expect(checkout.isNewCheckoutEnabled()).toBe(true);
testClient.setValue("new-checkout", false);
expect(checkout.isNewCheckoutEnabled()).toBe(false);
testClient.client is a ConfigDirectorClient, so it goes anywhere your code accepts one. It starts uninitialized, like a production client, because the code under test usually owns the call to initialize.
Values
values, setValue, and replaceValues take native values, and the config type follows from each value: a boolean, an integral number (integer), any other number (float), a string, or a plain object or array (json). Every value is served to every context. A null or undefined value, a non-finite number, an integer beyond Number.isSafeInteger, an empty key, or JSON contents that cannot be encoded (functions, Date or Map instances, bigint) throw a ConfigDirectorValidationError, which the entry point exports.
Reads behave as they do in production: setValue("k", true) read as a string returns the in-code default value with the type-mismatch reason, and setValue("k", "") serves the in-code default value with the value-missing reason.
Controls
| Control | Effect |
|---|---|
setValue(key, value) | Stores the value and, once the client is connected, delivers an update carrying only key. Watchers of key, configsUpdated handlers, and reads see it at once. |
removeValue(key) | Removes the value and delivers a full update without it. Reads return the in-code default value with the config-state-missing reason, watchers of key receive the default, and configsUpdated lists key in removedKeys. |
replaceValues(values) | Replaces every stored value, disarms any armed hold or failure, and delivers a full update. Use it to reset a test client shared across tests. |
holdInitialization() | The next initialize waits until completeInitialization() or failInitialization(), or until the client's timeout elapses. |
completeInitialization() | Delivers the stored values to the held initialize, which completes with the client ready. Called while a hold is armed but not picked up, it disarms the hold. |
failInitialization() | Fails the held initialize the way an invalid SDK key does: it completes promptly, the client is not ready, connectionError fires, and an error is logged. Called with no initialize held, it arms the next one to fail. |
holdContextUpdate(), completeContextUpdate(), failContextUpdate() | The same for the next updateContext. |
contextUpdates | The context of every initialize and updateContext call, in order. resumeNetwork is not recorded. |
trackedEvents | The events the code under test tracked with track, in call order, each as { name, context?, value?, details? }. An event whose name the SDK would not report is not recorded, and a value or details the SDK would leave out are missing from the record. The limit of 1,000 events per telemetry report does not apply to the record. |
The initialization controls act only on initialize and the context update controls only on updateContext; resumeNetwork is never held or failed. After a failed attempt, the next attempt succeeds and delivers the values stored in the meantime.
const testClient = createTestClient({ values: { "new-checkout": true }, timeout: 500 });
testClient.holdInitialization();
const initialization = testClient.client.initialize();
expect(page.showsLoadingState()).toBe(true);
testClient.completeInitialization();
await initialization;
expect(page.showsLoadingState()).toBe(false);
Options
createTestClient takes values, timeout (the client's connection timeout in milliseconds, which bounds how long a held attempt waits; defaults to the SDK's production timeout), and logger.
What to expect
- Under the test client,
initializeemitscontextUpdated, thenconfigsUpdated, thenclientReady. The first update arrives right after the connection is established, so watchers registered beforeinitializerun beforeisReadybecomestrue. - A held attempt that times out, or that is interrupted by
pauseNetwork,dispose, or a new attempt, leaves the client not ready until the next attempt; production streaming would keep retrying in the background. - Watchers fire on every update carrying their key, whether or not the value changed, as in production.
removeValueandreplaceValuesdeliver a full update, so they also fire the watchers of every remaining key. - An exception thrown by a watcher during a delivery is logged and does not propagate, as with the production transports.
- The
configEvaluatedevent is emitted from asetTimeout(0). With fake timers, advance them to receive it. - Creating a client writes an instance ID to
localStoragewhen there is none. Under jsdom this is shared by every test in one environment and is harmless. - After
dispose, the test client's controls are silent no-ops, and nothing from the SDK is left running.