Featurevisor

Getting started

How Featurevisor works

Featurevisor has three parts: a repository of definitions, a pipeline that turns them into datafiles, and SDKs that evaluate those datafiles in your application. This page follows one feature, checkout, through all three.

The repository

Definitions are YAML files in a Git repository, kept separate from your applications. Each kind has its own directory, and files refer to each other by key.

01Attribute

A value your application passes in.

attributes/country.yml
description: Country
type: string

02Segment

A named audience, built on the attribute.

uses country

segments/netherlands.yml
description: The Netherlands
conditions:
- attribute: country
operator: equals
value: nl

03Feature

Rules that use the segment, checked in order.

uses netherlands

features/checkout.yml
description: Checkout redesign
bucketBy: userId
rules:
- key: netherlands
conditions: netherlands
percentage: 100
- key: everyone
conditions: '*'
percentage: 25

Your application supplies country when it asks for a value. The netherlands segment names everyone whose country is nl. The checkout feature is enabled for that segment, then for 25% of everyone else. bucketBy: userId keeps each user's answer stable.

Building blocks

A project uses these directories by default. Change them in configuration.

Building blockDirectoryHolds
Attributesattributes/Context values that conditions read
Segmentssegments/Reusable groups of conditions
Featuresfeatures/Flags, variations and feature variables
Variablesvariables/Global variables with overrides
Targetstargets/What each datafile contains
Schemasschemas/Reusable variable types
Teststests/Specs that check results

Start with only the definitions your application needs. You do not need every building block to use Featurevisor.

The pipeline

A change to checkout reaches users through the same review and CI process as your code. Featurevisor provides the tools at each stage. Your team owns the process around them.

  1. Propose
    Featurevisor
    Definitions are plain files with a readable diff.
    You
    Open a pull request and choose who reviews it.
  2. Check
    Featurevisor
    lint validates definitions. test runs your specs.
    You
    Run both in CI and block merging when they fail.
  3. Build
    Featurevisor
    build writes one datafile per target, plus a revision and state files.
    You
    Build on merge and commit the state files back.
  4. Publish
    Featurevisor
    Datafiles are static JSON with no service behind them.
    You
    Upload them to your CDN and decide when apps refresh.

Testing, Building datafiles, State files and Deployment cover each stage in detail.

A release is therefore a change to these files, not to your application. Anyone who can open a pull request can propose one, such as a product manager widening a rollout, and the feature's owners review it like code. Once the new datafile is published, applications that fetch it pick up the change without a new build of their own.

What a datafile contains

A datafile is the compiled form of your definitions. This is the checkout entry from the one built for the project above:

datafiles/featurevisor-all.json
{
"schemaVersion": "4",
"revision": "2",
"features": {
"checkout": {
"bucketBy": "userId",
"traffic": [
{
"key": "netherlands",
"conditions": "netherlands",
"percentage": 100000
},
{
"key": "everyone",
"conditions": "*",
"percentage": 25000
}
]
}
}
}

The datafile also carries the netherlands segment's conditions, so the SDK can match them without your repository. Percentages are stored with three decimal places, so 25 becomes 25000. Targets decide which features and global variables each datafile contains, so every application loads only what it needs.

Anyone can read a datafile sent to a browser or a phone. Keep secrets out of your definitions, and keep authorization on your server.

The application

Your application fetches a datafile, gives it to an SDK and asks for values. The SDK evaluates in memory, without a network request for each decision. Fetching and refreshing the datafile are up to your application.

Context

Context describes who or what is asking: a user, an account, a device or a request. One datafile gives each context its own answer, so you never build a datafile per user.

const context = {
userId: 'user-4',
country: 'nl',
}
f.isEnabled('checkout', context) // true

How a feature decides

The SDK checks these in order and stops at the first one that decides:

  1. 01StickyA value pinned in the SDK always wins.
  2. 02ForceThe first matching force entry decides.
  3. 03ConditionsAn unmet root gate disables it.
  4. 04BucketCompute a stable position from the user and feature key.
  5. 05RulesUse the first matching rule, then check its percentage.

Bucketing turns the bucketBy value and the feature key into a stable position from 0 to 100. A user is in when their position falls inside the matching rule's percentage. Here is checkout for three users:

ContextruleBucketisEnabled
user-4 · nlnetherlands at 100%89.3true
user-5 · deeveryone at 25%15.5true
user-9 · deeveryone at 25%38.5false

user-4 matches netherlands, which is at 100%, so any position is inside. user-5 and user-9 both fall through to everyone, but only user-5 sits inside its 25%. A user keeps the same position for this feature on every device and in every SDK.

Check a context yourself with the CLI. --explain names the rule that matched and the user's bucket:

npx featurevisor evaluate --feature=checkout \
--context='{"userId":"user-5","country":"de"}' \
--explain
Value: true
Rule: everyone (25% rollout)
Bucket: 15501 / 100000 · key "user-5.checkout"

A feature's variations and variables follow from its flag. The evaluation flow lists every step, and Bucketing and Conditions cover the details.

Variations

A feature with variations also gives each enabled user one of its values. Weights split the users, and bucketing keeps each user in the same variation:

features/pricing.yml
description: Pricing page experiment
bucketBy: userId
variations:
- value: control
weight: 50
- value: treatment
weight: 50
rules:
- key: everyone
conditions: '*'
percentage: 100

Your application reads the variation it was given:

const context = { userId: 'user-2' }
f.getVariation('pricing', context) // 'treatment'

user-2 lands at 70.3 and gets treatment. user-4 lands at 35.7 and gets control, on the other side of the 50/50 split. See Variations.

Global variables

Global variables decide differently. They have no bucketBy and no percentages. A global variable starts from its default and takes the value of the first matching override:

variables/supportEmail.yml
description: Support email shown in checkout
type: string
defaultValue: [email protected]
overrides:
- key: netherlands
conditions: netherlands
value: support-[email protected]

Your application reads the value for its context:

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

Every context outside the Netherlands gets the default, [email protected]. An override with continue: true lets later overrides change the value further. See global variables.

As your project grows

Each of these is optional. A project without environments uses plain rule lists, like the checkout file above.

When you needUse
Different rules for staging and productionEnvironments
A smaller datafile for each applicationTargets and tags
Keys grouped by team or product areaNamespaces
Separate collections of definitionsSets
Reviewed changes moved between those setsPromotions

Build it yourself

The Quick start creates a project like this one, tests a feature and evaluates it in an application.

Background reading

Previous
Introduction