Skip to main content
All CollectionsAudits
ClickAll+ Solution Implementation Guide
ClickAll+ Solution Implementation Guide

A solution for massive scale testing of on-page interactions.

Luiza Gircoveanu avatar
Written by Luiza Gircoveanu
Updated over a week ago

Overview

Journeys and Audits are great tools to validate marketing and analytics data. ObservePoint's ClickAll+ Solution expands the capabilities of Journeys and Audits by giving users the option to click all elements on a page and perform other types of interactions.

One of the challenges of other solutions intended to click on all links and test them is being able to associate these interactions with specific standards. ClickAll+ addresses this problem and make wide-scale validation possible.

Implementation

Implementation for this solution is complex, but we have this guide and team members that can support you in the process.

The code below is the current version that can be executed within an Audit on-page action in order to interact with all elements on a specific set of pages. Steps for implementation are provided below.

  1. Copy the code below;

Note: The only portion of the code that you need to edit/configure is between the block of comments labeled USER CONFIGURATION.

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////USER CONFIGURATION////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

//Include filter must include the following after the domain: .*opClickAllData.*
//ex. https://example.com.*opClickAllData.*
//add two new Data Layer objects within the sub-folder: interactionObject,interactionValidation

const INTERACTION_TYPES = [{
'selector': 'test',
'targetedElementsType': 'test',
'limitInteractions': Infinity, //set max number of elements to interact with; leave as Infinity to interact with all that apply
'elementAttributeData': 'test,test', //leave blank if not applicable
'action': function(element) { //function that will perform action on elements found; if no 'action' specified in configuration (removing this action attribute), defaults to a click simulation
simulateClick(element);
},
}];

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

var interactionValidation = {},
interactionObject = {
'Element Selector': ''
};
async function main() {
let opClickAllData = getOpDataParam(window.location.href)
if (opClickAllData) {
let allData = opClickAllData;
let selector = allData['interactedElement'];
interactionObject['Element Selector'] = selector;
let interactionElement = document.querySelector(selector);
if (interactionElement) {
interactionObject['Element Inner Text'] = interactionElement.innerText;
if (interactionElement.href) interactionObject['href'] = interactionElement.href;
if (allData['elementAttributeData'] !== undefined) {
let datas = allData['elementAttributeData'].split(',');
datas.forEach(d => {
interactionValidation[d] = (interactionElement.getAttribute(d)) ? interactionElement.getAttribute(d) : 'DATA ELEMENT MISSING';
})
}
if (interactionElement.hasAttribute('target') && interactionElement.getAttribute('target') === '_blank') interactionElement.setAttribute('target', '_self');
performAction(interactionElement, allData['targetedElementsType']);
colorElement(interactionElement, 1, interactionObject);
console.log(interactionElement)
} else {
interactionObject = {
'Element Selector': 'ELEMENT NO LONGER AVAILABLE'
}
}
} else {
let linkSelectors = await getLinkSelectors(INTERACTION_TYPES);
linkSelectors.forEach(ls => {
var newLink = document.createElement('a');
var payload = {
'targetedElementsType': ls.targetedElementsType,
'interactedElement': ls.selector
}
if (ls.elementAttributeData !== '') payload['elementAttributeData'] = ls.elementAttributeData;
let newURL = getNewURL(window.location, payload);
newLink.href = newURL;
console.log(newLink.href);
console.log(document.querySelector(ls.selector));
document.body.appendChild(newLink);
})
console.log(`${linkSelectors.length} links found on page matching configuration`)
}

function getNewURL(location, payload) {
let protocol = location.protocol;
let hostname = location.hostname;
let path = location.pathname;
let queryParams = location.search;
let anchors = location.hash;
queryParams += (!/\?/.test(queryParams)) ? '?' : '&';
queryParams += `opClickAllData=${encodeURIComponent(JSON.stringify(payload))}`;
return `${protocol}//${hostname}${path}${queryParams}${anchors}`
}
}
main();

function performAction(element, targetedElementsType) {
let targetedElementsTypeObjs = INTERACTION_TYPES.filter(l => {
return l.targetedElementsType === targetedElementsType
});
if (targetedElementsTypeObjs.length === 0) return console.log('No configuration for targetedElementsType')
if (targetedElementsTypeObjs[0].action && typeof targetedElementsTypeObjs[0].action === 'function') {
targetedElementsTypeObjs[0].action(element);
} else {
simulateClick(element);
}
}

async function getLinkSelectors(INTERACTION_TYPES) {
let linkSelectors = new Array();
for (const t of INTERACTION_TYPES) {
let intLimit = (t.limitInteractions) ? t.limitInteractions : Infinity;
if (!t.elementAttributeData) t.elementAttributeData = '';
let allLinks = [...document.querySelectorAll(t.selector)];
if (allLinks.length > intLimit) {
allLinks = allLinks.slice(0, intLimit)
}
let selectors = allLinks.map(link => {
let querySelector = generateQuerySelector(link);
if (querySelector !== -1) {
return {
'selector': querySelector,
'targetedElementsType': t.targetedElementsType,
'elementAttributeData': t.elementAttributeData
}
} else {
console.log(`Failure collecting CSS selector for: ${link}`)
}
});
selectors.forEach(s => {
linkSelectors.push(s)
});
};
return linkSelectors
}

function generateQuerySelector(element) {
const selectorParts = [];
let currentElement = element;
let querylength = 0;
while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE && querylength !== 1) {
let selector = currentElement.nodeName.toLowerCase();
if (currentElement.id && !uuidCheck(currentElement.id)) {
selector = `[id="${currentElement.id}"]`;
} else {
let classes = Array.from(currentElement.classList).map(className => (classCheck(className) && !uuidCheck(className)) ? `.${className}` : '').join('');
selector += classes;

if (currentElement.parentElement) {
const siblings = Array.from(currentElement.parentElement.children);
const index = siblings.indexOf(currentElement) + 1;
if (index > 1) {
selector += `:nth-child(${index})`;
}
}
}
selectorParts.unshift(selector);
currentElement = currentElement.parentElement;
try {
querylength = document.querySelectorAll(selectorParts.join('>')).length;
} catch (e) {
return -1
}
}
return selectorParts.join('>')

function classCheck (className) {
//class cannot contain the following: no pound sign or colon, cannot have a string of numbers
return !/[#:]/.test(className) && !/\d+/.test(className)
}

function uuidCheck (s) {
//check if selector attribute contains a UUID, so if it does, we skip them (prevents using rotating attribute)
const uuidPattern = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/;
return uuidPattern.test(s);
}
}

function getOpDataParam(url_string) {
let opData = new URL(url_string).searchParams.get("opClickAllData");
return (opData) ? JSON.parse(decodeURIComponent(opData)) : null;
}

function simulateClick(element) {
element.scrollIntoView();
var clickEvent = new MouseEvent("click", {
bubbles: true,
cancelable: true,
view: window
});
element.dispatchEvent(clickEvent);
}

function returnRelevantParent (element,parentSelector) {
let checkElement = element;
while (checkElement.parentElement) {
checkElement = checkElement.parentElement;
let matchesParent = checkElement.matches(parentSelector);
if (matchesParent) {
return checkElement
} else if (checkElement.nodeName === 'HTML') {
return null
}
}
}

async function colorElement(element, iteration, interactionObject) {
element.scrollIntoView();
const isVisible = await isElementInViewport(element);
if (isVisible) {
element.style.color = 'black';
element.style.backgroundColor = '#f2cd14';
element.style.border = '2px solid #333';
element.style.opacity = 100;
} else if (iteration === 2) {
let notificationElement = document.createElement('div');
notificationElement.textContent = `Element not visible, but was successfully clicked\r\n\r\nIts selector is ${interactionObject['Element Selector'].trim()}\r\n\r\nIts innertext is ${interactionObject['Element Inner Text'].trim()}\r\n\r\nIts href is ${interactionObject['href'].trim()}`;
notificationElement.style.position = 'fixed';
notificationElement.style['white-space'] = 'break-spaces';
notificationElement.style['overflow-wrap'] = 'break-word';
notificationElement.style.top = '0';
notificationElement.style.left = '50%';
notificationElement.style.transform = 'translateX(-50%)';
notificationElement.style.width = '60%';
notificationElement.style.padding = '10px';
notificationElement.style.backgroundColor = '#f2cd14';
notificationElement.style.color = 'black';
notificationElement.style.border = '2px solid #333';
notificationElement.style.textAlign = 'center';
notificationElement.style.zIndex = '9999';
document.body.appendChild(notificationElement);
} else {
iteration++;
colorElement(element.parentElement, iteration, interactionObject)
}
}

async function isElementInViewport(element) {
return await new Promise(resolve => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
resolve(entry.isIntersecting);
});
observer.unobserve(element);
});
observer.observe(element);
});
}

2. Within the code, there is a section of code between comment blocks that is labeled USER CONFIGURATION - this is the area where you should make changes to INTERACTION_TYPES array of objects need configuration as it follows:

Required attributes:

  • selector

    - This is a CSS selector that will return back an array of all elements that match that CSS across the pages where this will run

    - The best way to get this CSS Selector and determine the elements that will be interacted with is to go to a page you intend to perform this solution on and open dev tool (F12 or right-clicking and clicking “Inspect”)

    - Once there, you can see tabs for “elements” and “console”

    - Elements are the list of elements on the document and ultimately the way to see the best selectors for the next step.

    - The console will allow you to run a small piece of JavaScript to help you know which elements you are going to have interacted with

    - That small piece of JavaScript is well documented in this help document

    - The array of elements that return from doing a “document.querySelectorAll()” with your CSS selector is ultimately what will be interacted with. That CSS selector you pass into querySelectorAll will be what you give to the selector attribute of this object

  • targetedElementsType: This is simply a label to identify within the code and later in the results of the Audit the “category” or “type” of thing that was interacted with (e.g. download links, header links, external links, etc. This value must have no spaces

Not Required/Optional Attributes:

  • limitInteractions: This allows you to specify the max number of configured elements you’d like to interact with. (e.g. a page has 10 CTA buttons, but I only need to validate one of them, you’d put a 1; if I want to test all 10, put Infinity - with the first letter capitalized - as the value or just remove the attribute entirely from the object.).

Note: For situations where this is not relevant, you can leave the attribute -1 or remove the attribute entirely from the object.

  • elementAttributeData: For some technologies you might be testing, there may be a desire to pull in attributes and their corresponding values to validate later with the results of the Audit. (e.g. data-gtm). This is usually seen in those using Google Analytics validation, but can be used for any suite of technologies/data layers

    - This value is a comma separated value

    - Simply put the name of the attributes you’d like to validate in this field

    - The results once the Audit runs will show up in a “data layer” tag called interactionValidation.

    - If that attribute is not available in the element that was interacted with, the solution will pass a value of DATA ELEMENT MISSING to that, meaning that the element that was interacted with didn’t have that attribute.

Note: For situations where this is not relevant, you can leave the attribute an empty string or remove the attribute entirely from the object.

  • action: When you want to perform small "actions" that are not just clicking on selected elements, you can put a function, whose argument is the element in question, to interact with the intended elements on the page. (e.g. I want to click on a header link in order to see a link to a sub-header; or I want to add a quantity before hitting an add-to-cart button.)

Note: For situations where this is not relevant, you can remove the attribute entirely from the object; doing so will default to a “click” event on all specified elements.

3. Now is time to build your Audit and apply the code you’ve configured above;

4. In the ObservePoint app, you can now create a new Audit with the following settings:

  • On the first tab, name your Audit and put it in a folder/subfolder appropriately;

  • Paste all URLs you’d like this solution applied to;

  • Put a limit that you’d like to have for this. The solution will interact with all elements it finds up to that limit (to prevent excessive usage);

  • Scroll down to “Additional Setup Options”;

  • You will see a section for “WHICH SECTIONS OF THE WEBSITE SHOULD BE INCLUDED?"

  • If you’ve pasted your URLs you want tested, you should see a include list with regex statements around them and in this section you MUST append .*opClickAllData.* to all include list items.

5. Take the code you’ve configured above, with your USER CONFIGURATION completed, create a new Execute JavaScript on-page action, and paste it in.

6. Select Prevent Navigation in the top right right corner of the on-page action. The only exception to this instruction is when a page load is required to fire the analytics call that you are testing.

7. This step needs to only be done the first time an Audit is added to a subfolder:

Once the Audit is saved and created, go back to your Data Sources in your account and click the Sort & Group on the top left, making sure you have Folder & Sub-Folder selected. Find the folder/sub-folder of the Audit you just created, edit the sub-folder and add the following to you data layer configuration (the field is comma separated, so if values are already in it, make sure you add commas appropriately for a comma separated list): interactionObject,interactionValidation

8. You can now run the Audit.

Audit Results

1. Once the Audit has completed, you can now view results of all interaction actions: Each page is a pageview as well as an interaction action of each element configured in the implementation steps, on each page specified in the Starting URLs of the Audit.

2. In order to view your segments, you can search the initial URL for the linkTypes you configured in the previous step OR you can look at specific pages by filtering for just the final URL search in the Audit putting in the specific URL you’d like to see.

3. The Audit will contain the pages that “collected” the interaction elements as well as the interaction pages (Example: if the Audit ran on 10 pages and the Audit resulted in 500 pages, the Audit has 490 “interaction” pages, like a link click, for example). You can filter for these pages by simply filtering URLs for “opClickAllData

Note: It is strongly advised that in the configuration steps above, you break out your “interaction” segments (INTERACTION_TYPES) by the elements you want tested so that you can test specific data on those pages. The reasoning for this is that you can now leverage specific rules and other data validation/standardization features in ObservePoint to check that each interaction is performing as intended.

Here is an example of a rule “if” condition that is highly recommended to be used as a template. In this example, the configuration had a “linkType” of “allLinks”

This configuration of the rule will only apply to those elements that were intended to interact with from the selectors we configured for “allLinks” (e.g. only apply this rule to clicking on exit links; download links; add to cart interactions; video play events)

The second condition leveraging the data layer element is in case the page resulted in no interactions. This may because of many reasons, but ultimately means that when the page was returned to, the script wasn’t able to find that element again

The condition above means that we do not apply rules where we did not end up clicking on anything.

Once the "if" logic is done, you can configure your “than” logic. Here's the rule:

  • Prop 33 should be set and Evar 34 should be a value found in the data layer

Each page that performed an interaction will have done its best to “highlight” the element that got interacted with and will look like the following on the page:

Note: Some elements are extremely hard to highlight for an array of reasons such as: hidden behind another element, not being visible until another action is performed, is within something like a carousel where it seems like it should be visible, but is only visible at a specific moment, etc.

If you need to see those elements highlighted and it is a need to interact with elements prior, you can leverage the “action” attribute in the configuration of the INTERACTION_TYPES to perform those actions prior to interacting with your desired elements.

If nothing is visibly highlighted, there is a data layer value that is passed to help you determine which thing was clicked on.

If you’re on a page where this has happened, go to tags and search “data layer” and you will see a data layer with an account called interactionObject.

This object will contain several variable/values that can be used to determine the element.

The easiest way to use this is to copy the value in the “Element Selector” variable. Click on the value in its cell. This is the unique CSS selector for the element that was interacted with on.

Click on the button next to the link at the top to open the URL in a new tab, open the dev tools and go to the console. Type: document.querySelector(“<<paste that selector here>>”).

Hit enter and that should result in the element that was interacted with. You can right click it and scroll it into view, and reveal it in the elements panel. This should be a great way to determine exactly what was interacted with on.

Conclusion

The method above is the solution to interact and analyze all the elements on a page through Audits using the ObservePoint app. Following this guide will provide you all the information you need to implement this solution. Should you need further assistance, please contact an ObservePoint representative.

Did this answer your question?