The browser runtime should explain itself.

The ABTestly snippet is served from Cloudflare's edge network, then buckets each visitor and applies the variation in their browser. It reapplies cleanly across single page app route changes, records every accepted event, and writes those events to a canonical ledger that your final numbers come from.

The short version. Config is delivered from the edge. Bucketing is deterministic and sticky. Variations apply as JavaScript and CSS in the browser, with cleanup before re-apply on every route change. Every accepted exposure and goal lands in the exact experiment ledger, with no sampling on the numbers you make decisions from.

Edge served, applied in the browser

ABTestly serves its experiment configuration from the edge and runs everything else in the browser. The experiment config is delivered from Cloudflare's network, served across more than 300 cities, with no origin round trip. The snippet itself is about 31 KB gzipped with a loader under 1.5 KB. What actually decides a variant and changes the page happens in the visitor's browser: the runtime reads the config, buckets the visitor, and applies the variation locally. Nothing about a visitor's assignment waits on your server.

The runtime pipeline

An ABTestly experiment moves through the same six stages on every page, and each one is named in the public docs rather than hidden in a black box.

Deterministic, sticky bucketing

Bucketing is the algorithm that turns a visitor into a variant. It is deterministic, the same inputs produce the same output every time, and it uses no shared state, so every visitor's bucket is computed from their stable id alone. ABTestly follows a two step model: first decide whether the visitor enters the experiment, then decide which variant they get.

The result is that a visitor sees the same variant whether they land on the tested page or navigate to it later in the session, and the same variant every time they return. The full method is public in the bucketing docs.

Applying variations

On the initial page load, the snippet boots, evaluates targeting and audiences for each experiment, and for every eligible one computes a bucket, fires an impression beacon, injects the variant's CSS into the head, and runs the variant's JavaScript once. Registered apply callbacks then fire. This is code first: the variation is the JavaScript and CSS you authored, running in place. For how those variations are written, read code first A/B testing with JavaScript and CSS.

Single page apps: cleanup, then reapply

If your site swaps the page in JavaScript instead of doing a full browser navigation, ABTestly supports it natively. With SPA support on, the snippet wraps history.pushState and history.replaceState, listens for back and forward navigation and hash changes, and runs a lifecycle on every detected route change. The order is the whole idea, cleanup, then impression, then apply:

The three helpers make this reliable. onApply runs each time an experiment becomes active on the current route, onCleanup runs when the visitor leaves a tested route and just before it reapplies, and waitFor handles elements that render asynchronously by polling every 50 ms and firing once when the target appears, giving up silently after a timeout that defaults to 5000 ms. Every callback and the route handler itself run inside their own try and catch, so a mistake in one experiment cannot break the page or another experiment. The full reference is the single page apps docs.

Event capture

The runtime records two kinds of event, exposures and goal fires, each carrying its full context: the variant key, and for revenue goals the order id and currency. Goals can fire declaratively from the dashboard or from your own code with abtestly.trackGoal(key, valueOrOpts), where an order id enables server side revenue deduplication so a retried beacon does not double count. On a route change, impressions batch into a single beacon per route, and page visit goals evaluate again against the new URL while listener goals stay live from initial load. Sensitive fields are redacted, and a variant error is reported back on first occurrence per session so a variant that breaks on a real page gets caught early.

The exact experiment ledger

Every event that arrives is written to two places. Cloudflare Analytics Engine is fast and cheap to query and drives the live counters on the dashboard. The exact experiment ledger is the canonical store your final numbers come from. Analytics Engine is optimized for a huge write rate and can sample its own data under very heavy bursts, which is Cloudflare's documented behaviour rather than a worry of ours: at very high volumes it downsamples to maintain performance, and marks the rate on each row, which is fine for a live counter and less fine for a report. The ledger is optimized for correctness: every accepted event, in order, retrievable.

The ledger keeps two things on different clocks. The per visitor records your results are computed from persist for the life of the experiment, so a number you read today is the same number you read a year ago. The raw individual events behind them are archived for 24 months and then purged nightly, which is what raw event export and audits read from. Both are stated in the privacy policy. For any experiment you have run, the ledger contains every exposure and every goal that was accepted, and the statistics are computed against that set, with no sampling step on any surface you make decisions from. See the exact ledger docs.

Debug it on the page

When something looks off, open devtools and call window.__abtestly.debug() right on the page. It tells you whether this visitor was even in the test, whether the change actually ran, and whether the result got tracked. The runtime object is a stable public surface you can call from variant JavaScript or your own scripts, documented in the window.abtestly API reference.

Quick answers

Does the experiment run at the edge?

The config is served from Cloudflare's edge network, but the bucketing and the variation apply both happen in the visitor's browser. The edge delivers the config with no origin round trip. The browser decides the variant and changes the page.

Will a visitor always see the same variant?

Yes. Bucketing is deterministic and sticky. It is computed from a per experiment salt and a stable visitor id, so the same visitor gets the same variant whether they land on the tested page or navigate to it later, and every time they return, unless they clear their cookies and localStorage.

How does it stay correct on a single page app?

On every route change the runtime cleans up, then records impressions before any variant code, then reapplies. Recording impressions first means a broken variant on one arm cannot undercount that arm, which protects against a sample ratio mismatch.

Are the results sampled?

No. Live dashboard counters read Analytics Engine, which can sample under heavy bursts, but every accepted event is also written to the exact experiment ledger, and the verdict is computed against that complete set with no sampling.

Behavior on this page reflects current ABTestly developer documentation. The public docs name the methods, the event path, the statistics, and the operational limits.

see it on your page

Read how it works
before you install it.

Drop the snippet on one page, open devtools, and watch the runtime bucket, apply, and track in real time.