

A JavaScript synchronous implementation of feature switches, with a few bells and whistles. In general, features are describe as plain JavaScript objects whose values are boolean:
const features = {
featureOne: true,
featureTwo: false
};
const featureManager = new FeatureManager(features);
console.log('featureOne', featureManager.isEnabled('featureOne'));
The FeatureManager object receives features at instantiation time; these features are cloned so that any changes to the initial feature object is not realized by the FeatureManager. Likewise any feature objects obtained from the FeatureManager are also clones and will not realize subsequent changes.
As well, the FeatureManager will normalize the passed in features; any value which is the boolean true or the string literal "true" will be considered true and all other values will be considered false. It is possible to customize this behavior (this is covered in detail later).
Advanced use scenarios support A/B testing and dark testing a new implemenation of some complex functionality (see examples below).
In addition to the FeatureManager, there are facilities to strip out features from HTML, JavaScript and CSS. Such functionality may be useful at build time in order to provide different builds based on which features are enabled. This functionality could be leveraged in middleware. For example, an express server may filter HTML documents or JavaScript examples based on server-side features.
Lastly, DOM management code is provided for supporting dynamic management of features within an HTML page.
It is noteworthy that there are no runtime depedencies required for this library.
Usage
Usage examples are provided below.
Basic Usage
The most basic usage:
const features = {
featureOne: true,
featureTwo: false
};
const featureManager = new FeatureManager(features);
console.log('featureOne', featureManager.isEnabled('featureOne'));
featureManager.disable('featureOne');
console.log('featureOne', featureManager.isEnabled('featureOne'));
featureManager.enable('featureOne');
console.log('featureOne', featureManager.isEnabled('featureOne'));
featureManager.toggle('featureOne');
console.log('featureOne', featureManager.isEnabled('featureOne'));
console.log('featureOne is enabled', featureManager.isEnabled('featureOne'));
console.log('featureOne is disabled', featureManager.isDisabled('featureOne'));
Advanced Usage
Some advanced usage:
const features = {
featureOne: true,
featureTwo: false
};
const featureManager = new FeatureManager(features);
console.log(featureManager.getFeatures().featureOne);
console.log(featureManager.hasFeature('featureOne'));
console.log(featureManager.hasFeature('invalidFeature'));
console.log(featureManager.isAnyEnabled(['featureOne', 'featureTwo']));
console.log(featureManager.isAllEnabled(['featureOne', 'featureTwo']));
console.log(featureManager.isAnyDisabled(['featureOne', 'featureTwo']));
console.log(featureManager.isAllDisabled(['featureOne', 'featureTwo']));
featureManager.ifEnabled('featureOne', (words) => console.log(words.join(' ')), [['feature', 'one']]);
featureManager.ifDisabled('featureTwo', (words) => console.log(words.join(' ')), [['feature', 'two']]);
featureManager.decide('featureOne', () => console.log('enabled'), () => console.log('disabled'));
featureManager.decide('featureTwo', () => console.log('disabled'), () => console.log('disabled'));
featureManager.addFeature('featureThree', true);
console.log(featureManager.isEnabled('featureThree'));
featureManager.removeFeature('featureThree');
console.log(featureManager.hasFeature('featureThree'));
featureManager.setEnabled('featureTwo', true);
console.log(featureManager.isEnabled('featureTwo'));
Feature state notification example:
It may be useful to respond to feature state changes.
const features = {
featureOne: true,
featureTwo: false
};
const featureManager = new FeatureManager(features);
const removeListener = featureManager.addChangeListener((featuresSnapshot, feature, value) => {
console.log('feature', name, 'was changed to', value);
removeListener();
});
featureManager.enable('featureTwo');
Function Generation
Sometimes it is useful to create a function whose body will execute only when a specific feature is enabled or disabled:
const features = {
featureOne: true
};
const featureManager = new FeatureManager(features);
const ifFeatureOne = featureManager.ifFunction('featureOne', (name) => console.log(`featureOne is enabled, ${name}`));
const elseFeatureOne = featureManager.elseFunction('featureOne', (name) => console.log(`featureOne is disabled, ${name}`));
ifFeatureOne('Sam');
elseFeatureOne('Mel');
featureManager.disable('featureOne');
ifFeatureOne('Sam');
elseFeatureOne('Mel');
It is possible to combine the above two functions as one:
const features = {
featureOne: true
};
const featureManager = new FeatureManager(features);
const ifElseFeatureOne = featureManager.ifElseFunction(
'featureOne',
(name) => console.log(`featureOne is enabled, ${name}`),
(name) => console.log(`featureOne is disabled, ${name}`)
);
ifElseFeatureOne('Sam');
featureManager.disable('featureOne');
ifElseFeatureOne('Mel');
Context
Sometimes it is required to extend some functionality of the FeatureManager. It is important to understand the Context object, how to use it and what can be done with it.
The 'Context' object:
{
execute: (fn, args) => fn.apply({}, args),
isTrue: (value) => true === value || 'true' === value,
isEnabled: (feature, features) => return features[feature],
canSet: (feature, enabled) => true,
canAddFeatures: () => return true,
canRemoveFeatures: () => return true
A Context object may be supplied to the FeatureManager at construction time. It is not required to provide all members; only the desired members.
const isBeta = () => Math.random() < .5;
const features = {
betaFeature: false
};
const context = {
isEnabled: (feature, features) => isBeta()
};
const featureManager = new FeatureManager(features, context);
console.log(featureManager.isEnabled('betaFeature'));
The FeatureManager API also has these functions, which dispatch to the Context.
const features = {
featureOne: true,
featureTwo: false
};
const featureManager = new FeatureManager(features);
console.log(featureManager.canAddFeatures());
console.log(featureManager.canRemoveFeatures());
console.log(featureManager.canSetFeature('someFeature', true));
console.log(featureManager.canEnable('someFeature'));
console.log(featureManager.canDisable('someFeature'));
console.log(featureManager.canToggle('someFeature'));
Example: Supporting Different API Version
Sometimes it might be helpful to be able to feature switch API client libraries. This can be done like so:
const features = {
apiV2: true
};
const featureManager = new FeatureManager(features);
const apiClient = featureManager.decide('apiV2', () => {return {version: 2}}, () => {return {version: 1}});
console.log('apiClient', apiClient);
Example: A/B Testing
Using a custom Context can be used to support A/B testing.
const features = {
abTestingFeatureX: true,
};
const context = {
isEnabled: (feature, features) => {
if ('abTestingFeatureX' === feature) {
return true;
}
return features[feature];
}
};
const featureManager = new FeatureManager(features, context);
const result = featureManager.decide('abTestingFeatureX', () => 'implemenationA', () => 'implemenationB');
console.log('result', result);
Example: Dark Testing a new Implementation
Testing a new implementation can introduce breaking changes. The FeatureManager can be used to allow the original implementation to continue to be used for all users, while also executing the new implementation for analysis, controlled by a feature switch.
const features = {
darkTestImplementationA: true
};
const attempt = (fn) => {
const attemptResult = {
result: undefined,
time: -1,
thrown: false
};
let startTime;
try {
startTime = new Date().getTime();
attemptResult.result = fn.apply({});
attemptResult.time = new Date().getTime() - startTime;
} catch (error) {
attemptResult.thrown = error;
attemptResult.time = new Date().getTime() - startTime;
}
return attemptResult;
};
const evaluateMetrics = (currentImplementation, nextImplementation) => {
const currentResult = attempt(currentImplementation);
const nextResult = attempt(nextImplementation);
console.log('current implementation results', currentResult);
console.log('next implementation results', nextResult);
console.log('the quicker implementation', (currentResult.time < nextResult.time) ? 'current' : 'next');
if (!!currentResult.thrown) {
throw currentResult.thrown;
}
return currentResult.result
};
const featureManager = new FeatureManager(features);
const result = featureManager.decide('darkTestImplementationA', () => evaluateMetrics(() => 'currentImplementation result', () => 'nextImplementation result'), () => 'currentImplementation result')
console.log('result', result);
Stripping Features
The file feature-switch-strip exports a function (strip), which reads a string and attempts to strip out features as described the an options object. Features may be described by HTML comments, slash-style comments or star-style comments. Within HTML files, features may also be described in markup.
It is important to note that the stripper is not a parser and therefore may act erratically when presented with complex configurations or situations. It is recommended to avoid embedded features all together, or to use the DOM manipulation functionality when managing complex DOM structures.
All comment types require two sets of comments, a start marker and an end marker. Having unbalanced blocks (i.e. missing end markers or end markers which are not in the correct order) may result in unexpected behavior. For this reason, it is recommended to be sure that both start and end marker blocks are formatted correctly and present in the correct place.
HTML comments are generally found within HTML or XML files and look like:
<div>content for feature-name</div>
Slash
Slash comments are generally found in JavaScript or LESS files:
console.log('feature-name is enabled');
Star
Star comments are common in JavaScript as well as CSS:
console.log('feature-name is enabled');
HTML Elements
In addition to comments in HTML files, some elements can also be configured to describe features. This functionality is experimental and may not work as desired. The DOM management functionality should produce more reliable results (as well as provide more options of specifying features).
Feature Name as Element
Using elements whose name is the feature name is supported.
<feature-name>feature-name content</feature-name>
Element with the "feature-name" Attribute
Supports the feature-name attribute on elements. This will not work well with DOM elements of the same type within the content of the feature. The DOM management functionality should produce more reliable results (as well as provide more options of specifying features).
<div feature-name="feature-name"></div>
Options
The strip function can take an optional second arguement, options. This is the structure of the options and represents the default options. Note that the replace attribute replaces disabled features and that ${FEATURE} will be replaced with the name of the feature being disabled.
{
starComments: {
enabled: true,
replace: '/* Feature [${FEATURE}] DISABLED */'
},
slashComments: {
enabled: true,
replace: '// Feature [${FEATURE}] DISABLED //'
},
htmlComments: {
enabled: true,
replace: '<!-- Feature [${FEATURE}] DISABLED -->'
},
htmlElements: {
enabled: true,
replace: '<!-- Feature [${FEATURE}] DISABLED -->'
},
htmlAttributes: {
enabled: true,
replace: '<!-- Feature [${FEATURE}] DISABLED -->'
}
};
DOM Manipulation
Live DOM manipulation can be used to alter the DOM to show or hide DOM elements which represent features.
HTML comments are generally found within HTML or XML files and look like:
<div>content for feature-name</div>
Feature Name as Element
Using elements whose name is the feature name is supported.
<feature-name>feature-name content</feature-name>
Element with the "feature-name" Attribute
Supports the feature-name attribute on elements.
<div feature-name="feature-name"></div>
Feature as Element with the "feature-name" Attribute
Using elements of type feature and whose name is specified by the feature-name attribute are supported.
<feature feature-name="feature-name">feature-name content</feature>
Using the FeatureSwitchDOM Object
The FeatureSwitchDOM is not aware of features per-se and instead operates solely on feature names. While simple cases may be serviced by the enable and disable functions, more complex cases should instead use syncToDom.
import { FeatureSwitchDOM } from './feature-switch-dom';
const fsDom = new FeatureSwitchDOM();
fsDOM.enable('feature-one');
fsDOM.disable('feature-one`);
// Synchronize all DOM elements to the specified features
fsDOM.syncToDom({'feature-one': true, 'feature-two': false});
It is more common to use the FeatureSwitchDOM class with a FeatureManager instance:
import { FeatureSwitchDOM } from './feature-switch-dom';
import { FeatureManager } from './feature-manager';
const features = {
featureone: true,
featuretwo: false,
featurethree: true,
featurefour: false
};
const fsDom = new FeatureSwitchDOM();
const featureManager = new FeatureManager(features);
featureManager.enable('featuretwo');
fsDom.syncToDom(featureManager.getFeatures());
Automatically managing the HTML can be accomplished fairly easily.
import { FeatureSwitchDOM } from './feature-switch-dom.js';
import { FeatureManager } from './feature-manager.js';
const features = {
featureone: true,
featuretwo: false,
featurethree: true,
featurefour: false
};
const featureManager = new FeatureManager(features);
const fsDom = new FeatureSwitchDOM(featureManager.getFeatures());
featureManager.addChangeListener((featureSnapshot, feature, enabled) => fsDom.syncToDom(featureManager.getFeatures(featureSnapshot)));
Custom Handlers
The FeatureSwitchDOM methods take optional handlers which can be used to change how a DOM element is rendered when enabled or disabled. The signature is:
const handler = (node, feature, enabled) => {};
NOTE: Because the FeatureSwichDOM is a parser and the stripper is not a parser, achieving the same functionality between the two is not possible (in particular, the stripper does not support the same functionality as the DOM management functionality). However, both the stripper and the DOM management functionality support HTML comments. For this reason, it is recommended to use strictly HTML comments if consistent behavior is desired across the stripper and the DOM management functionality.
Developing
To setup a development environment:
npm install
Testing
Tests are handled by jest. The following script will run tests continuously:
npm test
To see coverage:
npx jest --coverage
Playground
Using "live server" functionality with an IDE, serve up test/dom-files/dom-sample.html. If no live server is available:
npx http-server -o test/dom-files/dom-sample.html
Bugs
To report a defect or unexpected behavior, please visit the (GitHub issues page)[https://github.com/hal313/feature-switch-core/issues].