Skip to content

Latest commit

 

History

History
308 lines (260 loc) · 8.09 KB

README.md

File metadata and controls

308 lines (260 loc) · 8.09 KB

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: 'user@example.org'
})
.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 // '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')
})

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!