Custom Storefront / SPA
If your store is a custom build (a headless storefront, a React, Vue, or Next.js app, or any single-page application) it is an SPA: the page never fully reloads when someone moves between screens. Install the snippet once in your root HTML, then call the purchase API from your checkout-complete logic. If your router puts a # in the URL, there is one extra setting to turn on so A vs B still notices the route change.
Install the snippet
Your snippet comes from Project Settings → Snippet in the A vs B dashboard. Put both tags in the head of the HTML document that wraps your app: index.html for Vite-style apps, the root layout for frameworks:
<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>The first tag is a stub. It makes avsb.ready(...), avsb.on(...) and avsb.consent.set(...) calls safe before the real bundle loads: a call made too early is queued and replayed once the snippet arrives. For Next.js App Router, place both tags in app/layout.tsx inside the <head>. See Single Page Apps for how other frameworks place them.
Want the bucketing decision made on the server instead of in the browser? Bucketing is how A vs B sorts a visitor into a variation (one specific version being tested, control or one of the challengers) using a stable hash of their id, so the same visitor always lands in the same group. Doing it server-side avoids any flicker in a server-rendered storefront. Use one of the framework SDK guides alongside the snippet, or instead of it.
Client-side navigation
A vs B watches for route changes on every project automatically. There is nothing to turn on for this part. It patches pushState and replaceState and listens for popstate, so when your router changes the URL, A vs B notices, cleans up the old variation code, and re-checks experiments against the new page. Variation assignments carry over between navigations, because the same visitor always hashes to the same variation.
A URL like example.com/#/checkout only changes the part after the #. A vs B ignores that by default, so a link to #pricing on the same page does not count as a new page view. If your router uses # for navigation (some Vue Router and Angular setups do), go to Project Settings → Configuration, open Advanced settings, and turn on Hash-based routing. Leave it off if your router uses real URLs through the History API, which is what most modern routers do by default.
- The Hash-based routing toggle, in the Advanced settings card on the Configuration tab.
Report purchases
Call avsb.track.purchase from the code path that runs exactly once when an order is confirmed. That is typically your order-confirmation route's mount effect, or your checkout success callback:
/** The fields this example reads off your own order object. */interface ConfirmedOrder { id: string; total: number; currency?: string; subtotal?: number; shipping?: number; tax?: number; discount?: number; coupon?: string; lineItems: { sku: string; name?: string; unitPrice?: number; quantity?: number; category?: string; }[];}function onOrderConfirmed(order: ConfirmedOrder) { if (window.avsb && window.avsb.track) { window.avsb.track.purchase({ orderId: order.id, // required: unique order identifier total: order.total, // required: decimal, e.g. 49.99 currency: order.currency, // optional: defaults to project currency subtotal: order.subtotal, shipping: order.shipping, tax: order.tax, discount: order.discount, coupon: order.coupon, items: order.lineItems.map(function (li) { return { sku: li.sku, // required per item name: li.name, price: li.unitPrice, quantity: li.quantity, category: li.category }; }) }); }}A few SPA-specific notes:
- Dedup makes re-renders safe. Orders are deduplicated by
orderId, and the newest write wins. A re-mounted confirmation component or a page refresh never double-counts an order. - The call sends immediately. Purchases are not batched with other events. They go out right away over
sendBeacon, so a buyer who navigates away right after checkout does not lose the order. - Invalid input is a warn-and-skip. A missing
orderIdor a non-positivetotallogs a console warning and records nothing, rather than throwing inside your app. - Testing will not corrupt your revenue numbers. Add
test: trueto the call. A vs B still stores the order, but leaves it out of every revenue, profit, recommendation, and audience calculation.
If your storefront already pushes a GA4 purchase event to window.dataLayer, and your product catalog is connected, you have another option. Turn on Google Analytics 4 / dataLayer on your project's Product catalog page, instead of or alongside the explicit call above. A vs B deduplicates by order id, so running both at once is harmless.
Verify
Place a test order. Then open Commerce → Orders in the A vs B dashboard (see the orders & attribution guide) to confirm the order arrived. It should show as attributed to an experiment exposure, the moment a visitor is actually counted in an experiment. For checking experiment evaluation on route changes, see Verifying Installation.