Git Product home page Git Product logo

fflip's Introduction

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

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!

Getting Started

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

// Include fflip
let 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.

let ExampleCriteria = [
  {
    id: 'isPaidUser',
    check: function(user, isPaid) {
      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:

let 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 on both criteria & features
    description: 'Experimental feature still in development, useful for internal development', // user-defined
    owner: 'Fred K. Schott <[email protected]>', // user-defined
    enabled: false, // sets the feature on or off for all users, required if `criteria` is not present
  },
]

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
let getFeaturesSync = function() {
  let collection = db.collection('features');
  let featuresArr = collection.find().toArray();
  /* Process/Format `featuresArr` if needed (format described above) */
  return featuresArr;
}

// Load Features Asynchronously
let getFeaturesAsync = function(callback) {
  let 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
});

Special Thanks

Original logo designed by Luboš Volkov

fflip's People

Contributors

andreas avatar falseperfection avatar fredkschott avatar judikdavid avatar phillipj avatar suprememoocow avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fflip's Issues

Interface Cleanup (v3.0)

This project has gone through a few different changes over the last two years, and looking back now I can see a few glaring issues with the interface. I'd like to add some new features to fflip over the next few weeks, but first I'd like to do a bit of cleanup:

  1. Rename express_route() & express_middleware() to their lowerCamelCase equivalents. I was trying to be clever here by including an underscore to denote private within the express realm, but looking back now I have no idea why I thought that would make sense to anyone else.
  2. Remove express() in favor of using expressRoute() & expressMiddleware() directly. In the future I'd like us to pull these out of the core library and build the library so that it can be extended for any framework.
  3. Rename userFeatures() & userHasFeature() to getFeaturesForUser() & isFeatureEnabledForUser(). This will make room for getFeatures() & isFeatureEnabled() without the user object and new feature/criteria descriptions such as $or, veto, default.

All old method signatures should be deprecated for a major version before being completely removed in the next.

Thoughts?

Complex Feature Criteria (v3.0)

A few months ago in #8 @suprememoocow shared how Gitter had extended fflip with a more expressive criteria system that supported "veto" voting (check out the PR for more info). I'm going to build off of that and outline a new syntax for defining feature criteria. My goal is to support "veto" voting (where a criteria not being met should force a disable) as well as some other requested features.

Current Feature Format

var ExampleFeaturesObject = {
  newFeatureRollout: {
    name: "A New Feature Rollout",  // user-defined, optional, for the user's documentation purposes only
    description: "Rollout of that new feature over the next month", // user-defined
    owner: "FredKSchott",  // user-defined
    criteria: {isPaidUser: false, percentageOfUsers: 0.50} // Match ALL criteria
  },
  // ...
}

New Feature Criteria Format

// Match ONE criteria
criteria: {isPaidUser: true} 
// Match ALL criteria
criteria: {isPaidUser: true, percentageOfUsers: 0.50} 
// Match ANY criteria
criteria: [{isPaidUser: true}, {isModernBrowsers: true}, {percentageOfUsers: 0.50}] 
// Match ANY criteria set (first criteria OR ALL criteria in the second criteria set)
criteria: [{isPaidUser: true}, {isModernBrowsers: true, percentageOfUsers: 0.50}] 
// Match ANY criteria, unless `isModernBrowsers` = false
criteria: [{isPaidUser: true}, {percentageOfUsers: 0.50}, {isModernBrowsers: true, $veto: true}] 

Explained

  • A Criteria Set is an object that describes a set of different criteria that must be met for a feature to be enabled. If ALL of those criteria are met, we say that the criteria set evaluates to true. Otherwise, the set evaluates to false. {isPaidUser: true, percentageOfUsers: 0.50}
  • A Criteria Collection is an array of criteria sets and criteria collections. If ANY of its members evaluate to true, the entire collection evaluates to true. Otherwise, the collection evaluates to false. [{isPaidUser: true}, {percentageOfUsers: 0.50}]
  • The $veto property exist on a member of a criteria collection. If that collection set evaluates to false, it will override the entire criteria collection to evaluate to false no matter what ANY of its other members evaluate to.
  • A feature's criteria property can be either a criteria set or a criteria collection.

Reasoning

This system has a ton of benefits over the current system, and a few over the veto system outlined in #8:

  • Adds new OR logic that can match any criteria, instead of all criteria
  • OR and AND logic can be combined to create really expressive criteria rules
  • Features get to decide when a criteria is or isn't veto-worthy, so that they can be used in either scenario

Logically, the veto property just changes its criteria set to AND with everything in it's collection (instead of the default OR). So the name is a bit misleading, but it does a good job of communicating it in a relatable way.

The other direction we could go would be to create a more expressive syntax that could actually support the mixing and matching of different logical operators "NOT", "OR NOT", "XOR", etc. But I'm not sure that use case exists or if the extra complexity on our end is worth it, at least in this first step.


Any and all feedback appreciated!

Middleware: why JSON.stringify the features?

Hi Fred,

just started playing with this project, seems very promising!

As the title says; why did you decide to stringify the features object put on the request? I would like to check which features are enabled in some handlebars templates, it would have been alot for my use case that Features still was an object (not a string).

If the reason behind the stringification are clientside needs, as in your example: <script>var Features = <%= Features %></script>
IMHO the responsibility of stringification should be done nearer the clientside scripts: <script>var Features = JSON.stringify(<%= Features %>)</script>

Any thoughts?

config method can be made asynchronous

Initial loading of features in an asynchronous way can be improvised.

Let us assume someAysnchronousFunction is returning features loaded from redis

fflip.config({ criteria: {}, 
                     features: someAysnchronousFunction, 
                     reload: anyInterval });
fflip.isEnabledForUser(feature, user);  // This is returning null as the features are not yet loaded

can this be like

fflip.config({ criteria: {}, features: someAysnchronousFunction, reload: anyInterval }, function(){
  fflip.isEnabledForUser(feature, user); 
})

merge error handling, PR #23

Any blockers to merge PR #23 ?
Currently we use a workaround with a closure and fallback features, and are looking forward to native error handling.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.