Skip to content

Instantly share code, notes, and snippets.

@martinschierle
Created June 22, 2018 20:22
Show Gist options
  • Save martinschierle/c48a6fce06e010f5c4c3c1fb6c863dee to your computer and use it in GitHub Desktop.
Save martinschierle/c48a6fce06e010f5c4c3c1fb6c863dee to your computer and use it in GitHub Desktop.
New Perofrmance Metric - last time the CTA changed
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const pixelmatch = require('pixelmatch');
const extractDomain = require('extract-domain');
var fs = require('fs');
//we'll simulate a nexus 5x for this
const phone = devices['Nexus 5X'];
/**
Will be called after page is loaded to find the CTA and return some info about it,
mainly last time it changed
*/
function getResults() {
// go through all candidates, and find the one which comes first (smallest y coord)
// and fills at least 50% of screen width
for(var i = 0; i < window.ctaCandidates.length; i++) {
var node = window.ctaCandidates[i];
if(!node || !node.parentNode) continue;
console.log("Considering node: " + node.innerText + " - " + node.id + " - " + node.name + " - " +node.getAttribute('class'));
var yThresh = window.ctaButton==null ? Number.MAX_SAFE_INTEGER : window.ctaButton.getBoundingClientRect().top
if(node.clientWidth > 0.5*window.innerWidth && node.getBoundingClientRect().top < yThresh) {
window.ctaButton = node;
}
}
if(window.ctaButton)console.log("Decided node: " + window.ctaButton.innerText + " - " + window.ctaButton.id + " - " + window.ctaButton.name + " - " +window.ctaButton.getAttribute('class'));
// return some general performance metric with our new metric
return {
"url": document.location.href,
"ctaLabel": window.ctaButton ? window.ctaButton.innerText: "n/a",
"ctaId": window.ctaButton ? window.ctaButton.id: "n/a",
"domInteractive": window.performance.timing.domInteractive - window.performance.timing.navigationStart,
"ctaComplate": window.ctaButton ? window.ctaButton.ctaLastChange - window.performance.timing.navigationStart: "n/a",
"domContentLoadedEventStart": window.performance.timing.domContentLoadedEventStart - window.performance.timing.navigationStart,
"loadEventStart": window.performance.timing.loadEventStart - window.performance.timing.navigationStart,
};
}
/**
Let's make sure to find out when our cta candidates resized, and remember the time they did
*/
function addResizeObserver() {
window.ro = new ResizeObserver( entries => {
for (let entry of entries) {
entry.target.ctaLastChange = Date.now();
}
});
}
/**
Some general things to init
*/
function initCtaDetection() {
// small helper function to add a new candidate if it's eligible (buttons only for now)
// if we already know this cnadidate, adapt the timestamp of last change
// todo: think about clickable divs as CTAs
window.maybeAddCtaCandidate = function(n) {
if(!n) return;
if (n.tagName === 'BUTTON' || (n.tagName === 'INPUT' && n.type === 'submit')) {
if(!n.ctaLastChange) {
window.ctaCandidates.push(n);
window.ro.observe(n);
}
n.ctaLastChange = Date.now();
}
};
//in case the button is already totally stable, add all current buttons
if(!window.ctaCandidates) window.ctaCandidates = [];
var buttons = document.getElementsByTagName("BUTTON");
for(var i = 0; i<buttons.length; i++) window.maybeAddCtaCandidate(buttons[i]);
buttons = document.querySelectorAll("[type='submit']");
for(var i = 0; i<buttons.length; i++) window.maybeAddCtaCandidate(buttons[i]);
}
/**
The mutation observer - to find candidates for a CTA, and to trigger timestamp
updates for the ones we already identified whenever they are modified
*/
function addMutationObserver() {
var mutationObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
for (var i=0; i < mutations.length; i++){
window.maybeAddCtaCandidate(mutations[i].target);
for (var j=0; j < mutations[i].addedNodes.length; j++){
var n = mutations[i].addedNodes[j];
window.maybeAddCtaCandidate(n);
}
}
});
});
mutationObserver.observe(document, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
}
async function findCTA(url) {
var domain = extractDomain(url);
const browser = await puppeteer.launch();
const page = await browser.newPage();
//page.on('console', msg => console.log(msg.text()));
page.on('error', err=> {console.log('error happen at the page: ', err);});
page.on('pageerror', pageerr=> {console.log('pageerror occurred: ', pageerr);});
await page.emulate(phone);
await page.evaluateOnNewDocument(initCtaDetection);
await page.evaluateOnNewDocument(addResizeObserver);
await page.evaluateOnNewDocument(addMutationObserver);
await page.goto(url, {"waitUntil": "networkidle0"});
var results = await page.evaluate(getResults);
//screenshot page, helps with troubleshooting (look out for consent or country choser popups, etc.)
await page.screenshot({"path": domain+".jpg", "type": "jpeg"});
// screenshot the cta - also helpful for debugging, to check if it's the right button
var cta = await page.evaluateHandle("window.ctaButton");
cta = cta.asElement();
if(cta) {
await cta.asElement().screenshot({"path": domain+"_cta.jpg", "type": "jpeg"});
}
await browser.close();
return results;
}
const testURLs = [
"https://www.amazon.com/Intex-King-Inflatable-Lounge-Colors/dp/B00005O6B7?pd_rd_wg=e56B6&pd_rd_r=4d41d229-dfbc-449c-a983-8fad028d64c2&pd_rd_w=hJx7j&ref_=pd_gw_ri&pf_rd_r=KAVNYHEZ08THWK7RTJJ0&pf_rd_p=c116cecb-5676-58e0-b306-0894a1d0149e",
];
for(var i in testURLs) {
findCTA(testURLs[i]).then(result => console.log(JSON.stringify(result, null, 4)));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment