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.
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.
- Config. The experiment config is delivered from the edge.
- Assign. A deterministic, sticky split places the visitor in a variant.
- Apply. The variant's CSS and JavaScript run in the browser.
- Measure. Every accepted event is recorded.
- Ledger. Accepted events land in the unsampled canonical store.
- Verdict. The results page computes the decision against that store.
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 hash. MurmurHash3, well known, fast, and uniformly distributed for short strings. Two different suffixes separate the allocation decision from the variant decision, so raising allocation does not shuffle who already sits in which variant.
- A per experiment salt. Every experiment gets its own immutable salt at creation, which breaks the correlation where a visitor would otherwise always land in the same slot across tests. Cloning an experiment generates a new salt on purpose.
- A stable visitor id. A UUIDv7, the time ordered identifier standardised in RFC 9562, written to localStorage and a cookie on first visit, resolved to the registrable domain so subdomains share one id.
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:
- Cleanup first. Every cleanup callback for experiments that were active on the previous route fires, including those still active on the new route, so they reapply with no double bound listeners or duplicated styles. CSS the editor injected is removed automatically for truly deactivated experiments.
- Impressions before variant code. Targeting is evaluated again against the new URL and impressions fire for every eligible experiment in a first pass, before any variant DOM work, so a broken variant on one arm can never undercount impressions on that arm. That is how the runtime prevents a sample ratio mismatch from a silently undercounted arm.
- Reapply. CSS is injected again and apply callbacks fire again for every eligible experiment. Variant JavaScript does not run again, only the registered callbacks do, which stops callback registrations stacking on every navigation.
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.
- Trustworthy verdicts. The results page reads the ledger for its numbers, so a winner is only called against the complete accepted set.
- Raw event export. The export reads the ledger's archives, giving you the same rows it held.
- Audits. If a compliance team needs every event an experiment saw, the ledger is the source of truth.
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.