Utilities (avsb.utils)

avsb.utils is a toolkit of helpers built for experiment code. It covers six areas: Wait, Timing, DOM, Events, Data, and Log.

A variation is the version of the page a visitor sees in a test: control, or one of the challengers. Every helper here is also available as options.utils inside trigger and variation functions. Use options.utils there, and A vs B cleans up after itself. It tears down every observer and listener it made, the moment the variation is removed or a visitor navigates inside a single-page app. No manual cleanup needed.

Use options.utils inside variation code

Prefer options.utils.* over avsb.utils.* inside trigger and variation functions. Only options.utils cleans up automatically when the variation ends, which avoids memory leaks and stale callbacks on pages that navigate without a full reload.

Several examples below hand off to page code of your own. These are the stand-ins they assume, so you can see what each one is expected to take:

TypeScript
declare function showPaymentForm(): voiddeclare function trackSearch(term: string): voiddeclare function updatePrices(data: unknown): voiddeclare function updateStickyHeader(): voiddeclare function openUpsellModal(productId: string | undefined): voiddeclare function applyCheckoutVariant(): voiddeclare function skipIntroAnimation(): voiddeclare const reviewsHtmlFromApi: stringdeclare const trustedHtmlTemplate: string
TypeScript9 lines

Wait

Helpers that wait for the DOM or a JavaScript value to be ready. Each one returns an AvsbGuardedThenable. Its .catch() is optional: a failure is logged instead of throwing an unhandled error.

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.

TypeScript
// CSS selector: resolves with the elementoptions.utils.waitUntil<HTMLElement>('.price-block').then((el) => {  el.textContent = 'From $9/mo'})// window path: resolves with the valueoptions.utils.waitUntil<Record<string, unknown>[]>('window.dataLayer').then((dl) => {  dl.push({ event: 'exp_viewed' })})// Predicate function: resolves with the return valueoptions.utils.waitUntil(() => window.Stripe?.isReady).then(() => {  showPaymentForm()})// Array of targets: resolves with an array of resultsoptions.utils  .waitUntil<[HTMLElement, { firstName: string }]>(['.cart', 'window.user'])  .then(([cart, user]) => {    const name = cart.querySelector<HTMLElement>('.name')    if (name) name.textContent = user.firstName  })
TypeScript22 lines

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.

TypeScript
// Qubit-style code: works without modificationoptions.utils.poll<HTMLElement>('.hero', { timeout: 5000 }).then((el) => {  el.classList.add('variant')})
TypeScript4 lines

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.

TypeScript
// Scope the search to a shadow rootconst host = document.querySelector('checkout-widget')if (host?.shadowRoot) {  options.utils.waitForElement('button[type="submit"]', {    root: host.shadowRoot,    timeout: 8000,  }).then((btn) => {    btn.textContent = 'Place Order'  })}
TypeScript11 lines

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.

TypeScript
// Badge every product card that arrives, now and in the futureconst handle = options.utils.onMutation('.product-card', (card) => {  const badge = document.createElement('span')  badge.className = 'variant-badge'  badge.textContent = 'New'  card.prepend(badge)})// Stops automatically on variation removal; call this to stop it early:handle.stop()
TypeScript10 lines

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.

TypeScript
const heroSection = document.querySelector<HTMLElement>('.hero-section')// Annotate the parameter: `once` preserves whatever signature you give it.const applyOnce = options.utils.once((el: HTMLElement) => {  el.classList.add('highlighted')})// Calling multiple times is safe; only the first call takes effectif (heroSection) {  applyOnce(heroSection)  applyOnce(heroSection)}
TypeScript12 lines

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.

TypeScript
const stickyBanner = document.querySelector<HTMLElement>('.sticky-banner')const onScroll = options.utils.throttle(() => {  const y = window.scrollY  stickyBanner?.classList.toggle('visible', y > 300)}, 100)window.addEventListener('scroll', onScroll)
TypeScript8 lines

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.

TypeScript
const searchInput = document.querySelector<HTMLInputElement>('#search')const onInput = options.utils.debounce((e: Event) => {  if (e.target instanceof HTMLInputElement) trackSearch(e.target.value)}, 300)searchInput?.addEventListener('input', onInput)
TypeScript7 lines

utils.raf(fn)

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

TypeScript
options.utils.raf(() => {  // Runs on the next animation frame; minimises layout thrash  const hero = document.querySelector<HTMLElement>('.hero')  hero?.style.setProperty('--accent', '#e63')})
TypeScript5 lines

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.

TypeScript
// Track an impression without blocking the critical pathoptions.utils.idle(() => {  avsb.track.event(42)}, { timeout: 2000 })
TypeScript4 lines

utils.domReady()

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

TypeScript
options.utils.domReady().then(() => {  // Safe to query the full DOM here  const footer = document.querySelector('footer')  const ctaBanner = options.utils.createElement('div', {    class: 'cta-banner',    text: 'Start your free trial',  })  if (footer) footer.prepend(ctaBanner)})
TypeScript10 lines

utils.retry(fn, opts?)

Calls an async or sync function repeatedly until it resolves without throwing. Options: attempts (default 3), delay in ms before the next try (default 200), and factor to multiply that delay by after each failure (default 2, so waits grow 200ms, 400ms, 800ms...). Returns a native Promise.

TypeScript
options.utils.retry(  (): Promise<unknown> => fetch('/api/prices').then((r) => r.json()),  { attempts: 3, delay: 500, factor: 2 }).then((data) => {  updatePrices(data)})
TypeScript6 lines

DOM

Helpers for reading and changing the DOM. Use these instead of raw browser methods: they never throw on a missing element. setHtml also cleans third-party HTML by default, so it cannot run a hidden script.

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

Shorthand for querySelector and querySelectorAll. Both accept an optional root, so the search only looks inside it (useful for shadow DOM). $$ returns a plain array, not a NodeList.

TypeScript
const hero = options.utils.$('.hero-section')const cards = options.utils.$$('.product-card')cards.forEach((card) => {  card.classList.add('variant-card')})
TypeScript6 lines

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.

TypeScript
const badge = options.utils.createElement('span', {  class: 'sale-badge',  text: 'Sale',  style: { background: '#e63', color: '#fff' },})const priceEl = options.utils.$('.price')if (priceEl) options.utils.insertBefore(badge, priceEl)
TypeScript8 lines

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.

TypeScript
const div = options.utils.createElement('div', { class: 'highlight-ring' })const ctaButton = options.utils.$('.cta-button')if (ctaButton) options.utils.wrap(ctaButton, div)
TypeScript4 lines

utils.remove(node)

Removes a node from the DOM. Safe to call on a node with no parent, or one already removed: nothing happens.

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.

TypeScript
// Safe: scripts/handlers stripped automaticallyconst reviewsEl = options.utils.$('.reviews')if (reviewsEl) options.utils.setHtml(reviewsEl, reviewsHtmlFromApi)// Raw: you are responsible for the contentconst heroEl = options.utils.$('.hero')if (heroEl) options.utils.setHtml(heroEl, trustedHtmlTemplate, { raw: true })
TypeScript7 lines

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. Pass an EventTarget (an element, window) for a direct listener. Pass a CSS selector string instead, and the listener is delegated: one listener catches every matching element, including ones added to the page later. The handler receives the original event and, as a second argument, the matched element. Returns a { off() } handle.

TypeScript
// Direct listenerconst { off } = options.utils.on(window, 'scroll', () => {  updateStickyHeader()})// Delegated listener: catches current and future .add-to-cart buttonsoptions.utils.on('.add-to-cart', 'click', (event, matchedEl) => {  event.preventDefault()  if (matchedEl instanceof HTMLElement) openUpsellModal(matchedEl.dataset.productId)})
TypeScript10 lines

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.

TypeScript
options.utils.onUrlChange((url) => {  if (url.includes('/checkout')) {    applyCheckoutVariant()  }})
TypeScript5 lines

Data

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

utils.cookie

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.

TypeScript
const plan = options.utils.cookie.get('user_plan')if (plan === 'pro') {  document.querySelector('.upgrade-banner')?.remove()}// Set a cookie that expires in 7 daysoptions.utils.cookie.set('exp_shown', '1', { days: 7 })
TypeScript8 lines

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.

TypeScript
const source = options.utils.query.get('utm_source')if (source === 'email') {  document.querySelector('.email-hero')?.classList.add('active')}
TypeScript4 lines

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.

TypeScript
// Persist a flag across page loadsoptions.utils.storage.local.set('variantSeen', true)// Read it back (typed)const seen = options.utils.storage.local.get<boolean>('variantSeen')if (seen) skipIntroAnimation()
TypeScript6 lines

utils.state

A lightweight in-memory key-value store, shared between the triggers.js and variation.js files of the same experiment. The trigger computes a value once, for example the matched element or a fetched object, and stores it. The variation reads it back instead of computing it again.

TypeScript
// In triggers.js: compute and storeoptions.utils.waitUntil<HTMLElement>('.product').then((el) => {  options.utils.state.set('productEl', el)  activate()})
TypeScript5 lines
TypeScript
// In variation.js: retrieve what the trigger storedconst productEl = options.utils.state.get<HTMLElement>('productEl')if (productEl) {  productEl.classList.add('variant-highlight')}
TypeScript5 lines

Log

A scoped logger with three levels. log.debug() and log.info() emit only when the A vs B console is switched on: debug mode (?avsb_debug=1), a preview session, or dev mode. Real visitors never see them. log.warn() always emits. Messages are automatically prefixed so they are easy to filter in DevTools: the experiment's id inside trigger and variation code, or __project__ for top-level avsb.utils calls.

TypeScript
options.utils.log.debug('variation applied', { productId: '123' })options.utils.log.info('checkout form found')options.utils.log.warn('price element missing, skipping price change')
TypeScript3 lines
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.

Was this helpful?