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
- Open Project Settings, then the Snippet tab.
- Find the Global custom script card and select Edit Code.
- Write your code in the full-page editor that opens. It must be wrapped in a function named
initProject(see below). - Select Done to save your work as a draft and return to the Snippet tab.
- 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.
- Select Edit Code to open the full-page editor.
- Once you have unpublished changes, select Publish here to make them live.
- Select Done when you are finished. This saves a draft; it does not publish.
- 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. });}/** @type {(options: AvsbProjectOptions) => void} */function initProject(options) { // 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. });}The editor will not let you leave until the wrapper is present: removing or renaming initProject blocks the Done button until you fix it.
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
| Property | Type | Description |
|---|---|---|
options.projectId | string | The unique identifier for the current project. |
Lifecycle hooks
| Property | Type | Description |
|---|---|---|
options.onRemove | (callback: () => void) => void | Register 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.
| Property | Type | Description |
|---|---|---|
options.waitUntil | (target, opts?) => guarded promise | Waits 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) => handle | Like window.setTimeout, but cleared automatically on teardown. |
options.setInterval | (fn: () => void, ms?: number) => handle | Like window.setInterval, but cleared automatically on teardown. |
options.addEventListener | (target, type, handler, opts?) => void | Like target.addEventListener, but removed automatically on teardown. |
options.utils | AvsbUtils | The 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
| Property | Type | Description |
|---|---|---|
options.track.event | (eventKey: string | number, properties?: { revenue?: number; value?: number }) => void | Manually fire a custom metric event for this visitor. |
options.track.segment | (segmentKey: string, segmentValue: string) => void | Send a segment value for this visitor. |
options.getVisitorId | () => string | null | The 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 | null | Takes 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 }) => boolean | Forces 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.
/** @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);}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.
/** @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'); }); }}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.
/** @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; });}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.
/** @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 }); } });}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.
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
nullaccess, 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.
interface CurrentUser { id: string; plan: 'free' | 'pro' | 'enterprise';}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.