Thu, May 30, 2019
The original article was written on May 30, 2019, and it focused on sending data to Google Analytics Universal. However, during the migration of the blog to a new CMS, I decided to update the content to reflect the current version — Google Analytics 4.
There’s no need to elaborate much on the importance of tracking scroll depth and time spent on a page. The more insights we gather about user interaction with page content, the better. Especially when approached from two angles:
I’ve already written about scroll tracking using Google Tag Manager’s built-in functionality, and about how to evaluate content interaction by sending an event based on scroll depth and time spent on a page. Today, we’ll go through another solution based on a custom JavaScript script. This method allows you to capture both the maximum scroll depth and the precise time spent on a page.
You’ll need the following:
Google Tag Manager provides a built-in trigger for scroll depth tracking. However, it has one drawback: data is sent only upon reaching a predefined threshold. This means you can't track the maximum scroll percentage using this trigger alone. To work around this limitation, we use a custom script added to the site via a Custom HTML tag.
This script was originally created by Nastya Tymoshenko and later improved by me. Previously, the script measured time on page simply by calculating the time between page load and the beforeunload
event (when the page is about to be closed). Now, after improvements, the script only counts time when the tab is active.
<script>
(function ( $ ) {
//define the maximum scroll depth and time processing functions
$.scrollEvent = function(current_max) {
var documentObj = $(document);
var windowObj = $(window);
var documentHeight = documentObj.height();
var windowHeight = windowObj.height();
var currentHeight = windowHeight + documentObj.scrollTop();
current_value = getPercent(documentHeight, currentHeight);
return(current_max > current_value ? current_max : current_value);
}
$.fixTime = function() {
var dateObj = new Date();
return Math.floor(dateObj.getTime() / 1000);
}
function num(val){
val = Math.floor(val);
return val < 10 ? '0' + val : val;
}
$.timeFormat = function(ms){
var sec = ms, hours = sec / 3600 % 24, minutes = sec / 60 % 60, seconds = sec % 60;
return num(hours) + ":" + num(minutes) + ":" + num(seconds);
};
function getPercent(doc, cur) {
return !cur ? 0 : Math.floor(cur * 100 / doc);
}
})( jQuery );
//Define time intervals
function getTimeInterval(time) {
if (time >= 0 && time < 30)
return 'from 0 sec to 30 sec';
if (time >= 30 && time < 60)
return 'from 30 sec to 1 min';
if (time >= 60 && time < 120)
return 'from 1 min to 2 min';
if (time >= 120 && time < 180)
return 'from 2 min to 3 min';
if (time >= 180 && time < 300)
return 'from 3 min to 5 min';
if (time >= 300 && time < 480)
return 'from 5 min to 8 min';
if (time >= 480)
return 'more 8 min';
}
//Define scroll depth intervals
function getScrollingInterval(deep) {
if (deep >= 0 && deep < 20)
return 'from 0% to 20%';
if (deep >= 20 && deep < 40)
return 'from 20% to 40%';
if (deep >= 40 && deep < 60)
return 'from 40% to 60%';
if (deep >= 60 && deep < 80)
return 'from 60% to 80%';
if (deep >= 80 && deep <= 100)
return 'from 80% to 100%';
}
//Assign user type based on scroll depth and time on page
function getCharacters(deep, time) {
if (/Android|webOS|Windows Phone|Macintosh|Samsung|Nokia|Bada|Symbian|iPhone|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent))
var etalon = 21.1;
else
var etalon = 15.7;
var documentH = jQuery(document).height();
var etalonT = Math.floor(documentH / etalon, 2);
if (deep >= 70 && time >= 0.7 * etalonT)
return 'Read attentively';
if (deep >= 70 && time < 0.7 * etalonT)
return 'Skimmed through';
if (deep < 10)
return 'Did not read at all';
if (deep >= 40 && deep < 70 && time >= 0.5 * 0.7 * etalonT)
return 'Stopped reading halfway';
if (deep >= 10 && deep < 40 && time >= 0.25 * 0.7 * etalonT)
return 'Stopped reading at the beginning';
if (deep >= 40 && deep < 70 && time < 0.5 * 0.7 * etalonT)
return 'Glanced through to the middle';
if (deep >= 10 && deep < 40 && time < 0.25 * 0.7 * etalonT)
return 'Started to skim but quit early';
}
// Function to calculate tab inactivity time
function onVisibilityChange() {
var current_timestamp;
if (document.visibilityState == "hidden") {
invisibility_time = jQuery.fixTime();
} else {
current_timestamp = jQuery.fixTime();
window_invisibility_time += (current_timestamp - invisibility_time);
}
}
//Execute the above functions and send data to GTM
jQuery(document).ready(function() {
//start all counters when the tab becomes active (first time)
if (document.visibilityState == "visible")
{
window_invisibility_time = 0; // time when tab was inactive
document.addEventListener('visibilitychange', onVisibilityChange, false); // start visibility listener
var startLiveDoc = jQuery.fixTime();
var current_max = 0;
jQuery(window).scroll(function() {
current_max = jQuery.scrollEvent(current_max);
});
//assign an event to window that will fire when the user leaves the page (close, refresh, navigate)
jQuery(window).bind('beforeunload', function(){
current_max_string = current_max.toString() + '%';
var endLiveDoc = jQuery.fixTime();
var timeLiveDoc = jQuery.timeFormat(endLiveDoc - startLiveDoc);
var time_on_page_active = jQuery.timeFormat(endLiveDoc - startLiveDoc - window_invisibility_time);
var character = getCharacters(current_max, endLiveDoc - startLiveDoc - window_invisibility_time);
var percent_of_scrolling_int = getScrollingInterval(current_max);
var time_on_page_int = getTimeInterval(endLiveDoc - startLiveDoc);
var time_on_page_int_active = getTimeInterval(endLiveDoc - startLiveDoc - window_invisibility_time);
dataLayer.push({'event': 'Scroll to', 'percent_of_scrolling': current_max_string, 'time_on_page': timeLiveDoc, 'character' : character, 'percent_of_scrolling_interval' : percent_of_scrolling_int, 'time_on_page_interval' : time_on_page_int, 'time_on_page_active': time_on_page_active, 'time_on_page_interval_active' : time_on_page_int_active, 'time_invisibility' : window_invisibility_time});
});
}
});
</script>
The script includes grouping methods for both scroll depth and time intervals, which you can customize to fit your project. To add the script to your site, use a tag with the following settings:
After creating the tag, go into preview mode and try to trigger the beforeunload
event.
You can press the refresh button in your browser — this will trigger the necessary event. Right after, press the Esc key to prevent the page from fully reloading and loading a new one.
Your goal is to capture the following data in the Google Tag Manager debugger:
The event you’re looking for is Scroll to
. As you can see, a lot of valuable data is pushed to the dataLayer at the moment this event occurs:
time_on_page
– Time from page load to the beforeunload
eventtime_on_page_interval
– Same time, grouped into intervalstime_on_page_active
– Time the tab was active from page load to beforeunload
time_on_page_interval_active
– Same, grouped into intervalstime_invisibility
– Time the tab was inactivepercent_of_scrolling
– Maximum scroll depth reached (percentage)percent_of_scrolling_interval
– Same, grouped into intervalscharacter
– Reader type, determined based on scroll depth and time spent on the pageYou likely won’t need all of the data provided by the script, so create Data Layer variables only for the keys that are relevant to your needs.
As you may know, to retrieve values from the data layer, you need to create the corresponding variable. Below are screenshots of each variable:
time_on_page
:time_on_page_interval
:time_on_page_active
:time_on_page_interval_active
:time_invisibility
:percent_of_scrolling_interval
:character
:If everything is set up correctly, in preview mode during the Scroll to
event, your variables will hold the necessary values:
As mentioned above, Scroll to
is a custom event, and you need to create a Custom Event trigger for it. It looks like this:
Now that everything is ready, all that’s left is to compile it into a tag to send the event to Google Analytics 4. Below is one example of how to configure the tag. Depending on your project goals, you can send different values.
The solution described in this article will help you better understand how visitors interact with your content and allow you to create remarketing audiences based on this data. If you have questions about setting this up, feel free to ask them in the comments.
If you enjoyed this content, subscribe to my LinkedIn page.
I also run a LinkedIn newsletter with fresh analytics updates every two weeks — here’s the link to join.
Web Analyst, Marketer