Skip to content

mix/node-rollout

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Node Rollout

CircleCI Maintainability Greenkeeper badge Feature rollout management for Node.js built on Redis

Example Usage

Installation

npm install node-rollout --save

Basic Configuration

// 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

// 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)
  })
}

Advanced Configuration

clientFactory

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'
     }]);
  }
})

Prefix option

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'
})

API Options

rollout.get(key, uid, opt_values)

  • 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: '[email protected]'
})
.then(function () {
  render('blue_button')
})
.catch(function () {
  render('red_button')
})

rollout.multi(keys)

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 to get
  • returns Promise
rollout.multi([
  ['onboarding', 123, {}],
  ['email_inviter', 123, {}],
  ['facebook_chat', 123, {
    employees: req.user.email // '[email protected]'
  }]
])
.then(function (results) {
  results.forEach(function (r) {
    console.log(i.isFulfilled()) // Or 'isRejected()'
  })
})

rollout.get('another_feature', 123, {
  employee: '[email protected]'
})
.then(function () {
  render('blue_button')
})
.catch(function () {
  render('red_button')
})

rollout.handler(key, modifiers)

  • key: String The rollout feature key
  • modifiers: Object
  • modName: String The name of the modifier. Typically id, employee, ip, or any other arbitrary item you would want to modify the rollout
    • percentage:
      • Number from 0 - 100. Can be set to a third decimal place such as 0.001 or 99.999. Or simply 0 to turn off a feature, or 100 to give a feature to all users
      • Object containing min and max keys representing a range of Numbers between 0 - 100
    • condition: Function a white-listing method by which you can add users into a group. See examples.
      • if condition returns a Promise (a thenable object), then it will use the fulfillment of the Promise to resolve or reject the handler
      • Conditions will only be accepted if they return/resolve with a "truthy" value
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()
        })
      })
    }
  }
})

rollout.update(key, modifierPercentages)

  • key: String The rollout feature key
  • modifierPercentages: Object mapping of modName:String to percentage
    • Number from 0 - 100. Can be set to a third decimal place such as 0.001 or 99.999. Or simply 0 to turn off a feature, or 100 to give a feature to all users
    • Object containing min and max keys representing a range of Numbers between 0 - 100
  • returns Promise
rollout.update('new_homepage', {
  id: 33.333,
  employee: 50,
  geo_sf: 25
})
.then(function () {
  // values have been updated
})

rollout.modifiers(handlerName)

  • handlerName: String the rollout feature key
  • returns Promise: resolves to a modifiers Object mapping modName: 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)
})

rollout.handlers()

  • 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')
})

Tests

see tests/index-test.js

make test

User Interface

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.

License MIT

Happy rollout!