boomerangjs
Version:
boomerang always comes back, except when it hits something
1,373 lines (1,159 loc) • 67.8 kB
JavaScript
/**
* The roundtrip (RT) plugin measures page load time, or other timers associated with the page.
*
* For information on how to include this plugin, see the {@tutorial building} tutorial.
*
* ## Beacon Parameters
*
* This plugin adds the following parameters to the beacon:
*
* * `t_done`: Perceived load time of the page.
* * `t_page`: Time taken from the head of the page to {@link BOOMR#event:page_ready}.
* * `t_page.inv`: If there was a problem detected with the start/end times of `t_page`.
* This can happen due to bugs in NavigationTiming clients, where `responseEnd`
* happens after all other NavigationTiming events.
* * `t_resp`: Time taken from the user initiating the request to the first byte of the response.
* * `t_other`: Comma separated list of additional timers set by page developer.
* Each timer is of the format `name|value`
* * `t_load`: If the page were prerendered, this is the time to fetch and prerender the page.
* * `t_prerender`: If the page were prerendered, this is the time from start of
* prefetch to the actual page display. It may only be useful for debugging.
* * `t_postrender`: If the page were prerendered, this is the time from prerender
* finish to actual page display. It may only be useful for debugging.
* * `vis.pre`: `1` if the page transitioned from prerender to visible
* * `r`: URL of page that set the start time of the beacon.
* * `nu`: URL of next page if the user clicked a link or submitted a form
* * `rt.start`: Specifies where the start time came from. May be one of:
* - `cookie` for the start cookie
* - `navigation` for the W3C NavigationTiming API,
* - `csi` for older versions of Chrome or gtb for the Google Toolbar.
* - `manual` for XHR beacons
* - `none` if the start could not be detected
* * `rt.tstart`: The start time timestamp.
* * `rt.nstart`: The `navigationStart` timestamp, if different from `rt.tstart. This could
* happen for XHR beacons, where `rt.tstart` is the start of the XHR fetch, and `nt_nav_st`
* won't be on the beacon. It could also happen for SPA Soft beacons, where `rt.tstart`
* is the start of the Soft Navigation.
* * `rt.cstart`: The start time stored in the cookie if different from rt.tstart.
* * `rt.bstart`: The timestamp when boomerang started executing.
* * `rt.blstart`: The timestamp when the boomerang was added to the host page.
* * `rt.end`: The timestamp when the `t_done` timer ended
* (`rt.end - rt.tstart === t_done`)
* * `rt.bmr`: Several parameters that include resource timing information for
* boomerang itself, ie, how long did boomerang take to load
* * `rt.subres`: Set to `1` if this beacon is for a sub-resource of a primary
* page beacon. This is typically set by XHR beacons, and you will need to
* use a separate identifier to tie the primary beacon and the subresource
* beacon together on the server-side.
* * `rt.quit`: This parameter will exist (but have no value) if the beacon was
* fired as part of the `onbeforeunload` event. This is typically used to
* find out how much time the user spent on the page before leaving, and is
* not guaranteed to fire.
* * `rt.abld`: This parameter will exist (but have no value) if the `onbeforeunload`
* event fires before the `onload` event fires. This can happen, for example,
* if the user left the page before it completed loading.
* * `rt.ntvu`: This parameter will exist (but have no value) if the `onbeforeunload`
* event fires before the page ever became visible. This can happen if the
* user opened the page in a background tab, and closed it without viewing it,
* and also if the page was pre-rendered, but never made visible. Use this
* to check your pre-render success ratio.
* * `http.method`: For XHR beacons, the HTTP method if not `GET`.
* * `http.errno`: For XHR beacons, the HTTP result code if not 200.
* * `http.hdr`: For XHR beacons, headers if available.
* * `http.type`: For XHR beacons, value of `f` for fetch API requests. Not set for XHRs.
* * `xhr.sync`: For XHR beacons, `1` if it was sent synchronously.
* * `http.initiator`: The initiator of the beacon:
* - (empty/missing) for the page load beacon
* - `xhr` for XHR beacons
* - `spa` for SPA Soft Navigations
* - `spa_hard` for SPA Hard Navigations
* * `fetch.bnu`: For XHR beacons from fetch API requests, `1` if fetch response body was not used.
* * `rt.tt`: Sum of load times across session
* * `rt.obo`: Number of pages in session that did not have a load time
* * `xhr.ru`: Final response URL after any redirects
* - `XMLHttpRequest`: it will be present if any redirects happened
* and final URL is not equivalent to the final response URL after any redirects.
* - `fetch`: it will only be present if any redirects happened
*
* ## Cookie
*
* The session information is stored within a cookie.
*
* You can customise the name of the cookie where the session information
* will be stored via the {@link BOOMR.plugins.RT.init RT.cookie} option.
*
* By default this is set to `RT`.
*
* This cookie is set to expire in 7 days. You can change its lifetime using
* the {@link BOOMR.plugins.RT.init RT.cookie_exp} option.
*
* During that time, you can also read the value of the cookie on the server
* side. Its format is as follows:
*
* ```
* RT="ss=nnnnnnn&si=abc-123...";
* ```
*
* The parameters are defined as:
*
* * `ss` [string] [timestamp] Session Start (Base36)
* * `si` [string] [guid] Session ID
* * `sl` [string] [count] Session Length (Base36)
* * `tt` [string] [ms] Sum of Load Times across the session (Base36)
* * `obo` [string] [count] Number of pages in the session that had no load time (Base36)
* * `dm` [string] [domain] Cookie domain
* * `bcn` [string] [URL] Beacon URL
* * `rl` [number] [boolean] Whether or not the session is Rate Limited
* * `se` [string] [s] Session expiry (Base36)
* * `ld` [string] [timestamp] Last load time (Base36, offset by ss)
* * `ul` [string] [timestamp] Last beforeunload time (Base36, offset by ss)
* * `hd` [string] [timestamp] Last unload time (Base36, offset by ss)
* * `cl` [string] [timestamp] Last click time (Base36, offset by ss)
* * `r` [string] [URL] Referrer URL (hashed, only if NavigationTiming isn't
* * supported and if strict_referrer is enabled)
* * `nu` [string] [URL] Clicked URL (hashed)
* * `z` [number] [flags] Compression flags
*
* @class BOOMR.plugins.RT
*/
// This is the Round Trip Time plugin. Abbreviated to RT
// the parameter is the window
(function(w) {
var d, impl,
COOKIE_EXP = 60 * 60 * 24 * 7;
var SESSION_EXP = 60 * 30;
/**
* Whether or not the cookie has compressed timestamps
*/
var COOKIE_COMPRESSED_TIMESTAMPS = 0x1;
BOOMR = window.BOOMR || {};
BOOMR.plugins = BOOMR.plugins || {};
if (BOOMR.plugins.RT) {
return;
}
// private object
impl = {
// Set when the page_ready event fires.
// Use this to determine if unload fires before onload.
onloadfired: false,
// Set when the first unload event fires.
// Use this to make sure we don't beacon twice for beforeunload and
// unload.
unloadfired: false,
// Set when page becomes visible (for browsers that support it).
// Use this to determine if user bailed without opening the tab.
visiblefired: false,
// Set when init has completed to prevent double initialization.
initialized: false,
// Set when this plugin has completed.
complete: false,
// Whether or not Boomerang is set to run at onload.
autorun: true,
// Custom timers that the developer can use.
// Format for each timer is { start: XXX, end: YYY, delta: YYY-XXX }
timers: {},
// Name of the cookie that stores the start time and referrer.
cookie: "RT",
// Cookie expiry in seconds (7 days)
cookie_exp: COOKIE_EXP,
// Session expiry in seconds (30 minutes)
session_exp: SESSION_EXP,
// By default, don't beacon if referrers don't match.
// If set to false, beacon both referrer values and let the back-end decide.
strict_referrer: true,
// Navigation Type from the NavTiming API. We mainly care if this was
// BACK_FORWARD since cookie time will be incorrect in that case.
navigationType: 0,
// Navigation Start time.
navigationStart: undefined,
// Response Start time.
responseStart: undefined,
// Total load time for the user session.
loadTime: 0,
// Number of pages in the session that had no load time.
oboError: 0,
// t_start that came off the cookie.
t_start: undefined,
// Cached value of t_start once we know its real value.
cached_t_start: undefined,
// Cached value of xhr t_start once we know its real value.
cached_xhr_start: undefined,
// Approximate first byte time for browsers that don't support NavigationTiming.
t_fb_approx: undefined,
// Referrer (hash) from the cookie.
r: undefined,
// Beacon server for the current session.
// This could get reset at the end of the session.
beacon_url: undefined,
// beacon_url to use when session expires.
next_beacon_url: undefined,
// These timers are added directly as beacon variables.
BASIC_TIMERS: {
t_done: 1,
t_resp: 1,
t_page: 1
},
// Whether or not this is a Cross-Domain load and we're sending session
// details.
crossdomain_sending: false,
// navigationStart source: navigation, csi, gtb
navigationStartSource: "",
/**
* Merge new cookie `params` onto current cookie, and set `timer` param on cookie to current timestamp
*
* @param {object} params Object containing keys & values to merge onto current cookie. A value of `undefined`
* will remove the key from the cookie
* @param {string} timer String key name that will be set to the current timestamp on the cookie
*
* @returns {boolean} true if the cookie was updated, false if the cookie could not be set for any reason
*/
updateCookie: function(params, timer) {
var t_end, t_start, subcookies, k;
// Disable use of RT cookie by setting its name to a falsy value
if (!this.cookie) {
return false;
}
// Get the cookie (don't decompress the values)
subcookies = BOOMR.utils.getSubCookies(BOOMR.utils.getCookie(this.cookie)) || {};
// Numeric indices were a bug, we need to clean it up
for (k in subcookies) {
if (subcookies.hasOwnProperty(k)) {
if (!isNaN(parseInt(k, 10))) {
delete subcookies[k];
}
}
}
if (typeof params === "object") {
for (k in params) {
if (params.hasOwnProperty(k)) {
if (params[k] === undefined) {
if (subcookies.hasOwnProperty(k)) {
delete subcookies[k];
}
}
else {
subcookies[k] = params[k];
}
}
}
}
// compresion level
subcookies.z = COOKIE_COMPRESSED_TIMESTAMPS;
// domain
subcookies.dm = BOOMR.session.domain;
// session ID
subcookies.si = BOOMR.session.ID;
// session start
subcookies.ss = BOOMR.session.start.toString(36);
// session length
subcookies.sl = BOOMR.session.length.toString(36);
// session expiry
if (impl.session_exp !== SESSION_EXP) {
subcookies.se = impl.session_exp.toString(36);
}
// rate limited
if (BOOMR.session.rate_limited) {
subcookies.rl = 1;
}
// total time
subcookies.tt = this.loadTime.toString(36);
// off-by-one
if (this.oboError > 0) {
subcookies.obo = this.oboError.toString(36);
}
else {
delete subcookies.obo;
}
t_start = BOOMR.now();
// sub-timer
if (timer) {
subcookies[timer] = (t_start - BOOMR.session.start).toString(36);
impl.lastActionTime = t_start;
}
// If we got a beacon_url from config, set it into the cookie
if (this.beacon_url) {
subcookies.bcn = this.beacon_url;
}
BOOMR.debug("Setting cookie (timer=" + timer + ")\n" + BOOMR.utils.objectToString(subcookies), "rt");
if (!BOOMR.utils.setCookie(this.cookie, subcookies, this.cookie_exp)) {
BOOMR.error("cannot set start cookie", "rt");
return false;
}
t_end = BOOMR.now();
if (t_end - t_start > 50) {
// It took > 50ms to set the cookie
// The user Most likely has cookie prompting turned on so
// t_start won't be the actual unload time
// We bail at this point since we can't reliably tell t_done
BOOMR.utils.removeCookie(this.cookie);
// at some point we may want to log this info on the server side
BOOMR.error("took more than 50ms to set cookie... aborting: " +
t_start + " -> " + t_end, "rt");
}
return true;
},
/**
* Update in memory session with values from the cookie.
*
* For server-driven Boomerang, many of these values might come through
* a configuration file (config.json), but we need them before config.json comes through,
* or in cases where we're rate limited, or the server is down, config.json may never
* come through, so we hold them in a cookie.
*
* @param subcookies [optional] object containing cookie keys & values. If not set, will use current cookie value.
* Recognised keys:
* - ss: sesion start
* - si: session ID
* - sl: session length
* - tt: sum of load times across session
* - obo: pages in session that did not have a load time
* - dm: domain to use when setting cookies
* - se: session expiry time
* - bcn: URL that beacons should be sent to
* - rl: rate limited flag. 1 if rate limited
*/
refreshSession: function(subcookies) {
if (!subcookies) {
subcookies = BOOMR.plugins.RT.getCookie();
}
if (!subcookies) {
return;
}
if (subcookies.ss) {
BOOMR.session.start = subcookies.ss;
}
else {
// If the cookie didn't have a good session start time, we'll use the earliest
// time that we know about... either when the boomerang loader showed up on page
// or when the first bytes of boomerang loaded up.
BOOMR.session.start = BOOMR.plugins.RT.navigationStart() || BOOMR.t_lstart || BOOMR.t_start;
}
if (subcookies.si && subcookies.si.match(/-/)) {
BOOMR.session.ID = subcookies.si;
}
if (subcookies.sl) {
BOOMR.session.length = subcookies.sl;
}
if (subcookies.tt) {
this.loadTime = subcookies.tt;
}
if (subcookies.obo) {
this.oboError = subcookies.obo;
}
if (subcookies.dm && !BOOMR.session.domain) {
BOOMR.session.domain = subcookies.dm;
}
if (subcookies.se) {
impl.session_exp = subcookies.se;
}
if (subcookies.bcn) {
this.beacon_url = subcookies.bcn;
}
if (subcookies.rl && subcookies.rl === "1") {
BOOMR.session.rate_limited = true;
}
},
/**
* Determine if session has expired or not, and if so, reset session values to a new session.
*
* @param t_done The timestamp right now. Used to determine if the session is too old
* @param t_start The timestamp when this page was requested (or undefined if unknown).
* Used to reset session start time
*
*/
maybeResetSession: function(t_done, t_start) {
/* BEGIN_DEBUG */
BOOMR.debug("Current session meta:\n" + BOOMR.utils.objectToString(BOOMR.session), "rt");
BOOMR.debug("Timers: " +
"t_start=" + t_start +
", sessionLoad=" + impl.loadTime +
", sessionError=" + impl.oboError +
", lastAction=" + impl.lastActionTime,
"rt");
/* END_DEBUG */
// determine the average page session length, which is the session length over # of pages
var avgSessionLength = 0;
if (BOOMR.session.start && BOOMR.session.length) {
avgSessionLength = (BOOMR.now() - BOOMR.session.start) / BOOMR.session.length;
}
var sessionExp = impl.session_exp * 1000;
// if session hasn't started yet, or if it's been more than thirty minutes since the last beacon,
// reset the session (note 30 minutes is an industry standard limit on idle time for session expiry)
// no start time
if (!BOOMR.session.start ||
// or we have a better start time
(t_start && BOOMR.session.start > t_start) ||
// or it's been more than session_exp since the last action
t_done - (impl.lastActionTime || BOOMR.t_start) > sessionExp ||
// or the average page session length is longer than the session exp
(avgSessionLength > sessionExp)
) {
// Now we reset the session
BOOMR.session.start = t_start || BOOMR.plugins.RT.navigationStart() || BOOMR.t_lstart || BOOMR.t_start;
BOOMR.session.length = 0;
BOOMR.session.rate_limited = false;
impl.loadTime = 0;
impl.oboError = 0;
impl.beacon_url = impl.next_beacon_url;
impl.lastActionTime = t_done;
// Update the cookie with these new values
// we also reset the rate limited flag since
// new sessions do not inherit the rate limited
// state of old sessions
impl.updateCookie({
"rl": undefined,
"sl": BOOMR.session.length,
"ss": BOOMR.session.start,
"tt": impl.loadTime,
// since it's 0
"obo": undefined,
"bcn": impl.beacon_url
});
}
/* BEGIN_DEBUG */
BOOMR.debug("New session meta:\n" + BOOMR.utils.objectToString(BOOMR.session), "rt");
BOOMR.debug("Timers: " +
" t_start=" + t_start +
", sessionLoad=" + impl.loadTime +
", sessionError=" + impl.oboError,
"rt");
/* END_DEBUG */
},
/**
* Read initial values from cookie and clear out cookie values it cares about after reading.
* This makes sure that other pages (eg: loaded in new tabs) do not get an invalid cookie time.
* This method should only be called from init, and may be called more than once.
*
* Request start time is the greater of last page beforeunload or last click time
* If start time came from a click, we check that the clicked URL matches the current URL
* If it came from a beforeunload, we check that cookie referrer matches document.referrer
*
* If we had a pageHide time or unload time, we use that as a proxy for first byte on non-navtiming
* browsers.
*/
initFromCookie: function() {
var urlHash, docReferrerHash, subcookies;
subcookies = BOOMR.plugins.RT.getCookie();
if (!this.cookie) {
BOOMR.session.enabled = false;
}
if (!subcookies) {
return;
}
subcookies.s = Math.max(+subcookies.ld || 0, Math.max(+subcookies.ul || 0, +subcookies.cl || 0));
BOOMR.debug("Read from cookie " + BOOMR.utils.objectToString(subcookies), "rt");
// If we have a start time, and either a referrer, or a clicked on URL,
// we check if the start time is usable.
if (subcookies.s && (subcookies.r || subcookies.nu)) {
this.r = subcookies.r;
urlHash = BOOMR.utils.hashString(d.URL);
docReferrerHash = BOOMR.utils.hashString((d && d.referrer) || "");
// Either the URL of the page setting the cookie needs to match document.referrer
BOOMR.debug("referrer check: " + this.r + " =?= " + docReferrerHash, "rt");
// Or the start timer was no more than 15ms after a click or form submit
// and the URL clicked or submitted to matches the current page's URL
// (note the start timer may be later than click if both click and beforeunload fired
// on the previous page)
if (subcookies.cl) {
BOOMR.debug(subcookies.s + " <? " + (+subcookies.cl + 15), "rt");
}
if (subcookies.nu) {
BOOMR.debug(subcookies.nu + " =?= " + urlHash, "rt");
}
if (!this.strict_referrer ||
(subcookies.cl && subcookies.nu && subcookies.nu === urlHash && subcookies.s < +subcookies.cl + 15) ||
(subcookies.s === +subcookies.ul && this.r === docReferrerHash)
) {
this.t_start = subcookies.s;
// additionally, if we have a pagehide, or unload event, that's a proxy
// for the first byte of the current page, so use that wisely
if (+subcookies.hd > subcookies.s) {
this.t_fb_approx = subcookies.hd;
}
}
else {
this.t_start = this.t_fb_approx = undefined;
}
}
// regardless of whether the start time was usable or not, it's the last action that
// we measured, so use that for the session
if (subcookies.s) {
this.lastActionTime = subcookies.s;
}
this.refreshSession(subcookies);
// Now that we've pulled out the timers, we'll clear them so they don't pollute future calls
this.updateCookie({
//
// timers
//
// start timer
s: undefined,
// onbeforeunload time
ul: undefined,
// onclick time
cl: undefined,
// onunload or onpagehide time
hd: undefined,
// last load time
ld: undefined,
//
// session info
//
// rate limited
rl: undefined,
//
// URLs
//
// referrer
r: undefined,
// clicked url
nu: undefined,
//
// deprecated
//
// session history
sh: undefined
});
this.maybeResetSession(BOOMR.now());
},
/**
* Increment session length, and either session.obo or session.loadTime whichever is appropriate for this page
*/
incrementSessionDetails: function() {
BOOMR.debug("Incrementing Session Details... ", "RT");
BOOMR.session.length++;
if (!impl.timers.t_done || isNaN(impl.timers.t_done.delta)) {
impl.oboError++;
}
else {
impl.loadTime += impl.timers.t_done.delta;
}
},
/**
* Figure out how long boomerang and other URLs took to load using
* ResourceTiming if available, or built in timestamps.
*/
getBoomerangTimings: function() {
var res, urls, url, startTime, data;
function trimTiming(time, st) {
// strip from microseconds to milliseconds only
var timeMs = Math.round(time ? time : 0),
startTimeMs = Math.round(st ? st : 0);
timeMs = (timeMs === 0 ? 0 : (timeMs - startTimeMs));
return timeMs ? timeMs : "";
}
if (BOOMR.t_start) {
// How long does it take Boomerang to load up and execute (fb to lb)?
BOOMR.plugins.RT.startTimer("boomerang", BOOMR.t_start);
// t_end === null defaults to current time
BOOMR.plugins.RT.endTimer("boomerang", BOOMR.t_end);
// How long did it take from page request to boomerang fb?
BOOMR.plugins.RT.endTimer("boomr_fb", BOOMR.t_start);
if (BOOMR.t_lstart) {
// when did the boomerang loader start loading boomerang on the page?
BOOMR.plugins.RT.endTimer("boomr_ld", BOOMR.t_lstart);
// What was the network latency for boomerang (request to first byte)?
BOOMR.plugins.RT.setTimer("boomr_lat", BOOMR.t_start - BOOMR.t_lstart);
}
}
// use window and not w because we want the inner iframe
try {
if (window &&
"performance" in window &&
window.performance &&
typeof window.performance.getEntriesByName === "function") {
urls = { "rt.bmr": BOOMR.url };
if (BOOMR.config_url) {
urls["rt.cnf"] = BOOMR.config_url;
}
for (url in urls) {
if (urls.hasOwnProperty(url) && urls[url]) {
res = window.performance.getEntriesByName(urls[url]);
if (!res || res.length === 0 || !res[0]) {
continue;
}
res = res[0];
startTime = trimTiming(res.startTime, 0);
data = [
startTime,
trimTiming(res.responseEnd, startTime),
trimTiming(res.responseStart, startTime),
trimTiming(res.requestStart, startTime),
trimTiming(res.connectEnd, startTime),
trimTiming(res.secureConnectionStart, startTime),
trimTiming(res.connectStart, startTime),
trimTiming(res.domainLookupEnd, startTime),
trimTiming(res.domainLookupStart, startTime),
trimTiming(res.redirectEnd, startTime),
trimTiming(res.redirectStart, startTime)
].join(",").replace(/,+$/, "");
BOOMR.addVar(url, data, true);
}
}
}
}
catch (e) {
/**
* We wrap specific Firefox 31 and 32 when we get Error when inspecting window.performance
* Details: https://bugzilla.mozilla.org/show_bug.cgi?id=1045096
*/
if (e && e.name && e.name.hasOwnProperty("length") && e.name.indexOf("NS_ERROR_FAILURE") === -1) {
BOOMR.addError(e, "rt.getBoomerangTimings");
}
}
},
/**
* Check if we're in a legacy prerender state, and if we are, set additional timers.
*
* In old versions of Chrome that support <link rel="prerender">, a prerender state is when a
* page is completely rendered in an in-memory buffer, before a user requests that page. We do
* not beacon at this point because the user has not shown intent to view the page. If the
* user opens the page, the visibility state changes to visible, and we fire the beacon at that
* point, including any timing details for prerendering.
*
* Sets the `t_load` timer to the actual value of page load time (request initiated by browser to onload)
*
* @returns true if this is a legacy prerender state, false if not (or not supported)
*/
checkLegacyPrerender: function() {
if (BOOMR.visibilityState() === "prerender") {
//
// Older versions of Chrome via <link rel="prerender">
//
// This means that onload fired through a pre-render. We'll capture this
// time, but wait for t_done until after the page has become either visible
// or hidden (ie, it moved out of the pre-render state)
// this will measure actual onload time for a prerendered page
BOOMR.plugins.RT.startTimer("t_load", this.navigationStart);
BOOMR.plugins.RT.endTimer("t_load");
// time from navigation start to visible state
BOOMR.plugins.RT.startTimer("t_prerender", this.navigationStart);
// time from prerender to visible or hidden
BOOMR.plugins.RT.startTimer("t_postrender");
return true;
}
return false;
},
/**
* Check if we're in a modern prerender state, and if we are, adjust and set some timers.
*
* In recent versions of Chrome that support Prerendering via the Speculation Rules API,
* there is now an activationStart timestamp available in NavigationTiming that tells us when in
* the Page Load process did the user activate the page (before or after onload, for example).
*
* Sets the `t_load` timer to the actual value of page load time (request initiated by browser to onload)
*
* @returns true if this is a modern prerender state, false if not (or not supported)
*/
checkModernPrerender: function() {
var actSt = BOOMR.getActivationStart();
if (actSt === false) {
return;
}
//
// Newer versions of Chrome with Speculation Rules API, had Prerendered
//
var actStEpoch = actSt + impl.cached_t_start;
if (actStEpoch > impl.timers.t_done.end) {
//
// Activation after Page Load is complete
//
// Page Load time was 1ms
impl.timers.t_done = impl.timers.t_done || {};
impl.timers.t_done.delta = 1;
// Front-End Time was 1ms
impl.timers.t_page = impl.timers.t_page || {};
impl.timers.t_page.delta = 1;
// Back-End Time was 0
impl.timers.t_resp = impl.timers.t_resp || {};
impl.timers.t_resp.delta = 0;
}
else {
//
// Activation before Page Load was complete
//
// Page Load needs to be offset by the difference
// loadEventEnd - navigationStart - activationStart
impl.timers.t_done = impl.timers.t_done || {};
impl.timers.t_done.delta = (impl.timers.t_done.end - impl.cached_t_start - actSt);
if (actStEpoch > impl.timers.t_resp.end) {
// Activation after the Back-End was done
// Front-End Time was same as total Page Load Time
impl.timers.t_page = impl.timers.t_page || {};
impl.timers.t_page.delta = impl.timers.t_done.delta;
// Back-End Time was 0
impl.timers.t_resp = impl.timers.t_resp || {};
impl.timers.t_resp.delta = 0;
}
else {
// Activation before Back-End was done
// Front-End Time stays the same
// Back-End Time was offset by Act St
// reponseStart - navigationStart - activationStart
impl.timers.t_resp = impl.timers.t_resp || {};
impl.timers.t_resp.delta = (impl.timers.t_resp.end - impl.cached_t_start - actSt);
}
}
},
/**
* Initialize timers from the NavigationTiming API. This method looks at various sources for
* Navigation Timing, and also patches around bugs in various browser implementations.
*
* It sets the beacon parameter `rt.start` to the source of the timer.
*/
initFromNavTiming: function() {
var ti, p;
if (this.navigationStart) {
return;
}
// Get start time from WebTiming API see:
// https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/NavigationTiming/Overview.html
// http://blogs.msdn.com/b/ie/archive/2010/06/28/measuring-web-page-performance.aspx
// http://blog.chromium.org/2010/07/do-you-know-how-slow-your-web-page-is.html
p = BOOMR.getPerformance();
if (p && p.navigation) {
this.navigationType = p.navigation.type;
}
if (p && p.timing) {
ti = p.timing;
this.navigationStartSource = "navigation";
}
else if (w.chrome && w.chrome.csi && w.chrome.csi().startE) {
// Older versions of chrome also have a timing API that's sort of documented here:
// http://ecmanaut.blogspot.com/2010/06/google-bom-feature-ms-since-pageload.html
// source here:
// http://src.chromium.org/viewvc/chrome/trunk/src/chrome/renderer/loadtimes_extension_bindings.cc?view=markup
ti = {
navigationStart: w.chrome.csi().startE
};
this.navigationStartSource = "csi";
}
else if (w.gtbExternal && w.gtbExternal.startE()) {
// The Google Toolbar exposes navigation start time similar to old versions of chrome
// This would work for any browser that has the google toolbar installed
ti = {
navigationStart: w.gtbExternal.startE()
};
this.navigationStartSource = "gtb";
}
if (ti) {
// Always use navigationStart since it falls back to fetchStart (not with redirects)
// If not set, we leave t_start alone so that timers that depend
// on it don't get sent back. Never use requestStart since if
// the first request fails and the browser retries, it will contain
// the value for the new request.
this.navigationStart = ti.navigationStart || ti.fetchStart || undefined;
this.fetchStart = ti.fetchStart || undefined;
this.responseStart = ti.responseStart || undefined;
// bug in Firefox 7 & 8 https://bugzilla.mozilla.org/show_bug.cgi?id=691547
if (!navigator.userAgentData && navigator.userAgent.match(/Firefox\/[78]\./)) {
this.navigationStart = ti.unloadEventStart || ti.fetchStart || undefined;
}
}
else {
BOOMR.warn("This browser doesn't support the WebTiming API", "rt");
}
return;
},
/**
* Validate that the time we think is the load time is correct. This can be wrong if boomerang was loaded
* after onload, so in that case, if navigation timing is available, we use that instead.
*/
validateLoadTimestamp: function(t_now, data, ename) {
var p;
// beacon with detailed timing information
if (data && data.timing && data.timing.loadEventEnd) {
return data.timing.loadEventEnd;
}
else if (ename === "xhr" && (!data || !BOOMR.utils.inArray(data.initiator, BOOMR.constants.BEACON_TYPE_SPAS))) {
// if this is an XHR event, trust the input end "now" timestamp
return t_now;
}
else {
// use loadEventEnd from NavigationTiming
p = BOOMR.getPerformance();
// We have navigation timing,
if (p && p.timing) {
// and the loadEventEnd timestamp
if (p.timing.loadEventEnd) {
return p.timing.loadEventEnd;
}
}
// We don't have navigation timing,
else {
// So we'll just use the time when boomerang was added to the page
// Assuming that this means boomerang was added in onload. If we logged the
// onload timestamp (via loader snippet), use that first.
return BOOMR.t_onload || BOOMR.t_lstart || BOOMR.t_start || t_now;
}
}
// default to now
return t_now;
},
/**
* Set timers appropriate at page load time. This method should be called from done() only when
* the page_ready event fires. It sets the following timer values:
* - t_resp: time from request start to first byte
* - t_page: time from first byte to load
* - t_postrender: time from prerender state to visible state
* - t_prerender: time from navigation start to visible state
*
* @param {string} ename The Event name that initiated this control flow
* @param {number} t_done The timestamp when the done() method was called
* @param {object} data Event data passed in from the caller. For xhr beacons, this may contain
* detailed timing information
*
* @returns true if timers were set, false if we're in a prerender state, caller should abort on false.
*/
setPageLoadTimers: function(ename, t_done, data) {
var t_resp_start, t_fetch_start;
if (ename !== "xhr" &&
!(ename === "early" &&
data &&
BOOMR.utils.inArray(data.initiator, BOOMR.constants.BEACON_TYPE_SPAS))) {
impl.initFromCookie();
impl.initFromNavTiming();
// add rt.start for the source
BOOMR.addVar("rt.start", this.navigationStartSource);
if (impl.checkLegacyPrerender()) {
return false;
}
}
if (ename === "xhr") {
if (data.timers) {
// If we were given a list of timers, set those first
for (var timerName in data.timers) {
if (data.timers.hasOwnProperty(timerName)) {
BOOMR.plugins.RT.setTimer(timerName, data.timers[timerName]);
}
}
}
else if (data && data.timing) {
// Use details from XHR object to figure out responce latency and page time. Use
// responseEnd (instead of responseStart) since it's not until responseEnd
// that the browser can consume the data, and responseEnd is the only guarateed
// timestamp with cross-origin XHRs if ResourceTiming is enabled.
t_fetch_start = data.timing.fetchStart;
if (typeof t_fetch_start === "undefined" || data.timing.responseEnd >= t_fetch_start) {
t_resp_start = data.timing.responseEnd;
}
}
}
else if (impl.responseStart) {
// Use NavTiming API to figure out resp latency and page time
// t_resp will use the cookie if available or fallback to NavTiming
// only use if the time looks legit (after navigationStart/fetchStart)
if (impl.responseStart >= impl.navigationStart &&
impl.responseStart >= impl.fetchStart) {
t_resp_start = impl.responseStart;
}
}
else if (impl.timers.hasOwnProperty("t_page")) {
// If the dev has already started t_page timer, we can end it now as well
BOOMR.plugins.RT.endTimer("t_page");
}
else if (impl.t_fb_approx) {
// If we have an approximate first byte time from the cookie, use it
t_resp_start = impl.t_fb_approx;
}
// early beacons should not have t_resp and t_page since the load hasn't occurred yet
if (t_resp_start && ename !== "early") {
// if we have a fetch start as well, set the specific timestamps instead of from rt.start
if (t_fetch_start) {
BOOMR.plugins.RT.setTimer("t_resp", t_fetch_start, t_resp_start);
}
else {
BOOMR.plugins.RT.endTimer("t_resp", t_resp_start);
}
// t_load is the actual time load completed if using prerender
if (ename === "load" && impl.timers.t_load) {
BOOMR.plugins.RT.setTimer("t_page", impl.timers.t_load.end - t_resp_start);
}
else {
//
// Ensure that t_done is after t_resp_start. If not, set a var so we
// knew there was an inversion. This can happen due to bugs in NavTiming
// clients, where responseEnd happens after all other NavTiming events.
//
if (t_done < t_resp_start) {
BOOMR.addVar("t_page.inv", 1, true);
}
else if ((t_done - t_resp_start === 0) &&
(data && data.timing && data.timing.requestStart) &&
(impl.timers.t_resp && impl.timers.t_resp.delta)) {
// if t_page would be zero, try calculating t_page based on the inversion of t_resp
// this could happen for XHRs that were started by a click/DOM
BOOMR.plugins.RT.setTimer(
"t_page",
(t_done - data.timing.requestStart) - impl.timers.t_resp.delta);
}
else {
BOOMR.plugins.RT.setTimer("t_page", t_done - t_resp_start);
}
}
}
// If a prerender timer was started, we can end it now as well
if (ename === "load" && impl.timers.hasOwnProperty("t_postrender")) {
BOOMR.plugins.RT.endTimer("t_postrender");
BOOMR.plugins.RT.endTimer("t_prerender");
}
return true;
},
/**
* Writes a bunch of timestamps onto the beacon that help in request tracing on the server
* - rt.tstart: The value of t_start that we determined was appropriate
* - rt.nstart: The value of navigationStart if different from rt.tstart
* - rt.cstart: The value of t_start from the cookie if different from rt.tstart
* - rt.bstart: The timestamp when boomerang started
* - rt.blstart:The timestamp when boomerang was added to the host page
* - rt.end: The timestamp when the t_done timer ended
*
* @param t_start The value of t_start that we plan to use
* @param ename The event name that resulted in this call
*/
setSupportingTimestamps: function(t_start, ename) {
if (t_start) {
BOOMR.addVar("rt.tstart", t_start, true);
}
if (typeof impl.navigationStart === "number" && impl.navigationStart !== t_start) {
BOOMR.addVar("rt.nstart", impl.navigationStart, true);
}
if (typeof impl.t_start === "number" && impl.t_start !== t_start) {
BOOMR.addVar("rt.cstart", impl.t_start, true);
}
BOOMR.addVar("rt.bstart", BOOMR.t_start, true);
if (BOOMR.t_lstart) {
BOOMR.addVar("rt.blstart", BOOMR.t_lstart, true);
}
// early beacons don't have t_done, send t_start (or now) as rt.end
if (ename === "early") {
BOOMR.addVar("rt.end", t_start ? t_start : BOOMR.now(), true);
}
else {
if (impl.timers.t_done) {
// don't just use t_done because dev may have called endTimer before we did
BOOMR.addVar("rt.end", impl.timers.t_done.end, true);
}
}
},
/**
* Determines the best value to use for t_start.
* If called from an xhr call, then use the start time for that call
* Else, If we have navigation timing, use that
* Else, If we have a cookie time, and this isn't the result of a BACK button, use the cookie time
* Else, if we have a cached timestamp from an earlier call, use that
* Else, give up
*
* @param ename The event name that resulted in this call. Special consideration for "xhr"
* @param data Data passed in from the event caller. If the event name is "xhr",
* this should contain the page group name for the xhr call in an attribute called `name`
* and optionally, detailed timing information in a sub-object called `timing`
* and resource information in a sub-object called `resource`
*
* @returns the determined value of t_start or undefined if unknown
*/
determineTStart: function(ename, data) {
var t_start;
if (ename === "xhr" || (ename === "early" && data && data.initiator === "spa")) {
if (data && data.name && impl.timers[data.name]) {
// For xhr timers, t_start is stored in impl.timers.xhr_{page group name}
// and xhr.pg is set to {page group name}
t_start = impl.timers[data.name].start;
}
else if (data && data.timing && data.timing.requestStart) {
// For automatically instrumented xhr timers, we have detailed timing information
t_start = data.timing.requestStart;
}
if (typeof t_start === "undefined" &&
data &&
BOOMR.utils.inArray(data.initiator, BOOMR.constants.BEACON_TYPE_SPAS)) {
// if we don't have a start time, set to none so it can possibly be fixed up
BOOMR.addVar("rt.start", "none");
}
else {
BOOMR.addVar("rt.start", "manual");
}
impl.cached_xhr_start = t_start;
}
else {
if (impl.navigationStart) {
t_start = impl.navigationStart;
}
else if (impl.t_start && impl.navigationType !== 2) {
// 2 is TYPE_BACK_FORWARD but the constant may not be defined across browsers
t_start = impl.t_start;
// if the user hit the back button, referrer will match, and cookie will match
BOOMR.addVar("rt.start", "cookie");
}
else if (impl.cached_t_start) {
// but will have time of previous page start, so t_done will be wrong
t_start = impl.cached_t_start;
}
else {
// force all timers to NaN state
BOOMR.addVar("rt.start", "none");
t_start = undefined;
}
impl.cached_t_start = t_start;
}
BOOMR.debug("Got start time: " + t_start, "rt");
return t_start;
},
page_ready: function() {
// we need onloadfired because it's possible to reset "impl.complete"
// if you're measuring multiple xhr loads, but not possible to reset
// impl.onloadfired
this.onloadfired = true;
},
check_visibility: function() {
// we care if the page became visible at some point
if (BOOMR.visibilityState() === "visible") {
impl.visiblefired = true;
}
},
prerenderToVisible: function() {
if (impl.onloadfired &&
impl.autorun) {
BOOMR.debug("Transitioned from prerender to " + BOOMR.visibilityState(), "rt");
// note that we transitioned from prerender on the beacon for debugging
BOOMR.addVar("vis.pre", "1", true);
// send a beacon
BOOMR.plugins.RT.done(null, "visible");
}
},
page_unload: function(edata) {
BOOMR.debug("Unload called when unloadfired = " + this.unloadfired, "rt");
if (!this.unloadfired) {
// run done on abort or on page_unload to measure session length
BOOMR.plugins.RT.done(edata, "unload");
}
//
// Set cookie with r (the referrer) of this page, but only if the
// browser doesn't support NavigationTiming. The referrer is used
// in non-NT browsers to decide if the "ul" or "hd" timestamps can
// be used as the start of the navigation. Don't set if strict_referrer
// is disabled either.
//
// We use document.URL instead of location.href because of a bug in safari 4
// where location.href is URL decoded
//
this.updateCookie(
(!impl.navigationStart && impl.strict_referrer) ? {
"r": BOOMR.utils.hashString(d.URL)
} : null,
edata.type === "beforeunload" ? "ul" : "hd"
);
this.unloadfired = true;
},
_iterable_click: function(name, element, etarget, value_cb) {
var value;
if (!etarget) {
return;
}
BOOMR.debug(name + " called with " + etarget.nodeName, "rt");
while (etarget && etarget.nodeName && etarget.nodeName.toUpperCase() !== element) {
etarget = etarget.parentNode;
}
if (etarget && etarget.nodeName && etarget.nodeName.toUpperCase() === element) {
BOOMR.debug("passing through", "rt");
// we might need to reset the session first, as updateCookie()
// below sets the lastActionTime
this.refreshSession();
this.maybeResetSession(BOOMR.now());
// user event, they may be going to another page
// if this page is being opened in a different tab, then
// our unload handler won't fire, so we need to set our
// cookie on click or submit
value = value_cb(etarget);
this.updateCookie(
{
"nu": BOOMR.utils.hashString(value)
},
"cl");
BOOMR.addVar("nu", BOOMR.utils.cleanupURL(value), true);
}
},
onclick: function(etarget) {
impl._iterable_click("Click", "A", etarget, function(t) {
return t.href;
});
},
markComplete: function() {
if (this.onloadfired) {
// allow error beacons to send outside of page load without adding
// RT variables to the beacon
impl.complete = true;
}
},
onsubmit: function(etarget) {
impl._iterable_click("Submit", "FORM", etarget, function(t) {
var v = (typeof t.getAttribute === "function" && t.getAttribute("action")) || d.URL || "";
return v.match(/\?/) ? v : v + "?";
});
},
onconfig: function(config) {
if (config.beacon_url) {
impl.beacon_url = config.beacon_url;
}
if (config.RT) {
if (config.RT.oboError && !isNaN(config.RT.oboError) && config.RT.oboError > impl.oboError) {
impl.oboError = config.RT.oboError;
}
if (config.RT.loadTime && !isNaN(config.RT.loadTime) && config.RT.loadTime > impl.loadTime) {
impl.loadTime = config.RT.loadTime;
if (impl.timers.t_done && !isNaN(impl.timers.t_done.delta)) {
impl.loadTime += impl.timers.t_done.delta;
}
}
}
},
domloaded: function() {
if (BOOMR.plugins.RT) {
BOOMR.plugins.RT.endTimer("t_domloaded");
}
},
clear: function(edata) {
// if it's an early beacon we want to keep rt.start for the next beacon
if (!edata || typeof edata.early === "undefined") {
BOOMR.removeVar("rt.start");
}
},
spaNavigation: function() {
// a SPA navigation occured, force onloadfired to true
impl.onloadfired = true;
}
};
BOOMR.plugins.RT = {
/**
* Initializes the plugin.
*
* @param {object} config Configuration
* @param {string} [config.RT.cookie] The name of the cookie in which to store
* the start time for measuring page load time.
*
* The default name is `RT`.
*
* Set this to a falsy value to ignore cookies and depend completely on
* the NavigationTiming API for the start time.
* @param {string} [config.RT.cookie_exp] The lifetime in seconds of the roundtrip cookie.
*
* This only needs to live for as long as it takes for a single page to load.
*
* Something like 10 seconds or so should be good for most cases, but to be safe,
* and to cover people with really slow connections, or users that are geographically
* far away from you, keep it to a few minutes.
*
* The default is set to 10 minutes.
* @param {string} [config.RT.strict_referrer] By default, boomerang will not measure a
* page's roundtrip time if the URL in the RT cookie doesn't match the
* current page's `document.referrer`.
*
* This is because it generally means that the user visited a third page
* while their RT cookie was still valid, and this could render the page
* load time invalid.
*
* There may be cases, though, when this is a valid flow - for example,
* you have an SSL page in between and the referrer isn't passed through.
*
* In this case, you'll want to set `strict_referrer` to `false`.
*