From 8a0d240f0fc59cc70412ddaf3baa56a2f2055988 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:33:32 -0700 Subject: [PATCH 1/2] fix: return fallback config if not production --- app/api/ab-config/route.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/ab-config/route.ts b/app/api/ab-config/route.ts index 47fe7f9a01b..318b8061dc2 100644 --- a/app/api/ab-config/route.ts +++ b/app/api/ab-config/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server" -import { IS_PREVIEW_DEPLOY } from "@/lib/utils/env" +import { IS_PREVIEW_DEPLOY, IS_PROD } from "@/lib/utils/env" import type { ABTestConfig, MatomoExperiment } from "@/lib/ab-testing/types" @@ -33,7 +33,8 @@ const getPreviewConfig = () => ({ export async function GET() { // Preview mode: Show menu with original default - if (IS_PREVIEW_DEPLOY) return NextResponse.json(getPreviewConfig()) + if (!IS_PROD || IS_PREVIEW_DEPLOY) + return NextResponse.json(getPreviewConfig()) try { const matomoUrl = process.env.NEXT_PUBLIC_MATOMO_URL From 78b5ea83e321b5b86788785013cd4750f033be02 Mon Sep 17 00:00:00 2001 From: Paul Wackerow <54227730+wackerow@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:39:50 -0700 Subject: [PATCH 2/2] 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 --- src/lib/ab-testing/server.ts | 37 ++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/lib/ab-testing/server.ts b/src/lib/ab-testing/server.ts index 42df90fb1d6..8dd9032787f 100644 --- a/src/lib/ab-testing/server.ts +++ b/src/lib/ab-testing/server.ts @@ -24,12 +24,26 @@ export const getABTestAssignment = async ( if (!testConfig || !testConfig.enabled) return null - // Create deterministic assignment using IP + User-Agent fingerprint + // Create deterministic assignment using enhanced fingerprint const headers = await import("next/headers").then((m) => m.headers()) - const userAgent = headers.get("user-agent") || "" + + // Get IP and user agent (primary identifier) const forwardedFor = headers.get("x-forwarded-for") || headers.get("x-real-ip") || "unknown" - const fingerprint = `${forwardedFor}-${userAgent}` + const userAgent = headers.get("user-agent") || "" + + // Add privacy-preserving entropy sources + const acceptLanguage = headers.get("accept-language") || "" + const acceptEncoding = headers.get("accept-encoding") || "" + + // Create enhanced fingerprint with more entropy + const fingerprint = [ + forwardedFor, + userAgent, + acceptLanguage, + acceptEncoding, + testKey, // Include test key to ensure different tests get different distributions + ].join("|") const variantIndex = assignVariantIndexDeterministic(testConfig, fingerprint) const variant = testConfig.variants[variantIndex] @@ -56,23 +70,22 @@ const assignVariantIndexDeterministic = ( // Handle case where total weight is 0 if (totalWeight === 0) return 0 - // Use a better hash function for more uniform distribution - // This is a simple implementation of djb2 hash algorithm - let hash = 5381 + // Hash function to evenly distribute fingerprints amongst assignments + // Implementation of FNV-1a hash algorithm + let hash = 2166136261 // FNV offset basis for (let i = 0; i < fingerprint.length; i++) { - hash = (hash << 5) + hash + fingerprint.charCodeAt(i) + hash ^= fingerprint.charCodeAt(i) // XOR + hash = (hash * 16777619) >>> 0 // FNV prime, ensure 32-bit unsigned } - // Ensure positive value and create uniform distribution - const normalized = Math.abs(hash) / 0x7fffffff // Max 32-bit signed int + // Convert to uniform distribution [0, 1) + const normalized = hash / 0x100000000 // 2^32 for full 32-bit range const weighted = normalized * totalWeight let cumulativeWeight = 0 for (let i = 0; i < config.variants.length; i++) { cumulativeWeight += config.variants[i].weight - if (weighted <= cumulativeWeight) { - return i - } + if (weighted <= cumulativeWeight) return i } return 0