Featurevisor

SDKs

JavaScript SDK

Featurevisor's JavaScript SDK is universal, meaning it works in both Node.js and browser environments.

Installation

Install with npm in your application:

Command
$ npm install --save @featurevisor/sdk

Public API

The main runtime API is createFeaturevisor():

import {
createFeaturevisor,
type Featurevisor,
type FeaturevisorOptions,
type FeaturevisorModule,
type FeaturevisorDiagnostic,
} from '@featurevisor/sdk'

Most applications only need createFeaturevisor() at runtime. TypeScript users can import types like Featurevisor, FeaturevisorOptions, FeaturevisorModule, FeaturevisorDiagnostic, and datafile types from the same package.

Initialization

The SDK can be initialized by passing datafile content directly:

your-app/index.js
import { createFeaturevisor } from '@featurevisor/sdk'
const datafileUrl = 'https://cdn.yoursite.com/datafile.json'
const datafileContent = await fetch(datafileUrl)
.then((res) => res.json())
const f = createFeaturevisor({
datafile: datafileContent,
})

Evaluation types

Featurevisor evaluates three kinds of values against a feature:

  • Flag (boolean): whether the feature is enabled or not
  • Variation (string): the variation of the feature (if any)
  • Feature variables: variable values owned by the feature (if any)

Global variables use the same variable APIs, but are evaluated independently without a feature key or percentage bucketing.

These evaluations are run against the provided context.

Detailed evaluation and getter methods accept optional defaultVariationValue and defaultVariableValue options where applicable. Defaults are presence based. Values such as an empty string, 0, false, and null are valid explicit defaults and are not treated as missing.

const value = f.getVariable(
'my_feature',
'my_variable',
context,
{ defaultVariableValue: null },
)

Context

Contexts are attribute values that we pass to SDK for evaluating features against.

Think of the conditions that you define in your segments, which are used in your feature's rules.

They are plain objects:

const context = {
userId: '123',
country: 'nl',
// ...other attributes
}

Context can be passed to SDK instance in various different ways, depending on your needs:

Setting initial context

You can set context at the time of initialization:

your-app/index.js
import { createFeaturevisor } from '@featurevisor/sdk'
const f = createFeaturevisor({
context: {
deviceId: '123',
country: 'nl',
},
})

This is useful for values that don't change too frequently and available at the time of application startup.

Setting after initialization

You can also set more context after the SDK has been initialized:

f.setContext({
userId: '234',
})

This will merge the new context with the existing one (if already set).

Replacing existing context

If you wish to fully replace the existing context, you can pass true in second argument:

f.setContext(
{
deviceId: '123',
userId: '234',
country: 'nl',
browser: 'chrome',
},
true, // replace existing context
)

Manually passing context

You can optionally pass additional context manually for each and every evaluation separately, without needing to set it to the SDK instance affecting all evaluations:

const context = {
userId: '123',
country: 'nl',
}
const isEnabled = f.isEnabled('my_feature', context)
const variation = f.getVariation('my_feature', context)
const variableValue = f.getVariable('my_feature', 'my_variable', context)

When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value.

Further details for each evaluation types are described below.

Check if enabled

Once the SDK is initialized, you can check if a feature is enabled or not:

const featureKey = 'my_feature'
const isEnabled = f.isEnabled(featureKey)
if (isEnabled) {
// do something
}

You can also pass additional context per evaluation:

const isEnabled = f.isEnabled(featureKey, {
// ...additional context
})

Getting variation

If your feature has any variations defined, you can evaluate them as follows:

const featureKey = 'my_feature'
const variation = f.getVariation(featureKey)
if (variation === "treatment") {
// do something for treatment variation
} else {
// handle default/control variation
}

Additional context per evaluation can also be passed:

const variation = f.getVariation(featureKey, {
// ...additional context
})

TypeScript variation type

TypeScript users can optionally provide the expected variation values as a generic type:

type CheckoutVariation = 'control' | 'treatment'
const variation = f.getVariation<CheckoutVariation>('checkout')
// CheckoutVariation | null

The generic is optional. Existing usage continues to return string | null:

const variation = f.getVariation('checkout')
// string | null

Getting variables

Your features may include feature variables, which can be evaluated with a feature key and variable key:

const variableKey = 'bgColor'
const bgColorValue = f.getVariable(featureKey, variableKey)

Additional context per evaluation can also be passed:

const bgColorValue = f.getVariable(featureKey, variableKey, {
// ...additional context
})

Global variables use the same method with an overloaded signature:

const supportEmail = f.getVariable('supportEmail')
const localizedEmail = f.getVariable('supportEmail', { country: 'nl' })

A string second argument identifies a feature variable. An object second argument is evaluation context for a global variable. Type specific methods such as getVariableString and getVariableBoolean follow the same overloaded signatures.

TypeScript variable type

TypeScript users can optionally provide the expected variable type:

interface CheckoutConfig {
title: string
maxItems: number
}
const config = f.getVariable<CheckoutConfig>('checkout', 'config')
// CheckoutConfig | null

The generic is optional. Without it, getVariable() continues to return the general VariableValue | null type:

const config = f.getVariable('checkout', 'config')
// VariableValue | null

Generic types describe what the application expects. They do not validate or transform the value at runtime. Use the type specific methods below when runtime type checking is needed. For project aware feature keys, variable keys, variation values, and variable types, use code generation.

Type specific methods

Next to the general purpose getVariable() method, there are also type specific methods available for convenience:

f.getVariableBoolean(featureKey, variableKey, context)
f.getVariableString(featureKey, variableKey, context)
f.getVariableInteger(featureKey, variableKey, context)
f.getVariableDouble(featureKey, variableKey, context)
f.getVariableArray<T = string>(featureKey, variableKey, context)
f.getVariableObject<T = ObjectValue>(featureKey, variableKey, context)
f.getVariableJSON<T = VariableValue>(featureKey, variableKey, context)

Every type specific method also accepts a global variable key and optional context:

f.getVariableString('supportEmail', context)
f.getVariableObject<CheckoutSettings>('checkoutSettings', context)

Type specific methods do not coerce values. For example, getVariableInteger() returns null for the string "1", and getVariableBoolean() returns null for the string "true".

Without a type argument, getVariableArray() assumes an array of strings, getVariableObject() returns ObjectValue | null, and getVariableJSON() returns VariableValue | null. Pass an explicit array item type when the array contains numbers, booleans, or objects.

Getting multiple evaluations

You can evaluate every feature available in the SDK instance:

const featureEvaluations = f.getFeatureEvaluations(context)
console.log(featureEvaluations)
// {
// myFeature: {
// enabled: true,
// variation: "control",
// variables: {
// myVariableKey: "myVariableValue",
// },
// },
//
// anotherFeature: {
// enabled: true,
// variation: "treatment",
// }
// }

Use getVariableEvaluations() for global variables:

const variableEvaluations = f.getVariableEvaluations(context)
console.log(variableEvaluations)
// {
// supportEmail: "[email protected]",
// checkoutSettings: { ... },
// }

Both methods accept an optional array of keys as their second argument when you only need part of the loaded datafile:

const featureEvaluations = f.getFeatureEvaluations(context, ['checkout'])
const variableEvaluations = f.getVariableEvaluations(context, ['supportEmail'])

You can inspect the available keys before evaluating. Calling getVariableKeys() without an argument returns global variable keys. Passing a feature key returns the variables owned by that feature:

f.getVariableKeys() // global variable keys
f.getVariableKeys('checkout') // variables owned by checkout

getAllEvaluations() is deprecated. It remains as an alias of getFeatureEvaluations() for compatibility.

Server to client handoff

The two result maps have the same shapes accepted by stickyFeatures and stickyVariables. A backend can evaluate once for a request and pass both maps to the frontend:

// backend
const childF = f.spawn({ userId: '123' })
const stickyFeatures = childF.getFeatureEvaluations()
const stickyVariables = childF.getVariableEvaluations()
childF.close()
// frontend
const clientF = createFeaturevisor({
stickyFeatures,
stickyVariables,
})

This keeps the first client render consistent with the backend result. The client can load a datafile afterwards and release either sticky map when the application is ready to return to live evaluation.

Sticky

For the lifecycle of the SDK instance in your application, you can set features and global variables with sticky values. Sticky values take precedence over evaluation against the fetched datafile:

Initialize with sticky values

import { createFeaturevisor } from '@featurevisor/sdk'
const f = createFeaturevisor({
stickyFeatures: {
myFeatureKey: {
enabled: true,
// optional
variation: 'treatment',
variables: {
myVariableKey: 'myVariableValue',
},
},
anotherFeatureKey: {
enabled: false,
},
},
stickyVariables: {
supportEmail: '[email protected]',
},
})

Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process.

Sticky features and variables can also supply values before a datafile is available. This is useful during application startup and in focused unit tests.

Set sticky afterwards

You can also set sticky features after the SDK is initialized:

f.setStickyFeatures(
{
myFeatureKey: {
enabled: true,
variation: 'treatment',
variables: {
myVariableKey: 'myVariableValue',
},
},
anotherFeatureKey: {
enabled: false,
},
},
// replace existing sticky features (false by default)
true
)

Global variables can be updated separately. The second argument replaces the existing map when it is true, and merges by default:

f.setStickyVariables(
{
supportEmail: '[email protected]',
},
true,
)

Setting datafile

You may also initialize the SDK without passing datafile, and set it later on:

f.setDatafile(datafileContent)

Merging by default

By default, setDatafile merges the incoming datafile with the SDK instance's existing datafile:

  • incoming features, segments, and global variables override matching keys
  • existing entities that are missing from the incoming datafile are kept
  • revision, schemaVersion, and featurevisorVersion are taken from the incoming datafile

This means you can call setDatafile more than once with different datafiles, and the SDK instance accumulates their features, segments, and global variables together. This is what makes loading datafiles on demand possible.

Replacing

Pass true as the second argument to replace the existing datafile entirely, discarding anything loaded before:

f.setDatafile(datafileContent, true)

Loading datafiles on demand

Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront.

This pairs well with targets, where each target produces a smaller datafile for a specific part of your application. You can load the datafile for the current part, and load others only when the user reaches them:

your-app/index.js
import { createFeaturevisor } from '@featurevisor/sdk'
// one shared instance for the whole application
const f = createFeaturevisor({})
async function loadDatafile(target) {
const url = `https://cdn.yoursite.com/production/featurevisor-${target}.json`
const datafile = await fetch(url).then((res) => res.json())
// merges into whatever was loaded before
f.setDatafile(datafile)
}
// load the first part now
await loadDatafile('products')
// later, when the user navigates to checkout,
// load its datafile without losing the products features
await loadDatafile('checkout')

Each setDatafile call emits a datafile_set event with the affected features and global variables, so you can evaluate and render only the relevant parts of your UI again.

Learn more in Loading datafiles on demand.

Updating datafile

You can set the datafile as many times as you want in your application, which will result in emitting a datafile_set event that you can listen and react to accordingly.

The triggers for setting the datafile again can be:

  • periodic updates based on an interval (like every 5 minutes), or
  • reacting to:
    • a specific event in your application (like a user action), or
    • an event served via websocket or server-sent events (SSE)

Interval-based update

Here's an example of using interval-based update:

const interval = 5 * 60 * 1000 // 5 minutes
setTimeout(function () {
fetch(datafileUrl)
.then((res) => res.json())
.then((datafileContent) => {
// replace the previously loaded full datafile
f.setDatafile(datafileContent, true)
})
}, interval)

Diagnostics

By default, Featurevisor SDKs report diagnostics to the console for info level and above with a [Featurevisor] prefix.

Levels

These are all the available diagnostic levels:

  • fatal
  • error
  • warn
  • info
  • debug

Customizing levels

If you choose debug level to make diagnostics more verbose, you can set it at the time of SDK initialization.

Setting debug level will report all diagnostics, including info, warn, and error levels.

import { createFeaturevisor } from '@featurevisor/sdk'
const f = createFeaturevisor({
logLevel: 'debug',
})

You can also update the diagnostic level from SDK instance afterwards:

f.setLogLevel('debug')

Handler

You can pass your own diagnostic handler if you do not wish to print diagnostics to the console:

const f = createFeaturevisor({
logLevel: 'info',
onDiagnostic: function (diagnostic) {
const {
level,
code,
message,
details,
originalError,
} = diagnostic
// send to your observability system
},
})

Every diagnostic has level, code, message, and an object-shaped details field. Optional module, moduleName, and originalError fields describe module and error provenance. Evaluation-specific values such as featureKey, variableKey, reason, and evaluation are nested in details.

Diagnostic handlers are isolated from SDK behaviour. If a handler throws, Featurevisor reports the handler failure to the console and continues running other handlers and evaluations.

Further diagnostic levels like info and debug will help you understand how feature variations and variables are evaluated in the runtime against a given context.

Events

Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime.

You can listen to these events that can occur at various stages in your application:

datafile_set

const unsubscribe = f.on('datafile_set', function ({
revision, // new revision
previousRevision,
revisionChanged, // true if revision has changed
replaced, // true if datafile replaced previous content instead of merging
// list of feature keys that have new updates,
// and you should re-evaluate them
features,
// list of affected global variable keys
variables,
}) {
// handle here
})
// stop listening to the event
unsubscribe()

The features and variables arrays contain keys that have either been:

  • added, or
  • updated, or
  • removed

compared to the previous datafile content that existed in the SDK instance.

They also include entities whose evaluation may have changed because one of their dependencies changed. This includes features that reference an updated segment, features that require another affected feature, and global variables that depend on an affected segment or feature. Required feature dependencies are followed transitively.

This means event listeners can trust these arrays when deciding which values to evaluate again. A datafile can update only a segment or required feature while still reporting every affected feature and global variable. The same affected keys are reported whether the datafile is merged or replaces the stored datafile, so either update strategy notifies the same subscribers.

context_set

const unsubscribe = f.on("context_set", ({
replaced, // true if context was replaced
context, // the new context
}) => {
console.log('Context set')
})

sticky_features_set

const unsubscribe = f.on("sticky_features_set", ({
replaced, // true if sticky values got replaced
features, // list of all affected feature keys
}) => {
console.log('Sticky features set')
})

sticky_variables_set

const unsubscribe = f.on("sticky_variables_set", ({
replaced, // true if sticky values got replaced
variables, // list of all affected global variable keys
}) => {
console.log('Sticky variables set')
})

The older sticky option, setSticky method, and sticky_set event remain available for compatibility, but are deprecated. Use the feature and variable specific APIs in new code. A sticky_set event caused by setStickyVariables contains features: []; this reports a variable update and does not mean sticky features were cleared.

error

const unsubscribe = f.on('error', ({ diagnostic }) => {
console.error(diagnostic.message, diagnostic)
})

Evaluation details

Besides diagnostics with debug level enabled, you can also get more details about how the feature variations and variables are evaluated in the runtime against given context:

// flag
const evaluation = f.evaluateFlag(featureKey, context)
// variation
const evaluation = f.evaluateVariation(featureKey, context)
// variable
const evaluation = f.evaluateVariable(featureKey, variableKey, context)
// global variable
const variableEvaluation = f.evaluateVariable(variableKey, context)

Every method returns the same Evaluation type with type and reason. Feature evaluations contain featureKey. Variable evaluations include variableKey, variableValue, variableOverrideKey, and variableOverrideIndex when a keyed override matches. A nested global variable override also includes its authored variableOverridePath. Global variable evaluations use type: "variable" without featureKey.

The shared TypeScript type therefore makes featureKey optional. Code that consumes evaluations from more than one evaluation path should check that featureKey is present before using it as a string. Its absence identifies a global variable evaluation.

Feature evaluation results optionally include these properties depending on whether you are evaluating a feature variation or variable:

  • bucketValue: the bucket value between 0 and 100,000
  • ruleKey: the rule key
  • error: the error object
  • enabled: if feature itself is enabled or not
  • requiredFeatures: the canonical feature requirements when they prevent the feature from being enabled
  • variation: the variation object
  • variationValue: the variation value
  • variableKey: the variable key
  • variableValue: the variable value
  • variableSchema: the variable schema

Evaluation reasons

The reason field explains the path that produced an evaluation result. These values can also be asserted through expectedEvaluation.reason in test specs.

ReasonMeaning
feature_not_foundThe requested feature is not present in the datafile
disabledThe feature evaluated as disabled
requiredOne or more required features did not satisfy a feature requirement
out_of_rangeThe bucket value did not fall inside an eligible allocation range
no_variationsA variation was requested from a feature without variations
variation_disabledA disabled feature returned its configured disabledVariationValue
variable_not_foundThe requested feature variable or global variable was not found
variable_defaultThe variable returned its default value
variable_disabledA disabled feature returned the variable's disabled value
variable_override_variationA feature variable override matched inside a variation
variable_override_ruleA feature or global variable override matched
required_features_unmetA global variable's required features did not satisfy its requirements
no_matchNo rule, allocation, or variation matched. For a flag, the bucket value may be outside the rollout percentage or no rule matched
forcedA force definition supplied the result
stickyA sticky feature or global variable value supplied the result
ruleA matching feature rule supplied the result
allocatedPercentage bucketing selected the result
errorEvaluation failed and the error field contains more information

Modules

Modules allow you to intercept the evaluation process and customize it further as per your needs.

Defining a module

A module is a simple object with an optional unique name, an optional setup lifecycle function, and optional evaluation callbacks:

import type { FeaturevisorModule } from "@featurevisor/sdk"
const myCustomModule: FeaturevisorModule = {
name: 'my-custom-module',
setup: function ({ getRevision, onDiagnostic, reportDiagnostic }) {
const revision = getRevision()
onDiagnostic(
function (diagnostic) {
// modules can subscribe to diagnostics using their own log level
},
{
logLevel: 'warn',
},
)
reportDiagnostic({
level: 'info',
code: 'module_ready',
message: `Module ready for revision ${revision}`,
})
},
// before any feature or global variable evaluation
beforeEvaluation: function (options) {
const {
type, // `flag`, `variation`, or `variable`
featureKey, // omitted for global variables
variableKey,
context,
} = options
// update context before evaluation
options.context = {
...options.context,
someAdditionalAttribute: 'value',
}
return options
},
// after any evaluation
afterEvaluation: function (evaluation, options) {
const {
reason // `error` | `feature_not_found` | `variable_not_found` | ...
} = evaluation
if (reason === "error") {
// log error
}
return evaluation
},
// configure bucket key
bucketKey: function (options) {
const {
featureKey,
context,
bucketBy,
bucketKey, // default bucket key
} = options
// return custom bucket key
return bucketKey
},
// configure bucket value (between 0 and 100,000)
bucketValue: function (options) {
const {
featureKey,
context,
bucketKey,
bucketValue, // default bucket value
} = options
// return custom bucket value
return bucketValue
},
close: function () {
// clean up resources when f.close() is called
},
}

The older before and after callbacks continue to run for feature evaluations when supplied, but are deprecated. Use beforeEvaluation and afterEvaluation for new modules so one callback can handle both feature and global variable evaluations. Both callbacks receive the shared evaluation representation. A variable evaluation without featureKey is global.

Return a value from evaluation callbacks

beforeEvaluation must return the original or updated options. afterEvaluation must return the original or updated evaluation. Returning undefined does not skip the callback. It replaces the value flowing through the pipeline. In particular, an afterEvaluation callback that returns undefined can make feature getters report disabled flags and null variations across the application without raising an error. The same requirement applies when using the deprecated before and after callbacks.

If setup throws, the module is not registered. Featurevisor removes diagnostic subscriptions created during setup, reports a module_setup_error diagnostic, and calls the module's close callback when present.

Registering modules

You can register modules at the time of SDK initialization:

import { createFeaturevisor } from '@featurevisor/sdk'
const f = createFeaturevisor({
modules: [
myCustomModule
],
})

Or after initialization:

const removeModule = f.addModule(myCustomModule)
// removeModule()
f.removeModule('my-custom-module')

Child instance

When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications.

But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request.

That's where child instances come in handy:

const childF = f.spawn({
// user or request specific context
userId: '123',
})

The child snapshots the parent keys that exist when it is spawned. Child values win for those keys. Parent keys introduced later are still inherited. This keeps request context stable while still allowing new shared context fields to flow through.

Sticky state is isolated rather than inherited. A child created without sticky options starts with empty sticky feature and global variable maps, even when the parent has sticky values. Pass stickyFeatures or stickyVariables through the second argument to spawn when that child needs them:

const childF = f.spawn(
{ userId: '123' },
{
stickyFeatures: { my_feature: { enabled: true } },
stickyVariables: { supportEmail: '[email protected]' },
},
)

Now you can pass the child instance where your individual request is being handled, and you can continue to evaluate features targeting that specific user alone:

const isEnabled = childF.isEnabled('my_feature')
const variation = childF.getVariation('my_feature')
const variableValue = childF.getVariable('my_feature', 'my_variable')

Similar to parent SDK, child instances also support several additional methods:

  • setContext
  • setStickyFeatures
  • setStickyVariables
  • evaluateFlag
  • isEnabled
  • evaluateVariation
  • getVariation
  • evaluateVariable
  • getVariable
  • getVariableBoolean
  • getVariableString
  • getVariableInteger
  • getVariableDouble
  • getVariableArray
  • getVariableObject
  • getVariableJSON
  • getFeatureEvaluations
  • getVariableEvaluations
  • getAllEvaluations (deprecated)
  • on
  • close

Calling close() removes listeners owned by the child and event subscriptions that the child delegated to its parent.

Close

Both primary and child instances support a .close() method, that removes forgotten event listeners (via on method) and cleans up any potential memory leaks.

f.close()
Previous
Feature variables