Time On Visible and Hidden Pages – Google Analytics

Real Time On Page - Hidden and Visible Time

Last Updated on May 5, 2019 by Ritwik B

[Updated on 05-05-2019]
We’ll be looking at tracking visible and hidden time on the page using Pagevisibility API and sendBeacon feature.
As all of you might know that the time on the exit page and bounced page is not considered by Google Analytics, which results in skewed metrics like Avg. Session Duration, Avg. Time On Page,etc.

If unsure about these metrics, do check : Avg. Session Duration vs Avg. Time On Page

We’ll be using transport feature in analytics.js to solve this issue (more on this later) and also the Page Visibility API to know the total time for which the page was visible and hidden for a user.

I would like to start by saying this post is heavily influenced by the articles of Mr Simo Ahava and Mr. Yehoshua Coren. Do check out articles by them.

 

Visible and Hidden Time on Page – Understanding Logic

Let’s understand one simple scenario here and the code logic:

  • When the user lands on the page, two variables visibleTime and hiddenTime are created and the visibleTime will start counting.
  • When the user hides the tab or minimizes the browser, the visibleTime will be paused and hiddenTime will start counting.
  • When the user again resumes the page, the hiddenTime will be paused and the visibleTime will start counting from its previous value.
  • When the user navigates away from the page, these two variables will be sent to analytics. (you can also send these values to other tools)

 

Here is a small visual where your webpage is on Tab1 :

Real Time On Page - Hidden and Visible Time

Observations :

I guess this visual explains most of the part. Some points here:

  • If the user does not migrate to another tab, only visible time would be recorded and sent if the browser or tab is closed.
  • If the user opens the browser with pre-saved tabs and one of your pages is in the hidden tab, only the hidden time will be recorded and sent after the closing of hidden tab or browser.
  • If the user cancels the page unload process in between, the two variables are sent to analytics and are reset. This is done to ensure that the duplicate time values are not sent to analytics on another same page unload process.

 

Visible and Hidden Time On Page – Google Tag Manager

Step – 1

  • Create a Custom HTML tag in tag manager and paste the following code. (GitHub Link)
<script>
  
 (function(){
  // Initialize time 
  var startTime = new Date().getTime();
  var totalTime = {};
  if (typeof prefix() !== 'undefined') {
  var prevTime = 0;
  var visibilityEvent = prefix() + 'visibilitychange';
  var tabPath = 'visibleTab';
    
//Initialize the listeners
document.addEventListener(visibilityEvent,        
function(e){
  var isHidden=visibilityState(prefix())
  if (typeof isHidden !== 'undefined') {
  prevTime = 0 ? new Date().getTime() - startTime : new Date().getTime() - startTime - prevTime;  
  
    //Total Time for previous visibility state 
    if(isHidden) { totalTime.visibleTime=prevTime; tabPath+=">hiddenTab"} 
     else {totalTime.hiddenTime=prevTime; tabPath+=">visibleTab" } 
    }  
         //////Debugging Datalayer Event ///////// 
              dataLayer.push({
              'event' : 'visibilityChange',
              'visibleTime' : totalTime.visibleTime,
              'hiddenTime': totalTime.hiddenTime,
              'hidden' : isHidden,
              'tabPath': tabPath  
            });
         /////////////// 
}, false);
    
window.addEventListener('beforeunload', function(e)  
{
var isHidden=visibilityState(prefix())

if (typeof isHidden !== 'undefined') {
prevTime = 0 ? new Date().getTime() - startTime : new Date().getTime() - startTime - prevTime;  
if(!isHidden){totalTime.visibleTime=prevTime} 
else {totalTime.hiddenTime=prevTime} 
}  

if(tabPath.split('>').length > 3) {
var len = Math.floor(tabPath.split('>').length / 2);
var adtxt = ''
if(tabPath.split('>').length / 2 % 1 !== 0){adtxt+=' > visibleTab';}
tabPath = '(visibleTab > hiddenTab) x '+len+adtxt
}
  
dataLayer.push({
'event' : 'sendTimings',
'visibleTime' : totalTime.visibleTime,
'hiddenTime': totalTime.hiddenTime,
'hidden' : isHidden,
'tabPath': tabPath
}); 
  
  // reset the variables after sending data to analytics  
  prevTime=0;
  startTime = new Date().getTime();
  totalTime = {};
  tabPath = 'visibleTab';
}, false);

}
 })()
  
  function prefix() {
  var prefixes = ['moz', 'ms', 'o', 'webkit'];
  
    if ('hidden' in document) {
    return '';
  }
  
   // Loop through each prefix to see if it is supported. 
  for (var i = 0; i < prefixes.length; i++) {
    var testPrefix = prefixes[i] + 'Hidden';
    if (testPrefix in document) {
      return prefixes[i];
    }
  }

  return;
}

 function visibilityState(pref){
    switch (pref) {
    case '':
      return document['hidden'];
    case 'moz':
      return document['mozHidden'];
    case 'o':
      return document['oHidden'];
    case 'webkit':
      return document['webkitHidden'];
    default:
      return;
  }
}
    
  
</script>
  • Trigger it on All pages.

 

Step – 2

  • Create two dataLayer variables with name as ‘hiddenTime’ and ‘visibleTime’.

Hidden Time Data Layer Variable  Visible Time Data Layer Variable

 

Step – 3

  • Create a two User Timing Tag, with following values:

User Timing Tag - Visible Time On PageUser Timing Tag - Hidden Time On Page

 

 

 

 

 

 

 

 

 

 

 

 

 

  • In fields to set, add the fields name as ‘transport’  and ‘beacon’ as its value. This will result in using navigator.sendBeacon method, which asynchronously sends the data over HTTP to web servers, without delaying page unload process.

Analytics - Transport Feature

 

Step – 4:

  • Finally, you can create trigger for the User timing tags with the event name as ‘sendTimings’

Visible and Hidden Page Timing Trigger

  • Now, we are ready to preview the setup. Just hit the preview in the GTM console.

 

GTM Debugging 

  • After you open you web page, try opening other tabs or minimizing the browser. The visibilityChanged event gets triggered and logs the timings for the previous state.

Track time on page - Visibility Changed event

 

  • When the you navigate away from the page the total time gets recorded in sendTimings.

Track time on page - Send Timings event

  • You can comment the ‘Debugging Data layer Event’ code to stop pushing visibilityChange event.

 

 

Visible and Hidden Time On Page – analytics.js:

 

  • If you are not using GTM, you can simply replace the dataLayer.push method in sendTiming function with:
    (will soon update this with sendbeacon feature)

    //send visible timing hit
    ga('send', 'timing', {
      'timingCategory': location.pathname,
      'timingVar': 'Page Timings',
      'timingValue': totalTime.visibleTime,
      'timingLabel': 'Visible'
    });
    
    //send hidden time hit
    ga('send', 'timing', {
      'timingCategory': location.pathname,
      'timingVar': 'Page Timings',
      'timingValue': totalTime.hiddenTime,
      'timingLabel': 'Hidden'
    });
    
  • You can also use events and custom metrics to capture the timings.

 

Use Cases for Hidden and Visible Time On Page

  • You can check the user timings report, where you can see the hidden and visible page timings.

Time On Visible and Hidden Page - User Timings Report

 

  • Also you can use custom metrics feature in GA, I have created 2 custom metrics for page timings and one super metric ‘Page Visible Time / User’ . Now I can sort this metric to know the most engaging post..!!!!!!

Time On Hidden and Visible Pages - Custom Metrics

 

One thing to notice here is hidden timings are usually greater, i.e users might be opening the page in new tab for reading later, which increases the hidden timings drastically.

Also, segmenting would further leave you with gold mines…!!!!!

  • You can make use of the ‘hidden’ variable in dataLayer to know the behavior of the user and log events. Something like:

Page Visibility Path - Track Time On Page

Here, you might know how many users have your web page hidden while starting the browser?
Are your articles too lengthy or boring at the start?
Combining scroll tracking here, you’ll get  ‘Visible > 0% scroll > 25% scroll > Hidden’, so you get the idea.

  • You can also use these variables in other analytics tools or for other purposes.

 

Browser Compatibility – Page visibility and sendBeacon

  • Page visibility API has bad support for Opera mini and Android 4.2 , 4.3 browsers.
  • Beacon API has bad support for IE, Safari, Opera mini, android browsers 4.3, 4.4, 4.4.4. This is the main drawback. Hope there will be support the future versions.

 

Ritwik is a Web Analyst & Product Marketer. He loves to write technical & easy to understand blogs for Marketers & Entrepreneurs. Focused on Google Analytics, Facebook Analytics, Tag Management, Marketing & Automation Scripts & more. Google Certified Professional. A Firm Believer in Teaching -> Learning -> Growing. :)

Comments (30)

  1. Nice piece man. One of the few technical articles I read in the past 7 days.
    PS: There’s a spelling mistake in the beginning. Just CTRL + F for “anlaytics”

    1. Thank a lot @disqus_ftpw5fRTyx:disqus ..!!!
      Yes corrected 🙂

  2. Hi Ritwik,

    Is it possible that you forgot the ‘ms’ prefix in
    var visibilityState = function(pref){

    I can see moz, o, webkit, but no ms.

    Thanks for this, about to test it out.

    1. Thanks Sam. You can setup custom metrics in analytics dashboard. Check https://support.google.com/analytics/answer/2709829?hl=en
      For Custom Metrics, you’ll have to send the time in seconds (as opposed to milliseconds in timing hits). Use dataLayer.push({
      ‘event’ : ‘sendTimings’,
      ‘visibleTime’ : totalTime.visibleTime/1000,
      ‘hiddenTime’: totalTime.hiddenTime/1000,
      ‘hidden’ : isHidden
      });
      Send these visible & hidden time as custom metrics with event hit. I’ll update the post with these steps.

      1. So, I’ll have to set up another dataLayer push, separate from the first one? Thanks for updating, I’ll check back later to find the results 🙂

          1. I don’t think this might work :/ (not sure) . I actually created calculated metrics (Time) with {{Time visible}}/1000 & that worked!!

          2. Yeah, you’re right. Doesn’t work 🙂

        1. Hey @disqus_Vs9ZkLhacC:disqus, This method is Great!!!! If you want to use both.

      2. Very great post. Just a quick question, what’s the complete custom java script to use in the extra two variable in the dataLayer push on creating custom metrics?

        1. No Script. Its just Calculated Metrics (I missed that) defined in View Settings : {{Page Visible Time}} / {{User}}.

          Thanks!

  3. Hi Ritwik,

    Great article, I’ve read both Simo and Yehoshua’s article recently and you’ve managed to help me make sense of them!

    I have a problem, hope you can help? My sendTiming event is not firing, I see you mentioned about commenting the ‘Debugging Data layer Event’ code to stop pushing visibilityChange event. Is this necessary in order for the sendTimings event to fire? How do I do that?

    Thanks

    1. Hi,

      I just went to close the tab and saw the sendTimings event fire (very quickly!) so hopefully that means it’s done it.

      1. Hi Toms,

        Thanks for the message.!!
        Yes it happens very quickly, One way to find out is to reload the page & immediately cancel it. (pressing F5 + then Esc).

          1. Yes, you are right!! custom metrics (Time) will be displayed as hh:mm:ss.

  4. Hi Ritwik,

    Thanks for all the steps. It helped a lot while implementing above setup. Just had doubt about this though. Sending a Timing hit on these pages, will it affect my bounce rate and avg time on page? I dont want to loose on actual bounces data for some pages. I did a quick lookup for parameters included for sending a Timing Hit to GA. It does not have non-interaction thing. Would really appreciate if you could suggest a work around on this.

    1. Hey Abhishek,

      Better option would be to use Custom Metrics. Use the below method implemented by Sam. Check: http://disq.us/p/1hqadzy

      Thanks

  5. Hi Ritwik,

    I’ve been using this implementation for a while now, and I just wanted to set it up for a new account. However, when I want to publish, I’m getting the following error:

    Error at line 41, character 3: this language feature is only supported for ECMASCRIPT6 mode or better: block-scoped function declaration. Use –language_in=ECMASCRIPT6 or ECMASCRIPT6_STRICT or higher to enable ES6 features.

    Error at line 60, character 1: this language feature is only supported for ECMASCRIPT6 mode or better: block-scoped function declaration. Use –language_in=ECMASCRIPT6 or ECMASCRIPT6_STRICT or higher to enable ES6 features.

    So I’m assuming it has to do with the visibilityChanged and sendTimings functions. How do I fix this?

    Thanks so much!

      1. Sam – I had the same problem and that stackoverflow thread was really helpful, thanks for sending. The following worked for me:

        Change line 41 to:
        var visibilityChanged = function visibilityChanged()

        And change line 60 to:
        var sendTimings = function sendTimings()

        1. Hi Rachel,

          Thanks for that! I’m going to have a look at this tonight and test for the first results.
          Have you been able to test it already? Results are as they should be?

          1. Yep! Works as expected for me, but report back if it doesn’t work for you.

          2. This worked for me! Thanks for posting this fix

    1. Hey Sam,

      Try This
      – Copy the Above code
      – Go To https://es6console.com/ & paste it
      – Click transform & Use that code in GTM

      Let me know if that works

      Thanks,
      Ritwik

      1. No, that dit not work for me, unfortunately…

  6. Came here for the same issue as Sam had below. Has anybody found a fix for this? I tried the ES6 console that Ritwik proposed, but it did not work for me either…

    Also: does anybody know why this has been working for almost a year and giving me an error just now?

  7. Ritwik – Thank you for this! I haven’t been able to find step by step instructions anywhere else.

    Yehoshua at http://www.analytics-ninja.com posted some examples of ways to use this data, including a user timing category for total time visible (see screen shot). However, I’m not sure how to actually set it up. Can anyone explain in more detail how to create a variable that sums all visible time values. I thought once a new value was pushed to the dataLayer, the old state of that variable was lost. How would you access those values for the whole session to get to the total?

    Here’s the link to the article: http://www.analytics-ninja.com/blog/2015/02/real-time-page-google-analytics.html

    https://uploads.disquscdn.com/images/274c690d42f531c118dbab81b23c7496309f394f0fea667cda747403030a16d8.png

    1. Hi Rachel,

      Yes, I read the same article before writing this one. Actually it was not clear to me as he didn’t mentioned any code but he did gave some logic.
      You are right. The old values of DL are lost on pageload (beforeunload event), unless you store them in the cookie. (but that’ll help you calculate Total Visible Time By User, which is not restricted to a particular page.)

      I think here’s what he meant:
      (Correct me if I am wrong in interpreting his logic. 🙂 )
      His logic was (assume tab1 as your website)
      Tab1 (10sec) —> Tab 2 (5sec) —> Tab 1 (10sec) —-> Exit
      (DL = 10sec) ————————-> (DL = 10sec) —-> (beforeunload)

      Add total time on ‘beforeunload’ (in short ‘page reload’)
      So, Tab1 = 10+10 = 20sec. (by adding DL values)

      Here’s what my logic:
      Tab1 (10sec) —> Tab 2 (5sec) —> Tab 1 (10sec) —> Exit
      (DL = 10sec) ————————–> (DL = 20sec) —-> (beforeunload)

      So Tab1 = 20sec (final DL)
      I tried to add the previous DL values as soon as the visibilitychange event occured.

      Anyways, here’s the same report in Site Speed > User timings > (select page & visible event)

      https://uploads.disquscdn.com/images/79aa3a47c1f2e87d2231a25ec82a928cfd24a8394d48a026ea9cc1d73627c76b.png

  8. This looks very promising. Unfortunately, I couldn’t get it to work. Also, Google changed stuff e.g. the function block, I altered it but still had no luck tracking the time (Analytics is missing the events, even though they fire). Is this method still working?

Leave a Reply

Your email address will not be published. Required fields are marked *