Skip to content

Commit 54a0806

Browse files
authored
Merge pull request #15927 from ethereum/hot-matomo-ab
hot-fix: Matomo ab testing assignment distribution
2 parents 675798d + 154255f commit 54a0806

File tree

2 files changed

+28
-14
lines changed

2 files changed

+28
-14
lines changed

app/api/ab-config/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NextResponse } from "next/server"
22

3-
import { IS_PREVIEW_DEPLOY } from "@/lib/utils/env"
3+
import { IS_PREVIEW_DEPLOY, IS_PROD } from "@/lib/utils/env"
44

55
import type { ABTestConfig, MatomoExperiment } from "@/lib/ab-testing/types"
66

@@ -33,7 +33,8 @@ const getPreviewConfig = () => ({
3333

3434
export async function GET() {
3535
// Preview mode: Show menu with original default
36-
if (IS_PREVIEW_DEPLOY) return NextResponse.json(getPreviewConfig())
36+
if (!IS_PROD || IS_PREVIEW_DEPLOY)
37+
return NextResponse.json(getPreviewConfig())
3738

3839
try {
3940
const matomoUrl = process.env.NEXT_PUBLIC_MATOMO_URL

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)