Most "split tests" fail because they’re treated like a design change instead of a routing + attribution system. This guide shows the safe way to test variants in GHL - including sticky assignment, event mapping, and QA that prevents double-fires and misattribution.
If you skip these, your results will lie. You’ll think you found a winner, but you just changed attribution or created double-fires.
A user should see the same variant across refreshes (and ideally across steps). Otherwise, you contaminate the test.
Define the conversion step (booking confirm, checkout success, app submit) and track it once per journey.
If your UTM persistence, trigger links, or pixel firing is inconsistent, pause. Fix attribution first.
Variant A fires conversion on the success page, but Variant B fires conversion on the form submit (or both). You’ll "prove" B wins even if it doesn’t. Always map events by step and validate one-by-one.
External split test results for an HVAC company testing headline copy.
Don’t start by changing 15 things. Start with changes that improve clarity and intent matching - with minimal downstream effects.
| Use case | Recommendation |
|---|---|
| Simple 50/50 page variant | Native split is fine if tracking is clean |
| Need URL parameter control | External logic (query params + sticky storage) |
| Need segment rules (source/device) | External logic (rules + assignment) |
| Multi-step funnel consistency | External "sticky" assignment across steps |
You have two goals: (1) assign a variant consistently, (2) keep it easy to override for QA, demos, and ads routing.
Use ?v=a or ?v=b to force a variant.
If no param exists, randomly assign once and persist.
<script>
(function(){
var KEY = 'atj_ab_variant';
var param = new URLSearchParams(location.search).get('v'); // a | b
var stored = localStorage.getItem(KEY);
function pick(){ return (Math.random() < 0.5) ? 'a' : 'b'; }
var v = (param === 'a' || param === 'b') ? param : (stored || pick());
localStorage.setItem(KEY, v);
// Example: route user to variant URL (edit these)
var urlA = '/your-funnel-step-a';
var urlB = '/your-funnel-step-b';
// Only redirect if you're on the "router" page
// (Use a dedicated router step so you don't loop)
var isRouter = document.body && document.body.getAttribute('data-atj-router') === '1';
if(isRouter){
var target = (v === 'a') ? urlA : urlB;
// Strip v= from query string but keep other params (UTMs)
var u = new URL(location.href);
u.searchParams.delete('v');
if(location.pathname !== target){
location.replace(target + (u.search || '') + (u.hash || ''));
}
}
// Optional: expose variant for debugging
window.ATJ_AB = {variant:v};
})();
</script>
If you must keep the exact same URL, you can show/hide variant sections by class. This is harder to QA and easier to break - but it works when URL parity is required.
<style>
.ab-a{ display:none; } .ab-b{ display:none; }
</style>
<script>
(function(){
var v = (window.ATJ_AB && window.ATJ_AB.variant) || localStorage.getItem('atj_ab_variant') || 'a';
document.querySelectorAll('.ab-' + v).forEach(function(el){ el.style.display='block'; });
})();
</script>
The 4-step testing workflow we use for every external split test.
Don’t guess. Create an event map. Decide which scripts are global, which are step-level, and where conversions fire. This is the difference between "testing" and "hoping."
| Event | Where it fires | Notes | Anti-bug rule |
|---|---|---|---|
| ViewContent | Global or each landing step | Optional; avoid duplicates | One per page load |
| Lead | Form submit OR opt-in success page | Pick one and standardize | Never fire on both |
| Schedule | Booking confirmation step | Preferred for appointment funnels | Only on confirm |
| Purchase | Checkout success step | Revenue tracking must be step-specific | No purchase on submit |
| Variant tag | Any step (optional) | Store variant to pass into analytics logs | Use one variant key |
If you use GTM, push the variant once so you can segment results by A vs B without hacks. (Only do this if you actually have a reporting plan.)
<script>
(function(){
var v = (window.ATJ_AB && window.ATJ_AB.variant) || localStorage.getItem('atj_ab_variant') || 'a';
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ event:'ab_variant', ab_variant: v });
})();
</script>
If you skip QA, you’ll burn budget proving nothing. Run this checklist with real test submissions and real device checks.
?v=a / ?v=bConfirm routing is correct and sticky behavior persists across refreshes.
Verify pipeline stage, notifications, tags, and any workflow triggers.
Confirm your conversion event fires only on the intended step (confirm/success).
Variant B "winning" due to broken mobile layout is not a win - it’s a bug.
Make sure UTMs don’t get stripped by redirects or router steps.
Know how to force a winner immediately (router set to 100% to one variant).
Send your funnel URL and what you want to test. We’ll set up variants, assignment rules, event mapping, and QA - then hand it off with a simple "how to run tests" SOP.
High-intent answers for teams actually running split tests in GHL.
?v=a or ?v=b.
Persist it in localStorage so the experience stays consistent.
Everything in this guide runs on GoHighLevel. Try it free for 30 days and see why we chose it.
No credit card required · Cancel anytime