Skip to content

Instantly share code, notes, and snippets.

@przemkow
Last active January 19, 2021 10:59
Show Gist options
  • Save przemkow/fc38a2583ce23dba1f8ad775a26cbd22 to your computer and use it in GitHub Desktop.
Save przemkow/fc38a2583ce23dba1f8ad775a26cbd22 to your computer and use it in GitHub Desktop.
ab-testing-medium-article
{
"timestamp": 1610486506114,
"experiments": [
{
"experimentName": "ActivityCard",
"control": "old-activity-card",
"variant": "new-activity-card",
"variantPercentage": 50
}
]
}
/**
* @param experiment - A/B test experiment configuration
*/
function diceRoll(experiment) {
const diceRoll = Math.random() * 100;
if (diceRoll <= experiment.variantPercentage) {
return experiment.variant;
} else {
return experiment.control
}
}
const aws = require("aws-sdk");
const s3 = new aws.S3({ region: "YOU_S3_BUCKET_REGION" }); //ex. us-east-1
const s3Params = {
Bucket: "YOUR_BUCKET_NAME", // ex. com.musement-abtest-config
Key: "YOUR_S3_KEY_NAME", // ex. testConfig.json
};
const TTL = 1800000; // Cache Time to Live set for 30 minutes
async function fetchConfigFromS3() {
try {
const response = await s3.getObject(s3Params).promise();
return JSON.parse(response.Body.toString("utf-8"));
} catch (e) {
console.error("fetchConfigFromS3 error", e);
return {
timestamp: 0,
experiments: {},
};
}
}
let testsConfigCache;
exports.fetchConfig = async function (userTimestamp) {
const cachedTimestamp =
testsConfigCache && (await testsConfigCache).timestamp;
const hasValidCache =
cachedTimestamp && (!userTimestamp || cachedTimestamp >= userTimestamp);
if (hasValidCache) {
console.log("Tests config origin: Lambda Cache");
return testsConfigCache;
}
console.log("Tests config origin: S3 request");
testsConfigCache = fetchConfigFromS3();
setTimeout(() => {
testsConfigCache = undefined;
}, TTL);
return testsConfigCache;
};
const COOKIE_KEY = "abtests-user-config";
const getCookie = (headers, cookieKey) => {
if (headers.cookie) {
for (let cookieHeader of headers.cookie) {
const cookies = cookieHeader.value.split(";");
for (let cookie of cookies) {
const [key, val] = cookie.split("=");
if (key.trim() === cookieKey) {
return val;
}
}
}
}
return null;
};
const setCookie = function (response, cookie) {
response.headers["set-cookie"] = response.headers["set-cookie"] || [];
response.headers["set-cookie"].push({
key: "Set-Cookie",
value: cookie,
});
};
exports.handler = (event, context, callback) => {
console.log("event", JSON.stringify(event));
const response = event.Records[0].cf.response;
const request = event.Records[0].cf.request;
const headers = request.headers;
const configCookieVal = getCookie(headers, COOKIE_KEY);
if (configCookieVal != null) {
setCookie(
response,
`${COOKIE_KEY}=${configCookieVal}; Max-Age=31536000000`
);
}
callback(null, response);
};
<template>
<div>
<!-- Select a rendered component -->
<NewActivityComponent v-if="activityCardABTest === 'new-activity-card'"></NewActivityComponent>
<OldActivityComponent v-else></OldActivityComponent>
</div>
</template>
<script>
export default {
name: 'Homepage',
components: {
// Lazy load components to download only the visible one
OldActivityComponent: () => import('./OldActivityComponent.vue'),
NewActivityComponent: () => import('./NewActivityComponent.vue')
},
data() {
// Assign ActivityCard test to the variable
const activityCardABTest = this.$cookies.get('abtests-user-config')['ActivityCard'];
return {
activityCardABTest
}
}
}
</script>
const { fetchConfig } = require("./fetchConfig");
const COOKIE_KEY = "abtests-user-config";
const getParsedCookie = (headers, cookieKey) => {
if (headers.cookie) {
for (let cookieHeader of headers.cookie) {
const cookies = cookieHeader.value.split(";");
for (let cookie of cookies) {
const [key, val] = cookie.split("=");
if (key.trim() === cookieKey) {
return JSON.parse(decodeURIComponent(val));
}
}
}
}
return null;
};
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
try {
const savedUserConfig = getParsedCookie(headers, COOKIE_KEY);
const userTimestamp = savedUserConfig && savedUserConfig.timestamp;
/*
* Part 1: Get Config
*/
const testsConfig = await fetchConfig(userTimestamp);
/*
* Part 2: Forward request as-is for a valid config
*/
if (userTimestamp === testsConfig.timestamp) {
return request;
}
/*
* Part 3 & 4: calculate correct config for every experiment
*/
const newUserConfig = { timestamp: testsConfig.timestamp, experiments: {} };
for (let i = 0; i < testsConfig.experiments.length; i++) {
const experiment = testsConfig.experiments[i];
if (
savedUserConfig &&
savedUserConfig.experiments[experiment.experimentName]
) {
console.log("Experiment already assigned");
newUserConfig.experiments[experiment.experimentName] =
savedUserConfig.experiments[experiment.experimentName];
} else {
console.log("Throwing dice...");
const diceRoll = Math.random() * 100;
if (diceRoll <= experiment.variantPercentage) {
newUserConfig.experiments[experiment.experimentName] =
experiment.variant;
} else {
newUserConfig.experiments[experiment.experimentName] =
experiment.control;
}
}
}
const configCookie = `${COOKIE_KEY}=${JSON.stringify(newUserConfig)}`;
headers.cookie = headers.cookie || [];
if (!savedUserConfig) {
// If user had no experiments before - Add new cookie
headers.cookie.push({ key: "Cookie", value: configCookie });
} else {
// If user had already experiments - Replace old config with new one
const cookieKeyRegexp = new RegExp(`${COOKIE_KEY}=[^;]+`);
for (let i = 0; i < headers.cookie.length; i++) {
if (headers.cookie[i].value.indexOf(COOKIE_KEY) >= 0) {
headers.cookie[i].value = headers.cookie[i].value.replace(
new RegExp(cookieKeyRegexp),
configCookie
);
break;
}
}
}
return request;
} catch (e) {
console.error("Error:", e, "\n");
return request;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment