Project JavaScript
Project JavaScript is a block of code you write once that runs on every page load, before any experiments are evaluated. Use it for global setup that all your experiments share.
Where to write it
Go to Project Settings → Snippet. You will see a code editor section labelled Project JavaScript. Write your code there and click Publish. Changes take effect the next time the datafile is published.
The initProject wrapper
Your Project JavaScript must be wrapped in a function named initProject that receives a single options argument:
1function initProject(options) {2 // Your global setup code here3}The editor blocks saving if the wrapper function is missing or renamed. The snippet calls initProject(options) automatically on every page load and on every SPA navigation before experiments evaluate.
The options object
The options argument provides project context, a teardown hook, self-cleaning helpers, and tracking utilities. Because initProject runs on every page load and SPA navigation — it is not gated by an activation event — setup code goes directly in the function body.
Context properties
options.projectIdstringOptionalThe unique identifier for the current project.
Lifecycle hooks
options.onRemove(callback: () => void) => voidOptionalRegister a callback to run before Project JS is torn down on the next SPA navigation. This fires before each re-run, giving you a chance to clean up anything from the previous page before setup runs again for the new page.
Self-cleaning helpers
Anything started through these helpers is automatically torn down when options.onRemove fires (i.e. before each SPA navigation re-run).
options.waitUntil(conditionFn, callback, opts?) => handleOptionalPolls a condition function and calls the callback with the truthy result when the condition is met. Automatically cancelled on teardown.
options.setTimeout(fn: () => void, ms?: number) => handleOptionalLike window.setTimeout, but automatically cleared on teardown.
options.setInterval(fn: () => void, ms?: number) => handleOptionalLike window.setInterval, but automatically cleared on teardown.
options.addEventListener(target, type, handler, opts?) => voidOptionalLike target.addEventListener, but automatically removed on teardown.
Tracking utilities
options.track.event(shortId: number, properties?: { revenue?: number }) => voidOptionalManually fire a custom metric event for this visitor.
options.track.segment(segmentKey: string, segmentValue: string) => voidOptionalSend a segment value for this visitor.
options.getVisitorId() => stringOptionalReturns the current visitor's unique identifier.
options.getVariation(experimentId: string) => string | nullOptionalReturns the variation ID assigned to this visitor for a given experiment, or null if not assigned.
options.getActiveExperiments() => Array<{ experimentId: string; variationId: string }>OptionalReturns all experiments the current visitor is currently active in.
options.forceVariation(experimentId: string, variationId: string) => booleanOptionalForces the current visitor into a specific variation of an experiment. Returns true if the variation was applied successfully.
When it runs
Project JS runs after the window.avsb API is ready but before any experiments are evaluated. The page is still hidden (anti-flicker is active), so anything you set up here is available to your experiments without causing a visual flash.
On single-page applications, initProject re-runs on every client-side navigation. Before each re-run, the snippet fires options.onRemove and tears down all resources started via the self-cleaning helpers from the previous run. This means you do not need to guard against duplicate listeners — the previous run is fully cleaned up before initProject is called again.
What you can do with it
Set custom segments
If your experiments target specific audiences (for example, logged-in users or premium subscribers), use Project JS to tag visitors with custom segments that the audience evaluator can use.
1function initProject(options) {2 // initProject runs on every page load — apply setup directly in the body3 if (window.currentUser) {4 options.track.segment('plan', window.currentUser.plan);5 options.track.segment('userTier', window.currentUser.tier);6 }7}Track global custom events
Set up event listeners that fire across all experiments — useful for tracking conversions that are not tied to a single experiment. Use options.addEventListener so the listeners are automatically removed before each re-run on SPA navigation.
1function initProject(options) {2 const form = document.querySelector('#newsletter-form');3 if (form) {4 // options.addEventListener is automatically removed before each re-run5 options.addEventListener(form, 'submit', () => {6 // 12345 is the metric's short ID7 options.track.event(12345);8 });9 }10}Shared helper functions
Define utilities on window that your variation code can reuse, so you do not repeat the same code in every variation. Register cleanup with options.onRemove to avoid stale references across navigations.
1function initProject(options) {2 // Expose a helper to all variation JS3 window.myHelpers = {4 formatCurrency(value) {5 return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(value);6 }7 };8
9 options.onRemove(() => {10 delete window.myHelpers;11 });12}Initialize third-party integrations
If your variation code depends on a third-party library or SDK, you can configure it in Project JS so it is ready by the time experiments run.
1function initProject(options) {2 window.avsb.onEvent = function(event) {3 if (typeof gtag !== 'undefined') {4 gtag('event', 'experiment_impression', {5 experiment_id: event.experiment_id,6 variant_id: event.variation_id7 });8 }9 };10}initProject gets a fresh options context on every navigation and teardown happens automatically before each re-run, you no longer need to check a “already set up” flag to prevent duplicate listeners. Just use the self-cleaning helpers and let A vs B manage the lifecycle.Editor linting
The Project JS editor has a linting control in its toolbar with three levels. It applies to this project’s code only — each experiment carries its own separate setting.
- Off — the editor does no checking and shows 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 the reported type errors are fixed. The publish attempt returns the list of errors to resolve.
Linting checks are 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 automatically available in your Project JS andin every experiment’s trigger and variation files across the project, with no import needed.
1interface CurrentUser {2 id: string;3 plan: 'free' | 'pro' | 'enterprise';4}