Skip to content

Commit 154255f

Browse files
committed
fix: user test group assignment distribution
use upgraded fingerprint with more elements to increase entropy, and use improved hash function to help evenly distribute users
1 parent e1f25ba commit 154255f

File tree

1 file changed

+25
-12
lines changed

1 file changed

+25
-12
lines changed

src/lib/ab-testing/server.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,26 @@ export const getABTestAssignment = async (
2424

2525
if (!testConfig || !testConfig.enabled) return null
2626

27-
// Create deterministic assignment using IP + User-Agent fingerprint
27+
// Create deterministic assignment using enhanced fingerprint
2828
const headers = await import("next/headers").then((m) => m.headers())
29-
const userAgent = headers.get("user-agent") || ""
29+
30+
// Get IP and user agent (primary identifier)
3031
const forwardedFor =
3132
headers.get("x-forwarded-for") || headers.get("x-real-ip") || "unknown"
32-
const fingerprint = `${forwardedFor}-${userAgent}`
33+
const userAgent = headers.get("user-agent") || ""
34+
35+
// Add privacy-preserving entropy sources
36+
const acceptLanguage = headers.get("accept-language") || ""
37+
const acceptEncoding = headers.get("accept-encoding") || ""
38+
39+
// Create enhanced fingerprint with more entropy
40+
const fingerprint = [
41+
forwardedFor,
42+
userAgent,
43+
acceptLanguage,
44+
acceptEncoding,
45+
testKey, // Include test key to ensure different tests get different distributions
46+
].join("|")
3347

3448
const variantIndex = assignVariantIndexDeterministic(testConfig, fingerprint)
3549
const variant = testConfig.variants[variantIndex]
@@ -56,23 +70,22 @@ const assignVariantIndexDeterministic = (
5670
// Handle case where total weight is 0
5771
if (totalWeight === 0) return 0
5872

59-
// Use a better hash function for more uniform distribution
60-
// This is a simple implementation of djb2 hash algorithm
61-
let hash = 5381
73+
// Hash function to evenly distribute fingerprints amongst assignments
74+
// Implementation of FNV-1a hash algorithm
75+
let hash = 2166136261 // FNV offset basis
6276
for (let i = 0; i < fingerprint.length; i++) {
63-
hash = (hash << 5) + hash + fingerprint.charCodeAt(i)
77+
hash ^= fingerprint.charCodeAt(i) // XOR
78+
hash = (hash * 16777619) >>> 0 // FNV prime, ensure 32-bit unsigned
6479
}
6580

66-
// Ensure positive value and create uniform distribution
67-
const normalized = Math.abs(hash) / 0x7fffffff // Max 32-bit signed int
81+
// Convert to uniform distribution [0, 1)
82+
const normalized = hash / 0x100000000 // 2^32 for full 32-bit range
6883
const weighted = normalized * totalWeight
6984

7085
let cumulativeWeight = 0
7186
for (let i = 0; i < config.variants.length; i++) {
7287
cumulativeWeight += config.variants[i].weight
73-
if (weighted <= cumulativeWeight) {
74-
return i
75-
}
88+
if (weighted <= cumulativeWeight) return i
7689
}
7790

7891
return 0

0 commit comments

Comments
 (0)