Skip to content

Commit 3ea9aac

Browse files
committed
chore: ab-testing clean up and patches
1 parent 167da73 commit 3ea9aac

File tree

8 files changed

+113
-263
lines changed

8 files changed

+113
-263
lines changed

app/api/ab-config/route.ts

Lines changed: 53 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { NextResponse } from "next/server"
22

3+
import { IS_PREVIEW_DEPLOY } from "@/lib/utils/env"
4+
35
type MatomoExperiment = {
46
idexperiment: string
57
name: string
@@ -21,31 +23,37 @@ type ABTestConfig = {
2123
}>
2224
}
2325

24-
function isExperimentActive(experiment: MatomoExperiment): boolean {
26+
const isExperimentActive = (experiment: MatomoExperiment): boolean => {
2527
const now = new Date()
2628

2729
// Check start date - if scheduled for future, not active yet
2830
if (experiment.start_date) {
2931
const startDate = new Date(experiment.start_date)
30-
if (now < startDate) {
31-
return false // Not started yet
32-
}
32+
if (now < startDate) return false
3333
}
3434

3535
// Check end date - if past end date, not active anymore
3636
if (experiment.end_date) {
3737
const endDate = new Date(experiment.end_date)
38-
if (now > endDate) {
39-
return false // Already ended
40-
}
38+
if (now > endDate) return false
4139
}
4240

4341
// If no scheduling constraints, enabled if created or running
44-
// If within time window, enabled if running
4542
return ["created", "running"].includes(experiment.status)
4643
}
4744

45+
const getPreviewConfig = () => ({
46+
AppTest: {
47+
id: "preview",
48+
enabled: true,
49+
variants: [{ name: "Original", weight: 100 }],
50+
},
51+
})
52+
4853
export async function GET() {
54+
// Preview mode: Show menu with original default
55+
if (IS_PREVIEW_DEPLOY) return NextResponse.json(getPreviewConfig())
56+
4957
try {
5058
const matomoUrl = process.env.NEXT_PUBLIC_MATOMO_URL
5159
const apiToken = process.env.MATOMO_API_TOKEN
@@ -57,95 +65,65 @@ export async function GET() {
5765
)
5866
}
5967

60-
// Get the site ID from environment
61-
const siteId = process.env.NEXT_PUBLIC_MATOMO_SITE_ID || "1"
62-
63-
// Try different API methods for A/B testing
64-
const apiMethods = [
65-
"ExperimentsPlatform.getExperiments",
66-
"AbTesting.getExperiments",
67-
"Experiments.getExperiments",
68-
`AbTesting.getAllExperiments&idSite=${siteId}`,
69-
]
70-
71-
let experiments: MatomoExperiment[] = []
72-
let apiError = null
73-
74-
for (const method of apiMethods) {
75-
// Add cache busting for development
76-
const cacheBuster =
77-
process.env.NODE_ENV === "development" ? `&cb=${Date.now()}` : ""
78-
const matomoApiUrl = `${matomoUrl}/index.php?module=API&method=${method}&format=json&token_auth=${apiToken}${cacheBuster}`
79-
80-
try {
81-
const response = await fetch(matomoApiUrl, {
82-
next: {
83-
revalidate: process.env.NODE_ENV === "development" ? 0 : 3600,
84-
}, // No cache in dev
85-
headers: { "User-Agent": "ethereum.org-ab-testing/1.0" },
86-
})
87-
88-
const data = await response.json()
89-
90-
if (data.result !== "error" && Array.isArray(data)) {
91-
experiments = data
92-
break
93-
} else if (data.result === "error") {
94-
apiError = data.message
95-
}
96-
} catch (error) {
97-
// Continue to next method
98-
}
99-
}
68+
const siteId = process.env.NEXT_PUBLIC_MATOMO_SITE_ID || "4"
10069

101-
// If no API method worked, use fallback
102-
if (experiments.length === 0) {
103-
console.warn(
104-
`[AB Config] All API methods failed. Last error: ${apiError}`
105-
)
70+
// Add cache busting for development
71+
const cacheBuster =
72+
process.env.NODE_ENV === "development" ? `&cb=${Date.now()}` : ""
73+
const matomoApiUrl = `${matomoUrl}/index.php?module=API&method=AbTesting.getAllExperiments&idSite=${siteId}&format=json&token_auth=${apiToken}${cacheBuster}`
10674

107-
const fallbackConfig = {}
75+
const response = await fetch(matomoApiUrl, {
76+
next: { revalidate: process.env.NODE_ENV === "development" ? 0 : 3600 },
77+
headers: { "User-Agent": "ethereum.org-ab-testing/1.0" },
78+
})
10879

109-
return NextResponse.json(fallbackConfig, {
110-
headers: {
111-
"Cache-Control": "s-max-age=300, stale-while-revalidate=600",
112-
},
113-
})
80+
const data = await response.json()
81+
82+
if (data.result === "error" || !Array.isArray(data)) {
83+
console.error(
84+
"[AB Config] Matomo API error:",
85+
data.message || "Invalid response"
86+
)
87+
return NextResponse.json(
88+
{},
89+
{
90+
headers: {
91+
"Cache-Control": "s-max-age=300, stale-while-revalidate=600",
92+
},
93+
}
94+
)
11495
}
11596

97+
const experiments: MatomoExperiment[] = data
98+
11699
// Transform Matomo experiments to our config format
117100
const config: Record<string, ABTestConfig> = {}
118101

119-
experiments.forEach((exp) => {
120-
// Include all experiments with variations (let scheduling handle timing)
121-
if (exp.variations && exp.variations.length > 0) {
102+
experiments
103+
.filter((exp) => exp.variations && exp.variations.length > 0)
104+
.forEach((exp) => {
122105
// Calculate Original variant weight (100% - sum of all variations)
123106
const variationsTotalWeight = exp.variations.reduce(
124-
(sum, variation) => {
125-
return sum + (variation.percentage || 0)
126-
},
107+
(sum, variation) => sum + (variation.percentage || 0),
127108
0
128109
)
129110
const originalWeight = 100 - variationsTotalWeight
130111

131112
// Build variants array starting with "Original"
132-
const variants = [{ name: "Original", weight: originalWeight }]
133-
134-
// Add variations from Matomo (use actual percentages)
135-
exp.variations.forEach((variation) => {
136-
variants.push({
113+
const variants = [
114+
{ name: "Original", weight: originalWeight },
115+
...exp.variations.map((variation) => ({
137116
name: variation.name,
138117
weight: variation.percentage || 0,
139-
})
140-
})
118+
})),
119+
]
141120

142121
config[exp.name] = {
143122
id: exp.idexperiment,
144123
enabled: isExperimentActive(exp),
145-
variants: variants,
124+
variants,
146125
}
147-
}
148-
})
126+
})
149127

150128
return NextResponse.json(config, {
151129
headers: {

src/components/AB/TestDebugPanel.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,16 @@ import { Button } from "../ui/buttons/Button"
88

99
import { useLocalStorage } from "@/hooks/useLocalStorage"
1010
import { useOnClickOutside } from "@/hooks/useOnClickOutside"
11-
import { ABTestAssignment } from "@/lib/ab-testing/types"
1211

1312
type ABTestDebugPanelProps = {
1413
testKey: string
15-
currentAssignment: ABTestAssignment | null
1614
availableVariants: string[]
1715
}
1816

19-
export function ABTestDebugPanel({
17+
export const ABTestDebugPanel = ({
2018
testKey,
2119
availableVariants,
22-
}: ABTestDebugPanelProps) {
20+
}: ABTestDebugPanelProps) => {
2321
const [isOpen, setIsOpen] = useState(false)
2422
const [selectedVariant, setSelectedVariant] = useLocalStorage<number | null>(
2523
`ab-test-${testKey}`,
@@ -29,9 +27,8 @@ export function ABTestDebugPanel({
2927

3028
useOnClickOutside(panelRef, () => setIsOpen(false))
3129

32-
const forceVariant = (variantIndex: number) => {
30+
const forceVariant = (variantIndex: number) =>
3331
setSelectedVariant(variantIndex)
34-
}
3532

3633
return (
3734
<div
@@ -48,7 +45,7 @@ export function ABTestDebugPanel({
4845
{isOpen && (
4946
<div className="mt-2.5">
5047
<div>
51-
<strong>Test:</strong> {testKey}
48+
<strong>Experiment:</strong> {testKey}
5249
</div>
5350
<div>
5451
<strong>Select variant:</strong>

src/components/AB/TestTracker.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,10 @@ import { ABTestAssignment } from "@/lib/ab-testing/types"
99

1010
type ABTestTrackerProps = {
1111
assignment: ABTestAssignment
12-
testKey?: string
1312
}
1413

15-
export function ABTestTracker({ assignment, testKey }: ABTestTrackerProps) {
14+
export function ABTestTracker({ assignment }: ABTestTrackerProps) {
1615
useEffect(() => {
17-
// Don't set cookies here - let server handle cookie persistence
18-
// This component only handles Matomo tracking
19-
2016
if (!IS_PROD || IS_PREVIEW_DEPLOY) {
2117
console.debug(
2218
`DEV [Matomo] A/B test logged - Experiment: ${assignment.experimentName}, Variant: ${assignment.variant}`
@@ -39,7 +35,7 @@ export function ABTestTracker({ assignment, testKey }: ABTestTrackerProps) {
3935
variation: assignment.variant,
4036
},
4137
] as [string, Record<string, string>])
42-
}, [assignment, testKey])
38+
}, [assignment])
4339

4440
return null // This component doesn't render anything
4541
}

src/components/AB/TestWrapper.tsx

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import { ClientABTestWrapper } from "./ClientABTestWrapper"
66
import { ABTestDebugPanel } from "./TestDebugPanel"
77
import { ABTestTracker } from "./TestTracker"
88

9-
import { getABTestConfigs } from "@/lib/ab-testing/config"
10-
import { getABTestAssignment, getVariantIndex } from "@/lib/ab-testing/server"
9+
import {
10+
getABTestAssignment,
11+
getABTestConfigs,
12+
getVariantIndex,
13+
} from "@/lib/ab-testing/server"
1114
import { ABTestVariants } from "@/lib/ab-testing/types"
1215

1316
type ABTestWrapperProps = {
@@ -22,34 +25,28 @@ const ABTestWrapper = async ({
2225
fallback,
2326
}: ABTestWrapperProps) => {
2427
try {
25-
// Get deterministic assignment (cookie-less, based on fingerprint)
26-
const assignment = await getABTestAssignment(testKey)
28+
// Get deterministic assignment and configs
29+
const [assignment, configs] = await Promise.all([
30+
getABTestAssignment(testKey),
31+
getABTestConfigs(),
32+
])
2733

28-
if (!assignment) {
29-
// If no assignment, render fallback
30-
return <>{fallback || variants[0]}</>
31-
}
34+
if (!assignment) throw new Error("No AB test assignment found")
3235

3336
// Find the variant index based on the assignment
34-
const variantIndex = getVariantIndex(assignment.variant, testKey)
35-
36-
// Get available variants for debug panel
37-
const configs = await getABTestConfigs()
37+
const variantIndex = getVariantIndex(assignment.variant, configs, testKey)
3838
const availableVariants =
3939
configs[testKey]?.variants.map((v) => v.name) || []
4040

4141
return (
4242
<>
43-
{/* Track assignment - only in production, not in preview */}
44-
{!IS_PREVIEW_DEPLOY && (
45-
<ABTestTracker assignment={assignment} testKey={testKey} />
46-
)}
43+
{/* Analogous to <Matomo /> at app layout level, pushes "AbTesting::enter" in production */}
44+
<ABTestTracker assignment={assignment} />
4745

4846
{/* Preview panel for development and preview deploys */}
4947
{(!IS_PROD || IS_PREVIEW_DEPLOY) && (
5048
<ABTestDebugPanel
5149
testKey={testKey}
52-
currentAssignment={assignment}
5350
availableVariants={availableVariants}
5451
/>
5552
)}

src/lib/ab-testing/actions.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)