Comparing version 0.0.2 to 0.0.3
{ | ||
"name": "muton", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"description": "A feature toggling/throttling and multivariate testing tool", | ||
@@ -5,0 +5,0 @@ "main": "src/muton.js", |
158
README.md
@@ -47,3 +47,3 @@ Muton | ||
```javascript | ||
var features = muton.getFeatureMutations(userProperties, featureInstructions); | ||
var features = muton.getMutations(userProperties, featureInstructions); | ||
``` | ||
@@ -81,5 +81,8 @@ | ||
```javascript | ||
var featureInstructions = { | ||
'superCoolFeature' : true, | ||
'anotherCoolFeature' : false | ||
{ | ||
'toggles': { | ||
'superCoolFeature' : true, | ||
'anotherCoolFeature' : false | ||
} | ||
} | ||
``` | ||
@@ -116,3 +119,13 @@ | ||
``` | ||
A valid response for a portuguese user could be: | ||
```javascript | ||
{ | ||
'toggles' : { | ||
'superCoolFeature' : true, | ||
}, | ||
'throttles': ['superCoolFeature'] | ||
} | ||
``` | ||
#### Buckets (A/B testing, multivariant testing) | ||
@@ -143,8 +156,130 @@ | ||
```javascript | ||
var featureInstructions = { | ||
'superCoolFeature' : true, | ||
'superCoolFeature.smallButton' : true | ||
{ | ||
'toggles' : { | ||
'superCoolFeature' : true, | ||
'superCoolFeature.smallButton' : true | ||
}, | ||
'buckets': ['superCoolFeature.smallButton'] | ||
} | ||
``` | ||
### Matchers | ||
Muton supports non-trivial user properties matching. For example, you might want to toggle a feature on when the user bought over 10000 books in your store. Or even to activate a feature for users whose referral site belongs to a specific domain. | ||
#### Regular expressions | ||
To have user properties being matched by regexes, you must enclose the regex inside two '/'. The above example referral example with Google can be written like this: | ||
```javascript | ||
var featureInstructions = { | ||
'superCoolFeature' : { | ||
'referral' : { | ||
'/.*\.google\..{2,4}/' : { | ||
'toggle' : 'true' | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
#### Numeric quantifiers | ||
Numeric user properties can be matched against an expression. The property value must start with one of the following operators: '>', '<', '>=', '<='. | ||
Picking the bookstore example, a user with over 10000 bought books would have a special feature toggle on: | ||
```javascript | ||
var featureInstructions = { | ||
'superCoolFeature' : { | ||
'boughtBooks' : { | ||
'>=10000' : { | ||
'toggle' : 'true' | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
### Gene inheritance | ||
To inherit properties from previous mutations, you can use gene inheritance. This is useful when you want to maintain the same users on the same buckets or throttles to guarantee consistency in your features when the context changes. | ||
This is only applied to instructions that are subject to mutations, namely 'buckets' and 'throttles'. Inherited mutations from ancestors will only work when the new instructions and ancestor genes have the same kind of mutations (e.g.: ancestor and predecessor have a throttle on the same feature). For plain toggles, ancestor genes will be ignored. | ||
To inherit genes from an ancestor, you can use Muton in the following way: | ||
```javascript | ||
var features = muton.inheritMutations(userProperties, featureInstructions, ancestorGenes); | ||
``` | ||
#### Throttle inheritance | ||
For example, to inherit throttles, one could pass a set of instructions and a set of ancestor genes: | ||
```javascript | ||
var featureInstructions = { | ||
'superCoolFeature' : { | ||
'toggle' : true, | ||
'location' : { | ||
'PT' : { | ||
'throttle' : '50%' | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
You can use a previous Muton output to feed new mutations and inherit them from ancestors. The default behaviour is to inherit from ancestor. In the bellow example, Muton will always return true for the 'superCoolFeature' throttle, because the previous throttle was enabled. | ||
```javascript | ||
var ancestorGenes = { | ||
'toggles' : { | ||
'superCoolFeature' : true | ||
}, | ||
'throttles' : ['superCoolFeature'] | ||
}; | ||
``` | ||
If you choose to force mutations when inheriting throttles on a specific context, you can create a throttle object instead of a percentage value and pass a 'mutate' property and ignore the ancestor gene. You must specify it in the instructions: | ||
```javascript | ||
var featureInstructions = { | ||
'superCoolFeature' : { | ||
'toggle' : true, | ||
'location' : { | ||
'PT' : { | ||
'throttle' : '50%', | ||
'mutate': 'force' | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
#### Bucket inheritance | ||
To inherit buckets, you can pass a set of ancestor genes to Muton containing buckets: | ||
```javascript | ||
var featureInstructions = { | ||
'superCoolFeature' : { | ||
'toggle' : true, | ||
'location' : { | ||
'PT' : { | ||
'buckets' : ['bigRedButton', 'mediumSizeButton', 'smallButton'] | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
The ancestor genes (a previous Muton output) could be passed to Muton and the predecessor will inherit the ancestor bucket. However, if the ancestor gene contains a bucket that is not defined in the predecessor instructions, then a bucket mutation will occur and a random bucket from the new instructions will be picked up. | ||
```javascript | ||
var ancestorGenes = { | ||
'toggles' : { | ||
'superCoolFeature.bigRedButton' : true | ||
}, | ||
'buckets' : ['superCoolFeature.bigRedButton'] | ||
}; | ||
``` | ||
## Build | ||
@@ -170,6 +305,11 @@ | ||
* 0.0.1 - Initial release | ||
* 0.0.2 - Fixing bucket choice bug | ||
* 0.0.1 | ||
- Initial release | ||
* 0.0.2 | ||
- Fixing bucket choice bug | ||
* 0.1.0 | ||
- Adding support for regex and numeric matchers | ||
- Adding support for gene inheritance | ||
[npm-url]: https://npmjs.org/package/muton | ||
[npm-image]: https://badge.fury.io/js/muton.svg |
@@ -15,3 +15,3 @@ 'use strict'; | ||
var chemicalReactions = require('./../reactions/chemical.js'); | ||
var chemicalReactions = require('../reactions/chemical.js'); | ||
@@ -18,0 +18,0 @@ return { |
@@ -19,18 +19,24 @@ 'use strict'; | ||
define(function (require) { | ||
var bucketMutator = require('./../mutators/bucket'); | ||
var throttleMutator = require('./../mutators/throttle'); | ||
var proofReader = require('./../reactions/proof-reading'); | ||
var _ = require('lodash'); | ||
var bucketMutator = require('../mutators/bucket'); | ||
var throttleMutator = require('../mutators/throttle'); | ||
var genePairing = require('../mutators/gene-pairing'); | ||
var proofReader = require('../reactions/proof-reading'); | ||
function addToFeatures(features, featureName, toggle) { | ||
features[featureName] = toggle; | ||
return features.push(_.merge({ name: featureName }, toggle)); | ||
} | ||
function processFeatureInstructions(featureProperties) { | ||
var toggle = false; | ||
function processFeatureInstructions(featureProperties, gene) { | ||
var toggle = { | ||
type: 'toggle', | ||
toggle: false | ||
}; | ||
if (featureProperties.toggle !== false) { | ||
if (throttleMutator.isThrottleValid(featureProperties.throttle)) { | ||
toggle = throttleMutator.mutate(featureProperties.throttle); | ||
toggle.toggle = throttleMutator.mutate(featureProperties.throttle, gene); | ||
toggle.type = 'throttle'; | ||
} else if (featureProperties.toggle === true) { | ||
toggle = true; | ||
toggle.toggle = true; | ||
} | ||
@@ -43,8 +49,14 @@ } | ||
function containsBuckets(toggle, featureInstructions) { | ||
return toggle && bucketMutator.containsMultivariant(featureInstructions); | ||
return toggle.toggle && bucketMutator.containsMultivariant(featureInstructions); | ||
} | ||
function addBucketToFeatures(features, featureName, featureInstructions, toggle) { | ||
var bucketName = bucketMutator.mutate(featureInstructions); | ||
addToFeatures(features, featureName + "." + bucketName, toggle); | ||
function addBucketToFeatures(features, featureName, featureInstructions, toggle, gene) { | ||
var bucketName = bucketMutator.mutate(featureInstructions, gene); | ||
var bucketToggle = { | ||
toggle : toggle.toggle, | ||
type : 'bucket' | ||
}; | ||
addToFeatures(features, featureName + "." + bucketName, bucketToggle); | ||
} | ||
@@ -60,16 +72,20 @@ | ||
* @returns A resolved feature toggle, which may mutate to a bucket feature toggle | ||
* @param ancestorGenes An object containing 'genes' to inherit, this only applies to throttles and buckets | ||
*/ | ||
assembleFeatures: function(featureName, primerInstructions) { | ||
var features = {}; | ||
assembleFeatures: function(featureName, primerInstructions, ancestorGenes) { | ||
var features = []; | ||
if (proofReader.areInstructionsValid(primerInstructions)) { | ||
var toggle = processFeatureInstructions(primerInstructions); | ||
// Get the ancestor gene based on name to be able to copy it to the descendant | ||
var gene = genePairing.pairGene(ancestorGenes, featureName); | ||
var toggle = processFeatureInstructions(primerInstructions, gene); | ||
addToFeatures(features, featureName, toggle); | ||
if (containsBuckets(toggle, primerInstructions)) { | ||
addBucketToFeatures(features, featureName, primerInstructions, toggle); | ||
addBucketToFeatures(features, featureName, primerInstructions, toggle, gene); | ||
} | ||
} else { | ||
console.log('There are invalid feature instructions!'); | ||
addToFeatures(features, featureName, false); | ||
addToFeatures(features, featureName, { toggle: false, type: 'toggle' }); | ||
} | ||
@@ -76,0 +92,0 @@ return features; |
@@ -15,3 +15,4 @@ 'use strict'; | ||
var _ = require('lodash'); | ||
var chemicalReactions = require('./../reactions/chemical.js'); | ||
var chemicalReactions = require('../reactions/chemical.js'); | ||
var matchReading = require('../reactions/match-reading.js'); | ||
@@ -25,3 +26,3 @@ function mergeProperties(primer, feature) { | ||
var toggle = _.get(primer, 'toggle'); | ||
return root && (_.isUndefined(toggle) || toggle === false); | ||
return root && toggle === false; | ||
} | ||
@@ -38,6 +39,8 @@ | ||
function getPropertiesNode(userProperties, featurePropertyName, feature) { | ||
var propertyName = featurePropertyName; | ||
// Explode the current node to check if there are properties | ||
var featureProperty = feature[propertyName]; | ||
var properties = featureProperty[userProperties[propertyName]]; | ||
var featureProperty = feature[featurePropertyName]; | ||
var userPropertyValue = userProperties[featurePropertyName]; | ||
var properties = matchReading.getMatchedProperties(userPropertyValue, featureProperty); | ||
return pickMatchedProperties(properties, featureProperty); | ||
@@ -50,2 +53,6 @@ } | ||
function bindPrimer(primerInstructions, childPrimer) { | ||
_.merge(primerInstructions, childPrimer); | ||
} | ||
return { | ||
@@ -81,3 +88,4 @@ /** | ||
var childPrimer = self.preparePrimer(userProperties, propertiesNode, childStrands); | ||
_.merge(primerInstructions, childPrimer); | ||
bindPrimer(primerInstructions, childPrimer); | ||
} | ||
@@ -84,0 +92,0 @@ }); |
@@ -16,5 +16,26 @@ 'use strict'; | ||
function pickOneElement(array) { | ||
if (!_.isArray(array)) { | ||
throw 'Not an array!'; | ||
} | ||
var index = Math.floor(Math.random() * (array.length)); | ||
return array[index]; | ||
} | ||
function isBucketGene(gene) { | ||
return !_.isEmpty(gene) && gene.type === 'bucket'; | ||
} | ||
function containsGene(featureProperties, gene) { | ||
return _.includes(featureProperties.buckets, gene.toggle); | ||
} | ||
return { | ||
mutate: function(featureProperties) { | ||
return this.pickOneElement(featureProperties.buckets); | ||
mutate: function(featureProperties, gene) { | ||
if (isBucketGene(gene) && containsGene(featureProperties, gene)) { | ||
return gene.toggle; | ||
} else { | ||
return pickOneElement(featureProperties.buckets); | ||
} | ||
}, | ||
@@ -29,13 +50,4 @@ | ||
}, | ||
pickOneElement: function(array) { | ||
if (!_.isArray(array)) { | ||
throw 'Not an array!'; | ||
} | ||
var index = Math.floor(Math.random() * (array.length)); | ||
return array[index]; | ||
} | ||
}; | ||
}); |
@@ -15,21 +15,52 @@ 'use strict'; | ||
function getPercentageDecimal(throttle) { | ||
var percentage = extractPercentage(throttle); | ||
var value = percentage.substr(0, percentage.length - 2); | ||
return value / 10; | ||
} | ||
function extractPercentage(throttle) { | ||
var percentage; | ||
if (isThrottleNode(throttle)) { | ||
percentage = throttle['value']; | ||
} else { | ||
percentage = throttle; | ||
} | ||
return percentage; | ||
} | ||
function isThrottleValid(throttle) { | ||
return isThrottleNode(throttle) || isPercentage(throttle); | ||
} | ||
function isThrottleNode(throttle) { | ||
return _.isPlainObject(throttle) && _.isString(throttle['value']) && isPercentage(throttle['value']); | ||
} | ||
function isPercentage(value) { | ||
return !_.isUndefined(value) && _.isString(value) && value.match(/[0-100]%/); | ||
} | ||
function isThrottleGene(gene) { | ||
return !_.isEmpty(gene) && gene.type === 'throttle'; | ||
} | ||
function shouldMutate(throttle) { | ||
return !_.isUndefined(throttle['mutate']) && throttle['mutate'] === 'force'; | ||
} | ||
return { | ||
mutate: function (throttle) { | ||
var percentage = this.getPercentageDecimal(throttle); | ||
return Math.random() < percentage; | ||
mutate: function (throttle, gene) { | ||
if (!shouldMutate(throttle) && isThrottleGene(gene)) { | ||
return gene.toggle; | ||
} else { | ||
var percentage = getPercentageDecimal(throttle); | ||
return Math.random() < percentage; | ||
} | ||
}, | ||
isThrottleValid: function (throttle) { | ||
return !_.isUndefined(throttle) && this.isPercentage(throttle); | ||
}, | ||
isPercentage: function (value) { | ||
return value.match(/[0-100]%/); | ||
}, | ||
getPercentageDecimal: function (percentage) { | ||
var value = percentage.substr(0, percentage.length - 2); | ||
return value / 10; | ||
return isThrottleValid(throttle); | ||
} | ||
}; | ||
}); |
@@ -22,3 +22,3 @@ /** | ||
* | ||
* If you want to get a little deeper, please have a look at: | ||
* If you want to get a little deeper, please have a look at: | ||
* http://www.nature.com/scitable/topicpage/cells-can-replicate-their-dna-precisely-6524830 | ||
@@ -45,14 +45,73 @@ */ | ||
function joinToggles(features, resolvedFeatures) { | ||
features.toggles = _.reduce(resolvedFeatures, function (result, elem) { | ||
result[elem.name] = elem.toggle; | ||
return result; | ||
}, features.toggles); | ||
} | ||
function joinThrottles(features, resolvedFeatures) { | ||
var buckets = _.chain(resolvedFeatures) | ||
.filter({type : 'bucket'}) | ||
.pluck('name') | ||
.value(); | ||
features.buckets = features.buckets.concat(buckets); | ||
} | ||
function joinBuckets(features, resolvedFeatures) { | ||
var throttles = _.chain(resolvedFeatures) | ||
.filter({type : 'throttle'}) | ||
.pluck('name') | ||
.value(); | ||
features.throttles = features.throttles.concat(throttles); | ||
} | ||
function joinMutations(features, resolvedFeatures) { | ||
joinToggles(features, resolvedFeatures); | ||
joinThrottles(features, resolvedFeatures); | ||
joinBuckets(features, resolvedFeatures); | ||
} | ||
var muton = { | ||
/** | ||
* Given a list of user properties and feature instructions, it returns a collections of features toggles. | ||
* Given a list of user properties and feature instructions, it returns a collection of features toggles. | ||
* | ||
* @param userProperties A collection of user properties | ||
* @deprecated use getMutations or inheritMutations instead | ||
* | ||
* @param userProperties (optional) A collection of user properties | ||
* @param featureInstructions A collection of feature instructions which can be organized as a hierarchy of properties. | ||
* @returns An collection of feature toggles that are toggled on or off | ||
* @returns An collection of feature toggles | ||
*/ | ||
getFeatureMutations: function (userProperties, featureInstructions) { | ||
var features = {}; | ||
return this.getMutations(userProperties, featureInstructions).toggles; | ||
}, | ||
/** | ||
* Given a list of user properties and feature instructions, it returns a collection of features toggles. | ||
* | ||
* @param userProperties (optional) A collection of user properties | ||
* @param featureInstructions A collection of feature instructions which can be organized as a hierarchy of properties. | ||
* @returns {{toggles: {}, buckets: Array, throttles: Array}} An collection of feature toggles | ||
*/ | ||
getMutations: function (userProperties, featureInstructions) { | ||
return this.inheritMutations(userProperties, featureInstructions, {}); | ||
}, | ||
/** | ||
* Given a list of user properties and feature instructions, it returns a collection of features toggles. If specified, | ||
* it can inherit ancestor genes for buckets and throttle mutations | ||
* | ||
* @param userProperties (optional) A collection of user properties | ||
* @param featureInstructions A collection of feature instructions which can be organized as a hierarchy of properties. | ||
* @param ancestorGenes (optional) The ancestor genes, which is the output of previous mutations from Muton | ||
* @returns {{toggles: {}, buckets: Array, throttles: Array}} An collection of feature toggles | ||
*/ | ||
inheritMutations: function (userProperties, featureInstructions, ancestorGenes) { | ||
var features = { | ||
toggles: {}, | ||
buckets: [], | ||
throttles: [] | ||
}; | ||
proofReading.checkFeatureInstructions(featureInstructions); | ||
@@ -68,4 +127,6 @@ | ||
// Pick the primer, proof-read the instructions and then assemble the collection of feature toggles | ||
var resolvedFeatures = polymerase.assembleFeatures(featureName, primer); | ||
_.merge(features, resolvedFeatures); | ||
var resolvedFeatures = polymerase.assembleFeatures(featureName, primer, ancestorGenes); | ||
// Join all the mutations | ||
joinMutations(features, resolvedFeatures); | ||
}); | ||
@@ -72,0 +133,0 @@ |
@@ -14,4 +14,4 @@ 'use strict'; | ||
var _ = require('lodash'); | ||
var bucketMutator = require('./../mutators/bucket'); | ||
var throttleMutator = require('./../mutators/throttle'); | ||
var bucketMutator = require('../mutators/bucket'); | ||
var throttleMutator = require('../mutators/throttle'); | ||
@@ -40,5 +40,4 @@ return { | ||
checkFeatureInstructions: function (featureInstructions) { | ||
var valid = _.isUndefined(featureInstructions) || | ||
_.isNull(featureInstructions) || | ||
_.isPlainObject(featureInstructions); | ||
var valid = _.isUndefined(featureInstructions) || _.isNull(featureInstructions) || !_.isArray(featureInstructions) && _.isObject(featureInstructions); | ||
if (!valid) { | ||
@@ -49,2 +48,2 @@ throw new Error('Invalid feature instructions!'); | ||
}; | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
690187
311
0
19
13044
3