/Docs

Utilities (avsb.utils)

avsb.utils is a toolkit of helpers purpose-built for experiment code. Every helper is also available as options.utils inside trigger and variation functions, where observers and listeners registered through it are automatically torn down when the variation is removed or a SPA navigation fires — no manual cleanup needed.

The toolkit is grouped into six areas: Wait, Timing, DOM, Events, Data, and Log.

Use options.utils inside variation code
Inside trigger and variation functions, prefer options.utils.* over avsb.utils.*. Listeners and observers created through options.utils are automatically cleaned up when the variation lifecycle ends, preventing memory leaks and stale callbacks on SPA-navigated pages.

Wait

Helpers for deferring work until the DOM or a JavaScript value is ready. All return a GuardedThenable whose .catch() is optional — failures are logged gracefully instead of raising unhandled-rejection errors.

utils.waitUntil(target, opts?)

The primary wait primitive. Waits for a CSS selector, a window.x.y global path, or a predicate function to resolve, then chains with .then(). Pass an array of targets to wait for all of them at once. Default timeout is 10 000 ms.

javascript
1// CSS selector — resolves with the element
2options.utils.waitUntil('.price-block').then((el) => {
3 el.textContent = 'From $9/mo';
4});
5
6// window path — resolves with the value
7options.utils.waitUntil('window.dataLayer').then((dl) => {
8 dl.push({ event: 'exp_viewed' });
9});
10
11// Predicate function — resolves with the return value
12options.utils.waitUntil(() => window.Stripe?.isReady).then(() => {
13 showPaymentForm();
14});
15
16// Array of targets — resolves with an array of results
17options.utils.waitUntil(['.cart', 'window.user']).then(([cart, user]) => {
18 cart.querySelector('.name').textContent = user.firstName;
19});

See the Wait Until reference for the full parameter table, timeout options, and migration notes from the old callback API.

utils.poll(target, opts?)

Alias of waitUntil. Provided to ease copy-paste migration from Qubit experiment code that used a poll() helper. Identical in every other respect.

javascript
1// Qubit-style code — works without modification
2options.utils.poll('.hero', { timeout: 5000 }).then((el) => {
3 el.classList.add('variant');
4});

utils.waitForElement(selector, opts?)

Focused version of waitUntil for a single CSS selector. Supports a custom root element for scoped queries (including shadow DOM roots). Resolves with the matched Element.

javascript
1// Scope the search to a shadow root
2const host = document.querySelector('checkout-widget');
3options.utils.waitForElement('button[type="submit"]', {
4 root: host.shadowRoot,
5 timeout: 8000,
6}).then((btn) => {
7 btn.textContent = 'Place Order';
8});

utils.onMutation(selector, callback, opts?)

Runs callback(el) for every element matching selector that is present now and for every new match added to the DOM in the future via a MutationObserver. Useful for dynamically-rendered lists or carousels where new items appear after the initial page load.

Supports shadow DOM via opts.root. Pass opts.once: true to fire only on the first match. Pass opts.attributes: true to also fire when attributes on matching elements change.

javascript
1// Badge every product card that arrives — now and in the future
2const handle = options.utils.onMutation('.product-card', (card) => {
3 const badge = document.createElement('span');
4 badge.className = 'variant-badge';
5 badge.textContent = 'New';
6 card.prepend(badge);
7});
8
9// The observer is stopped automatically on variation removal.
10// To stop it early:
11handle.stop();

Timing

Utilities for controlling when and how often functions run. All returned handles are auto-cancelled when the variation is removed.

utils.once(fn)

Wraps a function so it executes at most once. Subsequent calls are silently ignored. Returns a wrapped version of the original function with the same signature.

javascript
1const applyOnce = options.utils.once((el) => {
2 el.classList.add('highlighted');
3});
4
5// Calling multiple times is safe — only the first call takes effect
6applyOnce(el);
7applyOnce(el);

utils.throttle(fn, ms)

Returns a throttled version of fn that fires at most once per ms milliseconds. Comes with a .cancel() method to discard a pending invocation.

javascript
1const onScroll = options.utils.throttle(() => {
2 const y = window.scrollY;
3 stickyBanner.classList.toggle('visible', y > 300);
4}, 100);
5
6window.addEventListener('scroll', onScroll);

utils.debounce(fn, ms)

Returns a debounced version of fn that waits ms milliseconds after the last call before firing. Comes with a .cancel() method.

javascript
1const onInput = options.utils.debounce((e) => {
2 trackSearch(e.target.value);
3}, 300);
4
5searchInput.addEventListener('input', onInput);

utils.raf(fn)

Schedules fn with requestAnimationFrame and returns a { cancel() } handle. Use for DOM writes that should happen on the next paint.

javascript
1options.utils.raf(() => {
2 // Runs on the next animation frame — minimises layout thrash
3 heroEl.style.setProperty('--accent', '#e63');
4});

utils.idle(fn, opts?)

Schedules fn via requestIdleCallback (or a setTimeout fallback). Accepts an optional timeout so the callback runs even if the browser is never truly idle. Returns a { cancel() } handle.

javascript
1// Track an impression without blocking the critical path
2options.utils.idle(() => {
3 avsb.track.event(42);
4}, { timeout: 2000 });

utils.domReady()

Returns a GuardedThenable that resolves when DOMContentLoaded has fired (or immediately if it already has). Useful when variation code may run before the document is fully parsed.

javascript
1options.utils.domReady().then(() => {
2 // Safe to query the full DOM here
3 const footer = document.querySelector('footer');
4 if (footer) footer.prepend(ctaBanner);
5});

utils.retry(fn, opts?)

Calls an async or sync function repeatedly until it resolves without throwing. Options: attempts (default 3), delay in ms between retries (default 0), and factor for exponential back-off (default 1). Returns a native Promise.

javascript
1options.utils.retry(
2 () => fetch('/api/prices').then((r) => r.json()),
3 { attempts: 3, delay: 500, factor: 2 }
4).then((data) => {
5 updatePrices(data);
6});

DOM

Helpers for reading and modifying the DOM. Prefer these over raw browser APIs — they have consistent null-safety and the sanitisation built into setHtml prevents accidental XSS from third-party data.

utils.$(selector, root?) and utils.$$(selector, root?)

Shorthand for querySelector and querySelectorAll respectively. Both accept an optional root to scope the query (useful for shadow DOM). $$ returns a plain array, not a NodeList.

javascript
1const hero = options.utils.$('.hero-section');
2const cards = options.utils.$$('.product-card');
3
4cards.forEach((card) => {
5 card.classList.add('variant-card');
6});

utils.createElement(tag, props?, children?)

Creates an HTMLElement from a tag name, optional props object (class, id, text, html, attrs, style, on), and an optional array of child nodes or strings.

javascript
1const badge = options.utils.createElement('span', {
2 class: 'sale-badge',
3 text: 'Sale',
4 style: { background: '#e63', color: '#fff' },
5});
6
7options.utils.insertBefore(badge, priceEl);

utils.insertAfter(newNode, ref) and utils.insertBefore(newNode, ref)

Insert newNode immediately after or before a reference node. Equivalent to the native after() / before() DOM methods with explicit node arguments.

utils.wrap(node, wrapper)

Wraps node inside wrapper, preserving its position in the DOM. The wrapper takes the node's original position and the node becomes the wrapper's child.

javascript
1const div = options.utils.createElement('div', { class: 'highlight-ring' });
2options.utils.wrap(ctaButton, div);

utils.remove(node)

Removes a node from the DOM. A null-safe wrapper around node.parentNode?.removeChild(node).

utils.setText(node, text)

Sets the textContent of a node. Safe for user-supplied strings — text is never interpreted as HTML.

utils.setHtml(node, html, opts?)

Sets the innerHTML of an element. By default, inline scripts and event handlers are scrubbed before insertion to prevent accidental XSS from third-party data. Pass { raw: true } to bypass sanitisation when you fully control the HTML source.

javascript
1// Safe — scripts/handlers stripped automatically
2options.utils.setHtml(reviewsEl, reviewsHtmlFromApi);
3
4// Raw — you are responsible for the content
5options.utils.setHtml(heroEl, trustedHtmlTemplate, { raw: true });

Events

Helpers for attaching and removing event listeners. All listeners registered through options.utils are automatically removed when the variation is torn down.

utils.on(target, type, handler, opts?)

Attaches an event listener. When target is a CSS selector string rather than an EventTarget, the listener is delegated — it fires for matching elements that exist now and for those added to the DOM in the future. The handler receives the original event and, as a second argument, the matched element. Returns a { off() } handle.

javascript
1// Direct listener
2const { off } = options.utils.on(window, 'scroll', () => {
3 updateStickyHeader();
4});
5
6// Delegated listener — catches current and future .add-to-cart buttons
7options.utils.on('.add-to-cart', 'click', (event, matchedEl) => {
8 event.preventDefault();
9 openUpsellModal(matchedEl.dataset.productId);
10});

utils.onUrlChange(callback)

Fires callback(url) on every SPA route change (pushState, replaceState, and popstate). Useful for re-applying variation changes after navigation on single-page apps. Returns a { off() } handle.

javascript
1options.utils.onUrlChange((url) => {
2 if (url.includes('/checkout')) {
3 applyCheckoutVariant();
4 }
5});

Data

Helpers for reading and writing browser storage, cookies, query parameters, and shared experiment state.

A simple cookie API with three methods: cookie.get(name), cookie.set(name, value, opts?), and cookie.remove(name). The set options accept days, path, domain, sameSite, and secure.

javascript
1const plan = options.utils.cookie.get('user_plan');
2
3if (plan === 'pro') {
4 document.querySelector('.upgrade-banner')?.remove();
5}
6
7// Set a cookie that expires in 7 days
8options.utils.cookie.set('exp_shown', '1', { days: 7 });

utils.query

Read URL query parameters. query.get(name) returns the value of a single parameter or null. query.getAll() returns all parameters as a plain object.

javascript
1const source = options.utils.query.get('utm_source');
2if (source === 'email') {
3 document.querySelector('.email-hero')?.classList.add('active');
4}

utils.storage.local and utils.storage.session

Type-safe wrappers around localStorage and sessionStorage. Each exposes get(key), set(key, value), and remove(key). Values are JSON-serialized automatically, and the generic parameter on get<T>(key) narrows the return type.

javascript
1// Persist a flag across page loads
2options.utils.storage.local.set('variantSeen', true);
3
4// Read it back (typed)
5const seen = options.utils.storage.local.get('variantSeen');
6if (seen) skipIntroAnimation();

utils.state

A lightweight in-memory key-value store that is shared between the triggers.js and variation.js files of the same experiment. Use it to pass values computed in the trigger (for example, the matched element or a resolved data object) into the variation code without re-computing.

triggers.js
javascript
1// In triggers.js — compute and store
2options.utils.waitUntil('.product').then((el) => {
3 options.utils.state.set('productEl', el);
4 activate();
5});
variation.js
javascript
1// In variation.js — retrieve what the trigger stored
2const productEl = options.utils.state.get('productEl');
3if (productEl) {
4 productEl.classList.add('variant-highlight');
5}

Log

utils.log

A scoped logger with three levels. log.debug() and log.info() emit only in preview or development mode — real visitors never see them. log.warn() always emits. Messages are automatically prefixed with the experiment name so they are easy to filter in DevTools.

javascript
1options.utils.log.debug('variation applied', { productId: '123' });
2options.utils.log.info('checkout form found');
3options.utils.log.warn('price element missing — skipping price change');
Use utils.log instead of console.log
Raw console.log calls in variation code are visible to every visitor who opens DevTools. utils.log.debug() and utils.log.info() are silenced in production, so your debugging output stays private. utils.log.warn() is intentionally always-on for genuinely unexpected conditions.