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.

JavaScript
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 }  ]});
JavaScript9 lines

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

FieldTypeRequiredDescription
orderIdstringYesUnique order identifier. Re-sending the same orderId replaces the previous record (last-write-wins deduplication).
totalnumberYesOrder total as a decimal (e.g. 49.99). Must be a finite number greater than 0.
currencystringNoISO 4217 currency code (e.g. 'USD', 'GBP', 'JPY'). Defaults to the project's configured currency when omitted.
subtotalnumberNoOrder subtotal before shipping, tax, and discounts.
shippingnumberNoShipping cost.
taxnumberNoTax amount.
discountnumberNoDiscount amount applied to the order.
couponstringNoCoupon or promo code used.
testbooleanNoMark 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.
itemsarrayNoLine 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:

JavaScript
// 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',  });}
JavaScript11 lines

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:

JavaScript
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 }    ]  }});
JavaScript15 lines

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:

JavaScript
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 }      ]    }  }});
JavaScript17 lines
Deduplication makes double instrumentation harmless

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.

TypeScript
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,    })),  });}
TypeScript23 lines

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.

Orders before any exposure

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.

Was this helpful?