Phil
penny
96x96
naturalmohican By Phillip David Penny
« Home  /  Blog

Aug 01, 2022

How to build a dynamic A/B test

A full-page A/B test using Google Optimize.

1060 words (Approximately a 6 minute read)

Google Optimise has character limits on the custom code you can write; in order to get around this, we need to load our code via external files by injecting a script tag into the <head> of the page for the JavaScript and a <link> tag for the CSS.

Setting up Google Optimise

Specify the template to run the test on using RegEx:

^https://(www.)?example.co.uk/subfolder/[^/]+/$

Add the following JS code to the head element, after the opening tag:

let js = document.createElement('script');
js.type = 'text/javascript';
js.src = 'javascript.js';
document.body.appendChild(js);

function addCss(fileName) {
  let head = document.head;
  let link = document.createElement('link');

  link.type = 'text/css';
  link.rel = 'stylesheet';
  link.href = fileName;

  head.appendChild(link);
}

addCss('styles.css');

Planning the test

When dealing with a dynamic page where change is heavily reliant on JavaScript, we need to ensure we are getting the latest version of the page when all loading has completed. We will need to use mutation observer to help achieve this, along with following a step-by-step approach—asynchronous JavaScript is required here, using the async and await features.

The basic steps are:

  1. Fetch dynamic page content
  2. Assign placeholder content
  3. Assign variables for dynamic content
  4. Build compound variables from dynamic content
  5. Build page element

Fetch dynamic content

innerHTML has a character limit, so in order to avoid the browser splitting the code out into a NodeList, we can do it ourselves. In our case, this has been semi-automated by inserting <!--JS_SPLIT--> comments into the pre-built page code to make it easier to segregate the page sections.

We have placeholders in our new page code following the format ##VARIABLE## which will be replaced once we have scraped the dynamic page content.

function isEmpty(element) {
	return element.innerHTML === ''
}

let abPreStyle = '<div ...',
abPostStyle = '<div ...',
abHero = '<section><div>##VARIABLE##</div>...',
abIntro = '<section ...',
abQuality = '<section...',
abTypes = '<section ...',

Assign placeholder content

Create all variables needed to store the dynamic page content once it’s scraped:

let pageTitle = '', infinity = '', teamManagerImg = '', teamManagerTitle = '', teamManagerName = ''...

Assign variables for dynamic content

Scrape the page content and store inside the variables:

Some content, such as images, may be lazy-loaded, so we may need to poll for them:

function getContent(){

	// A) Hero
	pageTitle =  document.querySelector('.page-title').innerHTML;
	// Real page number
	pageNumber =  document.querySelector('.existing-call-num').textContent.replace(/[\n\t\r]/g,"").trim();
	pageNumberCompressed =  pageNumber.replace(/\s+/g, '');

	// B) Information (we need to use `outerHTML` to preserve internal links.)
	pageIntroArray = document.querySelectorAll('.page-intro-copy p');
	for(let i = 0, l = pageIntroArray.length; i < l; i++ ){
		if(pageIntroArray[i].innerHTML != ''){
			if (pageIntroArray[i].className == '') {
				pageIntroArray[i].removeAttribute("style");
				pageIntro += pageIntroArray[i].outerHTML;
			}
		}
	}


	// F) Team

	// Fa) Manager
	//teamManagerImg = document.querySelector('.manager-image img').src;
	if(document.querySelector('.manager-image source').getAttribute('data-lazy-srcset') != null){
		teamManagerImg = document.querySelector('.manager-image source').getAttribute('data-lazy-srcset');
	}
	let teamManagerTitleString = document.querySelector('.page-manager-box h2.h4').textContent;
	teamManagerTitle = teamManagerTitleString.split(',')[1].split(',')[0].split('our ',)[1];
	teamManagerName = document.querySelector('.manager-image img').alt;
	teamManagerQuote = document.querySelector('.page-manager-box .text-left').innerHTML;
	teamManagerQuote = textTrim(teamManagerQuote, 700);



	// TODO: Images are lazy loaded so you may need to poll for them:

	function refresh() {
		console.log("...Refreshing manager image search...");
		teamManagerImg = '';
		if(document.querySelector('.manager-image source').getAttribute('data-lazy-srcset') != null){
			teamManagerImg = document.querySelector('.manager-image source').getAttribute('data-lazy-srcset');
		}
		if(teamManagerImg.indexOf('wp-content') > -1){
			teamManagerImg = document.querySelector('.teamManagerImg').src;
		} else {
			setTimeout(refresh, 5000);
		}
	}

	function refreshMember() {
		teamMemberImg = '';
		if(pageTeamArray[i].querySelector('source') != null){
			teamMemberImg = pageTeamArray[i].querySelector('source').getAttribute('data-lazy-srcset');
		} else {
			setTimeout(refreshMember, 5000);
		}
	}

	if(teamManagerImg.indexOf('wp-content') > -1){
		// Image has been found successfully
	} else {
		setTimeout(refresh, 500);
	}

	// Fb) Member(s)
	let pageTeamArray = document.querySelectorAll('.page-meet-team .page-other-team-boxes');
	for(let i = 0, l = pageTeamArray.length; i < l; i++ ){
		if(pageTeamArray[i]){
			let teamMemberName = pageTeamArray[i].querySelector('h3').innerHTML;
			let teamMemberDetail = pageTeamArray[i].querySelector('p').textContent;
			let teamMemberTitle = teamMemberDetail.split(',')[0];
			let teamMemberLocation = teamMemberDetail.split(', ')[1];
			if (teamMemberLocation == null) {
				teamMemberLocation = '';
			}
			//let teamMemberImg = pageTeamArray[i].querySelector('img').src;
			let teamMemberImg = "";
			if(pageTeamArray[i].querySelector('source') != null){
				teamMemberImg = pageTeamArray[i].querySelector('source').getAttribute('data-lazy-srcset');
			} else {
				setTimeout(refreshMember, 5000);
			}

			// Only add content if all fields exist
			if(teamMemberName && teamMemberImg){
				subTeam += abTeamTemplate.replace('##TEAMMEMBERNAME##',teamMemberName). ...
			}
		}
	}
}

Build compound variables from dynamic content

// 4) Build page element
function createContent(){

	let pageWrap = document.querySelector('.page-wrap'),
	dynamicCode = '';

	dynamicCode = abPreStyle + abPostStyle + abHero + abIntro + ...;
	dynamicCode = dynamicCode.replaceAll('##pageNAME##',pageTitle).replaceAll ...

// DON'T REPLACE CODE; APPEND TO IT THEN HIDE THE ORIGINAL
// This will allow us to poll for any elements that are lazy loaded and insert them later.

	let z = document.createElement('div'); // is a node
	z.innerHTML = dynamicCode;
	//referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
	pageWrap.parentNode.insertBefore(z, pageWrap.nextSibling);
	pageWrap.style.visibility = 'hidden';
	pageWrap.style.overflow = 'hidden';
	pageWrap.style.height = '0';

	// Check team member images have been lazy loaded:
	/*(function () {
	let teamImgArray = document.querySelectorAll('.teamMemberImg');
	for(let i = 0, l = teamImgArray.length; i < l; i++ ){
		if(teamImgArray[i]){
			let teamMemberImg = teamImgArray[i].querySelector('img').src;

			if(teamMemberImg.indexOf('wp-content') > -1){
		// Image has been found successfully
	} else {
		setTimeout(refreshCopy, 5000);
	}
}
}
})();*/

}

Handle any external content that is likely to load after a delay:

function runExternal() {
  // Check if the external content has changed
  // using mutation observer

  let infinityDom = document.querySelector('.InfinityNumber'),
    options = { childList: true }, // HTML child element is being changed
    observer = new MutationObserver(infinityWatcher);

  // What to do if it changes:
  function infinityWatcher(mutations) {
    for (let mutation of mutations) {
      //observer.disconnect(); //Only needed once
      infinity = infinityDom.querySelector('a').textContent;
      infinityFormatted = infinity.replace(/[^+\d]+/g, '');
      //desktop
      let infinityNumber = document.querySelectorAll('.page-infinity');
      for (i = 0, l = infinityNumber.length; i < l; i++) {
        infinityNumber[i].textContent = infinity;
        infinityNumber[i].href = 'tel:' + infinity.replace(/\s+/g, '');
      }
    }
  }

  observer.observe(infinityDom, options);
}
// This function finds the last full stop before the specified `chars` argument.
function textTrim(string, maxChars) {
  string = string.substring(0, maxChars);
  let findCharPos = string.lastIndexOf('.') + 1;
  string = string.substring(0, findCharPos);
  return string;
}

Build page element

Now it’s time to build the new test page from all the code we have collected.

To build an effective test, we need it to be as fast as possible, so we need to execute lots of JavaScript at the same time, a.k.a. asynchronously. By default, JavaScript is synchronous, meaning each operation blocks the next one while it waits for it to finish before moving on.

To write asynchronous code, we use async / await, wrap each of our ordered steps inside a function, and then call the functions in order:

// 5) Call code synchronously:
async function callGetContent() {
  getContent();
}
async function callProcessContent() {
  processContent();
}
async function callCreateContent(content) {
  createContent();
}
async function callRunExternal(newContent) {
  runExternal();
}

async function cleanAndSavecontent() {
  const content = await callGetContent();
  const processedContent = await callProcessContent(content);
  const newContent = await callCreateContent(processedContent);
  await callRunExternal(newContent);
}

cleanAndSavecontent();
^ Top