pixelthing

here here here

title picture for article

Honest on-screen timing in GTM

Skewed average on-screen time sucks

One question that UX & PMs often ask about in analytics is "how long is a user on page before they do X action". This has always been something thats easy to set up but results in thoroughly underwhelming results, the average time often seems absurdly long.

The average appears crazy because there is a very long tail of extremely high recorded on-screen times. The reason being that plenty of users will hide a tab, walk away from their computer and let it sleep, or switch to other applications before returning to the page. The time between page load and click event will therefore be very, very long - when our expectations are that it will only be when the page has the users attention.

Here we're going to explain one way to get around this to have a useful on-screen page time, by spotting when the window is - and is not - in front of the user.

How it works #

<!-- GTM custom HTML tag triggered by page load-->
<script>
(function() {
// start a timer. Set here direct on the DL to reduce DL event noise.
var gtm = window.google_tag_manager[{{Container ID}}];
gtm.dataLayer.set('meta.time.start', performance.now());

// Handle page visibility change - send a DL push when it changed
var hidden, visibilityChange;
if (typeof document.hidden !== "undefined") {
hidden = "hidden";
visibilityChange = "visibilitychange";
} else if (typeof document.msHidden !== "undefined") {
hidden = "msHidden";
visibilityChange = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
}
function handleVisibilityChange() {
if (document[hidden]) {
window.dataLayer.push({
event: 'pagehidden'
});
} else {
window.dataLayer.push({
event: 'pageunhidden'
});
}
}
document.addEventListener(visibilityChange, handleVisibilityChange, false);
})();
</script>
<!-- GTM custom HTML tag triggered by the pagehidden DL event -->
<script>
(function() {
var gtm = window.google_tag_manager[{{Container ID}}];
gtm.dataLayer.set('meta.time.hiddenlast',performance.now());
})();
</script>
<!-- GTM custom HTML tag triggered by the pageunhidden DL event -->
<script>
(function() {
var gtm = window.google_tag_manager[{{Container ID}}];
var timeNow = performance.now();
var timeLast = gtm.dataLayer.get('meta.time.hiddenlast') || 0;
var timeTotalSoFar = gtm.dataLayer.get('meta.time.hiddentotal') || 0;
var timeTotalNow = (timeNow - timeLast) + timeTotalSoFar;
gtm.dataLayer.set('meta.time.hiddenlast',0);
gtm.dataLayer.set('meta.time.hiddentotal',timeTotalNow);
})();
</script>
/* GTM custom JS variable */
function() {
var gtm = window.google_tag_manager[{{Container ID}}];
var timeStarted = gtm.dataLayer.get('meta.time.start') || 0;
var timeHidden = gtm.dataLayer.get('meta.time.hiddentotal') || 0;
var timeOnScreen = performance.now() - timeStarted - timeHidden;
timeOnScreen = Math.floor(timeOnScreen / 1000);
return timeOnScreen;
}

You can use this variable in any tracking tags to be an honest value of page onscreen time, or use it in further calculations you might want to carry out.

SPAs #

As usual Single Page Applications (SPAs) tend to ruin the party. The above tags and variables only work in a "regular" multi page application, because when the user moves between pages, the timer is not reset. To get around this, if you are running this in an SPA, you'll need a further GTM custom HTML tag to "clean up" when a virtual page view occurs (often spotted from a history event, but check what works best in your application). Note that we're using undefined to remove some of the DL keys.

<!-- GTM custom HTML tag triggered by a history event, or similar in your app -->
<script>
(function(){
var gtm = window.google_tag_manager[{{Container ID}}];
// timing
gtm.dataLayer.set('meta.time.start', performance.now());
gtm.dataLayer.set('meta.time.hiddenlast', undefined);
gtm.dataLayer.set('meta.time.hiddentotal', undefined);
})();
</script>

Be a little wary of setting dataLayer values in the .dataLayer.set() method - I use it here because we have so many events in the DL that I'm trying not to confuse the event flow with small maintenance jobs. But doing it this way can make debugging difficult - you could end up looking at GTM debugging for hours, wondering why you're setting a DL value, only to find it set as something else in the final DL - not realising a variable is updating the DL out of sight.

That's it #

There are any number of reasons you might want to collect good page onscreen timings. I'm hoping to use it to undertand the time that users spend in different sections of a page, or in pop-over dialogs, and also to uncover how people struggle in long forms.

But what I'm really looking forward to is usable, understandable averages and numbers that I can use in some more honest distribution analysis.