Skip to content

Instantly share code, notes, and snippets.

@mmoravec
Created February 22, 2016 16:38
Show Gist options
  • Save mmoravec/d34f135cfb296cae740f to your computer and use it in GitHub Desktop.
Save mmoravec/d34f135cfb296cae740f to your computer and use it in GitHub Desktop.
/* 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