node-rollout
Advanced tools
Comparing version
206
index.js
var crypto = require('crypto') | ||
, util = require('util') | ||
, Promise = require('bluebird') | ||
, EventEmitter = require('events').EventEmitter | ||
var Promise = require('bluebird') | ||
function defaultCondition() { | ||
return true | ||
} | ||
module.exports = function (client) { | ||
@@ -15,3 +9,2 @@ return new Rollout(client) | ||
function Rollout(client) { | ||
EventEmitter.call(this) | ||
this.client = client | ||
@@ -21,29 +14,27 @@ this._handlers = {} | ||
util.inherits(Rollout, EventEmitter) | ||
Rollout.prototype.handler = function (key, flags) { | ||
Rollout.prototype.handler = function (key, modifiers) { | ||
var self = this | ||
this._handlers[key] = flags | ||
var orig_percentages = [] | ||
var keys = Object.keys(flags).map(function (k) { | ||
orig_percentages.push(flags[k].percentage) | ||
return key + ':' + k | ||
this._handlers[key] = modifiers | ||
var configPercentages = [] | ||
var configKeys = Object.keys(modifiers).map(function (modName) { | ||
configPercentages.push(modifiers[modName].percentage) | ||
return key + ':' + modName | ||
}) | ||
this.client.mget(keys, function (err, percentages) { | ||
var _keys = [] | ||
var nullKey = false | ||
percentages.forEach(function (p, i) { | ||
return getRedisKeys(this.client, configKeys) | ||
.then(function(persistentPercentages) { | ||
var persistKeys = [] | ||
persistentPercentages.forEach(function (p, i) { | ||
if (p === null) { | ||
var val = Math.max(0, Math.min(100, orig_percentages[i] || 0)) | ||
nullKey = true | ||
_keys.push(keys[i], val) | ||
p = normalizePercentageRange(configPercentages[i]) | ||
persistKeys.push(configKeys[i], JSON.stringify(p)) | ||
persistentPercentages[i] = p | ||
} | ||
}) | ||
if (nullKey) { | ||
self.client.mset(_keys, function () { | ||
self.emit('ready') | ||
if (persistKeys.length) { | ||
return setRedisKeys(self.client, persistKeys) | ||
.then(function() { | ||
return persistentPercentages | ||
}) | ||
} else { | ||
self.emit('ready') | ||
} | ||
return persistentPercentages | ||
}) | ||
@@ -54,43 +45,38 @@ } | ||
var multi = this.client.multi() | ||
// Accumulate get calls into a single "multi" query | ||
var promises = keys.map(function (k) { | ||
return this.get(k[0], k[1], k[2], multi).reflect() | ||
}.bind(this)) | ||
// Perform the batch query | ||
return new Promise(function (resolve, reject) { | ||
multi.exec(function (err, result) { | ||
if (err) return reject(err) | ||
resolve(result) | ||
}) | ||
multi.exec(promiseCallback(resolve, reject)) | ||
}) | ||
.then(Promise.all.bind(Promise, promises)) | ||
.then(function () { | ||
return Promise.all(promises) | ||
}) | ||
} | ||
Rollout.prototype.get = function (key, id, opt_values, multi) { | ||
var flags = this._handlers[key] | ||
opt_values = opt_values || { id: id } | ||
opt_values.id = opt_values.id || id | ||
var modifiers = this._handlers[key] | ||
var likely = this.val_to_percent(key + id) | ||
var _id = { | ||
id: id | ||
} | ||
if (!opt_values) opt_values = _id | ||
if (!opt_values.id) opt_values.id = id | ||
var keys = Object.keys(flags).map(function (k) { | ||
return key + ':' + k | ||
var keys = Object.keys(modifiers).map(function (modName) { | ||
return key + ':' + modName | ||
}) | ||
var client = multi || this.client | ||
return new Promise(function (resolve, reject) { | ||
client.mget(keys, function (err, result) { | ||
if (err) return reject(err) | ||
resolve(result) | ||
}) | ||
}) | ||
return getRedisKeys(multi || this.client, keys) | ||
.then(function (percentages) { | ||
var i = 0 | ||
var deferreds = [] | ||
for (var modifier in flags) { | ||
var output | ||
for (var modName in modifiers) { | ||
// in the circumstance that the key is not found, default to original value | ||
if (percentages[i] === null) { | ||
percentages[i] = flags[modifier].percentage | ||
percentages[i] = normalizePercentageRange(modifiers[modName].percentage) | ||
} | ||
if (likely < percentages[i]) { | ||
if (!flags[modifier].condition) flags[modifier].condition = defaultCondition | ||
var output = flags[modifier].condition(opt_values[modifier]) | ||
if (isPercentageInRange(likely, percentages[i])) { | ||
if (!modifiers[modName].condition) { | ||
modifiers[modName].condition = defaultCondition | ||
} | ||
output = modifiers[modName].condition(opt_values[modName]) | ||
if (output) { | ||
@@ -100,5 +86,7 @@ if (typeof output.then === 'function') { | ||
// Reflect the Promise to coalesce rejections | ||
deferreds.push(Promise.resolve(output).reflect()) | ||
output = Promise.resolve(output).reflect() | ||
output.handlerModifier = modName | ||
deferreds.push(output) | ||
} else { | ||
return true | ||
return modName | ||
} | ||
@@ -118,6 +106,5 @@ } | ||
resultValue = resultPromise.value() | ||
// Treat resolved conditions with non-false values as affirmative | ||
// (This is to handle `Promise.resolve()` and `Promise.resolve(null)`) | ||
if (resultValue !== false) { | ||
return true | ||
// Treat resolved conditions with truthy values as affirmative | ||
if (resultValue) { | ||
return resultPromise.handlerModifier | ||
} | ||
@@ -130,42 +117,33 @@ } | ||
throw new Error('Not inclusive of any partition for key[' + key + '] id[' + id + ']') | ||
}.bind(this)) | ||
}) | ||
} | ||
Rollout.prototype.update = function (key, percentage_map) { | ||
var keys = [] | ||
for (var k in percentage_map) { | ||
keys.push(key + ':' + k, percentage_map[k]) | ||
Rollout.prototype.update = function (key, modifierPercentages) { | ||
var persistKeys = [], modName, p | ||
for (modName in modifierPercentages) { | ||
p = normalizePercentageRange(modifierPercentages[modName]) | ||
persistKeys.push(key + ':' + modName, JSON.stringify(p)) | ||
} | ||
return new Promise(function (resolve, reject) { | ||
this.client.mset(keys, function (err, result) { | ||
if (err) return reject(err) | ||
resolve(result) | ||
}) | ||
}.bind(this)) | ||
return setRedisKeys(this.client, persistKeys) | ||
} | ||
Rollout.prototype.mods = function (name) { | ||
Rollout.prototype.modifiers = function (handlerName) { | ||
var keys = [] | ||
var names = [] | ||
for (var flag in this._handlers[name]) { | ||
keys.push(name + ':' + flag) | ||
names.push(flag) | ||
var modNames = [] | ||
for (var modName in this._handlers[handlerName]) { | ||
keys.push(handlerName + ':' + modName) | ||
modNames.push(modName) | ||
} | ||
return new Promise(function (resolve, reject) { | ||
this.client.mget(keys, function (err, result) { | ||
if (err) return reject(err) | ||
resolve(result) | ||
return getRedisKeys(this.client, keys) | ||
.then(function (values) { | ||
var modifiers = {} | ||
values.forEach(function (val, i) { | ||
modifiers[modNames[i]] = JSON.parse(val) | ||
}) | ||
}.bind(this)) | ||
.then(function (values) { | ||
var flags = {} | ||
values.forEach(function (val, i) { | ||
flags[names[i]] = val | ||
}) | ||
return flags | ||
}) | ||
return modifiers | ||
}) | ||
} | ||
Rollout.prototype.flags = function () { | ||
return Object.keys(this._handlers) | ||
Rollout.prototype.handlers = function () { | ||
return Promise.resolve(Object.keys(this._handlers)) | ||
} | ||
@@ -178,1 +156,51 @@ | ||
} | ||
function defaultCondition() { | ||
return true | ||
} | ||
function clampPercentage(val) { | ||
return Math.max(0, Math.min(100, +(val || 0))) | ||
} | ||
function normalizePercentageRange(val) { | ||
if (val && typeof val === 'object') { | ||
return { | ||
min: clampPercentage(val.min), | ||
max: clampPercentage(val.max) | ||
} | ||
} | ||
return clampPercentage(val) | ||
} | ||
function isPercentageInRange(val, range) { | ||
// Redis stringifies everything, so ranges must be reified | ||
if (typeof range === 'string') { | ||
range = JSON.parse(range) | ||
} | ||
if (range && typeof range === 'object') { | ||
return val > range.min && val <= range.max | ||
} | ||
return val < range | ||
} | ||
function getRedisKeys(client, keys) { | ||
return new Promise(function (resolve, reject) { | ||
client.mget(keys, promiseCallback(resolve, reject)) | ||
}) | ||
} | ||
function setRedisKeys(client, keys) { | ||
return new Promise(function (resolve, reject) { | ||
client.mset(keys, promiseCallback(resolve, reject)) | ||
}) | ||
} | ||
function promiseCallback(resolve, reject) { | ||
return function (err, result) { | ||
if (err) { | ||
return reject(err) | ||
} | ||
resolve(result) | ||
} | ||
} |
{ | ||
"name": "node-rollout", | ||
"version": "0.4.4", | ||
"version": "1.0.0", | ||
"description": "feature rollout management", | ||
@@ -5,0 +5,0 @@ "author": "Ali Faiz & Dustin Diaz", |
168
README.md
@@ -6,2 +6,6 @@ ## Node Rollout | ||
### Example Usage | ||
#### Installation | ||
``` sh | ||
@@ -11,4 +15,6 @@ npm install node-rollout --save | ||
#### Basic Configuration | ||
``` js | ||
// configuration.js | ||
// basic_configuration.js | ||
var client = require('redis').createClient() | ||
@@ -25,3 +31,3 @@ var rollout = require('node-rollout')(client) | ||
condition: function (val) { | ||
return val.match(/@company-email\.com$/) | ||
return /@company-email\.com$/.test(val) | ||
} | ||
@@ -35,2 +41,12 @@ }, | ||
} | ||
}, | ||
// Asynchronous database lookup | ||
admin: { | ||
percentage: 100, | ||
condition: function (val) { | ||
return db.lookupUser(val) | ||
.then(function (user) { | ||
return user.isAdmin() | ||
}) | ||
} | ||
} | ||
@@ -43,5 +59,5 @@ }) | ||
``` js | ||
// A typical Express app | ||
// A typical Express app demonstrating rollout flags | ||
... | ||
var rollout = require('./configuration') | ||
var rollout = require('./basic_configuration') | ||
@@ -53,8 +69,9 @@ app.get('/', new_homepage, old_homepage) | ||
employee: req.current_user.email, | ||
geo: [req.current_user.lat, req.current_user.lon] | ||
geo: [req.current_user.lat, req.current_user.lon], | ||
admin: req.current_user.id | ||
}) | ||
.then(function () { | ||
res.render('home/new-index') | ||
}) | ||
.otherwise(next) | ||
.then(function () { | ||
res.render('home/new-index') | ||
}) | ||
.catch(next) | ||
} | ||
@@ -68,3 +85,41 @@ | ||
#### Experiment groups | ||
``` js | ||
// 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 | ||
``` | ||
``` js | ||
// 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) | ||
}) | ||
} | ||
``` | ||
### API Options | ||
@@ -81,8 +136,8 @@ | ||
rollout.get('button_test', 123) | ||
.then(function () { | ||
render('blue_button') | ||
}) | ||
.otherwise(function () { | ||
render('red_button') | ||
}) | ||
.then(function () { | ||
render('blue_button') | ||
}) | ||
.catch(function () { | ||
render('red_button') | ||
}) | ||
@@ -92,15 +147,15 @@ rollout.get('another_feature', 123, { | ||
}) | ||
.then(function () { | ||
render('blue_button') | ||
}) | ||
.otherwise(function () { | ||
render('red_button') | ||
}) | ||
.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 flags in one request | ||
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 `SettlePromise` | ||
- returns `Promise` | ||
@@ -115,7 +170,7 @@ ``` js | ||
]) | ||
.then(function (results) { | ||
results.forEach(function (r) { | ||
console.log(i.isFulfilled()) // Or 'isRejected()' | ||
}) | ||
.then(function (results) { | ||
results.forEach(function (r) { | ||
console.log(i.isFulfilled()) // Or 'isRejected()' | ||
}) | ||
}) | ||
@@ -125,17 +180,20 @@ rollout.get('another_feature', 123, { | ||
}) | ||
.then(function () { | ||
render('blue_button') | ||
}) | ||
.otherwise(function () { | ||
render('red_button') | ||
}) | ||
.then(function () { | ||
render('blue_button') | ||
}) | ||
.catch(function () { | ||
render('red_button') | ||
}) | ||
``` | ||
#### `rollout.handler(key, flags)` | ||
#### `rollout.handler(key, modifiers)` | ||
- `key`: `String` The rollout feature key | ||
- `flags`: `Object` | ||
- `flagname`: `String` The name of the flag. Typically `id`, `employee`, `ip`, or any other arbitrary item you would want to modify the rollout | ||
- `percentage`: `NumberRange` 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 | ||
- `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 `Number`s 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 | ||
@@ -152,3 +210,3 @@ ``` js | ||
condition: function (val) { | ||
return val.match(/@company-email\.com$/) | ||
return /@company-email\.com$/.test(val) | ||
} | ||
@@ -170,5 +228,7 @@ }, | ||
#### `rollout.update(key, flags)` | ||
#### `rollout.update(key, modifierPercentages)` | ||
- `key`: `String` The rollout feature key | ||
- `flags`: `Object` mapping of `flagname`:`String` to `percentage`:`Number` | ||
- `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 `Number`s between `0` - `100` | ||
- returns `Promise` | ||
@@ -187,18 +247,24 @@ | ||
#### `rollout.mods(flagname)` | ||
- `flagname`: `String` the rollout feature key | ||
- returns `Promise`: resolves with the flags, their names, and values | ||
#### `rollout.modifiers(handlerName)` | ||
- `handlerName`: `String` the rollout feature key | ||
- returns `Promise`: resolves to a modifiers `Object` mapping `modName`: `percentage` | ||
``` js | ||
rollout.mods('new_homepage').then(function (mods) { | ||
flags.employee == 100 | ||
flags.geo_sf == 50.000 | ||
flags.id == 33.333 | ||
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.flags()` | ||
#### `rollout.handlers()` | ||
- return `Promise`: resolves with an array of configured rollout handler names | ||
``` js | ||
rollout.flags() == ['new_homepage', 'other_secret_feature'] | ||
rollout.handlers() | ||
.then(function (handlers) { | ||
console.assert(handlers[0] === 'new_homepage') | ||
console.assert(handlers[1] === 'other_secret_feature') | ||
}) | ||
``` | ||
@@ -214,6 +280,8 @@ | ||
### User Interface | ||
Consider using [rollout-ui](https://github.com/ded/rollout-ui) to administrate the values of your rollout flags in real-time (as opposed to doing a full deploy). It will make your life much easier and you'll be happy :) | ||
Consider using [rollout-ui](https://github.com/ded/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! |
@@ -28,3 +28,3 @@ var chai = require('chai') | ||
it('fulfills', function () { | ||
subject.handler('secret_feature', { | ||
return subject.handler('secret_feature', { | ||
employee: { | ||
@@ -35,9 +35,75 @@ percentage: 100, | ||
}) | ||
return expect(subject.get('secret_feature', 123, { | ||
employee: 'me@company.com' | ||
})).to.be.fulfilled | ||
.then(function () { | ||
var result = subject.get('secret_feature', 123, { | ||
employee: 'me@company.com' | ||
}) | ||
return expect(result).to.be.fulfilled | ||
}) | ||
}) | ||
it('fulfills with applicable modifier for percentage', function () { | ||
return subject.handler('secret_feature', { | ||
everyone: { | ||
percentage: 0 | ||
}, | ||
employee: { | ||
percentage: 100, | ||
condition: isCompanyEmail | ||
} | ||
}) | ||
.then(function () { | ||
var result = subject.get('secret_feature', 123, { | ||
employee: 'me@company.com' | ||
}) | ||
return expect(result).to.eventually.equal('employee') | ||
}) | ||
}) | ||
context('percentage range', function () { | ||
beforeEach(function () { | ||
sinon.stub(subject, 'val_to_percent') | ||
return subject.handler('secret_feature', { | ||
groupA: { | ||
percentage: { min: 0, max: 25 } | ||
}, | ||
groupB: { | ||
percentage: { min: 25, max: 50 } | ||
}, | ||
groupC: { | ||
percentage: { min: 50, max: 100 } | ||
} | ||
}) | ||
}) | ||
afterEach(function () { | ||
subject.val_to_percent.restore() | ||
}) | ||
it('fulfills with applicable modifier for range', function () { | ||
subject.val_to_percent.onCall(0).returns(37) | ||
var result = subject.get('secret_feature', 123) | ||
return expect(result).to.eventually.equal('groupB') | ||
}) | ||
it('fulfills multiple with applicable modifiers for ranges', function () { | ||
subject.val_to_percent.onCall(0).returns(12) | ||
subject.val_to_percent.onCall(1).returns(49.97) | ||
subject.val_to_percent.onCall(2).returns(72) | ||
return subject.multi([ | ||
['secret_feature', 123], | ||
['secret_feature', 321], | ||
['secret_feature', 213] | ||
]) | ||
.then(function(results) { | ||
expect(results[0].isFulfilled()).to.be.true | ||
expect(results[0].value()).to.equal('groupA') | ||
expect(results[1].isFulfilled()).to.be.true | ||
expect(results[1].value()).to.equal('groupB') | ||
expect(results[2].isFulfilled()).to.be.true | ||
expect(results[2].value()).to.equal('groupC') | ||
}) | ||
}) | ||
}) | ||
it('fulfills when condition returns a resolved promise', function () { | ||
subject.handler('promise_secret_feature', { | ||
return subject.handler('promise_secret_feature', { | ||
beta_testa: { | ||
@@ -47,3 +113,3 @@ percentage: 100, | ||
return new Promise(function (resolve) { | ||
setTimeout(resolve, 10) | ||
setTimeout(resolve.bind(null, true), 10) | ||
}) | ||
@@ -53,9 +119,12 @@ } | ||
}) | ||
return expect(subject.get('promise_secret_feature', 123, { | ||
beta_testa: 'foo' | ||
})).to.be.fulfilled | ||
.then(function () { | ||
var result = subject.get('promise_secret_feature', 123, { | ||
beta_testa: 'foo' | ||
}) | ||
return expect(result).to.be.fulfilled | ||
}) | ||
}) | ||
it('rejects when condition returns a rejected promise', function () { | ||
subject.handler('promise_secret_feature', { | ||
return subject.handler('promise_secret_feature', { | ||
beta_testa: { | ||
@@ -68,13 +137,16 @@ percentage: 100, | ||
}) | ||
return expect(subject.get('promise_secret_feature', 123, { | ||
beta_testa: 'foo' | ||
})).to.be.rejected | ||
.then(function () { | ||
var result = subject.get('promise_secret_feature', 123, { | ||
beta_testa: 'foo' | ||
}) | ||
return expect(result).to.be.rejected | ||
}) | ||
}) | ||
it('fulfills if `any` condition passes', function () { | ||
subject.handler('mixed_secret_feature', { | ||
return subject.handler('mixed_secret_feature', { | ||
beta_testa: { | ||
percentage: 100, | ||
condition: function () { | ||
return Promise.resolve() | ||
return Promise.resolve(true) | ||
} | ||
@@ -85,3 +157,3 @@ }, | ||
condition: function () { | ||
return Promise.resolve() | ||
return Promise.resolve(true) | ||
} | ||
@@ -102,17 +174,19 @@ }, | ||
}) | ||
return expect(subject.get('mixed_secret_feature', 123, { | ||
beta_testa: 'foo', | ||
beta_testa1: 'foo', | ||
beta_testa2: 'foo', | ||
beta_testa3: 'foo' | ||
})).to.be.fulfilled | ||
.then(function () { | ||
var result = subject.get('mixed_secret_feature', 123, { | ||
beta_testa: 'foo', | ||
beta_testa1: 'foo', | ||
beta_testa2: 'foo', | ||
beta_testa3: 'foo' | ||
}) | ||
return expect(result).to.be.fulfilled | ||
}) | ||
}) | ||
it('rejects if all conditions fail', function () { | ||
subject.handler('mixed_secret_feature', { | ||
return subject.handler('mixed_secret_feature', { | ||
beta_testa1: { | ||
percentage: 0, | ||
condition: function () { | ||
return Promise.resolve() | ||
return Promise.resolve(true) | ||
} | ||
@@ -133,12 +207,14 @@ }, | ||
}) | ||
return expect(subject.get('mixed_secret_feature', 123, { | ||
beta_testa1: 'foo', | ||
beta_testa2: 'foo', | ||
beta_testa3: 'foo' | ||
})).to.be.rejected | ||
.then(function () { | ||
var result = subject.get('mixed_secret_feature', 123, { | ||
beta_testa1: 'foo', | ||
beta_testa2: 'foo', | ||
beta_testa3: 'foo' | ||
}) | ||
return expect(result).to.be.rejected | ||
}) | ||
}) | ||
it('can retrieve all mod values', function (done) { | ||
subject.handler('super_secret', { | ||
it('can retrieve percentage mod values', function () { | ||
return subject.handler('super_secret', { | ||
foo: { | ||
@@ -151,6 +227,7 @@ percentage: 12 | ||
}) | ||
subject.on('ready', function () { | ||
subject.mods('super_secret').then(function (mods) { | ||
expect(mods).to.deep.equal({foo: '12', bar: '34'}) | ||
done() | ||
.then(function () { | ||
var result = subject.modifiers('super_secret') | ||
return expect(result).to.eventually.deep.equal({ | ||
foo: 12, | ||
bar: 34 | ||
}) | ||
@@ -160,3 +237,21 @@ }) | ||
it('can retrieve all flagnames', function () { | ||
it('can retrieve range mod values', function () { | ||
return subject.handler('super_secret', { | ||
foo: { | ||
percentage: { min: 0, max: 50 } | ||
}, | ||
bar: { | ||
percentage: { min: 50, max: 100 } | ||
} | ||
}) | ||
.then(function () { | ||
var result = subject.modifiers('super_secret') | ||
return expect(result).to.eventually.deep.equal({ | ||
foo: { min: 0, max: 50 }, | ||
bar: { min: 50, max: 100 } | ||
}) | ||
}) | ||
}) | ||
it('can retrieve all handler names', function () { | ||
var o = { | ||
@@ -167,9 +262,14 @@ foo: { | ||
} | ||
subject.handler('youza', o) | ||
subject.handler('huzzah', o) | ||
expect(subject.flags()).to.deep.equal(['youza', 'huzzah']) | ||
return Promise.all([ | ||
subject.handler('youza', o), | ||
subject.handler('huzzah', o) | ||
]) | ||
.then(function () { | ||
var result = subject.handlers() | ||
return expect(result).to.eventually.deep.equal(['youza', 'huzzah']) | ||
}) | ||
}) | ||
it('gets multiple keys', function () { | ||
subject.handler('secret_feature', { | ||
return subject.handler('secret_feature', { | ||
employee: { | ||
@@ -180,5 +280,2 @@ percentage: 100, | ||
}) | ||
return subject.get('secret_feature', 123, { | ||
employee: 'me@company.com' | ||
}) | ||
.then(function () { | ||
@@ -192,5 +289,5 @@ return subject.multi([ | ||
expect(result[0].isFulfilled()).to.be.true | ||
expect(result[0].value()).to.be.true | ||
expect(result[0].value()).to.equal('employee') | ||
expect(result[1].isFulfilled()).to.be.true | ||
expect(result[1].value()).to.be.true | ||
expect(result[1].value()).to.equal('employee') | ||
expect(result[2].isRejected()).to.be.true | ||
@@ -211,3 +308,3 @@ }) | ||
subject.val_to_percent.returns(51.001) | ||
subject.handler('another_feature', { | ||
return subject.handler('another_feature', { | ||
id: { | ||
@@ -217,9 +314,11 @@ percentage: 51.000 | ||
}) | ||
var out = subject.get('another_feature', 123) | ||
return expect(out).to.be.rejected | ||
.then(function () { | ||
var result = subject.get('another_feature', 123) | ||
return expect(result).to.be.rejected | ||
}) | ||
}) | ||
it('should be able to update a key', function (done) { | ||
it('should be able to update a key with a percentage', function () { | ||
subject.val_to_percent.returns(50) | ||
subject.handler('button_test', { | ||
return subject.handler('button_test', { | ||
id: { | ||
@@ -229,30 +328,46 @@ percentage: 100 | ||
}) | ||
Promise.resolve() | ||
.then(function() { | ||
return new Promise(function(resolve) { | ||
subject.on('ready', function () { | ||
var out = subject.get('button_test', 123) | ||
expect(out).to.be.fulfilled | ||
resolve(null) | ||
}) | ||
var result = subject.get('button_test', 123) | ||
return expect(result).to.be.fulfilled | ||
}) | ||
.then(function () { | ||
return subject.update('button_test', { | ||
id: 49 | ||
}) | ||
.then(function () { | ||
var result = subject.get('button_test', 123) | ||
return expect(result).to.be.rejected | ||
}) | ||
}) | ||
}) | ||
it('should be able to update a key with a range', function () { | ||
subject.val_to_percent.returns(50) | ||
return subject.handler('experiment', { | ||
groupA: { | ||
percentage: 100 | ||
}, | ||
groupB: { | ||
percentage: 0 | ||
} | ||
}) | ||
.then(function() { | ||
var result = subject.get('experiment', 123) | ||
return expect(result).to.eventually.equal('groupA') | ||
}) | ||
.then(function () { | ||
return new Promise(function(resolve) { | ||
subject.update('button_test', { | ||
id: 49 | ||
}) | ||
.then(function () { | ||
var out = subject.get('button_test', 123) | ||
expect(out).to.be.rejected.notify(resolve) | ||
}) | ||
return subject.update('experiment', { | ||
groupA: { min: 0, max: 49 }, | ||
groupB: { min: 49, max: 100 } | ||
}) | ||
.then(function () { | ||
var result = subject.get('experiment', 123) | ||
return expect(result).to.eventually.equal('groupB') | ||
}) | ||
}) | ||
.then(done) | ||
}) | ||
it('is optimistic', function (done) { | ||
it('is optimistic', function () { | ||
subject.val_to_percent.returns(49) | ||
subject.handler('super_secret', { | ||
return subject.handler('super_secret', { | ||
id: { | ||
@@ -268,9 +383,8 @@ // give feature to 49% of users | ||
}) | ||
subject.on('ready', function () { | ||
var out = subject.get('super_secret', 123, { | ||
.then(function () { | ||
var result = subject.get('super_secret', 123, { | ||
employee: 'regular@gmail.com' | ||
}) | ||
// is rejected by company email, but falls within allowed regular users | ||
expect(out).to.be.fulfilled.notify(done) | ||
return expect(result).to.eventually.equal('id') | ||
}) | ||
@@ -277,0 +391,0 @@ }) |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
86363
323.89%19
72.73%560
31.76%1
-50%273
33.17%0
-100%