editor: experiments QA panel (P0 of experiments plan)

New Experiments section in the rail: lists tracking experiment rules
(status, traffic, arms, targeting), shows this session's assignment via
evaluate-experiments, per-arm analytics with winner/significance, a QA
arm override (sets the storefront variant cookie), and a two-step
pause/resume kill-switch. Admin reads degrade when the tracking gate
refuses; eligibility always works. Mock tracking endpoints + harness
tests.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
mats committed 2026-11-07 12:00:37 +01:00
1 parent 40b33f89a4
commit 922562e1c2
6 files changed
+325 -1

No files matched your search

+43
View File
@@ -15,6 +15,11 @@ export interface MockOptions {
export function createMockCms(opts: MockOptions = {}) {
let graph = seedGraph()
const published: string[] = []
// --- mock slask-tracking experiments (P0 QA panel) ---
const expRules: { experiment_id: string; targeting?: { field: string; op: string; value?: string }[]; traffic_fraction?: number; variant_weights?: Record<string, number>; disabled?: boolean }[] = [
{ experiment_id: 'exp_hero', targeting: [{ field: 'page_path', op: 'prefix', value: '/' }], traffic_fraction: 1, variant_weights: { a: 1, b: 1 } },
{ experiment_id: 'exp_checkout', targeting: [{ field: 'country', op: 'eq', value: 'SE' }], traffic_fraction: 0.5, variant_weights: { control: 9, onepage: 1 }, disabled: true },
]
const requests: string[] = []
let previewApi = true
// --- mock go-auth (RFC 8628 device grant) ---
@@ -102,6 +107,44 @@ export function createMockCms(opts: MockOptions = {}) {
}
return json(200, walk(r[1]))
}
if (path === '/api/tracking/config' && m === 'GET') return json(200, { experiment_rules: expRules })
if (path === '/api/tracking/evaluate-experiments' && m === 'POST') {
const body = (await req.json()) as { experiments?: { id: string; cookieName?: string }[] }
const cookies = Object.fromEntries((req.headers.get('cookie') ?? '').split(';').map((p) => p.trim().split('=').map((s) => s.trim())).filter((kv) => kv.length === 2)) as Record<string, string>
const assignments = (body.experiments ?? []).map((e) => {
const rule = expRules.find((r) => r.experiment_id === e.id)
const cookieName = e.cookieName || `exp_${e.id}`
if (!rule || rule.disabled) return { experimentId: e.id, cookieName, variant: '', eligible: false }
const arms = Object.keys(rule.variant_weights ?? { a: 1, b: 1 })
const forced = cookies[cookieName]
return { experimentId: e.id, cookieName, variant: forced && arms.includes(forced) ? forced : arms[0], eligible: true }
})
return json(200, { assignments })
}
if (path === '/api/tracking/experiments/analytics' && m === 'GET')
return json(200, [
{
experiment_id: 'exp_hero',
variants: [
{ variant: 'a', sessions: 120, conversions: 12, conversion_rate: 0.1 },
{ variant: 'b', sessions: 130, conversions: 22, conversion_rate: 0.169 },
],
winner: 'b',
winner_probability: 0.87,
significant: true,
conclusion: 'b beats a',
total_sessions: 250,
},
])
const expToggle = /^\/api\/tracking\/config\/experiments\/([^/]+)\/disabled$/.exec(path)
if (expToggle && m === 'PATCH') {
const rule = expRules.find((x) => x.experiment_id === decodeURIComponent(expToggle[1]))
if (!rule) return json(404, { error: 'experiment rule not found' })
const body = (await req.json()) as { disabled?: boolean }
if (typeof body.disabled !== 'boolean') return json(422, { error: 'missing disabled' })
rule.disabled = body.disabled
return json(200, { disabled: rule.disabled })
}
if (path === '/api/edit' && m === 'POST') {
const body = (await req.json()) as { ops: any[] }
const res = graph.edit(body.ops)