Featurevisor

Definitions in depth

Flags

A flag answers one question for each user: is this feature on? This page follows one flag, checkout, as it rolls out country by country, until you retire it.

What a flag answers

Every feature has a flag. Your application asks for it with isEnabled, which returns true or false. A feature missing from the datafile is false, so code behind a flag stays off until its definition arrives.

This is checkout, with a rule for each of three countries: off in the Netherlands, on for 80% of users in Germany and on for everyone in the USA:

features/checkout.yml
description: Checkout redesign
bucketBy: userId
rules:
- key: netherlands
conditions: netherlands
percentage: 0
- key: germany
conditions:
- attribute: country
operator: equals
value: de
percentage: 80
- key: usa
conditions:
- attribute: country
operator: equals
value: us
percentage: 100

How a rule decides

Each user has a fixed position from 0 to 100 for each feature, called their bucket. It comes from the feature key and the bucketBy attribute, here userId. The first rule whose conditions match decides, and the user is in when their bucket falls within that rule's percentage.

user-1, user-2 and user-3 at their buckets across a rule for each country. In the Netherlands the rule is at 0% and all three are off, in Germany it is at 80% and user-2 and user-3 are on, and in the USA it is at 100% and all three are on.

In the illustration, user-1, user-2 and user-3 keep the same position under every rule. Which rule matches them, and its percentage, decide whether they are in. For checkout, four users get these results:

UserCountryBucketRuleEnabled
user-1de97.3germanyNo
user-2de73.5germanyYes
user-3nl49.3netherlandsNo
user-4us89.3usaYes

user-2 is inside the first 80% in Germany, and user-1 is outside it. user-3 is off whatever their bucket, because the Netherlands is at 0%, and user-4 is on, because the USA is at 100%. A user outside the matching rule's percentage is off. They do not fall through to a later rule. A user in a country with no rule matches nothing and is off too.

Roll out gradually

Raise a rule's percentage in small steps, each through a pull request. Here the Netherlands goes from 0% to 50%:

features/checkout.yml
# ...
rules:
- key: netherlands
conditions: netherlands
percentage: 50
- key: germany
conditions:
- attribute: country
operator: equals
value: de
percentage: 80
- key: usa
conditions:
- attribute: country
operator: equals
value: us
percentage: 100

Buckets do not move, so everyone already in stays in. At 50%, user-3 joins, since their bucket is 49.3. Lowering the percentage takes out the users above the new value. Keep the rule's key unchanged as you go. See Bucketing for how variations stay stable too.

Switch it off fast

If something goes wrong, force the flag off for everyone. A force entry is checked before the rules, so they stay ready for when you switch it back on:

features/checkout.yml
# ...
force:
- conditions: '*'
enabled: false
The same three users and country rules, with a force layer for everyone above them. user-1, user-2 and user-3 all stop at force and get false, and the rules below are not reached.

The change reaches users when their applications load the new datafile, so how quickly it takes effect depends on how often they refresh. Plan that interval with your engineers before you need it. See how applications get updates, and Kill switches for planning the whole incident.

To switch off one audience instead, set its rule's percentage to 0. Its users get false and do not fall through to the next rule.

Let someone in early

Force the flag on for a tester, here user-1, who is outside the 80% in Germany:

features/checkout.yml
# ...
force:
- conditions:
- attribute: userId
operator: equals
value: 'user-1'
enabled: true

The first matching force entry wins, so put a kill switch for everyone above any entries like this one. Use a segment key in conditions to let a whole team in. See Testing in production.

Depend on another flag

A feature can require another one to be enabled first, such as file upload in a live chat. See Conditions and the example in Features.

Test it

A spec fixes the bucket with at, so it checks the rollout without real users:

tests/features/checkout.spec.yml
feature: checkout
assertions:
- description: Dutch users are out
at: 90
context:
country: nl
expectedToBeEnabled: false
- description: German users are in below 80
at: 10
context:
country: de
expectedToBeEnabled: true
- description: German users are out above 80
at: 90
context:
country: de
expectedToBeEnabled: false
- description: Users in the USA are always in
at: 90
context:
country: us
expectedToBeEnabled: true

To see why a real user got their result, add --explain to evaluate:

Command
npx featurevisor evaluate \
--feature=checkout \
--context='{"userId":"user-1","country":"de"}' \
--explain

Use it in your application

Pass the attributes your rules and bucketBy use:

const context = {
userId: 'user-2',
country: 'de',
}
if (f.isEnabled('checkout', context)) {
// show the new checkout
}

Always include userId. Without it, every context shares one bucket, so all anonymous visitors get the same answer together. For checkout, that bucket is 5.6, so every anonymous visitor in Germany or the USA is in. Use bucketBy: { or: [userId, deviceId] } for visitors who have not signed in. See the JavaScript SDK for evaluation details.

Retire it

Once checkout is at 100% everywhere and the old code is gone, retire the flag:

  1. Set deprecated: true. SDKs keep evaluating it and log a warning, so you can find remaining calls.
  2. Check what still uses it with npx featurevisor find-usage --feature=checkout.
  3. Remove the calls from your applications, then set archived: true. Linting stops you if another active definition, spec or target still refers to it. Keep the archived file, so the key is not reused.

See Deprecating and Archiving for the details.

Previous
Targets