UNPKG

boomerangjs

Version:

boomerang always comes back, except when it hits something

1,373 lines (1,159 loc) 67.8 kB
/** * 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`. *