Header Snippets

Header Snippets are small pieces of JavaScript code that can be injected into a Page's HTML so that even before Boomerang loads, components of the page load experience (such as JavaScript errors) are still fully measured.

These Header Snippets should be added as inline <script> tags within the <head> of a document, ideally right before the Non-Blocking Loader Snippet is included.

JavaScript Errors (for BOOMR.plugins.Errors)

Boomerang listens for JavaScript errors via the BOOMR.plugins.Errors plugin by monitoring the onerror global event handler. However, JavaScript errors that occur prior to Boomerang being loaded will be missed.

This Errors Snippet monitors for JavaScript Errors via the onerror event and will "hand off" the errors to Boomerang once it has loaded.

Source:

<script>
(function(w){
  w.BOOMR = w.BOOMR || {};

  w.BOOMR.globalOnErrorOrig = w.BOOMR.globalOnError = w.onerror;
  w.BOOMR.globalErrors = [];

  // Gathers a high-resolution timestamp (when available), or falls back to Date.getTime()
  var now = (function() {
    try {
      if ("performance" in w && w.performance.timing) {
        return function() {
          return Math.round(w.performance.now() + performance.timing.navigationStart);
        };
      }
    }
    catch (ignore) {
      // NOP
    }

    return Date.now || function() {
      return new Date().getTime();
    };
  })();

  // Overwrite the global onerror to listen for errors, but forward all messages to the original one if it exists
  w.onerror = function BOOMR_plugins_errors_onerror(message, fileName, lineNumber, columnNumber, error) {
    if (w.BOOMR.version) {
      // If Boomerang has already loaded, the only reason this function would still be alive would be if
      // we're in the chain from another handler that overwrote window.onerror.  In that case, we should
      // run globalOnErrorOrig which presumably hasn't been overwritten by Boomerang.
      if (typeof w.BOOMR.globalOnErrorOrig === "function") {
        w.BOOMR.globalOnErrorOrig.apply(w, arguments);
      }

      return;
    }

    // Save this error for when Boomerang loads
    if (typeof error !== "undefined" && error !== null) {
      error.timestamp = now();
      w.BOOMR.globalErrors.push(error);
    }
    else {
      w.BOOMR.globalErrors.push({
        message: message,
        fileName: fileName,
        lineNumber: lineNumber,
        columnNumber: columnNumber,
        noStack: true,
        timestamp: now()
      });
    }

    // Call the original window.onerror
    if (typeof w.BOOMR.globalOnError === "function") {
      w.BOOMR.globalOnError.apply(w, arguments);
    }
  };

  // make it easier to detect this is our wrapped handler
  w.onerror._bmr = true;
})(window);
</script>

Minified:

<script>(function(i){i.BOOMR=i.BOOMR||{};i.BOOMR.globalOnErrorOrig=i.BOOMR.globalOnError=i.onerror;i.BOOMR.globalErrors=[];var a=function(){try{if("performance"in i&&i.performance.timing){return function(){return Math.round(i.performance.now()+performance.timing.navigationStart)}}}catch(r){}return Date.now||function(){return(new Date).getTime()}}();i.onerror=function r(n,o,e,t,O){if(i.BOOMR.version){if(typeof i.BOOMR.globalOnErrorOrig==="function"){i.BOOMR.globalOnErrorOrig.apply(i,arguments)}return}if(typeof O!=="undefined"&&O!==null){O.timestamp=a();i.BOOMR.globalErrors.push(O)}else{i.BOOMR.globalErrors.push({message:n,fileName:o,lineNumber:e,columnNumber:t,noStack:true,timestamp:a()})}if(typeof i.BOOMR.globalOnError==="function"){i.BOOMR.globalOnError.apply(i,arguments)}};i.onerror._bmr=true})(window);</script>

Frame Rate (for BOOMR.plugins.Continuity)

The BOOMR.plugins.Continuity plugin measures performance and user experience metrics beyond just the traditional Page Load timings.

One of the metrics that the BOOMR.plugins.Continuity plugin measures is Frame Rate (FSP) data via requestAnimationFrame. However, FPS data is "real-time" and is not available for scripts like Boomerang that may load later in the page load process.

This Header Snippet will start monitoring requestAnimationFrame and hand the FPS data off to Boomerang once it has loaded.

Source:

<script>
(function() {
  if (window && window.requestAnimationFrame) {
    window.BOOMR = window.BOOMR || {};
    window.BOOMR.fpsLog = [];

    function frame(now) {
      // window.BOOMR.fpsLog will get deleted once Boomerang has loaded
      if (window.BOOMR.fpsLog) {
        window.BOOMR.fpsLog.push(Math.round(now));

        // if we've added more than 30 seconds of data, stop
        if (window.BOOMR.fpsLog.length > 30 * 60) {
          return;
        }

        window.requestAnimationFrame(frame);
      }
    }

    window.requestAnimationFrame(frame);
  }
})();
</script>

Minified:

<script>(function(){if(window&&window.requestAnimationFrame){window.BOOMR=window.BOOMR||{};window.BOOMR.fpsLog=[];function i(n){if(window.BOOMR.fpsLog){window.BOOMR.fpsLog.push(Math.round(n));if(window.BOOMR.fpsLog.length>30*60){return}window.requestAnimationFrame(i)}}window.requestAnimationFrame(i)}})();</script>

Instrumenting XMLHttpRequests (for BOOMR.plugins.AutoXHR)

The BOOMR.plugins.AutoXHR plugin monitors XMLHttpRequests on the page.

The performance data of XMLHttpRequests that start before Boomerang is loaded may not be monitored.

This Header Snippet will start monitoring XMLHttpRequests and hand the performance data off to Boomerang once it has loaded.

Source:

<script>
(function(w){
  if (!w.XMLHttpRequest || !(new w.XMLHttpRequest()).addEventListener) {
    return;
  }

  var a = document.createElement("A"),
      xhrNative = w.XMLHttpRequest,
      resources = [],
      sendResource,
      readyStateMap = ["uninitialized", "open", "responseStart", "domInteractive", "responseEnd"];

  w.BOOMR = w.BOOMR || {};

  // xhr object is what the AutoXHR plugin will interact with once it's loaded
  BOOMR.xhr = {
    /**
     * Stops XHR collection and forwards any additional reporting to Boomerang
     *
     * @param {function} sendResourceCallback Callback for any ongoing monitoring
     *
     * @returns {object} Array of XHRs
     */
    stop: function(sendResourceCallback) {
      sendResource = sendResourceCallback;

      // swap back in the native XHR function
      w.XMLHttpRequest = xhrNative;

      delete BOOMR.xhr;

      // clear our queue after a moment
      setTimeout(function(){
        resources = [];
      }, 10);

      return resources;
    }
  };

  // Gathers a high-resolution timestamp (when available), or falls back to Date.getTime()
  var now = (function() {
    try {
      if ("performance" in w && w.performance.timing) {
        return function() {
          return Math.round(w.performance.now() + performance.timing.navigationStart);
        };
      }
    }
    catch (ignore) {
      // NOP
    }

    return Date.now || function() {
      return new Date().getTime();
    };
  })();

  // Overwrite the native XHR with our monitored object
  w.XMLHttpRequest = function() {
    var xhr = new xhrNative(),
        open = xhr.open;

    xhr.open = function(method, url, async) {
      // Normalize the URL
      a.href = url;

      var resource = {
        timing: {},
        url: a.href,
        method: method
      };

      // Callback when the XHR is finished
      function loadFinished() {
        if (!resource.timing.loadEventEnd) {
          resource.timing.loadEventEnd = now();

          if ("performance" in w && w.performance && typeof w.performance.getEntriesByName === "function") {
            var entries = w.performance.getEntriesByName(resource.url);

            var entry = entries && entries.length && entries[entries.length - 1];

            if (entry) {
              var navSt = w.performance.timing.navigationStart;

              if (entry.responseEnd !== 0) {
                resource.timing.responseEnd = Math.round(navSt + entry.responseEnd);
              }

              if (entry.responseStart !== 0) {
                resource.timing.responseStart = Math.round(navSt + entry.responseStart);
              }

              if (entry.startTime !== 0) {
                resource.timing.requestStart = Math.round(navSt + entry.startTime);
              }
            }
          }

          if (sendResource) {
            sendResource(resource);
          }
          else {
            resources.push(resource);
          }
        }
      }

      function addListener(ename, stat) {
        xhr.addEventListener(
          ename,
          function() {
            if (ename === "readystatechange") {
              resource.timing[readyStateMap[xhr.readyState]] = now();

              if (xhr.readyState === 4) {
                loadFinished();
              }
            }
            else {
              resource.status = (stat === undefined ? xhr.status : stat);
              loadFinished();
            }
          },
          false);
      }

      if (async === true) {
        addListener("readystatechange");
      }
      else {
        resource.synchronous = true;
      }

      addListener("load");
      addListener("timeout", -1001);
      addListener("error",   -998);
      addListener("abort",   -999);

      try {
        open.apply(xhr, arguments);

        var send = xhr.send;

        xhr.send = function() {
          resource.timing.requestStart = now();

          send.apply(xhr, arguments);
        };
      }
      catch (e) {
        resource.status = -997;

        loadFinished();
      }
    };

    return xhr;
  };
})(window);
</script>

Minified:

<script>(function(f){if(!f.XMLHttpRequest||!(new f.XMLHttpRequest).addEventListener){return}var c=document.createElement("A"),t=f.XMLHttpRequest,p=[],d,m=["uninitialized","open","responseStart","domInteractive","responseEnd"];f.BOOMR=f.BOOMR||{};BOOMR.xhr={stop:function(e){d=e;f.XMLHttpRequest=t;delete BOOMR.xhr;setTimeout(function(){p=[]},10);return p}};var g=function(){try{if("performance"in f&&f.performance.timing){return function(){return Math.round(f.performance.now()+performance.timing.navigationStart)}}}catch(e){}return Date.now||function(){return(new Date).getTime()}}();f.XMLHttpRequest=function(){var s=new t,u=s.open;s.open=function(e,t,n){c.href=t;var r={timing:{},url:c.href,method:e};function i(){if(!r.timing.loadEventEnd){r.timing.loadEventEnd=g();if("performance"in f&&f.performance&&typeof f.performance.getEntriesByName==="function"){var e=f.performance.getEntriesByName(r.url);var t=e&&e.length&&e[e.length-1];if(t){var n=f.performance.timing.navigationStart;if(t.responseEnd!==0){r.timing.responseEnd=Math.round(n+t.responseEnd)}if(t.responseStart!==0){r.timing.responseStart=Math.round(n+t.responseStart)}if(t.startTime!==0){r.timing.requestStart=Math.round(n+t.startTime)}}}if(d){d(r)}else{p.push(r)}}}function a(e,t){s.addEventListener(e,function(){if(e==="readystatechange"){r.timing[m[s.readyState]]=g();if(s.readyState===4){i()}}else{r.status=t===undefined?s.status:t;i()}},false)}if(n===true){a("readystatechange")}else{r.synchronous=true}a("load");a("timeout",-1001);a("error",-998);a("abort",-999);try{u.apply(s,arguments);var o=s.send;s.send=function(){r.timing.requestStart=g();o.apply(s,arguments)}}catch(e){r.status=-997;i()}};return s}})(window);</script>