Skip to content
  1. Home
  2. Blog

how-to · · 9 min read

woocommerce conversion rate: how to measure one you can trust

WooCommerce has no conversion rate of its own. How to measure one you can trust: what to divide by what, why GA4 and WooCommerce disagree, and orders by source.

By Big Bad Agency

Every list of ways to raise your WooCommerce conversion rate starts after the step that decides whether any of it worked: knowing what your conversion rate actually is.

That’s harder than it sounds, because WooCommerce doesn’t have one. It knows about orders. It doesn’t know about visits, and a conversion rate is orders divided by visits. So the number comes from somewhere else, usually Google Analytics, and that number regularly disagrees with your order list by more than the improvement you’re hoping to measure.

Here’s how to get a conversion rate you can trust, why the two sources disagree, and how to use the attribution data WooCommerce already keeps on every order.

what you’re actually dividing

“Conversion rate” is a fraction, and both halves are choices.

The top: what counts as a conversion.

  • Orders, or sessions with an order? Someone who places two orders in one visit is one converting session but two orders. Most analytics tools count sessions. Your order list counts orders.
  • Which orders? An order that’s still pending or on hold, like a bank transfer that may never be paid, isn’t a sale yet. If you compare against revenue, count paid orders: processing and completed.

The bottom: what counts as a visit.

  • Sessions or people? A shopper who looks on Monday and buys on Thursday is two sessions and one person. A per-session rate is lower than a per-person rate for the same store, and neither is wrong.
  • All traffic, or shopping traffic? Visits to your blog, your returns policy and your login page all sit in the denominator. A store with a busy blog has a lower sitewide rate than one without, while selling exactly as well.

Pick one definition, write it down, and never compare a number from one definition with a number from another. That last mistake causes more fake “drops” than anything a customer does.

the number from Google Analytics

If your store sends ecommerce events to GA4 (through Google’s WooCommerce plugin or another integration), GA4 has the pieces:

  • Sessions, and Ecommerce purchases (the number of purchase events) in the Monetization reports.
  • Session key event rate: the share of sessions with a key event. If purchase is your only key event, this is a per-session conversion rate. If you’ve also marked newsletter signups or contact forms as key events, it isn’t; it’s the rate of anything you’ve marked.
  • Purchase journey and Checkout journey in the Monetization section, which show how many sessions reach each step: viewing a product, adding to cart, starting checkout, buying.

The journey reports are the useful ones. “2% of sessions buy” tells you very little. “Plenty of people add to cart, and most of them leave at the shipping step” tells you where to look.

why GA4 and WooCommerce disagree

Put GA4’s purchase count for last month next to WooCommerce’s paid orders for the same month. They almost never match. The usual reasons, roughly in order of size:

  • Blocked or declined tracking. Ad blockers and privacy browsers stop the GA4 script on some visits. On a site with a consent banner, visitors who decline analytics may not be recorded at all. Those orders exist in WooCommerce and not in GA4.
  • The thank-you page never loaded. The purchase event usually fires on the order-received page. A customer who pays with an off-site gateway and closes the tab before being sent back has a real order and no purchase event.
  • Orders that never touched the website. Orders you create in the admin, phone orders and point-of-sale orders are in WooCommerce and can’t be in GA4.
  • Unpaid orders that looked like purchases. A bank-transfer order shows the thank-you page too, and with most tracking setups that sends a purchase event straight away. If it’s never paid, GA4 counted a sale WooCommerce didn’t.
  • Different time zones. GA4 uses the property’s time zone and WooCommerce uses the store’s. If they differ, orders near midnight land on different days, and month totals shift at the edges.

None of this makes GA4 useless. It means GA4’s conversion rate is a consistent estimate, not a count. Treat it like one: watch its trend, and check its purchase count against WooCommerce’s paid orders once a month. If the gap between the two suddenly changes, something in the tracking changed, not your customers.

the part WooCommerce does know: where orders came from

Since version 8.5, WooCommerce records where each order came from, on by default. It’s called Order Attribution, and the setting lives under WooCommerce → Settings → Advanced → Features. Open any order and the Order attribution box shows its origin (for example Organic: Google, Referral: Facebook.com, Source: newsletter for a UTM-tagged link, or Direct), plus the device type and how many pages were viewed in that visit. The order list shows the same thing in its Origin column.

It’s stored on the order itself, so it covers every order the tracking could see, whatever happened to GA4 on that visit. Orders WooCommerce couldn’t attribute show as Unknown, and orders created in the admin show as Web admin.

That’s the top half of a per-channel conversion rate, counted from your own orders. This read-only WP-CLI script totals a date range’s paid orders by origin, using the same labels WooCommerce shows:

wp eval-file orders-by-origin.php 2026-10-01 2026-10-31
<?php
/**
 * Count a date range's paid WooCommerce orders by where they came from, using
 * WooCommerce's own Order Attribution data (WooCommerce 8.5+, on by default).
 *
 * Run from the WordPress root with WP-CLI:
 *   wp eval-file orders-by-origin.php 2026-10-01 2026-10-31
 *
 * Read-only. Uses wc_get_orders(), so it works with HPOS and with legacy post
 * storage alike.
 */

/**
 * The label WooCommerce shows in the Origin column, rebuilt from the two stored
 * values (source type and utm_source). Kept free of WordPress so it can be tested.
 */
function bb_obo_origin( $source_type, $source ) {
	$source = ucfirst( trim( (string) $source, '()' ) );
	switch ( $source_type ) {
		case 'utm':
			return 'Source: ' . $source;
		case 'organic':
			return 'Organic: ' . $source;
		case 'referral':
			return 'Referral: ' . $source;
		case 'typein':
			return 'Direct';
		case 'mobile_app':
			return 'Mobile app';
		case 'admin':
			return 'Web admin';
		case 'pos':
			return 'Point of Sale';
		default:
			return 'Unknown';
	}
}

/**
 * Group rows of array( source_type, source, revenue ) by origin, biggest first.
 */
function bb_obo_group( array $rows ) {
	$out = array();
	foreach ( $rows as $r ) {
		$label = bb_obo_origin( $r[0], $r[1] );
		if ( ! isset( $out[ $label ] ) ) {
			$out[ $label ] = array( 'orders' => 0, 'revenue' => 0.0 );
		}
		$out[ $label ]['orders']++;
		$out[ $label ]['revenue'] += (float) $r[2];
	}
	uasort(
		$out,
		function ( $a, $b ) {
			return $b['orders'] <=> $a['orders'];
		}
	);
	return $out;
}

if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
	return; // Loaded by the test: only the functions above are defined.
}

list( $from, $to ) = array_pad( $args, 2, '' );
if ( ! $from || ! $to ) {
	WP_CLI::error( 'Usage: wp eval-file orders-by-origin.php 2026-10-01 2026-10-31' );
}

$rows = array();
$page = 1;
do {
	$result = wc_get_orders(
		array(
			'status'       => wc_get_is_paid_statuses(),
			'type'         => 'shop_order',
			'date_created' => $from . '...' . $to, // Both days included, store time zone.
			'limit'        => 200,
			'page'         => $page,
			'paginate'     => true,
		)
	);
	foreach ( $result->orders as $order ) {
		$rows[] = array(
			$order->get_meta( '_wc_order_attribution_source_type' ),
			$order->get_meta( '_wc_order_attribution_utm_source' ),
			(float) $order->get_total() - (float) $order->get_total_refunded(),
		);
	}
	$page++;
} while ( $page <= $result->max_num_pages );

WP_CLI::log( sprintf( 'Paid orders %s to %s: %d', $from, $to, count( $rows ) ) );
foreach ( bb_obo_group( $rows ) as $label => $g ) {
	WP_CLI::log( sprintf( '%-34s orders: %5d   revenue after refunds: %s', $label, $g['orders'], wc_format_decimal( $g['revenue'], wc_get_price_decimals() ) ) );
}

What it does, plainly:

  • Paid orders only (processing and completed, WooCommerce’s own list), dated in your store’s time zone.
  • Revenue is the order total after refunds, including tax and shipping. It’s there to rank channels against each other, not for your accounts.
  • The labels match WooCommerce’s Origin column for each source type. A plugin that changes those labels with WooCommerce’s filters won’t change this script’s.
  • Orders from before attribution was on, or with tracking turned off, land in Unknown.

We tested the labels and the grouping with sample data. We have not run the order query on a live store, so try it on a staging copy first.

To turn it into a rate, divide each channel’s orders by the same channel’s sessions from GA4 (Reports → Acquisition → Traffic acquisition, same dates). It’s two sources, so the caveats above apply: the result is an estimate. But it uses your order count for the top of the fraction, which is the half you can trust.

before you change anything

The point of all this is to be able to tell whether a change worked. Three habits make that possible:

  • Write down the baseline first. Last full month’s rate, by your definition, from the same report you’ll check afterwards. Without it, every change “looks like it helped”.
  • Change one thing at a time, and give it long enough. A small store sees a lot of noise week to week. A difference of a few orders is usually chance, not the new checkout button.
  • When you can, test instead of comparing months. Show the change to half your visitors and not the other half, at the same time, so the season, the weather and your newsletter affect both groups equally. We did exactly this for a spin-to-win wheel, including how long to run it before the result means anything.

And remember the rate is only one of three numbers that make up revenue. A change that lifts the conversion rate by giving a discount can lower what each order earns. A change that brings in buyers who never return shows up later, in the repeat purchase rate. Measure all three, and the improvements you keep will be the real ones.

Disagree? Good.

Write to a person: hello@bigbad.agency. We answer within a business day.

Start a project