boomerangjs
Version:
boomerang always comes back, except when it hits something
509 lines (443 loc) • 18.3 kB
JavaScript
/*
* Copyright (c), Buddy Brewer.
*/
/**
* The Navigation Timing plugin collects performance metrics collected by modern
* user agents that support the W3C [NavigationTiming]{@link http://www.w3.org/TR/navigation-timing/}
* specification.
*
* This plugin also adds similar [ResourceTiming]{@link https://www.w3.org/TR/resource-timing-1/}
* metrics for any XHR beacons.
*
* For information on how to include this plugin, see the {@tutorial building} tutorial.
*
* ## Beacon Parameters
*
* All beacon parameters are prefixed with `nt_`.
*
* This plugin adds the following parameters to the beacon for Page Loads:
*
* * `nt_nav_st`: `performance.timing.navigationStart`
* * `nt_red_cnt`: `performance.navigation.redirectCount`
* * `nt_nav_type`: `performance.navigation.type`
* * `nt_red_st`: `performance.timing.redirectStart`
* * `nt_red_end`: `performance.timing.redirectEnd`
* * `nt_fet_st`: `performance.timing.fetchStart`
* * `nt_dns_st`: `performance.timing.domainLookupStart`
* * `nt_dns_end`: `performance.timing.domainLookupEnd`
* * `nt_con_st`: `performance.timing.connectStart`
* * `nt_con_end`: `performance.timing.connectEnd`
* * `nt_req_st`: `performance.timing.requestStart`
* * `nt_res_st`: `performance.timing.responseStart`
* * `nt_res_end`: `performance.timing.responseEnd`
* * `nt_domloading`: `performance.timing.domLoading`
* * `nt_domint`: `performance.timing.domInteractive`
* * `nt_domcontloaded_st`: `performance.timing.domContentLoadedEventStart`
* * `nt_domcontloaded_end`: `performance.timing.domContentLoadedEventEnd`
* * `nt_domcomp`: `performance.timing.domComplete`
* * `nt_load_st`: `performance.timing.loadEventStart`
* * `nt_load_end`: `performance.timing.loadEventEnd`
* * `nt_unload_st`: `performance.timing.unloadEventStart`
* * `nt_unload_end`: `performance.timing.unloadEventEnd`
* * `nt_ssl_st`: `performance.timing.secureConnectionStart`
* * `nt_act_st`: NavigationTiming2's `activationStart`
* * `nt_spdy`: `1` if page was loaded over SPDY, `0` otherwise. Only available
* in Chrome when it _doesn't_ support NavigationTiming2. If NavigationTiming2
* is supported, `nt_protocol` will be added instead.
* * `nt_first_paint`: The time when the first paint happened. If the browser
* supports the Paint Timing API, this is the `first-paint` time in milliseconds
* since the epoch. Else, on Internet Explorer, this is the `msFirstPaint`
* value, in milliseconds since the epoch. On Chrome, this is using
* `loadTimes().firstPaintTime` and is converted from seconds.microseconds
* into milliseconds since the epoch.
* * `nt_cinf`: Chrome `chrome.loadTimes().connectionInfo`. Only available
* in Chrome when it _doesn't_ support NavigationTiming2. If NavigationTiming2
* is supported, `nt_protocol` will be added instead.
* * `nt_protocol`: NavigationTiming2's `nextHopProtocol`
* * `nt_bad`: If we detected that any NavigationTiming metrics looked odd,
* such as `responseEnd` in the far future or `fetchStart` before `navigationStart`.
* * `nt_worker_start`: NavigationTiming2 `workerStart`
* * `nt_enc_size`: NavigationTiming2 `encodedBodySize`
* * `nt_dec_size`: NavigationTiming2 `decodedBodySize`
* * `nt_trn_size`: NavigationTiming2 `transferSize`
*
* For XHR beacons, the following parameters are added (via ResourceTiming):
*
* * `nt_red_st`: `redirectStart`
* * `nt_red_end`: `redirectEnd`
* * `nt_fet_st`: `fetchStart`
* * `nt_dns_st`: `domainLookupStart`
* * `nt_dns_end`: `domainLookupEnd`
* * `nt_con_st`: `connectStart`
* * `nt_con_end`: `connectEnd`
* * `nt_req_st`: `requestStart`
* * `nt_res_st`: `responseStart`
* * `nt_res_end`: `responseEnd`
* * `nt_load_st`: `loadEventStart`
* * `nt_load_end`: `loadEventEnd`
* * `nt_ssl_st`: `secureConnectionStart`
*
* @see {@link http://www.w3.org/TR/navigation-timing/}
* @see {@link https://www.w3.org/TR/resource-timing-1/}
* @class BOOMR.plugins.NavigationTiming
*/
(function() {
BOOMR = window.BOOMR || {};
BOOMR.plugins = BOOMR.plugins || {};
if (BOOMR.plugins.NavigationTiming) {
return;
}
/**
* Calculates a NavigationTiming timestamp for the beacon, in milliseconds
* since the Unix Epoch.
*
* The offset should be 0 if using a timestamp from performance.timing (which
* are already in milliseconds since Unix Epoch), or the value of navigationStart
* if using getEntriesByType("navigation") (which are DOMHighResTimestamps).
*
* The number is stripped of any decimals.
*
* @param {number} offset navigationStart offset (0 if using NavTiming1)
* @param {number} val DOMHighResTimestamp
* @param {boolean} forceIfZero Force the offset even if the value is zero
*
* @returns {number} Timestamp for beacon
*/
function calcNavTimingTimestamp(offset, val, forceIfZero) {
if (typeof val !== "number" || val === 0) {
// if the timestamp is 0 and forceIfZero is set,
// return the offset so the timestamp is still on the beacon
if (val === 0 && forceIfZero) {
return Math.floor(offset || 0);
}
return undefined;
}
return Math.floor((offset || 0) + val);
}
// A private object to encapsulate all your implementation details
var impl = {
/**
* Whether or not the plugin is complete (beacon has been sent)
*/
complete: false,
/**
* Whether or not all data has been sent (the document 'load' event is complete
* and loadEventEnd is set)
*/
fullySent: false,
/**
* Sends the beacon.
*/
sendBeacon: function() {
this.complete = true;
BOOMR.sendBeacon();
},
/**
* Called when an `xhr_load` event is fired (when an XHR beacon is
* about to be sent)
*
* @param {object} edata Event data
*/
xhr_done: function(edata) {
var p;
if (edata && edata.initiator === "spa_hard") {
// Single Page App - Hard refresh: Send page's NavigationTiming data, if
// available.
impl.done(edata);
return;
}
else if (edata && edata.initiator === "spa") {
// Single Page App - Soft refresh: The original hard navigation is no longer
// relevant for this soft refresh, nor is the "URL" for this page, so don't
// add NavigationTiming or ResourceTiming metrics.
impl.sendBeacon();
return;
}
var w = BOOMR.window,
res,
data = {},
k;
if (!edata) {
return;
}
if (edata.data) {
edata = edata.data;
}
p = BOOMR.getPerformance();
// if we previously saved the correct ResourceTiming entry, use it
if (p && edata.restiming) {
data = {
nt_red_st: edata.restiming.redirectStart,
nt_red_end: edata.restiming.redirectEnd,
nt_fet_st: edata.restiming.fetchStart,
nt_dns_st: edata.restiming.domainLookupStart,
nt_dns_end: edata.restiming.domainLookupEnd,
nt_con_st: edata.restiming.connectStart,
nt_con_end: edata.restiming.connectEnd,
nt_req_st: edata.restiming.requestStart,
nt_res_st: edata.restiming.responseStart,
nt_res_end: edata.restiming.responseEnd
};
if (edata.restiming.secureConnectionStart) {
// secureConnectionStart is OPTIONAL in the spec
data.nt_ssl_st = edata.restiming.secureConnectionStart;
}
for (k in data) {
if (data.hasOwnProperty(k) && data[k]) {
data[k] += p.timing.navigationStart;
// don't need to send microseconds
data[k] = Math.floor(data[k]);
}
}
}
if (edata.timing) {
res = edata.timing;
if (!data.nt_req_st) {
// requestStart will be 0 if Timing-Allow-Origin header isn't set on the xhr response
data.nt_req_st = res.requestStart;
}
if (!data.nt_res_st) {
// responseStart will be 0 if Timing-Allow-Origin header isn't set on the xhr response
data.nt_res_st = res.responseStart;
}
if (!data.nt_res_end) {
data.nt_res_end = res.responseEnd;
}
data.nt_domint = res.domInteractive;
data.nt_domcomp = res.domComplete;
data.nt_load_st = res.loadEventEnd;
data.nt_load_end = res.loadEventEnd;
}
for (k in data) {
if (data.hasOwnProperty(k) && !data[k]) {
delete data[k];
}
}
BOOMR.addVar(data, undefined, true);
impl.sendBeacon();
},
/**
* Called when an `page_ready` or 'before_unload' event to add
* data to the beacon
*/
done: function() {
var w = BOOMR.window,
p, pn, chromeTimes, pt,
data = {},
offset = 0,
i,
paintTiming,
paintTimingSupported = false,
k;
if (this.complete) {
return this;
}
p = BOOMR.getPerformance();
if (p) {
// Prioritize getting data from the PerformanceTimeline
if (typeof p.getEntriesByType === "function") {
pt = p.getEntriesByType("navigation");
if (pt && pt.length) {
BOOMR.info("This user agent supports NavigationTiming2", "nt");
pt = pt[0];
// ensure DOMHighResTimestamps are added to navigationStart
offset = p.timing ? p.timing.navigationStart : 0;
}
else {
pt = undefined;
}
}
// If not, get data from window.performance.timing
if (!pt && p.timing) {
BOOMR.info("This user agent supports NavigationTiming", "nt");
pt = p.timing;
}
if (pt) {
data = {
// start is `navigationStart` on .timing, `startTime` is always 0 on timeline entry
nt_nav_st: p.timing ? p.timing.navigationStart : 0,
// all other entries have the same name on .timing vs timeline entry
nt_red_st: calcNavTimingTimestamp(offset, pt.redirectStart),
nt_red_end: calcNavTimingTimestamp(offset, pt.redirectEnd),
nt_fet_st: calcNavTimingTimestamp(offset, pt.fetchStart, true),
nt_dns_st: calcNavTimingTimestamp(offset, pt.domainLookupStart, true),
nt_dns_end: calcNavTimingTimestamp(offset, pt.domainLookupEnd, true),
nt_con_st: calcNavTimingTimestamp(offset, pt.connectStart, true),
nt_con_end: calcNavTimingTimestamp(offset, pt.connectEnd, true),
nt_req_st: calcNavTimingTimestamp(offset, pt.requestStart),
nt_res_st: calcNavTimingTimestamp(offset, pt.responseStart),
nt_res_end: calcNavTimingTimestamp(offset, pt.responseEnd),
nt_domloading: calcNavTimingTimestamp(offset, pt.domLoading),
nt_domint: calcNavTimingTimestamp(offset, pt.domInteractive),
nt_domcontloaded_st: calcNavTimingTimestamp(offset, pt.domContentLoadedEventStart),
nt_domcontloaded_end: calcNavTimingTimestamp(offset, pt.domContentLoadedEventEnd),
nt_domcomp: calcNavTimingTimestamp(offset, pt.domComplete),
nt_load_st: calcNavTimingTimestamp(offset, pt.loadEventStart),
nt_load_end: calcNavTimingTimestamp(offset, pt.loadEventEnd),
nt_unload_st: calcNavTimingTimestamp(offset, pt.unloadEventStart),
nt_unload_end: calcNavTimingTimestamp(offset, pt.unloadEventEnd),
nt_act_st: calcNavTimingTimestamp(offset, pt.activationStart)
};
// domLoading doesn't exist on NavigationTiming2, so fetch it
// from performance.timing if available.
if (!data.nt_domloading && p && p.timing && p.timing.domLoading) {
// value on performance.timing will be in Unix Epoch milliseconds
data.nt_domloading = p.timing.domLoading;
}
if (pt.secureConnectionStart) {
// secureConnectionStart is OPTIONAL in the spec
data.nt_ssl_st = calcNavTimingTimestamp(offset, pt.secureConnectionStart);
}
if (p.timing && p.timing.msFirstPaint) {
// msFirstPaint is IE9+ http://msdn.microsoft.com/en-us/library/ff974719
// and is in Unix Epoch format
data.nt_first_paint = p.timing.msFirstPaint;
}
if (pt.workerStart) {
// ServiceWorker time
data.nt_worker_start = calcNavTimingTimestamp(offset, pt.workerStart);
}
// Need to check both decodedSize and transferSize as
// transferSize is 0 for cached responses and
// decodedSize is 0 for empty responses (eg: beacons, 204s, etc.)
if (pt.decodedBodySize || pt.transferSize) {
data.nt_enc_size = pt.encodedBodySize;
data.nt_dec_size = pt.decodedBodySize;
data.nt_trn_size = pt.transferSize;
}
if (pt.nextHopProtocol) {
data.nt_protocol = pt.nextHopProtocol;
}
}
//
// Get First Paint from Paint Timing API
// https://www.w3.org/TR/paint-timing/
//
if (!data.nt_first_paint && BOOMR.plugins.PaintTiming) {
paintTimingSupported = BOOMR.plugins.PaintTiming.is_supported();
paintTiming = BOOMR.plugins.PaintTiming.getTimingFor("first-paint");
if (paintTiming) {
data.nt_first_paint = calcNavTimingTimestamp(offset, paintTiming);
}
}
//
// Chrome provides window.chrome.loadTimes(), but this is deprecated
// in Chrome 64+ and will be removed at some point. The data it
// provides may be available in more modern performance APIs:
//
// * .connectionInfo (nt_cinf): Navigation Timing 2 nextHopProtocol
// * .wasFetchedViaSpdy (nt_spdy): Could be calculated via above,
// so we don't need to add if it's not available directly
// * .firstPaintTime (nt_first_paint): Paint Timing's first-paint
//
// If we've already queried that data, don't also query
// loadTimes() as it will generate a console warning.
//
if ((!data.nt_protocol || !data.nt_first_paint) &&
(!pt || pt.nextHopProtocol !== "") &&
!paintTimingSupported &&
w.chrome &&
typeof w.chrome.loadTimes === "function") {
chromeTimes = w.chrome.loadTimes();
if (chromeTimes) {
data.nt_spdy = (chromeTimes.wasFetchedViaSpdy ? 1 : 0);
data.nt_cinf = chromeTimes.connectionInfo;
// Chrome firstPaintTime is in seconds.microseconds, so
// we need to multiply it by 1000 to be consistent with
// msFirstPaint and other NavigationTiming timestamps that
// are in milliseconds.microseconds.
if (typeof chromeTimes.firstPaintTime === "number" && chromeTimes.firstPaintTime !== 0) {
data.nt_first_paint = Math.round(chromeTimes.firstPaintTime * 1000);
}
}
}
//
// Navigation Type and Redirect Count
//
if (p.navigation) {
pn = p.navigation;
data.nt_red_cnt = pn.redirectCount;
data.nt_nav_type = pn.type;
}
// Remove any properties that are undefined
for (k in data) {
if (data.hasOwnProperty(k) && data[k] === undefined) {
delete data[k];
}
}
BOOMR.addVar(data, undefined, true);
//
// Basic browser bug detection for known cases where NavigationTiming
// timestamps might not be trusted.
//
if (pt && (
(pt.requestStart && pt.navigationStart && pt.requestStart < pt.navigationStart) ||
(pt.responseStart && pt.navigationStart && pt.responseStart < pt.navigationStart) ||
(pt.responseStart && pt.fetchStart && pt.responseStart < pt.fetchStart) ||
(pt.navigationStart && pt.fetchStart < pt.navigationStart) ||
(pt.responseEnd && pt.responseEnd > BOOMR.now() + 8.64e+7)
)) {
BOOMR.addVar("nt_bad", 1, true);
}
if (data.nt_load_end > 0) {
this.fullySent = true;
}
}
impl.sendBeacon();
},
clear: function(edata) {
// Allow the data to go out on both an Early beacon and the regular Page Load beacon,
// but after that, if we ever sent the full data, we're complete for all times.
this.complete = !(edata && edata.early) && this.fullySent;
},
prerenderToVisible: function() {
// ensure we add our data to the beacon even if we had added it
// during prerender (in case another beacon went out in between)
this.complete = false;
// add our data to the beacon
this.done();
},
onBeforeEarlyBeacon: function(edata) {
// Add our data to the early beacon
if (!edata ||
typeof edata.initiator === "undefined" ||
edata.initiator === "spa_hard") {
this.done();
}
}
};
//
// Exports
//
BOOMR.plugins.NavigationTiming = {
/**
* Initializes the plugin.
*
* This plugin does not have any configuration.
* @returns {@link BOOMR.plugins.NavigationTiming} The NavigationTiming plugin for chaining
* @memberof BOOMR.plugins.NavigationTiming
*/
init: function() {
if (!impl.initialized) {
// we'll fire on whichever happens first
BOOMR.subscribe("page_ready", impl.done, null, impl);
BOOMR.subscribe("prerender_to_visible", impl.prerenderToVisible, null, impl);
BOOMR.subscribe("before_early_beacon", impl.onBeforeEarlyBeacon, null, impl);
BOOMR.subscribe("xhr_load", impl.xhr_done, null, impl);
BOOMR.subscribe("before_unload", impl.done, null, impl);
BOOMR.subscribe("beacon", impl.clear, null, impl);
impl.initialized = true;
}
return this;
},
/**
* Whether or not this plugin is complete
*
* @returns {boolean} `true` if the plugin is complete
* @memberof BOOMR.plugins.NavigationTiming
*/
is_complete: function() {
return true;
}
};
}());