Tracking Revenue
Revenue tracking lets A vs B compare how much money each variation earns, not just whether it converts more. A variation is one version being tested: the control, or one of the challengers. Record purchase events. The Results page then shows five numbers for each variation:
- Revenue per visitor
- Average order value (AOV)
- Purchase conversion rate
- Revenue per paying visitor
- Units per order
How revenue per visitor is analysed
Revenue per visitor is a continuous per-visitor test, not a did-they-buy test. A vs B looks at every visitor exposed to the experiment. That means every visitor actually counted in it, usually the moment they saw the part being tested. It includes visitors who bought nothing: they count as $0. A vs B analyses the amount each visitor spent. For each variation, it then shows:
- the mean revenue per visitor
- a 95% confidence interval around that mean. This is the range the true value is likely to sit inside.
- the lift: how the mean compares with the control
The headline lift on a revenue metric is this money lift, not the conversion-rate lift. The difference matters. A variation can convert slightly fewer visitors, yet still earn more per visitor, through fewer, bigger orders. Judge that test by conversion rate, and you'd pick the wrong winner. Judge it by revenue per visitor, and you'd get it right. Purchase conversion rate still appears as its own metric. It just doesn't stand in for revenue.
Outlier capping is on by default. One unusually huge order shouldn't decide an experiment. So A vs B caps revenue values above the 99th percentile down to it, per variation, before analysis. This is called winsorization: capping extreme values before averaging, so one giant order can't swing the result. The Results page notes when values were capped. You can adjust or turn off the cap in the metric's settings. See Statistical Methodology for the maths behind it.
Browser snippet: avsb.track.purchase
Call avsb.track.purchase as soon as you confirm a purchase. Amounts are plain decimal numbers in the order's currency: 49.99 means $49.99. You don't need to convert to minor units (cents); the snippet sends the value as-is.
avsb.track.purchase({ orderId: 'ORDER-8842', // Required. Unique identifier for this order. total: 49.99, // Required. Order total as a decimal. currency: 'USD', // Optional. Defaults to the project's configured currency. coupon: 'SPRING10', // Optional. items: [ // Optional line items. { sku: 'SKU-001', name: 'Widget', price: 24.99, quantity: 2 } ]});If you leave out orderId, or total isn't a finite number greater than 0, the snippet logs a console warning and records nothing. It never throws inside your page.
PurchaseOrder fields
| Field | Type | Required | Description |
|---|---|---|---|
orderId | string | Yes | Unique order identifier. Re-sending the same orderId replaces the previous record (last-write-wins deduplication). |
total | number | Yes | Order total as a decimal (e.g. 49.99). Must be a finite number greater than 0. |
currency | string | No | ISO 4217 currency code (e.g. 'USD', 'GBP', 'JPY'). Defaults to the project's configured currency when omitted. |
subtotal | number | No | Order subtotal before shipping, tax, and discounts. |
shipping | number | No | Shipping cost. |
tax | number | No | Tax amount. |
discount | number | No | Discount amount applied to the order. |
coupon | string | No | Coupon or promo code used. |
test | boolean | No | Mark this as a test purchase. A vs B still stores it, but leaves it out of every revenue, profit, and recommendation calculation, and out of every targeting group. Only send this when you're deliberately testing your integration. |
items | array | No | Line items: { sku, productKey?, name?, price?, quantity?, category? }. Each item needs a sku. Send productKey (the parent product id) when your sku identifies a variant: product analytics like bestsellers and viewed-together group by it, and it defaults to sku when you omit it. Item quantities feed the Units per order metric. |
E-commerce example
Read the order details from the confirmation page after checkout completes:
// Typically placed on your order confirmation pagevar orderIdEl = document.getElementById('order-id');var orderTotalEl = document.getElementById('order-total');if (orderIdEl && orderTotalEl) { avsb.track.purchase({ orderId: (orderIdEl.textContent || '').trim(), total: parseFloat(orderTotalEl.dataset.total || ''), currency: 'USD', });}Auto-collect from a GTM dataLayer
If your site already pushes a GA4 purchase event to window.dataLayer, A vs B can pick it up automatically without any extra code changes. Turn on Google Analytics 4 / dataLayer on your project's Product catalog page, under Reuse your existing analytics. It starts working once your catalog is connected and receiving products. See the product catalog guide.
Once it's on, A vs B scans entries already in the dataLayer, then intercepts future pushes. It converts anything matching the GA4 purchase shape or the legacy Universal Analytics Enhanced Ecommerce shape into a purchase record automatically. It silently skips malformed or partial entries, for example one missing a transaction ID or order total. It suppresses duplicate transaction IDs within the same page view too.
GA4 purchase shape: requires event: 'purchase' plus ecommerce.transaction_id and ecommerce.value:
window.dataLayer = window.dataLayer || [];window.dataLayer.push({ event: 'purchase', ecommerce: { transaction_id: 'ORDER-8842', value: 49.99, // Decimal amount, sent as-is currency: 'USD', tax: 4.10, shipping: 5.00, coupon: 'SPRING10', items: [ { item_id: 'SKU-001', item_name: 'Widget', quantity: 1, price: 49.99 } ] }});Legacy UA Enhanced Ecommerce shape: A vs B recognises this one by the nested ecommerce.purchase.actionField object. It needs no top-level event key, but it does need actionField.id and actionField.revenue:
window.dataLayer = window.dataLayer || [];window.dataLayer.push({ ecommerce: { purchase: { actionField: { id: 'ORDER-8842', revenue: 49.99, tax: 4.10, shipping: 5.00, coupon: 'SPRING10' }, products: [ { id: 'SKU-001', name: 'Widget', price: 49.99, quantity: 1 } ] } }});A vs B deduplicates both shapes by orderId. If you add avsb.track.purchase calls alongside your existing dataLayer pushes, A vs B still records the order once: whichever write arrives last wins. You don't need to remove your existing dataLayer code before switching to explicit calls.
Server-side: Node SDK trackPurchase
Use the Node SDK for server-rendered confirmation pages, or when your server confirms the order on its own. It records the purchase server-side, before the page renders.
import { AvsbServer } from '@avsbhq/node';const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY ?? '' });// Your own order record, whatever your checkout produces:interface OrderLine { sku: string; name: string; unitPrice: number; quantity: number }interface Order { id: string; total: number; coupon?: string; lineItems: OrderLine[] }// Inside your order-confirmed webhook handler:export async function recordPurchase(visitorId: string, order: Order): Promise<void> { await server.trackPurchase(visitorId, { orderId: order.id, total: order.total, // Decimal, e.g. 49.99 currency: 'USD', // Optional: defaults to the project currency coupon: order.coupon, items: order.lineItems.map((li) => ({ sku: li.sku, name: li.name, price: li.unitPrice, quantity: li.quantity, })), });}The order shape matches the browser snippet's PurchaseOrder exactly: same fields, same decimal amounts. The Node SDK sends each purchase right away, instead of batching it with other events.
The visitorId must match the visitor ID the browser snippet set, so A vs B can attribute the purchase to the right experiment exposure. Retrieve it from the visitor's session or cookie.
Verify it's working
Open Commerce → Orders to see the "Orders & attribution" page. It shows recent orders and totals per currency. It also shows how many orders A vs B couldn't attribute to an experiment, for example because they arrived before any exposure was recorded.
A vs B still stores an order that arrives before a visitor's first exposure to any experiment. It just credits that order to nothing. You'll see it in Commerce → Orders, and it won't affect experiment results.
Going further: profit metrics
Purchase tracking gives you revenue metrics. Add a product-costs dataset and A vs B unlocks Profit per visitor and Profit per order: the same revenue figures adjusted for your cost of goods.