Skip to main content
Every variant’s JavaScript runs inside a function that gets a small set of names passed in as function arguments. These are the runtime bindings.

The names

Legacy aliases exist for backwards compatibility (EXP_ID, VARIANT_KEY), do not rely on them for new code.

The order

If you inspect the compiled variant function, the parameters land at specific positions. The exp key is arguments[4]. This ordering is stable, a new binding at a later position does not shift existing ones. Rule of thumb: reference the bindings by name, not by position. The names are stable public API; the positions are an implementation detail.

Using them

Inside variant JS:

Why the key and not just the UUID

The UUID is stable but not human-readable. The key is short, human-readable, and immutable, cloning an experiment mints a new key so referencing by key does not silently target the wrong test. If your goal is “log which variant applied”, the key is what a human reading logs wants to see. If your goal is “call an internal API that stores per-experiment state”, either works, the UUID is a slightly better primary key because it is opaque and cannot be confused with anything else.

What is not in scope

  • this. The variant function’s this is set to the runtime’s internal apply context, not window. Reach for window explicitly if you need it.
  • Any DOM helper. Use document.querySelector, abtestly.waitFor, or your own libraries, the runtime does not inject jQuery-shaped shortcuts into scope.
  • A framework’s runtime. React, Vue, and friends are not auto-imported. If your variant needs a framework, load it via Dev libraries or from the page itself.

onApply callback signature

If you use abtestly.onApply(...), the callback receives an ApplyContext object with:
  • experimentId, the UUID, same as the binding above. The short key is not on the context, use the ABTESTLY_EXP_KEY binding.
  • variantKey, same as the binding.
  • routeRevision, a monotonic counter that ticks on every SPA route change. Compare against it inside an async apply to detect supersession:

onCleanup, undoing what you applied

abtestly.onCleanup(experimentIdOrKey, fn) registers the teardown half of a variant. The first argument accepts either the UUID or the short key, same as onApply. The callback takes no arguments, there is no ApplyContext on this side, so anything you need at teardown time you close over when you register. When it fires. SPA route changes, and nothing else. A route change means pushState, replaceState, popstate, or hashchange landing on a URL that differs from the current one. There is no teardown on tab close or pagehide, and none when an experiment is paused, archived, or unpublished mid-session, those only take effect on the visitor’s next full page load. On each route change the runtime fires onCleanup for every experiment that was active on the previous route, including the ones still active on the new route. That is deliberate: those experiments are about to have their onApply callbacks fire again, and cleaning up first is what stops listeners and injected nodes stacking. Do not read a cleanup as “this experiment is over”. Handlers fire in registration order, first registered, first to run. What the runtime removes for you. One thing: the <style data-abtestly-exp="<id>"> tag holding the variant’s CSS field, and only when the experiment has truly deactivated, meaning it was eligible on the old route and is not eligible on the new one. An experiment that stays eligible has its tag re-stamped instead. That is the whole of the automatic teardown. What stays yours. Variant JavaScript does not re-run on a route change, only your registered callbacks do. This is what makes top-level setup a bug rather than a matter of taste. Code at the top of your variant JS runs exactly once, on the initial page load, so it never gets a second chance against the DOM your framework rendered for the next route, and nothing the runtime does will put it back. Before, the listener is added again every time the visitor navigates back to a tested route, so on a persistent element one click fires the handler twice, then three times:
After:
For the full ordered sequence of a route change, cleanup, re-eval, CSS, apply, goals, see SPA lifecycle.

Stability

The three canonical names, ABTESTLY_EXP_ID, ABTESTLY_EXP_KEY, ABTESTLY_VARIANT_KEY, are stable public API. They do not change without a major version bump. Legacy aliases stay for at least one major version.
Last modified on August 1, 2026