Blog

Feature-flipping with node.js and MongoDB

July 25, 2012 9:08 am / by / 1 comment

Continuous Deployment means releasing newly-committed code to production very frequently after running an extensive suite of automated tests. However, there are inevitably differences between the test environment and production. Many organizations also want to have someone do at least a cursory examination of the new functionality before it goes truly ‘live. Feature flipping is one way to address these needs.

Feature flipping means adding code to your application to flip between the old version of a particular feature and the new feature based on a database parameter. This also enables developers to work on new features without having to create branches. There are many ways to determine if a particular user will see the new version or the old version of a feature – one of the most common and most coarse-grained is to show the new version of the feature to internal users (ie employees) while external users continue to see the old version. As your site grows in popularity, you may then wish to roll out the new feature to a random % of users, to a % of users by geography, by user type, etc. A good implementation of feature-flipping will allow you to build out these different methods of feature rollout as they become necessary.

We continuously deploy StriderApp.com – our hosted continuous deployment platform for node.js and Python – so in relatively short order we needed feature-flipping ourselves. Matheus Mendes from Yahoo has written feature-flipper.js which looks like a great option for node.js but didn’t quite meet our needs, so we decided just to write what we needed. (We are aiming to package up our code as an npm module at some point soon.)

Our goals for feature-flipping were to be able to quickly add in feature-flipping code for new features, to have useful defaults, and to abstract away the logic of whether the feature is on or off so that we can add more complex methods in the future. We decided that the most useful default for us would be for a feature to be ‘on’ for admin (ie internal) users. This is both our most common test scenario at this point and also meant that when we push code out to production, the feature won’t be live for our customers until we actively make a change in the production database.

Here’s an example of the library in action.

Controller

First, the javascript controller checks to see if the feature should be on and passes that parameter to the template:

var feature = require('feature');
 
feature("deactivate", req.user, function(err, is_enabled) {
  res.render('project_config', {repo: trepo, deactivate_enabled: deactivate_enabled});
});

Jade template

Then in the jade template, we just have an ‘if’ statement around the HTML in question:

if (deactivate_enabled)
  #deactivate_project.span8
    strong Deactivate / Activate Project
    p Temporarily turn off continuous integration and deployment. Tests and deployments will not be triggered by commits when a project is deactivated.
    a.#deactivate.btn.btn-danger Deactivate Project

Feature Module

In the feature module, we first check to see if the user is admin, in which case we don’t even need to look at the feature record:

// current logic - if user is admin, feature is ALWAYS on, whether it is in the db or not
if (self.user_obj.account_level != undefined && self.user_obj.account_level > 0) {
  console.debug("Feature.is_enabled() - %s is admin. Feature is enabled.",
      self.feature_name,self.user_obj.email);
  return callback(null, true);
}

If the user is NOT admin, we then retrieve the feature record from the database:

Step(
  function findFeature() {
    FeatureModel.findOne({name: self.feature_name}, this);
  },
  function foundFeature(err, feature_obj) {
 
    if (err) throw err;

If ‘global_enabled’ is true, then the feature is enabled:

if (feature_obj != undefined && feature_obj.global_enabled) {
  console.debug("Feature.is_enabled() - global_enabled flag enabled for feature '%s'.", self.feature_name, self.user_obj.email);
  return callback(null, true);
}

If the user is in the list of users for this feature, then the feature is enabled:

if (feature_obj != undefined && feature_obj.users_enabled !== undefined
  && typeof(feature_obj.users_enabled) === 'object'
  && _.indexOf(feature_obj.users_enabled, self.user_obj._id) !== -1) {
    console.debug("Feature.is_enabled() - user %s is in users_enabled list for feature '%s'.",
      self.user_obj.email, self.feature_name);
  return callback(null, true);
}

If none of the above were true, then the feature is disabled:

console.debug("Feature.is_enabled() - user is not admin, feature is not global enabled, user is not in user list. Default to disabled.");
return callback(null, false);

And that’s it.


Next Steps:

In an ideal world, we would not need to add conditionals to both the controller and to the template. Also, we use client-side javascript (using the Backbone framework as well as the Apres framework) for much of our front-end and would like to have an easy way to write feature-flipping code on the client that does not require an additional request nor modifying the node.js controller code. One way to do this would be to write middleware which loads all of the feature-flip flags for the currently logged-in user and sends this to the jade template which ensures that it is always available as a client-side global variable. This isn’t a particularly scalable solution however so for now we are sticking with the current process.

Have you come up with a way to implement feature-flipping that minimizes changes to both server and client-side code? Share in the comments.


Stephen Bronstein is a co-founder of BeyondFog, the creators of StriderCD.com, a hosted continuous integration and deployment platform for Python and node.js.

 
  • http://twitter.com/nomiddlename Gareth Jones

    There’s already a couple of feature flipping libraries for node.js: https://github.com/nomiddlename/flipper and https://github.com/bigodines/feature-flipper-js

    It might be worth taking a look at them. (Disclaimer: I wrote the first of those :) )