Skip to content

Instantly share code, notes, and snippets.

@ricardobeat
Last active April 8, 2024 12:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ricardobeat/889e736b2f85c1a0dcc3169d07ed7ee9 to your computer and use it in GitHub Desktop.
Save ricardobeat/889e736b2f85c1a0dcc3169d07ed7ee9 to your computer and use it in GitHub Desktop.
Experiment assignment for Cloudfront
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
}
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
}
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
}
@ricardobeat
Copy link
Author

@ricardobeat
Copy link
Author

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