Skip to content

Smart Add-Ons Cache-Safe A/B Testing: 4 Easy Facts

Smart Add-Ons cache-safe A/B testing: both variants tagged and shipped, a cookie picks one

Developer guide

How does Smart Add-Ons cache-safe A/B testing actually split traffic?

On a page that might be served from a full-page cache, the server never picks a variant per request — it sends both add-on groups, tagged with the test ID and split percentage, and a storefront script decides which one to show using a first-party cookie. The variant itself comes from a four-step fallback: an explicit assignment posted with the form, that cookie, which side's fields were actually submitted, and finally a deterministic crc32 hash of the visitor's session, sticky for the rest of that session.

Smart Add-Ons cache-safe A/B testing solves a problem a naive implementation runs straight into: if the server decides which variant to show and that decision gets baked into a cached page, every visitor who hits the cache afterward sees whichever variant the first visitor got — the test silently stops splitting traffic the moment a cache plugin does its job. This is how the actual resolver avoids that, and the fallback chain it uses to assign a variant consistently without needing a database write for every anonymous visitor.

Smart Add-Ons cache-safe A/B testing: both variants sent tagged, a cookie decides which one renders
Both variants ship in the response; a cookie decides which one renders.

Page-render mode: send both, let the browser pick

For anything that can be cached — a product page render, the public REST response — the resolver’s page mode never removes a group to make the split. Both sides of an active test are kept and tagged with ab_test_id, ab_variant, and ab_split:

if ( $assignments === null ) {
    $groups = $this->tag( $groups, $group_a, $test_id, 'a', $split );
    $groups = $this->tag( $groups, $group_b, $test_id, 'b', $split );
    continue;
}

The HTML that gets cached is identical for every visitor — both groups present, both tagged — and a small storefront script reads the tags client-side to hide one and show the other, writing a first-party cookie so the same visitor sees the same variant on their next page load. A cache doesn’t know or care that a test is running; it’s just caching one page that happens to contain both possibilities.

Visitor mode: four-step fallback to pick one

Add-to-cart validation is different — it needs to know which single variant this specific shopper actually saw, in order. visitorVariant() checks four sources, in a fixed priority order, stopping at the first one that resolves:

if ( isset( $assignments[ $test_id ] ) )       return $assignments[ $test_id ];   // 1. posted with the form
$cookie = self::cookieVariant( $test_id );
if ( $cookie !== null )                        return $cookie;                    // 2. saue_ab_{id} cookie
$inferred = $this->inferFromSubmission(...);
if ( $inferred !== null )                      return $inferred;                  // 3. which side's fields were submitted
return $this->sessionVariant( $test_id, $split );                                 // 4. deterministic hash, session-sticky

Steps 1–3 are all ways of recovering a variant the shopper’s own request already implies — an explicit assignment, a cookie the storefront script set, or simply noticing that only one side’s field IDs appear in what they submitted. Step 4 is the fallback for when none of that is available: a session with cookies disabled, a request with no prior context. That’s where the actual bucketing math lives.

The fallback: crc32, not random

sessionVariant() hashes a session-derived seed plus the test ID, buckets the result into 0–99, and compares against the test’s traffic split:

$seed    = $this->getSessionSeed() . ':' . $test_id;
$bucket  = abs( crc32( $seed ) ) % 100;
$variant = $bucket < $traffic_split ? 'a' : 'b';

A hash instead of rand() matters here specifically because it's deterministic: the same seed always produces the same bucket, so the very first computation for a given visitor and test already lands on the variant they'll keep seeing — no prior assignment needs to exist yet. It's then written into the WooCommerce session so a returning request in the same session reads the stored value directly rather than recomputing, but the recompute would give the identical answer either way.

What happens once a winner is declared

None of the splitting logic above runs once a test has a recorded winner — the resolver checks for that first and, if found, simply drops the losing group from the results entirely:

if ( $winner > 0 && in_array( $winner, [ $group_a, $group_b ], true ) ) {
    $groups = $this->removeGroup( $groups, $winner === $group_a ? $group_b : $group_a );
    continue;
}

A decided test costs nothing extra to serve afterward — no tagging, no cookie logic, no hashing — every visitor just gets the winning group as if it had always been the only one. It's the one state where Smart Add-Ons cache-safe A/B testing has nothing left to keep safe for the cache, because there's no longer a split to protect.

Common questions

Could a shopper manipulate their own crc32 bucket by changing their session?

Forcing a new WooCommerce session (clearing cookies, an incognito window) does reroll the bucket, the same way it would reset any session-scoped state. That's an accepted characteristic of session-based bucketing generally, not something specific to Smart Add-Ons cache-safe A/B testing, and it doesn't affect the aggregate split across real traffic.

Does Smart Add-Ons cache-safe A/B testing work with page caching plugins out of the box?

That's the entire point of the tag-both-groups design — the cached HTML is variant-neutral, so no cache-plugin-specific integration or cache-bypass rule is needed for the split itself to keep working. What does still need attention on a cached site is anything that fragments the page per visitor differently, which is a general page-caching consideration, not one unique to this feature. It's also why Smart Add-Ons cache-safe A/B testing needed its own resolver in the first place, rather than reusing generic split-testing logic built for uncached pages.

For the store-owner-facing walkthrough of setting up a test, see the A/B testing guide, and the PHP crc32() documentation (opens in new tab) for the hash function this bucketing is built on.

Dhyan Software Avatar

Written by

Ready to ship this in your store?

Smart Add-Ons for WooCommerce turns what you just read into a working product page — no code required.

Comments

Leave a comment

Your email is not published.