Project JavaScript

Project JavaScript is a block of code you write once that runs on every page load, before any experiment is evaluated. Use it for global setup that every variation (one specific version being tested, control or one of the challengers) on the project can rely on.

Where to write it

  1. Open Project Settings, then the Snippet tab.
  2. Find the Global custom script card and select Edit Code.
  3. Write your code in the full-page editor that opens. It must be wrapped in a function named initProject (see below).
  4. Select Done to save your work as a draft and return to the Snippet tab.
  5. On the Global custom script card, select Publish, then confirm. This is what actually makes your code live: a draft alone changes nothing your visitors see.
  1. Select Edit Code to open the full-page editor.
  2. Once you have unpublished changes, select Publish here to make them live.
  1. Select Done when you are finished. This saves a draft; it does not publish.
  2. TypeScript projects show a Lint level control here: Off, On, or Strict.

The initProject wrapper

Your Project JavaScript must be wrapped in a function named initProject that receives a single options argument. This is exactly what a new project starts with:

function initProject(options: AvsbProjectOptions) {  // Runs on every page (and again after each SPA navigation).  options.onRemove(function() {    // Runs before the next SPA navigation re-runs this code.    // Timers/listeners/waitUntil started through options.* are auto-cleaned.  });}
TypeScript8 lines

The editor will not let you leave until the wrapper is present: removing or renaming initProject blocks the Done button until you fix it.

Optional typing for plain JavaScript

The /** @type {...} */ comment above the project.js example is optional. It is an ordinary JavaScript comment, so it is safe to paste anywhere and never runs. Adding it is what gives the code editor autocomplete on options, even in a plain JavaScript file.

The options object

The options argument gives you project context, a teardown hook, self-cleaning helpers, and tracking utilities. initProject runs on every page load and every SPA navigation. It is not gated behind any activation event, so put your setup code directly in the function body.

Context properties

PropertyTypeDescription
options.projectIdstringThe unique identifier for the current project.

Lifecycle hooks

PropertyTypeDescription
options.onRemove(callback: () => void) => voidRegister a callback to run before Project JS is torn down on the next SPA navigation. It fires before each re-run, so you can clean up anything from the previous page before setup runs again for the new one.

Self-cleaning helpers

Anything started through these helpers is torn down automatically when options.onRemove fires, that is, before each SPA navigation re-run.

PropertyTypeDescription
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. Cancelled automatically on teardown. .catch() is optional: a wait that times out is logged, never thrown. Default timeout 10000ms.
options.setTimeout(fn: () => void, ms?: number) => handleLike window.setTimeout, but cleared automatically on teardown.
options.setInterval(fn: () => void, ms?: number) => handleLike window.setInterval, but cleared automatically on teardown.
options.addEventListener(target, type, handler, opts?) => voidLike target.addEventListener, but removed automatically on teardown.
options.utilsAvsbUtilsThe full helper toolkit: waitForElement, onMutation, $, $$, createElement, cookie, storage, state, and more. Anything it starts is torn down on teardown, exactly like the helpers above.

Tracking utilities

PropertyTypeDescription
options.track.event(eventKey: string | number, properties?: { revenue?: number; value?: number }) => voidManually fire a custom metric event for this visitor.
options.track.segment(segmentKey: string, segmentValue: string) => voidSend a segment value for this visitor.
options.getVisitorId() => string | nullThe current visitor's unique identifier, or null when there is no id yet: before avsb.init() in consent mode, or for 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 }>Every experiment the current visitor is active in right now, 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 an experiment, by its numeric ids. Returns true if it worked.

When it runs

Project JS runs after the window.avsb API is ready, but before any experiment is evaluated. The page is still hidden, since anti-flicker is still active. Anything you set up here is ready for your experiments to use, without a visible flash.

On single-page apps, initProject re-runs on every client-side navigation. Before each re-run, the snippet fires options.onRemove and tears down every resource the self-cleaning helpers started on the previous run. You never need to guard against duplicate listeners: the previous run is fully cleaned up before initProject runs again.

If your code throws

A synchronous error in initProject, one that throws before the function returns, is caught. It is reported to A vs B (see error capture), and it does not stop the page: your experiments still evaluate and activate normally.

An error inside a .then(), an async function, or anything else that runs after initProject has already returned is not caught this way. A vs B cannot tell it apart from an error in your site's own code, so it is never reported. Keep code you want tracked synchronous, or wrap your own async work in a try/catch.

What you can do with it

Set custom segments

Audiences are named, reusable groups of visitors, for example logged-in users or premium subscribers. If your experiments target specific audiences, use Project JS to tag visitors with custom segments the audience evaluator can use.

JavaScript
/** @type {(options: AvsbProjectOptions) => void} */function initProject(options) {  // initProject runs on every page load: apply setup directly in the body  const user = window.currentUser;  if (user?.plan) options.track.segment('plan', user.plan);  if (user?.tier) options.track.segment('userTier', user.tier);}
JavaScript7 lines

Track global custom events

Set up event listeners that fire across all experiments: useful for conversions that are not tied to a single experiment. Use options.addEventListener so listeners are removed automatically before each SPA re-run.

JavaScript
/** @type {(options: AvsbProjectOptions) => void} */function initProject(options) {  const form = document.querySelector('#newsletter-form');  if (form) {    // options.addEventListener is automatically removed before each re-run    options.addEventListener(form, 'submit', () => {      // 'newsletter_signup' is the metric's event key      options.track.event('newsletter_signup');    });  }}
JavaScript11 lines

Shared helper functions

Define utilities on window that your variation code can reuse, so you are not repeating the same code in every variation. Register cleanup with options.onRemove so a stale reference never survives into the next page.

JavaScript
/** @type {(options: AvsbProjectOptions) => void} */function initProject(options) {  // Expose a helper to all variation JS  window.myHelpers = {    /** @param {number} value */    formatCurrency(value) {      return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(value);    }  };  options.onRemove(() => {    delete window.myHelpers;  });}
JavaScript14 lines

Initialize third-party integrations

If your variation code depends on a third-party library or SDK, configure it in Project JS. It will be ready by the time experiments run.

JavaScript
/** @type {(options: AvsbProjectOptions) => void} */function initProject(options) {  // avsb.on is absent in the preview bypass runtime, so subscribe optionally  avsb.on?.('event', function (event) {    if (event.event_type !== 'exposure') return;    if (typeof gtag !== 'undefined') {      gtag('event', 'experiment_impression', {        experiment_id: event.experiment_id,        variant_id: event.variation_id      });    }  });}
JavaScript14 lines

Project JS runs before any experiment is evaluated, so subscribing here catches every event, including the first experiment a visitor sees. See the event bus reference for the full record.

SPA navigation: no more duplicate guards

initProject gets a fresh options context on every navigation, and teardown happens automatically before each re-run. You no longer need an "already set up" flag to prevent duplicate listeners. Use the self-cleaning helpers and let A vs B manage the lifecycle.

Editor linting

The Project JS editor has a linting control with three levels. It applies to this project's code only: each experiment carries its own separate setting.

  • Off: no checking, no underlines.
  • On (the default): the editor underlines type and syntax problems as you type. This is advisory; you can still publish.
  • Strict: turns on stricter TypeScript checks (unused variables, possible null access, missing types) and blocks publishing until you fix the reported errors.

Linting is only available for TypeScript projects. JavaScript projects still get basic syntax validation, but no type checking.

Shared types

TypeScript projects get a shared.d.ts file alongside project.ts. Anything you declare there, interfaces and type aliases, is available automatically in your Project JS. It is also available in every experiment's trigger and variation files across the project, with no import needed.

TypeScript
interface CurrentUser {  id: string;  plan: 'free' | 'pro' | 'enterprise';}
TypeScript4 lines
Types only, nothing ships

The shared types file holds type declarations only. Types are erased when your code compiles to JavaScript, so this file adds nothing to what runs in your visitors' browsers. It exists purely to help you while writing code.

Was this helpful?