Feature rollout management for Node.js built on Redis
npm install node-rollout --save
// basic_configuration.js
var client = require('redis').createClient()
var rollout = require('node-rollout')(client)
rollout.handler('new_homepage', {
// 1% of regular users
id: {
percentage: 1
},
// All users with the company email
employee: {
percentage: 100,
condition: function (val) {
return /@company-email\.com$/.test(val)
}
},
// 50% of users in San Francisco
geo_sf: {
percentage: 50,
condition: function (val) {
return geolib.getDistance([val.lat, val.lon], [37.768, -122.426], 'miles') < 7
}
},
// Asynchronous database lookup
admin: {
percentage: 100,
condition: function (val) {
return db.lookupUser(val)
.then(function (user) {
return user.isAdmin()
})
}
}
})
module.exports = rollout
// A typical Express app demonstrating rollout flags
...
var rollout = require('./basic_configuration')
app.get('/', new_homepage, old_homepage)
function new_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)
}
function old_home_page (req, res, next) {
res.render('home/index')
}
// experiment_groups_configuration.js
var client = require('redis').createClient()
var rollout = require('node-rollout')(client)
// An experiment with 3 randomly-assigned groups
rollout.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
...
var rollout = require('./experiment_groups_configuration')
app.get('/', homepage)
function homepage(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.js
var Redis = require('ioredis')
var rollout = require('node-rollout')({
clientFactory: function () {
return new Redis.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.
var client = require('redis').createClient()
var rollout = require('node-rollout')(client, {
prefix: 'my_rollouts'
})
key
:String
The rollout feature key. Eg "new_homepage"uid
:String
The identifier of which will determine likelyhood of falling in rollout. Typically a user id.opt_values
:Object
optional 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
:Array
A 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
:String
The rollout feature keymodifiers
:Object
modName
:String
The name of the modifier. Typicallyid
,employee
,ip
, or any other arbitrary item you would want to modify the rolloutpercentage
:Number
from0
-100
. Can be set to a third decimal place such as0.001
or99.999
. Or simply0
to turn off a feature, or100
to give a feature to all usersObject
containingmin
andmax
keys representing a range ofNumber
s between0
-100
condition
:Function
a white-listing method by which you can add users into a group. See examples.- if
condition
returns aPromise
(a thenable object), then it will use the fulfillment of thePromise
to 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 0
id: {
percentage: 0
},
// All users with the company email
employee: {
percentage: 100,
condition: function (val) {
return /@company-email\.com$/.test(val)
}
},
// special invited people
contractors: {
percentage: 100,
condition: function (user) {
return new Promise(function (resolve, reject) {
redisClient.get('contractors:' + user.id, function (err, is_awesome) {
is_awesome ? resolve() : reject()
})
})
}
}
})
key
:String
The rollout feature keymodifierPercentages
:Object
mapping ofmodName
:String
topercentage
Number
from0
-100
. Can be set to a third decimal place such as0.001
or99.999
. Or simply0
to turn off a feature, or100
to give a feature to all usersObject
containingmin
andmax
keys representing a range ofNumber
s between0
-100
- returns
Promise
rollout.update('new_homepage', {
id: 33.333,
employee: 50,
geo_sf: 25
})
.then(function () {
// values have been updated
})
handlerName
:String
the rollout feature key- returns
Promise
: resolves to a modifiersObject
mappingmodName
: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 test
Consider 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!