Scroll Tracking

Scroll tracking can be very helpful to track content engagement for an article or webpage.

Some interesting questions it can help you answer are: What percentage of an article has been read by a user? Has an article been completed by a user? How many users make it 50% of the way through an article? What percentage of users make it to the bottom of the page?

Using the code below, a great query for this data model would be the median of ratio_max grouped by page title. You could then see which pages have the best scroll-depth engagement.

Here’s an example that shows how to model and send events based on a user’s scrolling on a page.

Due to the nature of browsers, this recipe will only capture scroll depth for 75-85% of article reads. Many times, a session will end (e.g. person closes a tab) before an event can be tracked. The data is great for understanding patterns in scroll depth and comparing content performance, but wouldn’t be viable as a per-user source of truth.


    <meta charset="utf-8">
    <script src=""></script>
      body {
        /* Demo page content; not required for regular use. */
        height: 9000px;
    <h1>Open your developer console</h1>
    <a href="./index.html">Test Page Unload</a>
      Define client
    var client = new Keen({
      projectId: 'YOUR_KEEN_PROJECT_ID',
      writeKey: 'YOUR_KEEN_WRITE_KEY'
    // Log attempts
    client.on('recordEvent', console.log);
    Keen.debug = true;

      // Set listener to sample engagement on page unload
      // Define data model for 'page-unload' events here
        return {
          scroll_info: sampleScrollState()

      Define scroll-handler
    var winScroll = Keen.utils.listener('window');
    var pixel = 0, pixel_max = 0;
    winScroll.on('scroll', sampleScrollState);

    /* Demo logger; not required for regular use. */
    var scrollSampler = setInterval(function(){
    }, 2000);

      Scroll-depth sampling
    function sampleScrollState() {
      pixel = getScrollOffset() + getWindowHeight();
      if (pixel > pixel_max) {
        pixel_max = pixel;
      return {
        'pixel': pixel,
        'pixel_max': pixel_max,
        'ratio': parseFloat( Number(pixel / getScrollableArea()).toFixed(2) ),
        'ratio_max': parseFloat( Number(pixel_max / getScrollableArea()).toFixed(2) )

    function getWindowHeight() {
      return window.innerHeight || document.documentElement.clientHeight;

    function getScrollOffset() {
      return (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;

    function getScrollableArea() {
      var body = document.body, html = document.documentElement;
      return Math.max( body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight ) || null;

      Interrupt unload to perform synchronous request
    function setUnloadListener(dataModelHandler){
      console.log('Called: setUnloadListener()')
      var attachEvent, whichEvent;
      var handler = function(){
      if (window.onpagehide || window.onpagehide === null) {
        window.addEventListener('pagehide', handler, false);
      else {
        attachEvent = window.attachEvent || window.addEventListener
        whichEvent = window.attachEvent ? 'onbeforeunload' : 'beforeunload';
        attachEvent(whichEvent, handler);

    function handleUnloadEvent(dataModelHandler){
      var request = getXHR();
      var url = client.url('events', 'page-unload');
      if (request) {
        url += '?api_key=' + client.writeKey();
        url += '&data=' + encodeURIComponent(
        );'GET', url, false);

    function getXHR() {
      if (window.XMLHttpRequest && ('file:' != window.location.protocol || !window.ActiveXObject)) {
        return new XMLHttpRequest;
      } else {
        try { return new ActiveXObject('Microsoft.XMLHTTP'); } catch(e) {}
        try { return new ActiveXObject('Msxml2.XMLHTTP.6.0'); } catch(e) {}
        try { return new ActiveXObject('Msxml2.XMLHTTP.3.0'); } catch(e) {}
        try { return new ActiveXObject('Msxml2.XMLHTTP'); } catch(e) {}
      return false;

pixel and pixel_max are how far down a user has scrolled into the page, including the effective browser window. With pixel_max, you can see the farthest a user scrolled into the page.

ratio and ratio_max are the same but in terms of the % of total scrollable area on a page.

You could also find out how long a user is on a page by subtracting the page-unload time and page-load time.

var load_time = new Date().getTime();

// later
var time_on_page = new Date().getTime() - load_time;