Skip to main content
Four pieces do the work.

The dashboard

The dashboard at app.abtestly.com is where you build tests, pick goals, invite teammates, and read results. Every save is autosaved. Nothing goes live until you click Start or a scheduled start time arrives. Publishing writes a fresh config blob to Cloudflare KV, which the snippet reads on the visitor’s next request. The propagation window is on the order of sixty seconds across the edge. Republish always works; if the publish call fails on the network, an outbox retries it in the background until it succeeds so you never end up with a “saved but not live” experiment.

The snippet

The snippet is a small tag you drop into <head>. It fetches your site’s config from Cloudflare’s edge (cached, no origin round-trip), decides which variant a visitor should see, applies your JavaScript and CSS, and fires an exposure event. The runtime is around 15 KB gzipped. It has no third-party dependencies. It can wait for consent before firing, and it respects a visitor opt-out URL that a person can pass to remove themselves from every test on your site. The snippet also handles single-page apps. When you turn on SPA mode, a route change is treated like a new page load. JavaScript is cleaned up, CSS is torn down, and the next route’s variants are applied. That is one of the more subtle parts of the runtime; the SPA lifecycle page walks through it in detail.

Ingest

Events go to api.abtestly.com. There are only a handful of endpoints and they are all simple: Beacons use text/plain on purpose so that sendBeacon works without a CORS preflight. Payload shapes are documented on the beacon protocol page for anyone building their own integration.

Results

Every event ABTestly accepts is written to two places. The first is Cloudflare Analytics Engine, which is fast and cheap to query. It is what the dashboard reads for live-updating counters. Under very heavy write bursts, AE can sample its own data, that is fine for a live counter, not fine for a bill-of-materials. The second is the Exact Experiment Ledger. Events go through a queue, land in R2 as gzipped NDJSON, and get projected into a per-experiment Durable Object. The ledger is what your final numbers come from, no sampling, no approximation. If the ledger is a little behind while it catches up on a burst, the results page tells you that in plain English instead of quietly serving old numbers. The results page turns those events into a verdict using one of three statistics engines: frequentist, sequential, or Bayesian. You pick the engine per experiment. Verdict text is written in plain English, and it is honest about what it can and cannot say yet.

Where each piece lives in the app

Sites & experiments

Every site you own, and every test attached to it.

Goals, locations, audiences

Reusable definitions your experiments attach to.

Settings

Per-site config: snippet, analytics adapters, SPA, global JS, blocked IPs, opt-out URLs.

Team & access

Members, roles, and twenty per-site permission keys.

What is deliberately not on this page

Sampling and approximations. ABTestly counts every event you send it. The results page can trail live counters by a few seconds while the ledger projects; that is not the same thing as dropping data. Any implicit “trust us” step. The methodology behind the verdict is documented in full, including which formula is applied to your numbers today, and locked per-experiment so an engine upgrade cannot silently change a running test’s math. If you want the developer’s version, bucket hashing, the wire format, runtime bindings, start with the developer reference.