Triggers

By default, a variation is injected immediately after a visitor is bucketed into it. Triggers let you override this behaviour and control exactly when (and under what conditions) the variation is applied and removed.

Warning

Exposure is only counted when activate() is called. If your trigger never calls activate(), the visitor will not be counted as exposed to the experiment. This means your trigger directly controls when impressions are recorded.

What is a trigger?

A trigger is a JavaScript function named initTrigger that you write as part of an experiment. The A vs B snippet calls your function automatically and passes three arguments:

  1. options: an object with experiment context, lifecycle hooks, self-cleaning helpers, and tracking utilities.
  2. activate: a function you call to apply the variation and count the exposure.
  3. deactivate: a function you call to remove the variation.

Without a trigger, the variation is activated (and exposure counted) immediately. With a trigger, you have full control. Common use cases:

  • Wait for a specific DOM element to appear before applying the variation (important for SPAs and lazy-loaded content).
  • Only activate when the visitor scrolls past a certain point on the page.
  • Activate after a user interaction (clicking a button, focusing an input).
  • Deactivate the variation when the visitor navigates to a different section of the page.

Function signature

Your trigger function must be named initTrigger and accept three parameters:

JavaScript
/** @type {(options: AvsbTriggerOptions, activate: () => void, deactivate: () => void) => void} */function initTrigger(options, activate, deactivate) {  // Your logic here: call activate() when ready}
JavaScript4 lines

The @type comment above the function is optional. It is an ordinary JavaScript comment, so it is safe to paste anywhere, and it is what gives the code editor autocomplete for everything on options.

The snippet wraps your code and calls it like this internally:

JavaScript
// Simplified: this is what the snippet does behind the scenesinitTrigger(options, activate, deactivate)
JavaScript2 lines
Warning

The function must be named initTrigger. The editor will block saving if the wrapper function is missing or named anything else (including the old name triggers, which is no longer supported).

activate() and deactivate()

These two functions are passed as the second and third arguments to your trigger. They are not properties on the options object.

NameTypeDescription
activate() => voidApplies the variation (CSS + JS) and counts the exposure. Can only fire once: subsequent calls are ignored. This is the only way an exposure is recorded when a trigger is present.
deactivate() => voidRemoves the variation CSS/JS and runs all cleanup registered via options.onRemove. Useful for SPAs where conditions may change on navigation.
Warning

activate() can only be called once per experiment evaluation. After activate() or deactivate() has been called, calling activate() again has no effect.

The options object

The first argument to your trigger function is an options object containing experiment context, lifecycle hooks, self-cleaning helpers, and tracking utilities.

A variant is one specific version being tested, other than the control.

Context properties

NameTypeDescription
options.experimentIdstringA vs B's internal id for the experiment (the UUID row in the Experiment details panel), not the numeric Experiment ID.
options.experimentNamestringThe display name of the experiment.
options.variationIdstringA vs B's internal id for the variation assigned to this visitor. To address an experiment or variation from code, use the numeric ids that avsb.getVariation() and avsb.forceVariation() take.
options.variationNamestringThe display name of the variation (e.g. "Control", "Variant 1").
options.variationType'control' | 'variant'Whether this visitor was assigned the control or a variant.

Lifecycle hooks

NameTypeDescription
options.onActivation(callback: () => void) => voidRegister a callback to run immediately after activate() applies the variation. If activate() has already been called, the callback fires immediately.
options.onRemove(callback: () => void) => voidRegister a callback to run when the variation is removed: either because deactivate() was called or because A vs B tears down the experiment (e.g. on SPA navigation). Use for cleanup that cannot be handled by the self-cleaning helpers.

Self-cleaning helpers

These helpers work like their native browser equivalents, but anything you start through them is automatically torn down when the variation is removed. You do not need to manually cancel them in options.onRemove.

NameTypeDescription
options.waitUntil(target, opts?) => guarded promiseWaits for a CSS selector, a window.x.y global path, or a predicate function, then resolves with whatever the target matched. Automatically cancelled on variation removal. .catch() is optional: a wait that times out is logged, never thrown. See the waitUntil section below for full details.
options.setTimeout(fn: () => void, ms?: number) => handleLike window.setTimeout, but the timer is automatically cleared when the variation is removed.
options.setInterval(fn: () => void, ms?: number) => handleLike window.setInterval, but the interval is automatically cleared when the variation is removed.
options.addEventListener(target, type, handler, opts?) => voidLike target.addEventListener, but the listener is automatically removed when the variation is removed.
options.utilsAvsbUtilsThe full helper toolkit: waitForElement, onMutation, $, $$, createElement, cookie, storage, state, and more. Anything it starts is torn down with the variation, exactly like the helpers above.

Tracking utilities

NameTypeDescription
options.track.event(eventKey: string | number, properties?: { revenue?: number; value?: number }) => voidManually fire a custom metric event for this visitor. The eventKey must match a metric configured in your project.
options.track.segment(segmentKey: string, segmentValue: string) => voidSend a segment value for this visitor.
options.getVisitorId() => string | nullReturns the current visitor's unique identifier, or null when there is no id yet (consent mode before avsb.init(), or a visitor who denied analytics).
options.getVariation(experimentId: string | number) => string | nullTakes the numeric Experiment ID from the experiment details panel and returns the numeric Variation ID this visitor is assigned, or null if not assigned.
options.getActiveExperiments() => Array<{ experimentId: string; variationId: string }>Returns the list of all experiments the current visitor is currently active in, as an array of { experimentId, variationId } pairs, using those same numeric ids.
options.forceVariation(experimentId: string | number, variationId: string | number, opts?: { track?: boolean }) => booleanForces the current visitor into a specific variation of another experiment, by its numeric ids. Pass { track: false } to apply it without recording an exposure. Returns true if the variation was applied successfully. Primarily useful for QA and multi-experiment coordination.

Basic trigger example

The simplest trigger just calls activate() immediately, which is equivalent to having no trigger at all:

JavaScript
/** @type {(options: AvsbTriggerOptions, activate: () => void, deactivate: () => void) => void} */function initTrigger(options, activate, deactivate) {  activate();}
JavaScript4 lines

Waiting for a DOM element

This is the most common use case. Sometimes the element you want to modify is not in the DOM yet when the experiment evaluates. A JavaScript framework, for example, may render it only after the initial page load. Use options.waitUntil to delay activation until it appears. Because the wait is started via options, it is automatically cancelled if the variation is torn down before the element arrives.

JavaScript
/** @type {(options: AvsbTriggerOptions, activate: () => void, deactivate: () => void) => void} */function initTrigger(options, activate, deactivate) {  // waitUntil takes a CSS selector, a window.x.y path, or a predicate  // function, and resolves as soon as the target is there  options.waitUntil('#hero-section').then(() => {    // The element exists: safe to activate    activate();  });}
JavaScript9 lines
Info

options.waitUntil re-checks on every DOM mutation and on a backed-off timer (50ms, doubling to a 1 second cap), so a DOM target resolves almost immediately. If the target never appears, the wait gives up after the timeout (10 seconds by default). The variation is never activated, and no exposure is counted, so the visitor sees the control. That failure is logged rather than thrown, so leaving off .catch() never raises an unhandled rejection.

You can also set a timeout to stop waiting after a certain period:

JavaScript
/** @type {(options: AvsbTriggerOptions, activate: () => void, deactivate: () => void) => void} */function initTrigger(options, activate, deactivate) {  // Give up after 5 seconds instead of the default 10  options.waitUntil('#hero-section', { timeout: 5000 }).then(() => activate());}
JavaScript5 lines

options.waitUntil is not limited to DOM elements. You can wait for any condition (a global variable, a data attribute, or a computed value):

JavaScript
/** @type {(options: AvsbTriggerOptions, activate: () => void, deactivate: () => void) => void} */function initTrigger(options, activate, deactivate) {  options.waitUntil(() => window.appState?.isReady).then(() => activate());}
JavaScript4 lines

Activating on scroll

Use options.addEventListener instead of bare window.addEventListener: the listener is automatically removed when the variation is torn down.

JavaScript
/** @type {(options: AvsbTriggerOptions, activate: () => void, deactivate: () => void) => void} */function initTrigger(options, activate, deactivate) {  options.addEventListener(window, 'scroll', function onScroll() {    const scrollPercent =      window.scrollY / (document.body.scrollHeight - window.innerHeight);    if (scrollPercent >= 0.5) {      activate();    }  }, { passive: true });}
JavaScript10 lines

Activating on user interaction

When you need the element itself, reach for options.utils.waitForElement. It resolves with the matching element, and like options.waitUntil it is cancelled automatically if the variation is torn down first.

JavaScript
/** @type {(options: AvsbTriggerOptions, activate: () => void, deactivate: () => void) => void} */function initTrigger(options, activate, deactivate) {  options.utils.waitForElement('#upgrade-btn').then((button) => {    options.addEventListener(button, 'click', () => activate(), { once: true });  });}
JavaScript6 lines

Manual cleanup with onRemove

The self-cleaning helpers (options.waitUntil, options.setTimeout, options.setInterval, options.addEventListener) handle their own teardown automatically. Use options.onRemove for anything that falls outside those helpers (for example, a native IntersectionObserver or a third-party subscription).

JavaScript
/** @type {(options: AvsbTriggerOptions, activate: () => void, deactivate: () => void) => void} */function initTrigger(options, activate, deactivate) {  const observer = new IntersectionObserver((entries) => {    if (entries[0].isIntersecting) {      observer.disconnect();      activate();    }  });  const target = document.querySelector('#pricing-section');  if (target) {    observer.observe(target);  }  // IntersectionObserver is not a self-cleaning helper, so register cleanup manually  options.onRemove(() => {    observer.disconnect();  });}
JavaScript19 lines

How exposure tracking works

Understanding when exposures are counted is critical for accurate experiment results:

  • Without a trigger: The visitor is bucketed, the variation is applied, and the exposure is counted, all immediately.
  • With a trigger: The visitor is bucketed immediately, but the variation is not applied and the exposure is not counted until your trigger calls activate().
  • If activate() is never called, the visitor sees the original page (control experience) and is not counted in the experiment results at all.
Tip

This behaviour is intentional. It stops your sample size from including visitors who never actually saw the experiment. For example, a visitor who leaves the page before scrolling to the tested section is not counted.

What happens without a trigger

When an experiment has no trigger code, the snippet follows this flow. Targeting rules are the URL conditions that decide which page a visitor must be on. Traffic allocation is the slice of visitors let into the experiment at all.

  1. Visitor lands on a matching page (targeting rules pass).
  2. Visitor is bucketed into a variation based on traffic allocation.
  3. Variation CSS and JS are injected immediately.
  4. An exposure event is sent immediately.

Adding a trigger inserts a gate between steps 2 and 3: the variation is not injected until you explicitly call activate().

Where to write trigger code

Trigger code is written in the experiment builder on the Variations step (Step 2). Trigger code is optional: if you don't add any, the variation is applied and the exposure is counted immediately when the visitor is bucketed. If you do add trigger code, the editor requires it to be wrapped in an initTrigger(options, activate, deactivate) function. The snippet calls this function and waits for you to call activate() before injecting the variation or counting the exposure.

Was this helpful?