feat(ab-testing): Flags SDK precompute infrastructure#18807
Conversation
Replaces the legacy ABTestWrapper system (which read headers() at render time, forcing any A/B-tested page into dynamic rendering) with the Flags SDK precompute pattern: the proxy assigns variants at the edge via header fingerprinting and rewrites to signed, statically prerendered variant pages. Experiments stay Matomo-driven (status, weights, scheduling - no deploys) and cookie-less. Merges inert: abTestRoutes is empty, so the proxy no-ops until a route is registered. See docs/ab-testing.md for the full architecture and the recipe for adding an experiment. - src/lib/ab-testing: defineABFlag helper, header fingerprinting with client-IP-only x-forwarded-for (#17612), Matomo adapter with FNV-1a weighted assignment, dot-encoding for signed codes (dotted URL segments + trailingSlash: true trigger an origin 308 that leaks the internal URL - the #17265 failure mode) - ABTest component + cookie-based debug panel (dev/preview) + tracker with custom dimension 1 for segment queries - Removed legacy infra: ABTestWrapper, ClientABTestWrapper, server.ts, /api/ab-config - X-Robots-Tag noindex on /ab-code/* as defense in depth - Rewrote docs/ab-testing.md; updated CLAUDE.md / AGENTS.md Verified end-to-end on the deploy preview of #18777 (spike): silent rewrites, edge cache hits on variant pages, invalid codes 404, non-EN locales untouched.
✅ Deploy Preview for ethereumorg ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
🔎 First-pass review — 🔧 Needs workSolid rework — moves A/B assignment to the edge/precompute path so tested pages stay statically prerendered, and it merges inert (
AnalysisLane: code (proxy.ts, Checked: unused vars (only Non-blocking questions/notes:
CI still running — not commenting on pending checks. Routing to an engineer for the version bump call.
|
|
Addressed in 9e01d52:
Fixes are also propagated to the stacked examples branch (#18808). |
- Restore version 11.14.0 (stale package.json from the spike branch) - Complete defineABFlag JSDoc
8cf047f to
9e01d52
Compare
Deploy previews send a blanket X-Robots-Tag: noindex, so the rule can't be verified pre-production - and if the CDN matched it against the middleware-rewrite target it would noindex the public A/B-tested pages. The rule was redundant defense anyway: direct /ab-code visits already 404 via signature verification and coded pages declare the public URL as canonical.
|
Dropped the |
There was a problem hiding this comment.
@pettinarip looks clean! A few things before the companion PR turns on a real experiment:
1. Matomo config is fetched live in the proxy, uncached and untimed. fetchMatomoExperiments() runs inside decide() → precompute() → the proxy (edge middleware), on every request to a registered route (it's what selects the cached variant page, so it can't be cached itself). Two problems:
cache: "force-cache"is a no-op in middleware — the Next Data Cache isn't available there, so there's no cross-request reuse. Every hit to a tested route (incl. the homepage) adds a blocking Matomo round-trip on the edge critical path. The page render is cached; the variant lookup isn't.- No timeout on the
fetch. A refusal is handled, but a slow/hanging Matomo would stallawait precomputeup to the function timeout on every request to that route.
Can we add a module-level TTL cache (persists within a warm edge instance), an AbortSignal.timeout(...), and dedupe the fetch so multi-flag routes don't multiply calls? Did the #18777 spike give any read on the added edge latency per request?
2. isExperimentActive counts "created" as active. An experiment created but not launched (no start_date) will bucket and track users before it's started. Intended, or should it be "running" only?
Minor:
assignedAt: Date.now()is never read by the tracker, and in the static coded page it bakes in build time anyway — probably drop it fromABTestAssignment.- Debug override value isn't bounded by
numVariants, but it's dev/preview-only andABTestclamps at render — just noting.
Approving once #1 is handled here or explicitly slated as the first task in the companion PR, with a word on #2.
- Opus 4.8 / @wackerow

Summary
Replaces the legacy A/B testing infrastructure with the Flags SDK precompute pattern. This PR merges inert —
abTestRoutesis empty, so the proxy no-ops until an experiment is registered. A companion PR stacked on this branch demos two working experiments on its deploy preview.Why: the legacy
ABTestWrapperreadheaders()at render time, forcing any A/B-tested page into dynamic rendering — running a test on the homepage meant losing edge caching on our highest-traffic page (#17294 → reverted in #17742). With this setup, variants are assigned in the proxy and served as statically prerendered, edge-cached pages. Verified on the #18777 spike preview: variant responses areNetlify Edge: hit.How it works
proxy.tsmatches the path againstabTestRoutes, fingerprints request headers (cookie-less, GDPR-compliant), evaluates the route's flags against Matomo experiment config, and packs the result into an HMAC-signed code/en/ab-code/<code>/<path>— internal-only; the visitor's URL never changesgenerateStaticParams)ABTestrenders the variant;ABTestTrackerpushesAbTesting::enter+ custom dimension 1 to MatomoExperiments stay Matomo-driven: status, traffic weights, and scheduling are dashboard changes, no deploys. On any failure (Matomo down, no experiment, tampered code) everything falls through to the original page.
Scope & safety
/es/,/de/)/ab-code/…visits get case-mangled by the sitewide lowercase 301, fail signature verification, and 404; coded pages re-export the original page's canonical metadata as a second layer.→~): dotted URL segments +trailingSlash: truetrigger an origin 308 that leaks the internal URL — this was the root cause that killed feat(ab-testing): add Vercel Flags SDK integration with Matomo #17265, finally pinned down and fixedChanges
src/lib/ab-testing/:defineABFlaghelper,identifyfingerprinting (client-IP-onlyx-forwarded-for, fix(ab-test): extract client IP from x-forwarded-for header #17612), Matomo adapter (FNV-1a weighted assignment, mock experiments for local dev),encodeABCode/decodeABCodesrc/components/AB/: newABTest, debug panel now drives proxy re-evaluation viaflag_override_*cookies (dev/preview only), tracker hardenedABTestWrapper,ClientABTestWrapper,src/lib/ab-testing/server.ts,app/api/ab-config/(unused since revert: remove homepage A/B test setup #17742)docs/ab-testing.md(architecture + step-by-step recipe); updatedCLAUDE.md/AGENTS.md.env.example:FLAGS_SECRET,USE_MOCK_EXPERIMENTS(both already configured on Netlify)Test plan
History
Supersedes #17265 (closed: blocked by what turned out to be the dot/trailingSlash 308) and the #18777 spike, where this implementation was developed and verified. #17581 (remove trailingSlash) is not needed.
🤖 Generated with Claude Code