Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

fflip

fredkschott50.1k4.0.0

Advanced Feature Flipping/Toggling Across the Server and Client

feature, flip, toggle, flag, feature flipping, feature toggling, feature flag, feature flagging, continuous integration

readme

# fflip

Working on an experimental new design? Starting a closed beta? Rolling out a new feature over the next few weeks? Fa-fa-fa-flip it! fflip gives you complete control over releasing new functionality to your users based on their user id, join date, membership status, and whatever else you can think of. fflip's goal is to be the most powerful and extensible feature flipping/toggling module out there.

  • Create custom criteria to segment users & features based on your audience.
  • View & edit feature access in one easy place, and not scattered around your code base.
  • System-Agnostic: Support any database, user representation or web framework you can throw at it.
  • Extensible: Supports 3rd-party plugins for your favorite libraries (like our Express integration!)
npm install fflip --save

Getting Started

Below is a simple example that uses fflip to deliver a closed beta to a fraction of users:

// Include fflip
var fflip = require('fflip');

fflip.config({
  criteria: ExampleCriteria, // defined below
  features: ExampleFeatures  // defined below
});

// Get all of a user's enabled features...
someFreeUser.features = fflip.getFeaturesForUser(someFreeUser);
if(someFreeUser.features.closedBeta === true) {
  console.log('Welcome to the Closed Beta!');
}

// ... or just check this single feature.
if (fflip.isFeatureEnabledForUser('closedBeta', someFreeUser) === true) {
  console.log('Welcome to the Closed Beta!');
}

Criteria

Criteria are the rules that define access to different features. Each criteria takes a user object and some data as arguments, and returns true/false if the user matches that criteria. You will use these criteria to restrict/allow features for different subsets of your userbase.

var ExampleCriteria = [
  {
    id: 'isPaidUser', // required
    check: function(user, isPaid) { // required
      return user.isPaid == isPaid;
    }
  },
  {
    id: 'percentageOfUsers',
    check: function(user, percent) {
      return (user.id % 100 < percent * 100);
    }
  },
  {
    id: 'allowUserIDs',
    check: function(user, allowedIDs) {
      return allowedIDs.indexOf(user.id) > -1;
    }
  }
];

Features

Features represent some special behaviors in your application. They also define a set of criteria to test users against for each feature. When you ask fflip if a feature is enabled for some user, it will check that user against each rule/criteria, and return "true" if the user passes.

Features are described in the following way:

var ExampleFeatures = [
  {
    id: 'closedBeta', // required
    // if `criteria` is in an object, ALL criteria in that set must evaluate to true to enable for user
    criteria: {isPaidUser: true, percentageOfUsers: 0.50}
  },
  {
    id: 'newFeatureRollout',
    // if `criteria` is in an array, ANY ONE set of criteria must evaluate to true to enable for user
    criteria: [{isPaidUser: true}, {percentageOfUsers: 0.50}]
  },
  {
    id: 'experimentalFeature',
    name: 'An Experimental Feature', // user-defined properties are optional but can be used to add important metadata
    description: 'Experimental feature still in development, useful for internal development', // user-defined
    owner: 'Fred K. Schott <fkschott@gmail.com>', // user-defined
    enabled: false, // sets the feature on or off for all users, required unless `criteria` is present instead
  },
]

The value present for each rule is passed in as the data argument to it's criteria function. This allows you to write more general, flexible, reusable rules.

Rule sets & lists can be nested and combined. It can help to think of criteria sets as a group of AND operators, and lists as a set of OR operators.

Veto Criteria

If you'd like to allow wider access to your feature while still preventing a specific group of users, you can use the $veto property. If the $veto property is present on a member of a criteria list (array), and that member evaluates to false, the entire list will evaluate to false regardless of it's other members.

{
  // Enabled if user is paid OR in the lucky 50% group of other users currently using a modern browser
  criteria: [{isPaidUser: true}, {percentageOfUsers: 0.50, usingModernBrowser: true}]
  // Enabled if user is paid OR in the lucky 50% group of other users, BUT ONLY if using a modern browser
  criteria: [{isPaidUser: true}, {percentageOfUsers: 0.50}, {usingModernBrowser: true, $veto: true}]
}

Usage

  • .config(options) -> void: Configure fflip (see below)
  • .isFeatureEnabledForUser(featureName, user) -> boolean: Return true/false if featureName is enabled for user
  • .getFeaturesForUser(user) -> Object: Return object of true/false for all features for user
  • .reload() -> void: Force a reload (if loading features dynamically)

Configuration

Configure fflip using any of the following options:

fflip.config({
  criteria: {}, // Criteria Array
  features: {}, // Features Array | Function (see below)
  reload: 30,   // Interval for refreshing features, in seconds
});

Loading Features Dynamically

fflip also accepts functions for loading features. If fflip is passed a function with no arguments it will call the function and accept the return value. To load asynchronously, pass a function that sends a features object to a callback. fflip will receive the callback and set the data accordingly. In both cases, fflip will save the function and call it again every X seconds, as set by the reload parameter.

// Load Criteria Synchronously
var getFeaturesSync = function() {
  var collection = db.collection('features');
  var featuresArr = collection.find().toArray();
  /* Process/Format `featuresArr` if needed (format described above) */
  return featuresArr;
}

// Load Features Asynchronously
var getFeaturesAsync = function(callback) {
  var collection = db.collection('features');
  collection.find().toArray(function(err, featuresArr) {
    /* Handle err
     * Process/Format `featuresArr` if needed (format described above) */
    callback(featuresArr);
  });
}

fflip.config({
  criteria: ExampleCriteriaObject,
  features: getFeaturesAsync, // or: getFeaturesSync
  reload: 60 // update available features every 60 seconds
});

Integrations

As mentioned, fflip's goal is to be flexible enough to integrate with any web framework, database, or ORM. The following integrations are known to exist:

If you're interested in creating an integration, don't hesitate to reach out or create an issue if some functionality is missing. And if you've created an integration, please add it to the list above!

Special Thanks

Original logo designed by Luboš Volkov

changelog

Changelog

v4.0

v4.0 brings some new changes to make fflip even more flexible.

New! Plugin Support

FFlip has always promised to support every database & web framework. But up to this point, integrating fflip with your web framework of choice hasn't been easy. Support for Express did come bundled, but it also took advantage of some private logic to work properly.

Starting with v4.0, fflip will support a more open plugin architecture for integrations. The library interface will be more stable, and all previously private properties have been made public. We'll keep integrations open-ended for now, so how you build yours will be up to you. You can check out the new fflip-express integration for some inspiration and to see what's possible. We'll also try to keep an up-to-date list of available plugins in this repo's README.

Express Support Gets Pulled Out

Given our new dedication to plugins, express support has been pulled out of the main library and into the new fflip-express package. As such, the following methods & properties are now longer available directly on fflip:

  • fflip.expressMiddleware() (Will throw error when used)
  • fflip.expressRoute() (Will throw error when used)
  • fflip.express() (Will throw error when used)
  • fflip.maxCookieAge is no longer available / will do nothing if set directly

Check out the fflip-express README for instructions on how to set up the new plugin. At the time of writing most logic has remained unchanged, so moving to the new library should only require a couple small code changes.

Updated Interface

The following methods have new function signatures:

  • fflip.userHasFeature(user, featureName) -> fflip.isFeatureEnabledForUser(featureName, user) (Note the new argument order)
  • fflip.userFeatures(user) -> fflip.getFeaturesForUser(user)

The following private properties have been made public:

  • fflip._features -> fflip.features
  • fflip._criteria -> fflip.criteria

Old Criteria Format No Longer Supported

Now that the criteria property is publicly exposed and maintained, we can no longer support two different data formats. Instead of adding new logic to translate the old format to the new, we've decided to remove support for the v2.x format.

v3.0

v3.0 brings some major changes to make fflip feature & criteria logic even more powerful. To help ease the transition, v3.x will strive to be backwards compatible with the v2.x interface and expected behavior. That means no breaking changes are expected in this release, even though we're using the 3.0 version number.

Updated Interface

The following methods have new signatures:

  • fflip.express_middleware() -> fflip.expressMiddleware()
  • fflip.express_route() -> fflip.expressRoute()

New Criteria & Features Format

  • All criteria & features should now be provided as arrays instead of objects.

View the README for a more in depth explanation of this new format.

New, More Powerful Feature Logic

  • Feature criteria now supports matching multiple groups of users: If a list of criteria exists for a feature, any one criteria may be met to evaluate to true for a given user.
  • Feature criteria now support "vetoes": If that vetoing criteria evaluates to false, it's entire parent array will also evaluate to false regardless of other criteria met.

View the README for a more in depth explanation of this new behavior.