Last active
April 8, 2024 12:14
-
-
Save ricardobeat/889e736b2f85c1a0dcc3169d07ed7ee9 to your computer and use it in GitHub Desktop.
Experiment assignment for Cloudfront
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 BUCKETS = 120 // multiple of 3 for correct split on 3-variant experiments | |
const seed = '8fc40cab' // random seed, change to reallocate users | |
function hashFNV(s, h = 0x811c9dc5) { | |
for (let i = 0; i < s.length; i++) { | |
h ^= s.charCodeAt(i); | |
h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24); | |
} | |
return h >>> 0; | |
} | |
function getBucket(key) { | |
let n = hashFNV(key); | |
return n % BUCKETS | |
} | |
function trackImpression(userId) { | |
// ... | |
} | |
function getVariant(userId, experiment, trafficPercentage = 100, variants = 2) { | |
const key = `${userId}-${experiment}-${seed}` | |
const bucket = getBucket(key) | |
for (let i = 0; i < variants; i++) { | |
let threshold = (i + 1) * (trafficPercentage / variants / 100 * BUCKETS) | |
if (bucket < threshold) { | |
trackImpression(userId) | |
return i | |
} | |
} | |
return undefined // not in experiment | |
} |
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 BUCKETS = 100 | |
function toInt32(buf) { | |
return Math.abs(new Uint8Array(buf).slice(0,4).reduce((p,c) => p << 8 | c, 0)) | |
} | |
async function hash(input){ | |
const data = new TextEncoder().encode(input); | |
const buf = await crypto.subtle.digest("SHA-256", data); | |
return toInt32(buf) | |
} | |
async function getBucket(key) { | |
let n = await hash(key); | |
return n % BUCKETS | |
} | |
const seed = '8fc40cab' // change will reallocate users | |
async function getVariant (userID, exp) { | |
const key = `${userID}-${exp}-${seed}` | |
const bucket = await getBucket(key) | |
return bucket < (BUCKETS/2) ? 0 : 1 | |
} |
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 seed = '8fc40cab' // random seed, change to force reallocation of users | |
function hashFNV(s, h = 0x811c9dc5) { | |
for (let i = 0; i < s.length; i++) { | |
h ^= s.charCodeAt(i); | |
h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24); | |
} | |
return h >>> 0; | |
} | |
/* simplified version that works with a traffic % from 0 to 100 */ | |
function isFeatureEnabled (userID, featureName, trafficPercentage = 50) { | |
const key = `${userID}-${featureName}-${seed}` | |
const bucket = hashFNV(key) % 100 | |
return bucket < trafficPercentage | |
} |
test
const N_USERS = 1000000
const N = 1_000_000
const userIds = new Array(N_USERS).fill(1).map(n => (Math.random() * 1000000 | 0).toString())
const count = {}
console.time('benchmark')
for (let i = 0; i < N; i++) {
const treatment = getVariant(userIds[i % N_USERS], 'my_experiment', 60, 5)
count[treatment] = (count[treatment] || 0) + 1
}
console.timeEnd('benchmark')
console.log(count)
for (let key in count) {
console.log(key, count[key] / N)
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://codepen.io/ricardobeat/pen/oNVvyPX