Event bus

avsb.on('event', callback) calls your function every time A vs B records an experiment event: a visitor being shown a variation, a goal firing, a purchase, or a recommendation being seen or clicked.

Use it to send experiment data somewhere A vs B does not talk to directly, or to watch what is happening while you build.

onEvent has been removed

window.avsb.onEvent no longer exists. It held one callback, so a second integration silently replaced the first. avsb.on takes as many subscribers as you like and hands each one an unsubscribe function. If you have window.avsb.onEvent = fn anywhere, change it to avsb.on('event', fn). Nothing else about the data changes.

Subscribing

TypeScript
const unsubscribe = avsb.on?.('event', function (event) {  console.log(event.event_name, event.experiment_name, event.variation_name)})
TypeScript3 lines

Call the returned function to stop receiving events:

TypeScript
unsubscribe?.()
TypeScript1 line
Why the examples write avsb.on?.(...)

on is declared optional in @avsbhq/snippet-types because the preview and edit bypass runtimes build a cut-down avsb object without it. Every normal page has it, so plain avsb.on('event', fn) runs fine. The optional call is what keeps TypeScript happy, and it costs nothing in plain JavaScript.

Subscribing more than once is fine. Each subscriber gets every event, and unsubscribing one leaves the others alone.

Subscribe early enough to catch the first event

The most important event usually fires within milliseconds of the snippet loading, so where you subscribe decides whether you see it.

  • Project JS: subscribing inside your project's JavaScript is the simplest reliable option. It runs before any experiment is evaluated, so you catch everything.
  • Your own inline script: this works too, as long as the install stub sits above it. The stub holds early avsb.on calls and replays them the moment the snippet installs, so nothing is missed.

The stub is the first of the two tags you already paste from Project Settings > Snippet:

HTML
<script>window.avsb=window.avsb||{};window.avsb.q=window.avsb.q||[];window.avsb.ready=window.avsb.ready||function(f){window.avsb.q.push(f)};window.avsb.on=window.avsb.on||function(n,f){var t=['on',n,f];window.avsb.q.push(t);return function(){t[0]=null}};window.avsb.consent=window.avsb.consent||{set:function(s){window.avsb.q.push(['consent.set',s])},get:function(){return{}}};window.avsb.track=window.avsb.track||{};['event','segment','purchase','cart'].forEach(function(m){window.avsb.track[m]=window.avsb.track[m]||function(a,b){window.avsb.q.push(['track.'+m,a,b])}});(function(){window.avsb._t0=Date.now();window.avsb._df=fetch('https://cdn.avsb.cloud/YOUR_SNIPPET_KEY/datafile.json').catch(function(){});var d=document,e=d.documentElement,s=d.currentScript,a=s&&s.getAttribute('data-avsb-timeout'),t=a==null?3000:+a;if(!(t>0)||window.avsb.version)return;e.style.opacity='0';e.style.pointerEvents='none';window.avsb._t=setTimeout(function(){e.style.removeProperty('opacity');e.style.removeProperty('pointer-events')},t)})();</script><script src="https://cdn.avsb.cloud/snippet.js?id=YOUR_SNIPPET_KEY" data-avsb="YOUR_SNIPPET_KEY" async></script>
HTML9 lines

With that in place, subscribe from any script below it:

HTML
<script>  avsb.on('event', function (event) {    // your integration  });</script>
HTML5 lines
avsb.ready is too late for this

Callbacks passed to avsb.ready() run after experiments have been evaluated, so a subscription made there misses the experiment views that already fired. Use Project JS, or subscribe below the stub.

Installed before this existed?

Older installs have a stub without the avsb.on line, and early subscriptions are dropped on those pages. Re-copy the snippet from Project Settings > Snippet to fix it. Subscribing from Project JS works either way.

What your callback receives

Every event is the same record, whatever kind it is.

NameTypeAlways presentDescription
experiment_idstringYesThe experiment's ID, the same one shown in A vs B.
experiment_namestringYesThe experiment's name.
variation_idstringYesThe ID of the variation the visitor saw.
variation_namestringYesThe name of that variation, such as "Control" or "Variant B".
event_typestringYesOne of exposure, goal, purchase, rec_impression, rec_click, custom, test. Branch on this rather than on the name.
event_namestringYesThe name this event is being sent under.
event_idstringYesA unique ID for this event, for lining records up between systems.
revenuenumber or nullYesThe revenue for this event, or null when there is none.
valuenumber or nullYesThe generic value for this event, such as load time or scroll depth, or null.
currencystringNoPurchases only, when you supplied one.
transaction_idstringNoPurchases only: your order ID.
goal_keystringNoConversions and your own tracked events: which goal or event key fired.
visitor_idstringYesThe A vs B visitor ID.
page_urlstringYesThe page the event happened on.
timestampnumberYesWhen it happened, in milliseconds.
sdk_versionstringYesThe snippet version that sent it.
attributesobjectNoRecommendation events only: recipe, item, position, and similar.
sourcestringNoPresent only for preview or internal traffic. Absent means an ordinary visitor.
event_type is the field to branch on

event_name changes if you rename events on the Integrations tab. event_type never does, so use it for anything conditional.

The bus sees more than your analytics tools do

Two differences are worth knowing, because they are deliberate:

  • Preview and internal traffic reaches the bus. When you are previewing a variation or your traffic is marked internal, nothing is sent to your analytics tools, but the bus still fires so you can watch it while testing. Those records carry source. Check for it if you want to ignore them.
  • Recommendation events outside an experiment reach the bus too. Recommendations shown outside any experiment arrive with the experiment and variation fields empty. Analytics tools do not receive those.

Events that a visitor's consent choice has suppressed do not reach the bus at all.

Examples

Only experiment views:

TypeScript
avsb.on?.('event', function (event) {  if (event.event_type !== 'exposure') return  myAnalytics.track('Experiment Viewed', {    experiment: event.experiment_name,    variation: event.variation_name,  })})
TypeScript8 lines

Ignore preview and internal traffic:

TypeScript
avsb.on?.('event', function (event) {  if (event.source) return // preview or internal QA  myAnalytics.track(event.event_name, { ...event })})
TypeScript4 lines

Purchases with revenue:

TypeScript
avsb.on?.('event', function (event) {  if (event.event_type !== 'purchase') return  myAnalytics.revenue({    orderId: event.transaction_id,    amount: event.revenue,    currency: event.currency,    variation: event.variation_name,  })})
TypeScript10 lines

Two integrations, kept separate:

TypeScript
declare function sendToWarehouse(event: AvsbIntegrationRecord): voidconst stopWarehouse = avsb.on?.('event', sendToWarehouse)const stopConsole = avsb.on?.('event', function (event) {  console.log('[A vs B]', event.event_type, event.experiment_name)})// Later, stop just the logging.stopConsole?.()
TypeScript9 lines
TypeScript in Project JS

If you write your Project JS in TypeScript, the record passed to your callback is fully typed, with autocomplete for every field above.

Keep your callback quick

Your function runs while A vs B is processing the event. Avoid anything blocking, such as a synchronous network request. Analytics calls like gtag, analytics.track(), and mixpanel.track() are non blocking and safe here. If your subscriber throws, A vs B carries on and every other subscriber still runs.

You may not need this at all

For the nine tools on the Integrations tab, switching a card on does all of this for you, with the right event shape for each tool. Reach for the bus when you want something those cards do not cover.

Was this helpful?