Feature rollout management for Node.js built on Redis
npm install node-rollout --save// basic_configuration.jsvarclient=require('redis').createClient()varrollout=require('node-rollout')(client)rollout.handler('new_homepage',{// 1% of regular usersid: {percentage: 1},// All users with the company emailemployee: {percentage: 100,condition: function(val){return/@company-email\.com$/.test(val)}},// 50% of users in San Franciscogeo_sf: {percentage: 50,condition: function(val){returngeolib.getDistance([val.lat,val.lon],[37.768,-122.426],'miles')<7}},// Asynchronous database lookupadmin: {percentage: 100,condition: function(val){returndb.lookupUser(val).then(function(user){returnuser.isAdmin()})}}})module.exports=rollout// A typical Express app demonstrating rollout flags
...
varrollout=require('./basic_configuration')app.get('/',new_homepage,old_homepage)functionnew_home_page(req,res,next){rollout.get('new_homepage',req.current_user.id,{employee: req.current_user.email,geo: [req.current_user.lat,req.current_user.lon],admin: req.current_user.id}).then(function(){res.render('home/new-index')}).catch(next)}functionold_home_page(req,res,next){res.render('home/index')}// experiment_groups_configuration.jsvarclient=require('redis').createClient()varrollout=require('node-rollout')(client)// An experiment with 3 randomly-assigned groupsrollout.handler('homepage_variant',{versionA: {percentage: {min: 0,max: 33}},versionB: {percentage: {min: 33,max: 66}},versionC: {percentage: {min: 66,max: 100}}})module.exports=rollout// A typical Express app demonstrating experiment groups
...
varrollout=require('./experiment_groups_configuration')app.get('/',homepage)functionhomepage(req,res,next){rollout.get('homepage_variant',req.current_user.id).then(function(version){console.assert(/^version(A|B|C)$/.test(version)===true)res.render('home/'+version)})}For clients that require a client factory or function that returns connections, the clientFactory can be given a
function that returns a client.
This can be useful when using ioredis with Cluster support.
Note: Functions like multi() may not work as expected with ioredis clusters.
// client_factory_configuration.jsvarRedis=require('ioredis')varrollout=require('node-rollout')({clientFactory: function(){returnnewRedis.Cluster([{port: 6380,host: '127.0.0.1'},{port: 6381,host: '127.0.0.1'}]);}})An optional prefix can be passed to the constructor that prepends all keys used by the rollout library.
varclient=require('redis').createClient()varrollout=require('node-rollout')(client,{prefix: 'my_rollouts'})key:StringThe rollout feature key. Eg "new_homepage"uid:StringThe identifier of which will determine likelyhood of falling in rollout. Typically a user id.opt_values:Objectoptional A lookup object with default percentages and conditions. Defaults to{id: args.uid}- returns
Promise
rollout.get('button_test',123).then(function(){render('blue_button')}).catch(function(){render('red_button')})rollout.get('another_feature',123,{employee: 'user@example.org'}).then(function(){render('blue_button')}).catch(function(){render('red_button')})The value of this method lets you do a batch redis call (using redis.multi()) allowing you to get multiple rollout handler results in one request
keys:ArrayA list of tuples containing what you would ordinarily pass toget- returns
Promise
rollout.multi([['onboarding',123,{}],['email_inviter',123,{}],['facebook_chat',123,{employees: req.user.email// 'joe@company.com'}]]).then(function(results){results.forEach(function(r){console.log(i.isFulfilled())// Or 'isRejected()'})})rollout.get('another_feature',123,{employee: 'user@example.org'}).then(function(){render('blue_button')}).catch(function(){render('red_button')})key:StringThe rollout feature keymodifiers:ObjectmodName:StringThe name of the modifier. Typicallyid,employee,ip, or any other arbitrary item you would want to modify the rolloutpercentage:Numberfrom0-100. Can be set to a third decimal place such as0.001or99.999. Or simply0to turn off a feature, or100to give a feature to all usersObjectcontainingminandmaxkeys representing a range ofNumbers between0-100
condition:Functiona white-listing method by which you can add users into a group. See examples.- if
conditionreturns aPromise(a thenable object), then it will use the fulfillment of thePromiseto resolve or reject thehandler - Conditions will only be accepted if they return/resolve with a "truthy" value
- if
rollout.handler('admin_section',{// 0% of regular users. You may omit `id` since it will default to 0id: {percentage: 0},// All users with the company emailemployee: {percentage: 100,condition: function(val){return/@company-email\.com$/.test(val)}},// special invited peoplecontractors: {percentage: 100,condition: function(user){returnnewPromise(function(resolve,reject){redisClient.get('contractors:'+user.id,function(err,is_awesome){is_awesome ? resolve() : reject()})})}}})key:StringThe rollout feature keymodifierPercentages:Objectmapping ofmodName:StringtopercentageNumberfrom0-100. Can be set to a third decimal place such as0.001or99.999. Or simply0to turn off a feature, or100to give a feature to all usersObjectcontainingminandmaxkeys representing a range ofNumbers between0-100
- returns
Promise
rollout.update('new_homepage',{id: 33.333,employee: 50,geo_sf: 25}).then(function(){// values have been updated})handlerName:Stringthe rollout feature key- returns
Promise: resolves to a modifiersObjectmappingmodName:percentage
rollout.modifiers('new_homepage').then(function(modifiers){console.assert(modifiers.employee==100)console.assert(modifiers.geo_sf==50.000)console.assert(modifiers.id==33.333)})- return
Promise: resolves with an array of configured rollout handler names
rollout.handlers().then(function(handlers){console.assert(handlers[0]==='new_homepage')console.assert(handlers[1]==='other_secret_feature')})make testConsider using rollout-ui to administrate the values of your rollouts in real-time (as opposed to doing a full deploy). It will make your life much easier and you'll be happy :)
Note:rollout-ui does not yet support experiment groups and percentage ranges.
Happy rollout!