Created
February 22, 2016 16:38
-
-
Save mmoravec/d34f135cfb296cae740f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Place this code in Project JS. | |
* This code randomly selects either one experiment marked with an [ME], one experiment from each group denominated with [Group_A] where A is any character the user wishes to use. The code is evaluated before url targeting and audiencing. | |
* | |
* The way it works is: | |
* - Let's say we have 3 groups with 3 experiments in each group, 3 experiments marked with an [ME], and 5 experiments not marked with a [Group_X] or an [ME]. | |
* [Group_A] Exp 1, [Group_A] Exp 2, [Group_A] Exp 3, [Group_A] holdout | |
* [Group_B] Exp 4, [Group_B] Exp 5, [Group_B] Exp 6, [Group_B] holdout | |
* [Group_C] Exp 7, [Group_C] Exp 8, [Group_C] Exp 9, [Group_C] holdout | |
* [ME] Exp 10, [ME] Exp 11, [ME] Exp 12, [ME] holdout | |
* | |
* - First evaluate whether none of the experiments will be picked. There is a 5% chance for this condition and its called a global holdout as designated by the '[ME] holdout' | |
* - The code then pick a random number, if that number corresponds to the position of an element marked with [ME], it disable all other experiments in the project | |
* - If that number is not an [ME], it then go through each group and randomly select an experiment from each | |
* - Each group can have a holdout which would act as a baseline for that experiment and group | |
* - After all experiments are picked, we update the buckets by disabling all experiments that weren't picked | |
* | |
* This way, a customer will get 100% clean data (guaranteed no pollution from other running experiments) without any effort from the client except including [EXCLUSIVE] in the name, when the customer wants to, and no exclusiveness when the customer doesn't want to. | |
* | |
* Downside: with more exclusive experiments, the experiments will get less and less (chances for) visitors. | |
* | |
* You can use the ?optimizely_exclusive_force=EXPERIMENTID to force yourself into a variation (and auto-exclude yourself from other relevant experiments). | |
*/ | |
function getParameterByName(name) { | |
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); | |
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), | |
results = regex.exec(location.search); | |
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); | |
} | |
var docCookies = { | |
getItem: function(sKey) { | |
return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null; | |
}, | |
setItem: function(sKey, sValue, vEnd, sPath, sDomain, bSecure) { | |
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { | |
return false; | |
} | |
var sExpires = ""; | |
if (vEnd) { | |
switch (vEnd.constructor) { | |
case Number: | |
sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd; | |
break; | |
case String: | |
sExpires = "; expires=" + vEnd; | |
break; | |
case Date: | |
sExpires = "; expires=" + vEnd.toUTCString(); | |
break; | |
} | |
} | |
document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : ""); | |
return true; | |
}, | |
removeItem: function(sKey, sPath, sDomain) { | |
if (!sKey || !this.hasItem(sKey)) { | |
return false; | |
} | |
document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : ""); | |
return true; | |
}, | |
hasItem: function(sKey) { | |
return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie); | |
}, | |
keys: /* optional method: you can safely remove it! */ | |
function() { | |
var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/); | |
for (var nIdx = 0; nIdx < aKeys.length; nIdx++) { | |
aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]); | |
} | |
return aKeys; | |
} | |
}; | |
function getRunningExperiments() { | |
var running_experiments = []; | |
for (var exp_id in DATA.experiments) { | |
var exp = DATA.experiments[exp_id]; | |
if (exp.enabled && window.location.search.indexOf("x" + exp_id) === -1) { | |
running_experiments.push(exp_id); | |
} | |
} | |
window.running = running_experiments; | |
return running_experiments; | |
} | |
function getRunningExclusiveExperiments(exclusive) { | |
var running_experiments = []; | |
for (var exp_id in DATA.experiments) { | |
var exp = DATA.experiments[exp_id]; | |
if (exp.enabled && window.location.search.indexOf("x" + exp_id) === -1 && DATA.experiments[exp_id].name !== undefined) { | |
var groupIndex = DATA.experiments[exp_id].name.toLowerCase().indexOf("[Group_".toLowerCase()); | |
var meIndex = DATA.experiments[exp_id].name.toLowerCase().indexOf("[ME]".toLowerCase()); | |
if ((groupIndex > -1 && exclusive) || (meIndex > -1 && exclusive)) { | |
running_experiments.push(exp_id); | |
} else if (groupIndex == -1 && meIndex == -1 && !exclusive) { | |
running_experiments.push(exp_id); | |
} | |
} | |
} | |
if (exclusive) { | |
window.running_exclusive = running_experiments; | |
} else { | |
window.running_non_exclusive = running_experiments; | |
} | |
return running_experiments; | |
} | |
function expArrayToJSON(arr, item, exclusive_only) { | |
var result = {}; | |
if (!exclusive_only) { | |
for (var exp_id in arr) { | |
if (exp_id != item) { | |
result[exp_id] = "0"; | |
} | |
} | |
} else { | |
for (var exp_id in arr) { | |
if (exp_id != item && arr[exp_id].name.toLowerCase().indexOf("[EXCLUSIVE]".toLowerCase()) > -1) { | |
result[exp_id] = "0"; | |
} | |
} | |
} | |
return result; | |
} | |
function log(msg) { | |
window['log_arr'] = window['log_arr'] || []; | |
window['log_arr'].push(msg); | |
} | |
window.print_log = function() { | |
window['log_arr'] = window['log_arr'] || []; | |
console.log(window['log_arr'].join('\n ')); | |
}; | |
function groupFlow(items, exclusive_items) { | |
for (var key in exclusive_items.groups) { | |
var groupKey = exclusive_items.groups[key]; | |
//check for group holdout | |
if(groupKey.holdout !== false && (Math.floor(Math.random() * 100) > 95)) { | |
items.push(groupKey.holdout); | |
//else push group experiments | |
} else { | |
var randomNum = Math.floor(Math.random() * (groupKey.exps.length + groupKey.me.length)); | |
if(randomNum < groupKey.exps.length) { | |
//push all group experiments if non 'me' is randomly selected | |
items = items.concat(groupKey.exps); | |
} else { | |
//push one 'me' experiment if 'me' is randomly selected | |
items.push(groupKey.me[(randomNum-groupKey.exps.length)]); | |
} | |
} | |
} | |
return items; | |
} | |
function pickExperiment(pick_exp) { | |
var topdomain = location.hostname.split("."); | |
topdomain = "." + topdomain[topdomain.length - 2] + "." + topdomain[topdomain.length - 1]; | |
var exclusive_items = { | |
allExp: getRunningExclusiveExperiments(true) | |
}; | |
var non_exclusive_items = { | |
allExp: getRunningExclusiveExperiments(false) | |
}; | |
var items = []; | |
exclusive_items = sortGroups(exclusive_items); | |
//check if experiment exists | |
if ((exclusive_items.allExp.indexOf(pick_exp) > -1) || (non_exclusive_items.allExp.indexOf(pick_exp) > -1)) { | |
items.push(pick_exp); | |
//check 'ME' holdout chance | |
} else if (exclusive_items['me'] !== undefined && exclusive_items['me']['holdout'] !== undefined && Math.floor(Math.random() * 100) > 95) { | |
items.push(exclusive_items['me']['holdout']); | |
//check if groups exist, if not choose me | |
} else if(exclusive_items['groups'] === undefined && exclusive_items['me'] !== undefined) { | |
items.push(exclusive_items['me']['exps'][Math.floor(Math.random() * exclusive_items['me']['exps'].length)]); | |
//check if me exist, if not go through group flow | |
} else if(exclusive_items['me'] === undefined && exclusive_items['groups'] !== undefined) { | |
//do group flow | |
items = groupFlow(items, exclusive_items); | |
} else if(exclusive_items['groups'] !== undefined && exclusive_items['me'] !== undefined) { | |
var groupExps = exclusive_items.allExp.length - exclusive_items.me.exps.length; | |
if(Math.floor(Math.random() * exclusive_items.allExp.length) < groupExps) { | |
//do group flow | |
items = groupFlow(items, exclusive_items); | |
//push an me experiment | |
} else { | |
items.push(exclusive_items['me']['exps'][Math.floor(Math.random() * exclusive_items['me']['exps'].length)]); | |
} | |
} | |
items = items.concat(non_exclusive_items.allExp); | |
docCookies.setItem('optimizelyExp', items, 2000 * 86400000, "/", topdomain); | |
return items; | |
}; | |
function sortGroups(items) { | |
for (var i = 0; i < items.allExp.length; i++) { | |
var exp = DATA.experiments[items.allExp[i]]; | |
var expName = exp.name.toLowerCase(); | |
//experiment is in a group | |
if(expName.indexOf("group_") > -1) { | |
//set group object if it doesn't exist | |
if(items['groups'] === undefined) { | |
items['groups'] = {}; | |
} | |
//set specific group if it doesn't exist | |
if(items['groups'][expName.charAt(7)] === undefined) { | |
items['groups'][expName.charAt(7)] = {'holdout': false, 'me': [], 'exps': [] }; | |
} | |
//check if holdout first | |
if(expName.indexOf('holdout') > -1) { | |
items['groups'][expName.charAt(7)]['holdout'] = items.allExp[i]; | |
//check if mutually exclusive within groups | |
} else if(expName.indexOf('me') > -1) { | |
items['groups'][expName.charAt(7)]['me'].push(items.allExp[i]); | |
//push to all group exps otherwise | |
} else { | |
items['groups'][expName.charAt(7)]['exps'].push(items.allExp[i]); | |
} | |
//experiment is an me experiment | |
} else { | |
//if undefined, create object | |
if(items['me'] === undefined) { | |
items['me'] = { 'holdout': false, 'exps': [] }; | |
} | |
if(expName.indexOf('holdout') > -1) { | |
items['me']['holdout'] = items.allExp[i]; | |
} else { | |
items['me']['exps'].push(items.allExp[i]); | |
} | |
} | |
} | |
return items; | |
}; | |
function updateBuckets(items) { | |
window['optimizely'] = window['optimizely'] || []; | |
log("Mutual exclusion chose experiment " + items); | |
for (var exp_id in DATA.experiments) { | |
if (items.indexOf(exp_id) === -1 && window.location.search.indexOf("x" + exp_id) === -1) { | |
DATA.experiments[exp_id].enabled = false; | |
log("Disable experiment " + exp_id); | |
} else { | |
console.log('Active - ' + exp_id + ': ' + DATA.experiments[exp_id].name); | |
} | |
} | |
} | |
if (typeof DATA != 'undefined') // DATA object isn't defined in the Optimizely editor but is when the code runs on the page | |
{ | |
var force_exp = getParameterByName("optimizely_exclusive_force"); | |
var exp = docCookies.getItem('optimizelyExp'); | |
if(exp !== null && exp.indexOf(',') > -1 && force_exp == "") { | |
exp = exp.split(','); | |
for(var i = 0; i < exp.length; i++) { | |
if(!(DATA.experiments.hasOwnProperty(exp[i]) && DATA.experiments[exp[i]].enabled)) { | |
var exp = pickExperiment(force_exp); | |
} | |
} | |
} else { | |
if ((!exp || !(DATA.experiments.hasOwnProperty(exp) && DATA.experiments[exp].enabled)) || force_exp != "") { | |
var exp = pickExperiment(force_exp); | |
} | |
} | |
window.opt_expid = exp; | |
updateBuckets(exp); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment