Last active
January 19, 2021 10:59
-
-
Save przemkow/fc38a2583ce23dba1f8ad775a26cbd22 to your computer and use it in GitHub Desktop.
ab-testing-medium-article
This file contains 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
{ | |
"timestamp": 1610486506114, | |
"experiments": [ | |
{ | |
"experimentName": "ActivityCard", | |
"control": "old-activity-card", | |
"variant": "new-activity-card", | |
"variantPercentage": 50 | |
} | |
] | |
} |
This file contains 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
/** | |
* @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 | |
} | |
} |
This file contains 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
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; | |
}; |
This file contains 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
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); | |
}; |
This file contains 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
<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> |
This file contains 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
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