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

We can evaluate 3 types of values against a particular feature:

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

These evaluations are run against the provided context.

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
})

Getting variables

Your features may also include variables, which can be evaluated as follows:

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
})

Type specific methods

Next to generic getVariable() methods, 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>(featureKey, variableKey, context = {})
f.getVariableObject<T>(featureKey, variableKey, context = {})
f.getVariableJSON<T>(featureKey, variableKey, 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".

Getting all evaluations

You can get evaluations of all features available in the SDK instance:

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

This is handy especially when you want to pass all evaluations from a backend application to the frontend.

Sticky

For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched datafile:

Initialize with sticky

import { createFeaturevisor } from '@featurevisor/sdk'
const f = createFeaturevisor({
sticky: {
myFeatureKey: {
enabled: true,
// optional
variation: 'treatment',
variables: {
myVariableKey: 'myVariableValue',
},
},
anotherFeatureKey: {
enabled: false,
},
},
})

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

Set sticky afterwards

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

f.setSticky(
{
myFeatureKey: {
enabled: true,
variation: 'treatment',
variables: {
myVariableKey: 'myVariableValue',
},
},
anotherFeatureKey: {
enabled: false,
},
},
// replace existing sticky features (false by default)
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 and segments override matching keys
  • existing features and segments 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 and segments 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 list of affected features, so you can re-evaluate and re-render the relevant parts of your UI.

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 behavior. 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,
}) {
// handle here
})
// stop listening to the event
unsubscribe()

The features array will contain keys of features that have either been:

  • added, or
  • updated, or
  • removed

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

context_set

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

sticky_set

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

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 = {})

The returned object will always contain the following properties:

  • featureKey: the feature key
  • reason: the reason how the value was evaluated

And optionally these properties depending on whether you are evaluating a feature variation or a 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
  • variation: the variation object
  • variationValue: the variation value
  • variableKey: the variable key
  • variableValue: the variable value
  • variableSchema: the variable schema

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 evaluation
before: function (options) {
const {
type, // `flag` | `variation` | `variable`
featureKey,
variableKey, // if type is `variable`
context,
} = options
// update context before evaluation
options.context = {
...options.context,
someAdditionalAttribute: 'value',
}
return options
},
// after evaluation
after: 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
},
}

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',
})

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
  • setSticky
  • isEnabled
  • getVariation
  • getVariable
  • getVariableBoolean
  • getVariableString
  • getVariableInteger
  • getVariableDouble
  • getVariableArray
  • getVariableObject
  • getVariableJSON
  • getAllEvaluations
  • on
  • close

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
Variables