This is a node library that implements an API and config format for enabling feature toggling and/or A/B testing. The idea being, you combine configuration with per-request data (e.g. cookies) in order to turn bits of the application on or off.
feature-toggle-lib
exports a single class, FeatureToggle
, that represents your generic config. For each user (typically an http request on the server), you should create an instance of Ftoggle
(a particular user's settings within the global config) via featureToggleInstance.create()
.
Features have traffic between 0 and 1 (where 0 means no traffic and 1 means all traffic) that indicate how toggles should be set for a user (using Math.random
). Features are nestable and recursively calculated (which is useful for A/B testing).
Example config:
const config = {
version: 1,
name: "somelabel",
features: {
feature_1: {
traffic: 0.5
},
feature_2: {
traffic: 0.3,
features: {
subfeature_2_1: {
traffic: 0
},
subfeature_2_2: {
traffic: 1
}
}
}
}
};
Example (simplified) setup using express:
const express = require('express');
const app = express();
const FeatureToggle = require('feature-toggle-lib');
const config = require('./config/ftoggle.js')
const ftoggle = new FeatureToggle(config);
// Or
// ftoggle.setConfig(config);
// Initialize the ftoggle settings for this user
app.use((req, res, next) => {
req.ftoggle = ftoggle.create(req.cookies.ftoggle_cookie);
next();
});
// All your normal middleware
// ...
// Write the ftoggle cookie so this user keeps their current settings across pages
app.use((req, res, next) => {
res.cookie(req.ftoggle.toggleName, req.ftoggle.serialize, { maxAge: 31536000000 });
next();
});
Create an instance of FeatureToggle
.
- config - A config object containing the features to toggle a user into and out of. Ftoggle recognizes the following properties in a config object:
-
config.name - Top level only. The name of the config, which can be used to set a unique cookie. This allows you to use multiple ftoggle configs with different names, if necessary.
-
config.version - Top level only. The version of the config. When a user with an existing ftoggle config matching
config.name
comes in, if the version in that config does not match this value, the config is recalculated (i.e. the user's toggles are updated to the current config). -
config.features - A set of key/value pairs where the key is the feature name and the value is a sub-instance of
config
(but withoutname
andversion
). That is, the value can (recursively) have all the same keys outlined here exceptname
andversion
(technically it can still have those but they will be ignored). -
config.traffic - The amount of traffic this feature should receive. Note, the top level does also have this field and setting it to anything less than 1 means some traffic will be toggled into anything. To make an abTest, create a feature with
exclusiveSplit
(see below) that receives some portion of traffic (it's up to you whether this is all traffic or some subset), and within that, create the various test versions with traffic that totals 1. So you might have acontrol
feature with traffic0.34
, atest_a
feature with traffic0.33
, and atest_b
feature with traffic0.33
. -
config.exclusiveSplit - Normally, traffic for a feature is calculated indepently of all other features, so if there are multiple top-level features with traffic
1
, they will all be turned on. SettingexclusiveSplit
signalsftoggle
that only one feature at a particular level should be turned on. In an A/B test, for example, you only want the user to be toggled into one of the test versions. For example:{ features: { someTest: { traffic: 1, // All user's get one of these features exclusiveSplit: true, // and ONLY one of these features features: { control: { traffic: 0.34 // 34% of traffic }, test_a: { traffic: 0.33 // 33% of traffic } test_b: { traffic: 0.33 // 33% of traffic } } } } }
It is not strictly necessary that traffic total 100% (or
1
), but if it's less than 100%, some user's will end up in none of the features, and if it's more, the excess will be ignored effectively making it total 100%. I.e. probably just don't do that. -
config.settings - Ftoggle supports setting flags based on which features a user is toggled into. These are merged into a single
settings
object accessible viaftoggle.settings
orftoggle.getSettings()
. For example:{ settings: { fooEnabled: false, barEnabled: false }, traffic: 1, features: { foo: { traffic: 1, settings: { fooEnabled: true } }, bar: { traffic: 0 settings: { barEnabled: true } } } }
Here,
settings
will be{ fooEnabled: true, barEnabled: false }
because the featurefoo
will be toggled on and thefooEnabled
setting will be enabled, but the featurebar
will not be toggled on and therefore thebarEnabled
setting will not be enabled. We recommend only usingsettings
for conditional logic, as opposed toisFeatureEnabled
orfindEnabledChildren
orftoggle.toggles
. There are two reasons for this. First, it's significantly more clear (semantically) to sayif (ftoggle.settings.fooEnabled)
thanif (ftoggle.toggles.foo.e)
orif (ftoggle.isFeatureEnabled('foo'))
and second, it allows you to reuse settings in different features, so you can sayif (ftoggle.settings.extremeVersion)
instead ofif (ftoggle.settings.abTests.extreme.extreme_a || ftoggle.settings.abTests.extreme.extreme_b)
.
-
Create a user-specific config instance. Uses the existing serialization if present. Returns an instance of Ftoggle
.
- serialization - A previously calculated ftoggle configuration, in the form version + z + config + z + extra bits.
Set the config for this feature toggle lib instance. You can pass this into the constructor as well, but you might use this function if you're storing your config remotely (e.g. s3), and updating it occasionally from a polling function.
- config - A config object containing the features to toggle a user into and out of.
Merge new configuration in with existing. You might use this if you want to separate parts of your configuration files for clarity. For instance, we use to keep all toggles and traffic in one file and settings for various buckets in several other files, and then merge them all together using this function.
- config - A config object containing the features to toggle a user into and out of. Note that the key/value heirarchy must be the same in your configs for this to work correctly. That is, if you want to separate out your
settings
, you still need to put them in the right object path, like:{ features: { foo: { features: { bar: { settings: { giveMeSomeFoo: true, andSomeBar: true } }}}}}
Create a new user-specific ftoggle instance.
- toggles - An object of enabled and disabled features in the form:
{ v: 12, foo: { bar: { e: 1 }, baz: { e: 0 } } }
- settings - An object of flags enabled (or disabled) by the toggles above, in the form:
{ firstThingOn: true, secondThingOn: false }
- featureConfig - The original config used to create the
FeatureToggle
instance. This is used for calculating/recalculating manually enabled and disabled features (see .enable and .disable
Determine with a particular feature is on or off.
- featurePath - A dot-notated path to a toggle.
Example:
ftoggle.isFeatureEnabled('foo.bar'); // -> true
Get a list of subfeatures enabled beneath parentPath
.
- parentPath - The feature path to inspect.
Example:
ftoggle.findEnabledChildren('foo.bar'); // -> ['bar', 'quux']
Check whether a particular feature exists in the config.
- featurePath - A dot-notated path to a toggle.
Example:
ftoggle.doesFeatureExist('foo.bar'); // -> true
Returns the complete toggles object (same as using ftoggle.toggles
).
Get the value of a particular setting.
- key - The setting name.
Example:
ftoggle.getSetting('allYourBaseAreBelongToUs'); // -> "someone set up us the bomb"
Get all settings (same as using ftoggle.settings
).
Get the raw non-toggle-specific settings for a feature path.
- featurePath - A dot-notated path to a toggle.
Example:
ftoggle.getSettingsForFeature('foo.bar') // -> { 'fooBarEnabled': true }
Add the settings under featurePath
to the settings
object as if those features were toggled on.
- featurePath - A dot-notated path to a toggle.
Example:
ftoggle.setFeatureSettings('foo.bar');
Delete settings under featurePath
from the settings
object. Note that this does not reset those settings to parent level settings, which is a good reason to always make your defaults falsy.
- featurePath - A dot-notated path to a toggle.
ftoggle.unsetFeatureSettings('foo.bar');
Enable a feature on the fly, including setting all of it's settings in the settings
object. Sometimes you want to use something other than randomization for determining particular site logic. E.g. you might want to enable a particular flag for users who come to your site from a particular referer. This function allows you to do that.
- featurePath - A dot-notated path to a toggle.
Example:
ftoggle.enable('abTests.ppc_checkout');
Enable a list of feature. This is just sugar for calling enable
on each feature individually.
- features - An array or comma-separated list of feature paths.
Example:
ftoggle.enableAll(['foo', 'bar']);
//or
ftoggle.enableAll('foo,bar');
Disable a feaure on the fly, including unsetting all of it's settings in the settings
object. The reverse of enable
above.
- featurePath - A dot-notated path to a toggle.
Example:
ftoggle.disable('foo.bar');
Disable a list of features. Like enableAll
, this is sugar for calling disable
on each feature.
- features - An array or comma-separated list of feature paths.
Example:
ftoggle.disableAll('foo', 'bar');
// or
ftoggle.disableAll('foo,bar');
Most ftoggle function make use of short paths (e.g. foo.bar.baz) because that's how the toggles object is shaped. However, the original feature config always has a features
object at each level. This simple utitlity converts a path usable on the toggles
object to one usable on the featureConfig
object.
- shortPath - A dot-notated path to a toggle.
Example:
ftoggle.makeFeaturePath('foo.bar'); // -> features.foo.features.bar
Get a list of all child nodes under a particular feature.
- featureConfig - Any level (top or nested) of a feature config.
- key - The feature to return child nodes of.
Example:
ftoggle.getAllChildNodes(mainConfig, 'foo');
// -> ['bar', 'bar.features.baz', 'bar.features.quux']
Recursively change e: 1
to e: 0
in a partial (or whole) toggle object.
- toggleObject - The portion of the toggle object to change.
Example:
ftoggle.unsetAll(ftoggle.toggles.abTests);
Ftoggle uses a packed bit algorithm to calculate small string value representing a user's toggles. Use this value to preserve toggles across pages, by persisting it in, for example, a cookie or a redis store or memcached or some other session manager or a database (etc.).
Example:
ftoggle.serialize()
Unpack a serialization into a set of toggles. Because this requires a toggle object, it's much easier to call feautreToggle.create(serialization)
instead.
- serialization - The output from a previous call to
.serialize()
. - toggles - A fresh toggle object. Note that this is only used to know what toggles exist. The values are ignored/overwritten by the values in the serialization.
Example:
ftoggle.deserialize(req.cookies.ftoggle, toggle);
There are two files in dist
that you can include in a frontend bundle: dist/ftoggle.js
which includes it's own version of lodash and dist/ftoggle-standalone.js
which does not and relies on lodash being available on window. If you're already using lodash in your client bundle, use the standalone version to prevent multiple copies of lodash being bundled.