70

I would like to measure a time (in seconds in integers or minutes in floats) a user spends on a page. I know there is an unload event which I can trigger when they leave the page. But how to get a time they have already spent there?

Richard Knop
  • 81,041
  • 149
  • 392
  • 552
  • 4
    I wrote this library to measure active time on page and execute certain things when active time reaches a treshold. Check it out and feedback is very welcome. https://github.com/atlassian/browser-interaction-time. https://atlassian.github.io/browser-interaction-time/ – meandmax Mar 28 '19 at 02:14

11 Answers11

74

The accepted answer is good, but (as an alternative) I've put some work into a small JavaScript library that times how long a user is on a web page. It has the added benefit of more accurately (not perfectly, though) tracking how long a user is actually interacting with the page. It ignore times that a user switches to different tabs, goes idle, minimizes the browser, etc. The Google Analytics method suggested in the accepted answer has the shortcoming (as I understand it) that it only checks when a new request is handled by your domain. It compares the previous request time against the new request time, and calls that the 'time spent on your web page'. It doesn't actually know if someone is viewing your page, has minimized the browser, has switched tabs to 3 different web pages since last loading your page, etc.

Edit: I have updated the example to include the current API usage.

Edit 2: Updating domain where project is hosted

https://github.com/jasonzissman/TimeMe.js/

An example of its usage:

Include in your page:

<!-- Download library from https://github.com/jasonzissman/TimeMe.js/ -->
<script src="timeme.js"></script>
<script type="text/javascript">
TimeMe.initialize({
    currentPageName: "home-page", // page name
    idleTimeoutInSeconds: 15 // time before user considered idle
});
</script>

If you want to report the times yourself to your backend:

xmlhttp=new XMLHttpRequest();
xmlhttp.open("POST","ENTER_URL_HERE",true);
xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
var timeSpentOnPage = TimeMe.getTimeOnCurrentPageInSeconds();
xmlhttp.send(timeSpentOnPage);

TimeMe.js also supports sending timing data via websockets, so you don't have to try to force a full http request into the document.onbeforeunload event.

jason.zissman
  • 2,750
  • 2
  • 19
  • 24
  • 1
    I really like your script! It has one piece of strange behavior though on the demo page: when I switch to a different window (browser not minimized, just another window has the focus) then the timer keeps running. Is this intended? – user1111929 Apr 22 '16 at 02:05
  • 1
    Thanks! The library relies on browser events to tell it when a user has gained/lost focus. I assume the browser doesn't report the 'lost focus' event when switching windows because users with multiple monitors may still be able to see the browser. Just a thought! – jason.zissman Apr 23 '16 at 11:24
  • 1
    A different way to get this metric could be to detect mouse movements and scroll/page movements, keypresses. This is a different measure. On multiscreen setups, I often have a reference page up on one monitor, while working in another. The reference page shouldn't count as time-on-page since it's there, but I'm not looking at it. With a bit of calibrating I suspect that one mouse movement/keypress/scroll event per X seconds would indicate time still on page. X is somewhere between 10 and 30 seconds I think. – Sherwood Botsford Feb 09 '17 at 23:15
  • 2
    Great thought. TimeMe actually does exactly that - if you are 'idle' (no mouse movement, key press, etc.) for a configurable amount of time, then it stops tracking your time. I actually updated TimeMe.js in the last month or so to rework a lot of the inner logic. The original comment implying that the timer still runs when focusing a new window no longer appears to be a problem. – jason.zissman Feb 24 '17 at 09:54
  • 2
    TimeMe doesn't capture time spent within an iframe (which doesn't also have TimeMe running). – Joseph Coco Aug 02 '17 at 01:15
  • Look like this domain is no longer in service. So much for that then – Michael d Feb 27 '18 at 09:09
  • @jason.zissman Really liked your library. Great work !! Only one thing, As mentioned by joseph It doesn't capture time spent within an iframe. Can you suggest what can we do to handle this case? – Deepak Kumar May 08 '19 at 08:36
  • @JosephCoco Were you able to find any solution? – Deepak Kumar May 08 '19 at 08:37
  • @DeepakKumar unfortunately not. I just set expectations of the client that time keeping won't be great. You may be able to use that trick that spammers use to move a single transparent pixel from the main page beneath the mouse and just see if you can propagate the click from that pixel into the iframe. But I have no idea if it would work. It would almost definitely keep TimeMe updating though. – Joseph Coco May 08 '19 at 22:00
  • I have given the iFrame problem some thought, but I have not come up with a good solution. Browsers report a blur even both when changing focus to an iFrame AND when switching tabs. I do not see an easy, effective way to distinguish the two, making iFrame monitoring difficult. – jason.zissman May 24 '19 at 22:41
43

If you use Google Analytics, they provide this statistic, though I am unsure exactly how they get it.

If you want to roll your own, you'll need to have some AJAX request that gets sent to your server for logging.

jQuery has a .unload(...) method you can use like:

$(document).ready(function() {
  var start = new Date();

  $(window).unload(function() {
      var end = new Date();
      $.ajax({ 
        url: "log.php",
        data: {'timeSpent': end - start},
        async: false
      })
   });
});

See more here: http://api.jquery.com/unload/

The only caveat here is that it uses javascript's beforeunload event, which doesn't always fire with enough time to make an AJAX request like this, so reasonably you will lose alot of data.

Another method would be to periodically poll the server with some type of "STILL HERE" message that can be processed more consistently, but obviously way more costly.

Paweł Szczur
  • 5,484
  • 3
  • 29
  • 32
Kevin Dolan
  • 4,952
  • 3
  • 35
  • 47
  • 3
    It is probably a better idea to send a request when the user first loads JS and another when he leaves the page. That way you cannot make any sort of hack. – Rob Fox Mar 08 '12 at 12:13
  • 1
    Even better would be to set a `$_SESSION` variable with the date, only updating when the server gets the signal to update. Since the `$_SESSION`-var is set when the page loads, it's never too much. – jdepypere Jun 28 '13 at 00:12
  • Add `async : false` to the ajax call above to avoid intermittent firing. – user217562 Jul 17 '17 at 07:59
  • You're leaking the variable `end` in the global scope. – dodov Jul 30 '17 at 11:58
17

In addition to Jason's answer, here's a small piece of code that should do the trick if you prefer to not use a library, it considers when the user switch tabs or focus another window.

let startDate = new Date();
let elapsedTime = 0;

const focus = function() {
    startDate = new Date();
};

const blur = function() {
    const endDate = new Date();
    const spentTime = endDate.getTime() - startDate.getTime();
    elapsedTime += spentTime;
};

const beforeunload = function() {
    const endDate = new Date();
    const spentTime = endDate.getTime() - startDate.getTime();
    elapsedTime += spentTime;

    // elapsedTime contains the time spent on page in milliseconds
};

window.addEventListener('focus', focus);
window.addEventListener('blur', blur);
window.addEventListener('beforeunload', beforeunload);
martpie
  • 5,510
  • 1
  • 21
  • 25
  • Does it considered the idle time ? – kabrice Feb 19 '17 at 03:18
  • @kabrice it depends what you mean by "idle". If the tab is focused, you cannot know if the user is reading your page or not. This snippet basically get the time the user focused the current tab. That means when your tab is in foreground. – martpie Apr 11 '17 at 12:04
10

.()

Running inline code to get the time that the user got to the page blocks the loading of the page. Instead, use performance.now() which shows how many milliseconds have elapsed since the user first navigated to the page. Date.now, however, measures clock-time which can differ from navigation-time by a second or more due to factors such as Time resynchonization and leap seconds. performance.now() is supported in IE10+ and all evergreen browsers (evergreen=made for fun, not for profit). The earliest version of internet explorer still around today is Internet Explorer 11 (the last version) since Microsoft discontinued Windows XP in 2014.

(function(){"use strict";

var secondsSpentElement = document.getElementById("seconds-spent");
var millisecondsSpentElement = document.getElementById("milliseconds-spent");

requestAnimationFrame(function updateTimeSpent(){
    var timeNow = performance.now();
    
    secondsSpentElement.value = round(timeNow/1000);
    millisecondsSpentElement.value = round(timeNow);
    
    requestAnimationFrame(updateTimeSpent);
});
var performance = window.performance, round = Math.round;
})();
Seconds spent on page:&nbsp; <input id="seconds-spent" size="6" readonly="" /><br />
Milliseconds spent here: <input id="milliseconds-spent" size="6" readonly="" />
Jack G
  • 4,553
  • 2
  • 41
  • 50
9

I'd say your best bet is to keep track of the timing of requests per session ID at your server. The time the user spent on the last page is the difference between the time of the current request, and the time of the prior request.

This won't catch the very last page the user visits (i.e. when there isn't going to be another request), but I'd still go with this approach, as you'd otherwise have to submit a request at onunload, which would be extremely error prone.

David Hedlund
  • 128,221
  • 31
  • 203
  • 222
  • I want to track a video_page with this approach but problem is when user open new page in new tab in this case middel-ware detect a new url hit. and get time from session and hit DB, but user just open new tab not close current tab and timeme.js and ajax method is buggy for me :( – GrvTyagi Feb 29 '16 at 10:48
  • For now i think ajax call in every 30sec. work for me or not ? – GrvTyagi Feb 29 '16 at 10:50
6

i think the best way is to store time in onload and unload event handlers in cookies e.g. and then analyze them in server-side scripts

heximal
  • 10,327
  • 5
  • 46
  • 69
2

According to the right answer I think thats is not the best solution. Because according to the jQuery docs:

The exact handling of the unload event has varied from version to version of browsers. For example, some versions of Firefox trigger the event when a link is followed, but not when the window is closed. In practical usage, behavior should be tested on all supported browsers and contrasted with the similar beforeunload event.

Another thing is that you shouldn't use it after documents load because the result of substraction of time can be fake.

So the better solution is to add it to the onbeforeunload event in the end of the <head> section like this:

<script>
var startTime = (new Date()).getTime();

window.onbeforeunload = function (event) {
    var timeSpent = (new Date()).getTime() - startTime,
        xmlhttp= new XMLHttpRequest();
    xmlhttp.open("POST", "your_url");
    xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    var timeSpentOnPage = TimeMe.getTimeOnCurrentPageInSeconds();
    xmlhttp.send(timeSpent);
};
</script>

Of course if you want to count the time using Idle detector you can use:

https://github.com/serkanyersen/ifvisible.js/

TimeMe is a wrapper for the package that I paste above.

Filip Koblański
  • 9,718
  • 4
  • 31
  • 36
1

Neither of the top voted answers suited me. Because there is a possibility that a page is opened without shifting a focus on it (e.g. when using the middle mouse button or the "Ctrl" + left click combination).

TimeMe.js has an awful drawback. When you open a link in a new tab in a way that it doesn't gain focus on it ("Ctrl" + left clicking on the link), the library starts the activity counter for the tab anyway as if the tab is in the focus. TimeMe.js only stops the counter when a user activates and then leaves the tab (the action triggers the blur event for a window).

I suggest everyone to look at Page Visibility API

For my view it offers a simple and reliable solution to check if a tab is actually active (or visible on screen).

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    // The tab is in the focus.
    // Let's measure a user time on the page
  } else {
    // Tab is not in the focus
    // Stop the counter
  }
});

Otherwise you can put the document.hidden conditional inside the time interval loop:

let elapsedTime = 0;

setInterval(function() {
    

   if (!document.hidden) {
    elapsedTime += 1;

    // Send to server or do other stuff

    console.log(elapsedTime);
   }

  
}, 1000);
Eugeny K
  • 56
  • 3
0
<body onLoad="myFunction()">
<script src="jquery.min.js"></script>
<script>
var arr = [];
window.onbeforeunload = function(){
var d = new Date();
var n = d.getTime();
arr.push(n);
var diff= n-arr[0];
var sec = diff/1000;
var r = Math.round(sec);
return "Time spent on page: "+r+" seconds";
};
function myFunction() {
var d = new Date();
var n = d.getTime();
arr.push(n);
}
</script>
babiro
  • 95
  • 2
  • 11
0

I've found using beforeunload event to be unreliable, actually failing more often than not. Usually the page has been destroyed before the request gets sent, and you get a "network failure" error.

As others have stated, there is no sure-fire way to tell how long a user has been on a page. You can send up some clues however.

Clicking and scrolling are pretty fair indicators that someone is actively viewing the page. I would suggest listening for click and scroll events, and sending a request whenever one is fired, though not more often than say, every 30 or 60 seconds.

One can use a little intelligence in the calculations, eg, if there were events fired every 30 seconds or so for 5 minutes, then no events for 30 minutes, then a couple more events fired, chances are, the user was getting coffee during the 30 minute lapse.

let sessionid;

function utilize(action) {
  // This just gets the data on the server, all the calculation is done server-side.

  let href = window.location.href;
  let timestamp = Date.now();
  sessionid = sessionid || timestamp;

  let formData = new FormData();
  formData.append('sessionid', sessionid);
  formData.append('timestamp', timestamp);
  formData.append('href', href);
  formData.append('action', action || "");

  let url = "/php/track.php";

  let response = fetch(url, {
    method: "POST",
    body: formData
  });
}

let inhibitCall = false;

function onEvent() {
  // Don't allow an update any more often than every 30 seconds.

  if (!inhibitCall) {
    inhibitCall = true;
    utilize('update');
    setTimeout(() => {
      inhibitCall = false;
    }, 30000);
  }
}

window.addEventListener("scroll", onEvent);
window.addEventListener("click", onEvent);

utilize("open");
KevinHJ
  • 1,014
  • 11
  • 24
0

I think I have found a simple solution for this.

Just use recurring interval and LocalStorage along with some PHP (or any server side language)

Set LocalStorage

if (typeof localStorage.getItem("time_spent_current") === "undefined" || localStorage.getItem("time_spent_current") == null) {
    localStorage.setItem("time_spent_current", -5);
}

Add time every 5 seconds to Local Storage

var intervalId = window.setInterval(function(){
    // call your function here
    var val = localStorage.getItem("time_spent_current");
    var time_spent_current = parseInt(val) + 5;
    localStorage.setItem("time_spent_current", time_spent_current);
    console.log(time_spent_current);
}, 5000);

If you want to update the data to server, then upload the data for every new session.

<?php   if(!isset($_SESSION["time_spent_current"])){    ?>
    $(document).ready(function() {
      var start = new Date();

      $(window).unload(function() {
          var end = new Date();
          $.ajax({ 
            url: "log.php",
            data: {'timeSpent': time_spent_current},
            async: false
          })
       });
    });
<?php
    $_SESSION["time_spent_current"] = 1;
        }   
?>

P.s.: Used script from Kevin Dolan (Originally accepted answer) to upload data to server

Observer
  • 345
  • 1
  • 4
  • 21