Tracking Events

avsb.track.event() is how you tell A vs B that a visitor has converted on a custom event metric. You call it from your own JavaScript at exactly the right moment: after a form submission, when a purchase confirms, when a scroll threshold is crossed, or at any other meaningful point in your application.

Method signature

TypeScript
// `avsb.track` is an `AvsbTrackFunction` from @avsbhq/snippet-types.// This page documents its `event` member:declare const track: {  event(eventKey: string | number, properties?: AvsbTrackEventProperties): void}// `AvsbTrackEventProperties`, from the same package:interface AvsbTrackEventProperties {  revenue?: number  value?: number}
TypeScript11 lines

Parameters

NameTypeRequiredDescription
eventKeystring | numberYesThe event key of the metric, exactly as you typed it when you created the metric (for example signup_completed). A number is coerced to text before matching, so 12345 fires a metric whose key is 12345. Prefer a readable string key.
propertiesobjectNoOptional extra data to attach to the conversion.
properties.revenuenumberNoA monetary amount for this conversion, used by revenue metrics and the Revenue Impact calculation. A plain number with no currency symbol: 49.99, not "$49.99".
properties.valuenumberNoAny other continuous measurement for this conversion: order value, load time in milliseconds, scroll depth, time on task. Used by percentile metrics and by metrics whose value source is a per-event property.

Finding the event key

The event key is the field labelled Metric key when you create a Custom metric. As soon as you save it, A vs B shows you the exact avsb.track.event() line to copy into your code. You can see the key again any time on the metric's row in Metrics.

Pick something readable and keep it stable: signup_completed, added_to_cart, demo_requested. Whatever you type there is what your code passes here, character for character.

TypeScript
// Metric created with the event key "signup_completed"avsb.track.event('signup_completed')
TypeScript2 lines
Numbers work too, if the key is digits

The key is coerced to text before it is matched, so avsb.track.event(12345) and avsb.track.event('12345') do exactly the same thing. Both fire only if a metric on this project has the event key 12345. Nothing matches a metric by its short ID, so prefer a readable key.

An unmatched key does nothing, and says so

If no metric on the project uses the key you passed, the call records nothing and the console tells you, listing the keys that do work:

Plain text
[avsb] track.event("signup") dropped: no metric has that key. Known: signup_completed, added_to_cart
Plain text1 line

This warning always prints, with or without debug mode: it is a plain browser console message, so open your browser's console to see it. So create the metric first, or fix the typo the message points at. Turn on debug mode too, for the rest of what A vs B decided on this page load.

One key can feed several metrics

If three metrics share the event key purchase (a conversion rate, a count, and a 95th-percentile of order value), one call feeds all three:

TypeScript
avsb.track.event('purchase', { revenue: 49.99, value: 49.99 })
TypeScript1 line

That is the point of keys rather than ids: you instrument the event once, and add or change the metrics built on it later without touching your site.

When to call it

Call avsb.track.event() at the precise moment the conversion happens. The right moment depends on what you are measuring:

  • Form submissions: in the form's submit event handler, or better yet, after a successful server response confirming the submission was processed.
  • Purchases: after your payment API confirms the transaction. Never fire before the transaction is confirmed, as failed payments would be counted as conversions. For orders, prefer avsb.track.purchase(), which carries the order id, currency, and line items.
  • Button clicks: in a click event listener. This counts the intent to act, not the outcome. Good for measuring engagement with a CTA when there is no server-side confirmation step.
  • Scroll milestones: inside a scroll event listener once the visitor has scrolled past a threshold (50%, 100%, etc.). Remember to fire only once per page load using a boolean guard.
  • Video completions: in the video player's ended event callback.
  • Time on page: inside a setTimeout() callback after a meaningful dwell time like 30 or 60 seconds.

Calling it before the snippet has loaded

The loader tag is async, so your code can run first. The install tag defines avsb.track.* from the first byte of the page, so an early call is queued and replayed as soon as experiments have been evaluated. Nothing throws, and nothing is lost.

That means a conversion fired at the top of an order confirmation page is recorded even when the bundle arrives late. If you would rather be explicit, wrap the call:

TypeScript
avsb.ready?.(function () {  avsb.track.event('purchase', { revenue: 49.99 })})
TypeScript3 lines

Code examples

Track when a visitor submits a lead form:

TypeScript
document.querySelector('#lead-form')?.addEventListener('submit', function () {  avsb.track.event('lead_submitted')})
TypeScript3 lines

Track a completed purchase and include the order total:

TypeScript
avsb.track.event('purchase', { revenue: 49.99 })
TypeScript1 line

Track when a visitor clicks the buy button:

TypeScript
document.querySelector('.buy-btn')?.addEventListener('click', function () {  avsb.track.event('buy_clicked', { revenue: 29.99 })})
TypeScript3 lines

Only track when the server confirms the action:

TypeScript
const subscribeForm = document.querySelector<HTMLFormElement>('#subscribe-form')const formData = new FormData(subscribeForm ?? undefined)fetch('/api/subscribe', { method: 'POST', body: formData })  .then(function (response) {    if (response.ok) {      avsb.track.event('subscribed')    }  })
TypeScript9 lines

Track when a visitor reaches 50% scroll depth:

TypeScript
let scrollFired = falsewindow.addEventListener('scroll', function () {  if (scrollFired) return  const pct = (window.scrollY + window.innerHeight) / document.body.scrollHeight  if (pct >= 0.5) {    avsb.track.event('scrolled_half')    scrollFired = true // only fire once  }})
TypeScript9 lines

Track a page-load time as a continuous value for a percentile metric:

TypeScript
avsb.ready?.(function () {  const nav = performance.getEntriesByType('navigation')[0]  if (nav) avsb.track.event('page_load', { value: Math.round(nav.duration) })})
TypeScript4 lines

How event batching works

A vs B does not send each event immediately as it happens. Instead, it buffers events and flushes them in batches to reduce network overhead and avoid disrupting the user's browsing experience.

Events are flushed automatically when any of these happens:

  • The batch reaches 10 events.
  • A flush timer that ticks every 2 seconds fires and finds events waiting.
  • The page is hidden (the visitor switches tab, navigates away, or closes it).

While the page is open, each batch is sent with a keepalive fetch and the answer is read, so a batch the server could not take is kept rather than lost. If the rate limit refused it, the snippet waits the number of seconds the server asked for (at most 30) and sends it again. If the server failed or the connection dropped, it retries on the next flushes and gives up only after three failed attempts. Every event carries a stable id, so a retry never double-counts.

When the page is being hidden or unloaded, the batch goes out with navigator.sendBeacon() instead, a browser API designed for exactly that moment: the browser takes the request off your page's hands and keeps sending it after the page is gone, which is why a conversion fired immediately before a navigation or a redirect normally still arrives. That last send is best effort rather than a guarantee: a beacon can still be lost to a dropped connection or a device going offline.

Conversion-rate metrics count once per visitor

For a conversion-rate metric (the default), A vs B counts at most one conversion per visitor: if you call avsb.track.event() multiple times for the same metric, only the first call moves the conversion rate. This is by design: conversion rates measure the proportion of visitors who converted, not the number of events fired. Count and percentile metrics are the exception: they take every call into account, so fire avsb.track.event() each time the event genuinely happens.

Revenue tracking

When you pass a revenue value, A vs B records it alongside the conversion event. This unlocks the Revenue observed card on your experiment results page. It shows the total revenue recorded so far on your test's variations (a variation is one version being shown, control or a challenger), taken from whichever attached metric has a genuine revenue total. It reports what actually happened, not a forecast: nothing is subtracted for what the control would have earned, and no future number is projected.

Revenue should be a plain JavaScript number representing the transaction amount. Do not include currency symbols, formatting, or strings.

TypeScript
// Correct: plain numberavsb.track.event('purchase', { revenue: 49.99 })avsb.track.event('purchase', { revenue: 100 })
TypeScript3 lines

These two do not work. revenue is typed number, so TypeScript rejects both outright, and at runtime a string is not a revenue figure:

Plain text
avsb.track.event('purchase', { revenue: '$49.99' }); // string with symbolavsb.track.event('purchase', { revenue: '49.99' });  // string number
Plain text2 lines
Revenue from the first conversion only

Because only the first conversion is counted per visitor, only the revenue value from the first call is stored. For experiments where visitors might make multiple purchases, design your metric around the first purchase event rather than trying to sum revenue across multiple calls.

Was this helpful?