Analytics snippets
How to Track Purchase Revenue in GA4 for an A/B Test
★★★ Technical level 3 of 3
Written by Neil Webley · Last updated
Track revenue in GA4 with GTM, prevent duplicate purchases, measure repeat orders in A/B tests, and set up Shopify with FreeCROTool.
Quick answer
Track a completed order with GA4's recommended purchase event. Send the order total as a number in value, its three-letter currency in currency, and a stable, unique order number in transaction_id. Fire it only after payment is confirmed, not when somebody clicks Buy or starts checkout.
For an A/B test, send the experiment and variant with the purchase so revenue can be attributed to the version the visitor actually saw. Count each purchaser once when calculating conversion rate, but include every different order when calculating total revenue and average order value.
What GA4 needs to track purchase revenue
GA4 does not infer reliable revenue from a thank-you page URL. The checkout must expose trusted completed-order data and send a purchase event. At minimum, send:
-
transaction_id: the unique order or confirmation number. Never send an empty string. -
value: the monetary total as a number, not a formatted string such as£42.50. -
currency: the order currency as an ISO 4217 code such asGBP,USDorEUR.
An items array is strongly recommended when you need product, quantity or item-revenue reporting. You can also send tax, shipping, coupon and affiliation. Decide what value includes and use that definition consistently. GA4's recommended convention is the sum of price multiplied by quantity, excluding shipping and tax.
Direct gtag.js example
gtag('event', 'purchase', {
transaction_id: completedOrder.id,
value: Number(completedOrder.total),
currency: completedOrder.currency,
tax: Number(completedOrder.tax),
shipping: Number(completedOrder.shipping),
items: completedOrder.items.map(function (item) {
return {
item_id: item.sku,
item_name: item.name,
price: Number(item.price),
quantity: Number(item.quantity)
};
})
});
completedOrder is an example name, not a browser feature. Replace it with server-rendered or checkout-provided data that can only represent a confirmed order. Do not scrape a visually formatted total when structured order data is available, and never send names, email addresses, postal addresses or other personal information to GA4.
How to track purchase revenue with Google Tag Manager
The cleanest GTM setup separates the website's job, placing trusted order data in the data layer, from GTM's job of sending it to GA4.
1. Push the completed order to the data layer
Run this only after the order is confirmed. The event value is the name GTM will use as its trigger.
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'purchase_complete',
ecommerce: {
transaction_id: completedOrder.id,
value: Number(completedOrder.total),
currency: completedOrder.currency,
tax: Number(completedOrder.tax),
shipping: Number(completedOrder.shipping),
items: completedOrder.items
}
});
Clearing ecommerce first prevents values left by an earlier ecommerce event being merged into this purchase.
2. Create the GTM trigger and tag
-
Create a Custom Event trigger whose event name is
purchase_complete. -
Create the data layer variables needed by your GA4 tag, including
ecommerce.transaction_id,ecommerce.valueandecommerce.currency. -
Create a GA4 Event tag named
purchase, map the ecommerce data, and attach the custom-event trigger. - Preview the container and complete a test order. Check the data layer, the tag firing once, and the received event in GA4 DebugView.
Use either the site's Google tag or GTM as the route that sends this event. Do not send the same purchase through both, and check whether an ecommerce platform integration already sends purchases before adding another implementation.
How revenue attribution works during an A/B test
A purchase may happen several pages, or several days, after the tested page. The experiment tool therefore needs to retain the visitor's assigned test and variant and attach that assignment when the order completes. FreeCROTool uses its test cookies for this attribution and only fires a test conversion when the visitor has entered that test.
Judge an ecommerce experiment with several related measures:
| Measure | Calculation | What it answers |
|---|---|---|
| Purchaser conversion rate | Unique purchasers ÷ test visitors | Did the version persuade more people to buy? |
| Orders | Distinct transaction IDs | How many genuine orders were placed? |
| Total revenue | Sum of all distinct order values | How much revenue did the version produce? |
| Average order value | Total revenue ÷ distinct orders | Was the typical basket larger? |
| Revenue per visitor | Total revenue ÷ test visitors | What was each assigned visitor worth on average? |
Revenue per visitor is often the most useful single commercial measure because it reflects both the likelihood of buying and the amount spent. Still inspect conversion rate and AOV separately: the same revenue-per-visitor movement can come from very different customer behaviour.
Do not use a button click as a substitute for a completed order. A checkout click is a useful supporting event, but it can include abandoned or failed payments.
How to stop thank-you page refreshes duplicating revenue
Use the order number as the primary idempotency key. GA4 web streams deduplicate purchase events that have the same transaction_id, but your own code should also avoid sending the duplicate. This reduces unnecessary requests and protects any other analytics destination that does not apply GA4's rule.
Order-ID guard
function sendPurchaseOnce(order) {
var transactionId = String(order.id || '');
if (!transactionId) return;
var storageKey = 'purchase_sent_' + transactionId;
if (localStorage.getItem(storageKey) === '1') return;
gtag('event', 'purchase', {
transaction_id: transactionId,
value: Number(order.total),
currency: order.currency
});
localStorage.setItem(storageKey, '1');
}
For stronger delivery guarantees, mark the order on the server or use an endpoint that treats repeated submissions of the same transaction ID as the same operation. Browser storage can be cleared, blocked or unavailable, and marking an event before a failed request can undercount while marking it afterwards can allow a race.
Fallback when no order number is available
A short-lived, first-party “checkout pending” cookie can prevent an ordinary thank-you-page revisit from counting. Set it on the final step before payment; on the confirmation page, require it, send the event, and immediately delete it. This is a fallback, not equivalent to an order ID: it can miss orders completed in a different browser context and cannot distinguish two genuine orders reliably.
// On the final step before the customer leaves to complete payment:
document.cookie = 'purchase_pending=1; Max-Age=3600; Path=/; SameSite=Lax; Secure';
// On the confirmed thank-you page:
if (document.cookie.split('; ').includes('purchase_pending=1')) {
// Send the confirmed purchase here.
document.cookie = 'purchase_pending=; Max-Age=0; Path=/; SameSite=Lax; Secure';
}
Do not manufacture a transaction ID from the time or page load. A refresh would create a new value and defeat deduplication.
Repeat visitors and multiple genuine orders
A returning customer who places a second genuine order during a two-week test has produced one purchaser but two orders. That distinction matters for food delivery, subscriptions with add-ons, tickets and other frequent or low-value purchases.
- For purchaser conversion rate, count the person once for that test and variant.
- For order count and revenue, count every purchase with a new transaction ID.
- For AOV, divide all valid revenue by all distinct orders, not by unique purchasers.
- Keep the visitor in the originally assigned variant for the test unless your experiment design explicitly says otherwise.
GA4 events are normally counted each time they occur. Marking purchase as a Key event lets GA4 report it as an important outcome; its Key-event counting setting can be once per event or once per session. Neither option represents “once per visitor for a two-week experiment”. Use the user or purchaser metric for that question, and transaction IDs for orders. Selecting once per session would also suppress a second valid purchase in the same session, so it is not a duplicate-order solution.
Track purchase revenue in a coded FreeCROTool test
Inside a coded A/B test, call FreeCROTool's firePurchaseEvent helper after you have trusted completed-order data. The helper associates the event with the visitor's assigned test and variant.
var completedOrder = {
id: 'ORDER-10482',
currency: 'GBP',
value: 42.50
};
window.cro_tests.firePurchaseEvent(TEST_ID, {
name: 'purchase',
transaction_id: completedOrder.id,
currency: completedOrder.currency,
value: completedOrder.value
});
Replace TEST_ID and every example value. The order ID must remain the same on refresh but differ for a later genuine order. Put the call behind an order-ID guard when the code can run more than once. See the FreeCROTool hand-coded test helper reference for the concise developer pattern and the firePurchaseEvent tracking reference for its surrounding analytics context.
Do not put test-only purchase code in a visual change
If every variant can lead to the same checkout, purchase measurement must be available to every assigned visitor, including control. A project-wide or checkout-wide implementation is safer than code that only runs when one visual variant is applied.
How Shopify purchase tracking works with FreeCROTool
For the standard GA4 ecommerce reports, Shopify recommends connecting GA4 through its Google & YouTube channel; supported ecommerce events are then collected automatically. Check this first so you do not add a second purchase tag.
FreeCROTool needs an additional experiment-aware event because Shopify checkout is separate from the ordinary storefront. The current Shopify setup on the Getting Started page is designed to run as a Shopify Customer Events custom pixel. It subscribes to Shopify's checkout_completed event, reads the confirmed checkout total, currency and order ID, reads active FreeCROTool assignments, and sends freecro_purchase for each relevant test.
The supplied script also stores a key made from the transaction ID and test ID in Shopify pixel local storage. Refreshing or revisiting the confirmation therefore does not send the same order again for that test. A returning customer with a new Shopify order ID is still counted as a new order and its revenue is retained.
Set up the Shopify custom pixel
- Complete the normal FreeCROTool global tag setup on the storefront so test assignments can be created.
- In Shopify admin, open Settings → Customer events and add a custom pixel.
- Copy the current Shopify checkout script from the Getting Started guide. Use that maintained copy rather than duplicating it from this page.
-
Replace its
GA_MEASUREMENT_IDwith theG-ID from the project's GA4 web stream. -
Create a Measurement Protocol API secret for that web stream and replace
GA_API_SECRET. - Set the fallback currency, connect the pixel, and complete a consented test order while the FreeCROTool test is in Staging.
- After testing, turn debug mode off so production purchases are not continually marked as debug traffic.
Shopify's older checkout additional scripts and checkout.liquid approach has been deprecated. A normal theme or global storefront script should not be assumed to execute on the Thank you or Order status page. Use Shopify Customer Events for the checkout confirmation while retaining the FreeCROTool global tag on the storefront.
The custom pixel should supplement experiment attribution, not duplicate your standard Shopify GA4 purchase. Keep freecro_purchase for FreeCROTool reporting and the standard purchase event for GA4 ecommerce reporting unless your implementation has been deliberately designed otherwise.
How to test revenue tracking before launch
- Use a test property or distinctive test order and put the FreeCROTool experiment in Staging.
- Test control and every variant. Confirm each retains the correct variant through checkout.
- Inspect GTM Preview or the browser/pixel console and GA4 DebugView. Confirm the event name and parameters are correct.
-
Check that
valueis numeric, currency matches the order, and the transaction ID is present and unique. - Refresh the confirmation page several times. The original order must not create additional purchase events.
- Place a second test order as the same visitor. It should keep the variant, create a new transaction, and add its value to revenue.
- Check that a visitor who never entered the experiment does not create a FreeCROTool purchase for it.
- Test consent accepted and rejected. Analytics must follow the site's consent policy and applicable law.
- After processed data is available, reconcile a small set of transaction IDs and totals against the ecommerce platform.
Common causes of missing or incorrect GA4 revenue
-
Revenue is zero:
valueis missing, formatted as text, or sent withoutcurrency. -
Every order appears once per refresh: there is no stable
transaction_id, or the ID is regenerated on each page load. - Purchases are doubled: Shopify's integration, GTM and a direct Google tag are sending the same event independently.
- The A/B report has purchases but no variant: assignment was not retained into checkout or the purchase bypassed FreeCROTool's helper/pixel.
- Shopify thank-you tracking stopped: the implementation relied on deprecated additional scripts instead of Customer Events.
- A second real order is missing: deduplication uses a visitor/session flag instead of the individual order ID.
- Reports do not match immediately: DebugView is for validation; standard GA4 reports can take time to process.
Refunds, cancellations and data quality
A completed purchase can later be refunded or cancelled. For accurate long-term revenue, send GA4's recommended refund event with the original transaction ID and reconcile analytics totals against the order system. Client-side tracking can be blocked, so GA4 should guide experiment decisions rather than replace financial records.
Document the event name, value definition, currency behaviour, consent rule and duplicate strategy before launch. Do not change them part-way through a test: a tracking change can look like variant performance.