UNPKG

boomerangjs

Version:

boomerang always comes back, except when it hits something

1,694 lines (1,469 loc) 163 kB
/** * @copyright (c) 2011, Yahoo! Inc. All rights reserved. * @copyright (c) 2012, Log-Normal, Inc. All rights reserved. * @copyright (c) 2012-2017, SOASTA, Inc. All rights reserved. * @copyright (c) 2017-2023, Akamai Technologies, Inc. All rights reserved. * Copyrights licensed under the BSD License. See the accompanying LICENSE.txt file for terms. */ /** * @class BOOMR * @desc * boomerang measures various performance characteristics of your user's browsing * experience and beacons it back to your server. * * To use this you'll need a web site, lots of users and the ability to do * something with the data you collect. How you collect the data is up to * you, but we have a few ideas. * * Everything in boomerang is accessed through the `BOOMR` object, which is * available on `window.BOOMR`. It contains the public API, utility functions * ({@link BOOMR.utils}) and all of the plugins ({@link BOOMR.plugins}). * * Each plugin has its own API, but is reachable through {@link BOOMR.plugins}. * * ## Beacon Parameters * * The core boomerang object will add the following parameters to the beacon. * * Note that each individual {@link BOOMR.plugins plugin} will add its own * parameters as well. * * * `v`: Boomerang version * * `sv`: Boomerang Loader Snippet version * * `sm`: Boomerang Loader Snippet method * * `u`: The page's URL (for most beacons), or the `XMLHttpRequest` URL * * `n`: The beacon number * * `pgu`: The page's URL (for `XMLHttpRequest` beacons) * * `pid`: Page ID (8 characters) * * `r`: Navigation referrer (from `document.location`) * * `vis.pre`: `1` if the page transitioned from prerender to visible * * `vis.st`: Document's visibility state when beacon was sent * * `vis.lh`: Timestamp when page was last hidden * * `vis.lv`: Timestamp when page was last visible * * `xhr.pg`: The `XMLHttpRequest` page group * * `errors`: Error messages of errors detected in Boomerang code, separated by a newline * * `rt.si`: Session ID * * `rt.ss`: Session start timestamp * * `rt.sl`: Session length (number of pages), can be increased by XHR beacons as well * * `ua.plt`: `navigator.platform` or if available `navigator.userAgentData.platform` * * `ua.arch`: navigator userAgentData architecture, if client hints requested * * `ua.model`: navigator userAgentData model, if client hints requested * * `ua.pltv`: navigator userAgentData platform version, if client hints requested * * `ua.vnd`: `navigator.vendor` */ /** * @typedef TimeStamp * @type {number} * * @desc * A [Unix Epoch](https://en.wikipedia.org/wiki/Unix_time) timestamp (milliseconds * since 1970) created by [BOOMR.now()]{@link BOOMR.now}. * * If `DOMHighResTimeStamp` (`performance.now()`) is supported, it is * a `DOMHighResTimeStamp` (with microsecond resolution in the fractional), * otherwise, it is `Date.now()`. */ /* BEGIN_DEBUG */ // we don't yet have BOOMR.utils.mark() if ("performance" in window && window.performance && typeof window.performance.mark === "function" && !window.BOOMR_no_mark) { window.performance.mark("boomr:startup"); } /* END_DEBUG */ /** * @global * @type {TimeStamp} * @desc * This variable is added to the global scope (`window`) until Boomerang loads, * at which point it is removed. * * Timestamp the boomerang.js script started executing. * * This has to be global so that we don't wait for this entire * script to download and execute before measuring the * time. We also declare it without `var` so that we can later * `delete` it. This is the only way that works on Internet Explorer. */ BOOMR_start = new Date().getTime(); /** * @function * @global * @desc * This function is added to the global scope (`window`). * * Check the value of `document.domain` and fix it if incorrect. * * This function is run at the top of boomerang, and then whenever * {@link BOOMR.init} is called. If boomerang is running within an IFRAME, this * function checks to see if it can access elements in the parent * IFRAME. If not, it will fudge around with `document.domain` until * it finds a value that works. * * This allows site owners to change the value of `document.domain` at * any point within their page's load process, and we will adapt to * it. * * @param {string} domain Domain name as retrieved from page URL */ function BOOMR_check_doc_domain(domain) { /* eslint no-unused-vars:0 */ var test; /* BEGIN_DEBUG */ // we don't yet have BOOMR.utils.mark() if ("performance" in window && window.performance && typeof window.performance.mark === "function" && !window.BOOMR_no_mark) { window.performance.mark("boomr:check_doc_domain"); } /* END_DEBUG */ if (!window) { return; } // If domain is not passed in, then this is a global call // domain is only passed in if we call ourselves, so we // skip the frame check at that point if (!domain) { // If we're running in the main window, then we don't need this if (window.parent === window || !document.getElementById("boomr-if-as")) { // nothing to do return; } if (window.BOOMR && BOOMR.boomerang_frame && BOOMR.window) { try { // If document.domain is changed during page load (from www.blah.com to blah.com, for example), // BOOMR.window.location.href throws "Permission Denied" in IE. // Resetting the inner domain to match the outer makes location accessible once again if (BOOMR.boomerang_frame.document.domain !== BOOMR.window.document.domain) { BOOMR.boomerang_frame.document.domain = BOOMR.window.document.domain; } } catch (err) { if (!BOOMR.isCrossOriginError(err)) { BOOMR.addError(err, "BOOMR_check_doc_domain.domainFix"); } } } domain = document.domain; } if (!domain || domain.indexOf(".") === -1) { // not okay, but we did our best return; } // window.parent might be null if we're running during unload from // a detached iframe if (!window.parent) { return; } // 1. Test without setting document.domain try { test = window.parent.document; // all okay return; } // 2. Test with document.domain catch (err) { try { document.domain = domain; } catch (err2) { // An exception might be thrown if the document is unloaded // or when the domain is incorrect. If so, we can't do anything // more, so bail. return; } } try { test = window.parent.document; // all okay return; } // 3. Strip off leading part and try again catch (err) { domain = domain.replace(/^[\w\-]+\./, ""); } BOOMR_check_doc_domain(domain); } BOOMR_check_doc_domain(); // Construct BOOMR // w is window (function(w) { var impl, boomr, d, createCustomEvent, dispatchEvent, visibilityState, visibilityChange, orig_w = w; // If the window that boomerang is running in is not top level (ie, we're running in an iframe) // and if this iframe contains a script node with an id of "boomr-if-as", // Then that indicates that we are using the iframe loader, so the page we're trying to measure // is w.parent // // Note that we use `document` rather than `w.document` because we're specifically interested in // the document of the currently executing context rather than a passed in proxy. // // The only other place we do this is in `BOOMR.utils.getMyURL` below, for the same reason, we // need the full URL of the currently executing (boomerang) script. if (w.parent !== w && document.getElementById("boomr-if-as") && document.getElementById("boomr-if-as").nodeName.toLowerCase() === "script") { w = w.parent; } d = w.document; // Short namespace because I don't want to keep typing BOOMERANG if (!w.BOOMR) { w.BOOMR = {}; } BOOMR = w.BOOMR; // don't allow this code to be included twice if (BOOMR.version) { return; } /** * Boomerang version, formatted as major.minor.patchlevel. * * This variable is replaced during build (`grunt build`). * * @type {string} * * @memberof BOOMR */ BOOMR.version = "%boomerang_version%"; /** * The main document window. * * If Boomerang was loaded in an IFRAME, this is the parent window * * If Boomerang was loaded inline, this is the current window * * @type {Window} * * @memberof BOOMR */ BOOMR.window = w; /** * The Boomerang frame: * * If Boomerang was loaded in an IFRAME, this is the IFRAME * * If Boomerang was loaded inline, this is the current window * * @type {Window} * * @memberof BOOMR */ BOOMR.boomerang_frame = orig_w; /** * @class BOOMR.plugins * @desc * Boomerang plugin namespace. * * All plugins should add their plugin object to `BOOMR.plugins`. * * A plugin should have, at minimum, the following exported functions: * * `init(config)` * * `is_complete()` * * See {@tutorial creating-plugins} for details. */ if (!BOOMR.plugins) { BOOMR.plugins = {}; } // CustomEvent proxy for IE9 & 10 from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent (function() { try { if (new w.CustomEvent("CustomEvent") !== undefined) { createCustomEvent = function(e_name, params) { return new w.CustomEvent(e_name, params); }; } } catch (ignore) { // empty } try { if (!createCustomEvent && d.createEvent && d.createEvent("CustomEvent")) { createCustomEvent = function(e_name, params) { var evt = d.createEvent("CustomEvent"); params = params || { cancelable: false, bubbles: false }; evt.initCustomEvent(e_name, params.bubbles, params.cancelable, params.detail); return evt; }; } } catch (ignore) { // empty } if (!createCustomEvent && d.createEventObject) { createCustomEvent = function(e_name, params) { var evt = d.createEventObject(); evt.type = evt.propertyName = e_name; evt.detail = params.detail; return evt; }; } if (!createCustomEvent) { createCustomEvent = function() { return undefined; }; } }()); /** * Dispatch a custom event to the browser * @param {string} e_name The custom event name that consumers can subscribe to * @param {object} e_data Any data passed to subscribers of the custom event via the `event.detail` property * @param {boolean} async By default, custom events are dispatched immediately. * Set to true if the event should be dispatched once the browser has finished its current * JavaScript execution. */ dispatchEvent = function(e_name, e_data, async) { var ev = createCustomEvent(e_name, {"detail": e_data}); if (!ev) { return; } function dispatch() { try { if (d.dispatchEvent) { d.dispatchEvent(ev); } else if (d.fireEvent) { d.fireEvent("onpropertychange", ev); } } catch (e) { BOOMR.debug("Error when dispatching " + e_name); } } if (async) { BOOMR.setImmediate(dispatch); } else { dispatch(); } }; // visibilitychange is useful to detect if the page loaded through prerender // or if the page never became visible // https://www.w3.org/TR/2011/WD-page-visibility-20110602/ // https://www.nczonline.net/blog/2011/08/09/introduction-to-the-page-visibility-api/ // https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API // Set the name of the hidden property and the change event for visibility if (typeof d.hidden !== "undefined") { visibilityState = "visibilityState"; visibilityChange = "visibilitychange"; } else if (typeof d.mozHidden !== "undefined") { visibilityState = "mozVisibilityState"; visibilityChange = "mozvisibilitychange"; } else if (typeof d.msHidden !== "undefined") { visibilityState = "msVisibilityState"; visibilityChange = "msvisibilitychange"; } else if (typeof d.webkitHidden !== "undefined") { visibilityState = "webkitVisibilityState"; visibilityChange = "webkitvisibilitychange"; } // // Internal implementation (impl) // // impl is a private object not reachable from outside the BOOMR object. // Users can set properties by passing in to the init() method. // impl = { // // Private Members // // Beacon URL beacon_url: "", // Forces protocol-relative URLs to HTTPS beacon_url_force_https: true, // List of string regular expressions that must match the beacon_url. If // not set, or the list is empty, all beacon URLs are allowed. beacon_urls_allowed: [], // Beacon request method, either GET, POST or AUTO. AUTO will check the // request size then use GET if the request URL is less than MAX_GET_LENGTH // chars. Otherwise, it will fall back to a POST request. beacon_type: "AUTO", // Beacon authorization key value. Most systems will use the 'Authentication' // keyword, but some some services use keys like 'X-Auth-Token' or other // custom keys. beacon_auth_key: "Authorization", // Beacon authorization token. This is only needed if your are using a POST // and the beacon requires an Authorization token to accept your data. This // disables use of the browser sendBeacon() API. beacon_auth_token: undefined, // Sends beacons with Credentials (applies to XHR beacons, not IMG or `sendBeacon()`). // If you need this, you may want to enable `beacon_disable_sendbeacon` as // `sendBeacon()` does not support credentials. beacon_with_credentials: false, // Disables navigator.sendBeacon() support beacon_disable_sendbeacon: false, // Strip out everything except last two parts of hostname. // This doesn't work well for domains that end with a country tld, // but we allow the developer to override site_domain for that. // You can disable all cookies by setting site_domain to a falsy value. site_domain: w.location.hostname. replace(/.*?([^.]+\.[^.]+)\.?$/, "$1"). toLowerCase(), // User's IP address determined on the server. Used for the BW cookie. user_ip: "", // Whether or not to send beacons on page load autorun: true, // Whether or not we've sent a page load beacon hasSentPageLoadBeacon: false, // document.referrer r: undefined, // Whether or not to strip the Query String strip_query_string: false, // Whether or not the page's 'onload' event has fired onloadFired: false, // Whether or not we've attached all of the page event handlers we want on startup handlers_attached: false, // Whether or not we're waiting for configuration to initialize waiting_for_config: false, // All Boomerang cookies will be created with SameSite=Lax by default same_site_cookie: "Lax", // All Boomerang cookies will be without Secure attribute by default secure_cookie: false, // Sometimes we would like to be able to set the SameSite=None from a Boomerang plugin forced_same_site_cookie_none: false, // Navigator User Agent data object holding Architecture, Model and Platform information from Client Hints API userAgentData: undefined, // Client Hints use for Architecture, Model and Platform detail is disabled by default request_client_hints: false, // Disables all Unload handlers and Unload beacons no_unload: false, // Number of page_unload or before_unload callbacks registered unloadEventsCount: 0, // Number of page_unload or before_unload callbacks called unloadEventCalled: 0, // Event listener callbacks listenerCallbacks: {}, // Beacon variables vars: {}, // Beacon variables for only the next beacon singleBeaconVars: {}, /** * Variable priority lists: * -1 = first * 1 = last */ varPriority: { "-1": {}, "1": {} }, // Internal boomerang.js errors errors: {}, // Plugins that are disabled disabled_plugins: {}, // Whether or not localStorage is supported localStorageSupported: false, // Prefix for localStorage LOCAL_STORAGE_PREFIX: "_boomr_", // Native functions that were overwritten and should be restored when // the Boomerang IFRAME is unloaded nativeOverwrites: [], // Prerendered offset (via activationStart). null if not yet checked, // false if Prerender is supported but did not occur, an integer if // there was a Prerender (activationStart time). prerenderedOffset: null, // (End Private Members) // // Events (internal and public) // /** * Internal Events */ events: { /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired when the page is usable by the user. * * By default this is fired when `window.onload` fires, but if you * set `autorun` to false when calling {@link BOOMR.init}, then you * must explicitly fire this event by calling {@link BOOMR#event:page_ready}. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onload} * @event BOOMR#page_ready * @property {Event} [event] Event triggering the page_ready */ "page_ready": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired just before the browser unloads the page. * * The first event of `window.pagehide`, `window.beforeunload`, * or `window.unload` will trigger this. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/Events/pagehide} * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload} * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onunload} * @event BOOMR#page_unload */ "page_unload": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired before the document is about to be unloaded. * * `window.beforeunload` will trigger this. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload} * @event BOOMR#before_unload */ "before_unload": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired on `document.DOMContentLoaded`. * * The `DOMContentLoaded` event is fired when the initial HTML document * has been completely loaded and parsed, without waiting for stylesheets, * images, and subframes to finish loading * * @see {@link https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded} * @event BOOMR#dom_loaded */ "dom_loaded": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired on `document.visibilitychange`. * * The `visibilitychange` event is fired when the content of a tab has * become visible or has been hidden. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/Events/visibilitychange} * @event BOOMR#visibility_changed */ "visibility_changed": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired when the `visibilityState` of the document has changed from * `prerender` to `visible` * * @see {@link https://developer.mozilla.org/en-US/docs/Web/Events/visibilitychange} * @event BOOMR#prerender_to_visible */ "prerender_to_visible": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired when a beacon is about to be sent. * * The subscriber can still add variables to the beacon at this point, * either by modifying the `vars` paramter or calling {@link BOOMR.addVar}. * * @event BOOMR#before_beacon * @property {object} vars Beacon variables */ "before_beacon": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired when a beacon was sent. * * The beacon variables cannot be modified at this point. Any calls * to {@link BOOMR.addVar} or {@link BOOMR.removeVar} will apply to the * next beacon. * * Also known as `onbeacon`. * * @event BOOMR#beacon * @property {object} vars Beacon variables */ "beacon": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired when the page load beacon has been sent. * * This event should only happen once on a page. It does not apply * to SPA soft navigations. * * @event BOOMR#page_load_beacon * @property {object} vars Beacon variables */ "page_load_beacon": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired when an XMLHttpRequest has finished, or, if something calls * {@link BOOMR.responseEnd}. * * @event BOOMR#xhr_load * @property {object} data Event data */ "xhr_load": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired when the `click` event has happened on the `document`. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onclick} * @event BOOMR#click */ "click": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired when any `FORM` element is submitted. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit} * @event BOOMR#form_submit */ "form_submit": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired whenever new configuration data is applied via {@link BOOMR.init}. * * Also known as `onconfig`. * * @event BOOMR#config * @property {object} data Configuration data */ "config": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired whenever `XMLHttpRequest.open` is called. * * This event will only happen if {@link BOOMR.plugins.AutoXHR} is enabled. * * @event BOOMR#xhr_init * @property {string} type XHR type ("xhr") */ "xhr_init": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired whenever a SPA plugin is about to track a new navigation. * * @event BOOMR#spa_init * @property {object[]} parameters Navigation type (`spa` or `spa_hard`), URL and timings */ "spa_init": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired whenever a SPA navigation is complete. * * @event BOOMR#spa_navigation * @property {object[]} parameters Timings */ "spa_navigation": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired whenever a SPA navigation is cancelled. * * @event BOOMR#spa_cancel */ "spa_cancel": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired whenever `XMLHttpRequest.send` is called. * * This event will only happen if {@link BOOMR.plugins.AutoXHR} is enabled. * * @event BOOMR#xhr_send * @property {object} xhr `XMLHttpRequest` object */ "xhr_send": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired whenever and `XMLHttpRequest` has an error (if its `status` is * set). * * This event will only happen if {@link BOOMR.plugins.AutoXHR} is enabled. * * Also known as `onxhrerror`. * * @event BOOMR#xhr_error * @property {object} data XHR data */ "xhr_error": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired whenever a page error has happened. * * This event will only happen if {@link BOOMR.plugins.Errors} is enabled. * * Also known as `onerror`. * * @event BOOMR#error * @property {object} err Error */ "error": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired whenever connection information changes via the * Network Information API. * * This event will only happen if {@link BOOMR.plugins.Mobile} is enabled. * * @event BOOMR#netinfo * @property {object} connection `navigator.connection` */ "netinfo": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired whenever a Rage Click is detected. * * This event will only happen if {@link BOOMR.plugins.Continuity} is enabled. * * @event BOOMR#rage_click * @property {Event} e Event */ "rage_click": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired when an early beacon is about to be sent. * * The subscriber can still add variables to the early beacon at this point * by calling {@link BOOMR.addVar}. * * This event will only happen if {@link BOOMR.plugins.Early} is enabled. * * @event BOOMR#before_early_beacon * @property {object} data Event data */ "before_early_beacon": [], /** * Boomerang event, subscribe via {@link BOOMR.subscribe}. * * Fired when an BFCache navigation occurs. * * The subscriber can still add variables to the BFCache beacon at this point * by calling {@link BOOMR.addVar}. * * This event will only happen if {@link BOOMR.plugins.BFCache} is enabled. * * @event BOOMR#bfcache * @property {object} data Event data */ "bfcache": [] }, /** * Public events */ public_events: { /** * Public event (fired on `document`), and can be subscribed via * `document.addEventListener("onBeforeBoomerangBeacon", ...)` or * `document.attachEvent("onpropertychange", ...)`. * * Maps to {@link BOOMR#event:before_beacon} * * @event document#onBeforeBoomerangBeacon * @property {object} vars Beacon variables */ "before_beacon": "onBeforeBoomerangBeacon", /** * Public event (fired on `document`), and can be subscribed via * `document.addEventListener("onBoomerangBeacon", ...)` or * `document.attachEvent("onpropertychange", ...)`. * * Maps to {@link BOOMR#event:before_beacon} * * @event document#onBoomerangBeacon * @property {object} vars Beacon variables */ "beacon": "onBoomerangBeacon", /** * Public event (fired on `document`), and can be subscribed via * `document.addEventListener("onBoomerangLoaded", ...)` or * `document.attachEvent("onpropertychange", ...)`. * * Fired when {@link BOOMR} has loaded and can be used. * * @event document#onBoomerangLoaded */ "onboomerangloaded": "onBoomerangLoaded" }, /** * Maps old event names to their updated name */ translate_events: { "onbeacon": "beacon", "onconfig": "config", "onerror": "error", "onxhrerror": "xhr_error" }, // (End events) // // Private Functions // /** * Creates a callback handler for the specified event type * * @param {string} type Event type * @returns {function} Callback handler */ createCallbackHandler: function(type) { return function(ev) { var target; if (!ev) { ev = w.event; } if (ev.target) { target = ev.target; } else if (ev.srcElement) { target = ev.srcElement; } if (target.nodeType === 3) { // defeat Safari bug target = target.parentNode; } // don't capture events on flash objects // because of context slowdowns in PepperFlash if (target && target.nodeName && target.nodeName.toUpperCase() === "OBJECT" && target.type === "application/x-shockwave-flash") { return; } impl.fireEvent(type, target); }; }, /** * Clears all events */ clearEvents: function() { var eventName; for (eventName in this.events) { if (this.events.hasOwnProperty(eventName)) { this.events[eventName] = []; } } }, /** * Clears all event listeners */ clearListeners: function() { var type, i; for (type in impl.listenerCallbacks) { if (impl.listenerCallbacks.hasOwnProperty(type)) { // remove all callbacks -- removeListener is guaranteed // to remove the element we're calling with while (impl.listenerCallbacks[type].length) { BOOMR.utils.removeListener( impl.listenerCallbacks[type][0].el, type, impl.listenerCallbacks[type][0].fn); } } } impl.listenerCallbacks = {}; }, /** * Fires the specified boomerang.js event. * * @param {string} e_name Event name * @param {object} data Event data */ fireEvent: function(e_name, data) { var i, handler, handlers, handlersLen; e_name = e_name.toLowerCase(); /* BEGIN_DEBUG */ BOOMR.utils.mark("fire_event"); BOOMR.utils.mark("fire_event:" + e_name + ":start"); /* END_DEBUG */ // translate old names if (this.translate_events[e_name]) { e_name = this.translate_events[e_name]; } if (!this.events.hasOwnProperty(e_name)) { return; } if (this.public_events.hasOwnProperty(e_name)) { dispatchEvent(this.public_events[e_name], data); } handlers = this.events[e_name]; // Before we fire any event listeners, let's call real_sendBeacon() to flush // any beacon that is being held by the setImmediate. if (e_name !== "before_beacon" && e_name !== "beacon" && e_name !== "before_early_beacon") { BOOMR.real_sendBeacon(); } // only call handlers at the time of fireEvent (and not handlers that are // added during this callback to avoid an infinite loop) handlersLen = handlers.length; for (i = 0; i < handlersLen; i++) { try { handler = handlers[i]; handler.fn.call(handler.scope, data, handler.cb_data); } catch (err) { BOOMR.addError(err, "fireEvent." + e_name + "<" + i + ">"); } } // remove any 'once' handlers now that we've fired all of them for (i = 0; i < handlersLen; i++) { if (handlers[i].once) { handlers.splice(i, 1); handlersLen--; i--; } } /* BEGIN_DEBUG */ BOOMR.utils.mark("fire_event:" + e_name + ":end"); BOOMR.utils.measure( "fire_event:" + e_name, "fire_event:" + e_name + ":start", "fire_event:" + e_name + ":end"); /* END_DEBUG */ return; }, /** * Notes when a SPA navigation has happened. */ spaNavigation: function() { // a SPA navigation occured, force onloadfired to true impl.onloadfired = true; }, /** * Determines whether a beacon URL is allowed based on * `beacon_urls_allowed` config * * @param {string} url URL to test * */ beaconUrlAllowed: function(url) { if (!impl.beacon_urls_allowed || impl.beacon_urls_allowed.length === 0) { return true; } for (var i = 0; i < impl.beacon_urls_allowed.length; i++) { var regEx = new RegExp(impl.beacon_urls_allowed[i]); if (regEx.exec(url)) { return true; } } return false; }, /** * Checks browser for localStorage support */ checkLocalStorageSupport: function() { var name = impl.LOCAL_STORAGE_PREFIX + "clss"; impl.localStorageSupported = false; // Browsers with cookies disabled or in private/incognito mode may throw an // error when accessing the localStorage variable try { // we need JSON and localStorage support if (!w.JSON || !w.localStorage) { return; } w.localStorage.setItem(name, name); impl.localStorageSupported = (w.localStorage.getItem(name) === name); } catch (ignore) { impl.localStorageSupported = false; } finally { // If unsupported, then setItem threw and removeItem will also throw. try { if (w.localStorage) { w.localStorage.removeItem(name); } } catch (ignore) { // empty } } }, /** * Fired when the Boomerang IFRAME is unloaded. * * If Boomerang was loaded into the root document, this code * will not run. */ onFrameUnloaded: function() { var i, prop; BOOMR.isUnloaded = true; // swap the original function back in for any overwrites for (i = 0; i < impl.nativeOverwrites.length; i++) { prop = impl.nativeOverwrites[i]; prop.obj[prop.functionName] = prop.origFn; } impl.nativeOverwrites = []; } }; // // Public BOOMR object // // We create a boomr object and then copy all its properties to BOOMR so that // we don't overwrite anything additional that was added to BOOMR before this // was called... for example, a plugin. boomr = { /** * The timestamp when boomerang.js showed up on the page. * * This is the value of `BOOMR_start` we set earlier. * @type {TimeStamp} * * @memberof BOOMR */ t_start: BOOMR_start, /** * When the Boomerang plugins have all run. * * This value is generally set in zzz-last-plugin.js. * @type {TimeStamp} * * @memberof BOOMR */ t_end: undefined, /** * URL of boomerang.js. * * @type {string} * * @memberof BOOMR */ url: "", /** * (Optional) URL of configuration file * * @type {string} * * @memberof BOOMR */ config_url: null, /** * Whether or not Boomerang was loaded after the `onload` event. * * @type {boolean} * * @memberof BOOMR */ loadedLate: false, /** * Current number of beacons sent. * * Will be incremented and added to outgoing beacon as `n`. * * @type {number} * */ beaconsSent: 0, /** * Whether or not Boomerang thinks it has been unloaded (if it was * loaded in an IFRAME) * * @type {boolean} */ isUnloaded: false, /** * Whether or not we're in the middle of building a beacon. * * If so, the code desiring to send a beacon should wait until the beacon * event and try again. At that point, it should set this flag to true. * * @type {boolean} */ beaconInQueue: false, /* * Cache of cookies set */ cookies: {}, /** * Whether or not we've tested cookie setting */ testedCookies: false, /** * Constants visible to the world * @class BOOMR.constants */ constants: { /** * SPA beacon types * * @type {string[]} * * @memberof BOOMR.constants */ BEACON_TYPE_SPAS: ["spa", "spa_hard"], /** * Maximum GET URL length. * Using 2000 here as a de facto maximum URL length based on: * https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers * * @type {number} * * @memberof BOOMR.constants */ MAX_GET_LENGTH: 2000 }, /** * Session data * @class BOOMR.session */ session: { /** * Session Domain. * * You can disable all cookies by setting site_domain to a falsy value. * * @type {string} * * @memberof BOOMR.session */ domain: impl.site_domain, /** * Session ID. This will be randomly generated in the client but may * be overwritten by the server if not set. * * @type {string} * * @memberof BOOMR.session */ ID: undefined, /** * Session start time. * * @type {TimeStamp} * * @memberof BOOMR.session */ start: undefined, /** * Session length (number of pages) * * @type {number} * * @memberof BOOMR.session */ length: 0, /** * Session enabled (Are session cookies enabled?) * * @type {boolean} * * @memberof BOOMR.session */ enabled: true }, /** * @class BOOMR.utils */ utils: { /** * Determines whether or not the browser has `postMessage` support * * @returns {boolean} True if supported */ hasPostMessageSupport: function() { if (!w.postMessage || typeof w.postMessage !== "function" && typeof w.postMessage !== "object") { return false; } return true; }, /** * Converts an object to a string. * * @param {object} o Object * @param {string} separator Member separator * @param {number} nest_level Number of levels to recurse * * @returns {string} String representation of the object * * @memberof BOOMR.utils */ objectToString: function(o, separator, nest_level) { var value = [], k; if (!o || typeof o !== "object") { return o; } if (separator === undefined) { separator = "\n\t"; } if (!nest_level) { nest_level = 0; } if (BOOMR.utils.isArray(o)) { for (k = 0; k < o.length; k++) { if (nest_level > 0 && o[k] !== null && typeof o[k] === "object") { value.push( this.objectToString( o[k], separator + (separator === "\n\t" ? "\t" : ""), nest_level - 1 ) ); } else { if (separator === "&") { value.push(encodeURIComponent(o[k])); } else { value.push(o[k]); } } } separator = ","; } else { for (k in o) { if (Object.prototype.hasOwnProperty.call(o, k)) { if (nest_level > 0 && o[k] !== null && typeof o[k] === "object") { value.push(encodeURIComponent(k) + "=" + this.objectToString( o[k], separator + (separator === "\n\t" ? "\t" : ""), nest_level - 1 ) ); } else { if (separator === "&") { value.push(encodeURIComponent(k) + "=" + encodeURIComponent(o[k])); } else { value.push(k + "=" + o[k]); } } } } } return value.join(separator); }, /** * Gets the cached value of the cookie identified by `name`. * * @param {string} name Cookie name * * @returns {string|undefined} Cookie value, if set. * * @memberof BOOMR.utils */ getCookie: function(name) { var cookieVal; if (!name) { return null; } /* BEGIN_DEBUG */ BOOMR.utils.mark("get_cookie"); /* END_DEBUG */ if (typeof BOOMR.cookies[name] !== "undefined") { // a cached value of false indicates that the value doesn't exist, if so, // return undefined per the API return BOOMR.cookies[name] === false ? undefined : BOOMR.cookies[name]; } // unknown value cookieVal = this.getRawCookie(name); if (typeof cookieVal === "undefined") { // set to false to indicate we've attempted to get this cookie BOOMR.cookies[name] = false; // but return undefined per the API return undefined; } BOOMR.cookies[name] = cookieVal; return BOOMR.cookies[name]; }, /** * Gets the value of the cookie identified by `name`. * * @param {string} name Cookie name * * @returns {string|null} Cookie value, if set. * * @memberof BOOMR.utils */ getRawCookie: function(name) { if (!name) { return null; } /* BEGIN_DEBUG */ BOOMR.utils.mark("get_raw_cookie"); /* END_DEBUG */ name = " " + name + "="; var i, cookies; cookies = " " + d.cookie + ";"; if ((i = cookies.indexOf(name)) >= 0) { i += name.length; return cookies.substring(i, cookies.indexOf(";", i)).replace(/^"/, "").replace(/"$/, ""); } }, /** * Sets the cookie named `name` to the serialized value of `subcookies`. * * @param {string} name The name of the cookie * @param {object} subcookies Key/value pairs to write into the cookie. * These will be serialized as an & separated list of URL encoded key=value pairs. * @param {number} max_age Lifetime in seconds of the cookie. * Set this to 0 to create a session cookie that expires when * the browser is closed. If not set, defaults to 0. * * @returns {boolean} True if the cookie was set successfully * * @example * BOOMR.utils.setCookie("RT", { s: t_start, r: url }); * * @memberof BOOMR.utils */ setCookie: function(name, subcookies, max_age) { var value, nameval, savedval, c, exp; if (!name || !BOOMR.session.domain || typeof subcookies === "undefined") { BOOMR.debug("Invalid parameters or site domain: " + name + "/" + subcookies + "/" + BOOMR.session.domain); BOOMR.addVar("nocookie", 1); return false; } /* BEGIN_DEBUG */ BOOMR.utils.mark("set_cookie"); /* END_DEBUG */ value = this.objectToString(subcookies, "&"); if (value === BOOMR.cookies[name]) { // no change return true; } nameval = name + "=\"" + value + "\""; if (nameval.length < 500) { c = [nameval, "path=/", "domain=" + BOOMR.session.domain]; if (typeof max_age === "number") { exp = new Date(); exp.setTime(exp.getTime() + max_age * 1000); exp = exp.toGMTString(); c.push("expires=" + exp); } var extraAttributes = this.getSameSiteAttributeParts(); /** * 1. We check if the Secure attribute wasn't added already because SameSite=None will force adding it. * 2. We check the current protocol because if we are on HTTP and we try to create a secure cookie with * SameSite=Strict then a cookie will be created with SameSite=Lax. */ if (location.protocol === "https:" && impl.secure_cookie === true && extraAttributes.indexOf("Secure") === -1) { extraAttributes.push("Secure"); } // add extra attributes c = c.concat(extraAttributes); /* BEGIN_DEBUG */ BOOMR.utils.mark("set_cookie_real"); /* END_DEBUG */ // set the cookie d.cookie = c.join("; "); // we only need to test setting the cookie once if (BOOMR.testedCookies) { // only cache this cookie value if the expiry is in the future if (typeof max_age !== "number" || max_age > 0) { BOOMR.cookies[name] = value; } else { // the cookie is going to expire right away, don't cache it BOOMR.cookies[name] = undefined; } return true; } // unset the cached cookie value, in case the set doesn't work BOOMR.cookies[name] = undefined; // confirm cookie was set (could be blocked by user's settings, etc.) savedval = this.getRawCookie(name); // the saved cookie should be the same or undefined in the case of removeCookie if (value === savedval || (typeof savedval === "undefined" && typeof max_age === "number" && max_age <= 0)) { // re-set the cached value BOOMR.cookies[name] = value; // note we've saved successfully BOOMR.testedCookies = true; BOOMR.removeVar("nocookie"); return true; } BOOMR.warn("Saved cookie value doesn't match what we tried to set:\n" + value + "\n" + savedval); } else { BOOMR.warn("Cookie too long: " + nameval.length + " " + nameval); } BOOMR.addVar("nocookie", 1); return false; }, /** * Parse a cookie string returned by {@link BOOMR.utils.getCookie} and * split it into its constituent subcookies. * * @param {string} cookie Cookie value * * @returns {object} On success, an object of key/value pairs of all * sub cookies. Note that some subcookies may have empty values. * `null` if `cookie` was not set or did not contain valid subcookies. * * @memberof BOOMR.utils */ getSubCookies: function(cookie) { var cookies_a, i, l, kv, gotcookies = false, cookies = {}; if (!cookie) { return null; } if (typeof cookie !== "string") { BOOMR.debug("TypeError: cookie is not a string: " + typeof cookie); return null; } cookies_a = cookie.split("&"); for (i = 0, l = cookies_a.length; i < l; i++) { kv = cookies_a[i].split("="); if (kv[0]) { // just in case there's no value kv.push(""); cookies[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]); gotcookies = true; } } return gotcookies ? cookies : null; }, /** * Removes the cookie identified by `name` by nullifying its value, * and making it a session cookie. * * @param {string} name Cookie name * * @memberof BOOMR.utils */ removeCookie: function(name) { return this.setCookie(name, {}, -86400); }, /** * Depending on Boomerang configuration and checks of current protocol and * compatible browsers the logic below will provide an array of cookie * attributes that are needed for a successful creation of a cookie that * contains the SameSite attribute. * * How it works: * 1. We read the Boomerang configuration key `same_site_cookie` where * one of the following values `None`, `Lax` or `Strict` is expected. * 2. A configuration value of `same_site_cookie` will be read in case-insensitive * manner. E.g. `Lax`, `lax` and `lAx` will produce same result - `SameSite=Lax`. * 3. If a `same_site_cookie` configuration value is not specified a cookie * will be created with `SameSite=Lax`. * 4. If a `same_site_cookie` configuration value does't match any of * `None`, `Lax` or `Strict` then a cookie will be created with `SameSite=Lax`. * 5. The `Secure` cookie attribute will be added when a cookie is created * with `SameSite=None`. * 6. It's possible that a Boomerang plugin or external code may need cookies * to be created with `SameSite=None`. In such cases we check a special * flag `forced_same_site_cookie_none`. If the value of this flag is equal to `true` * then the `same_site_cookie` value will be ignored and Boomerang cookies * will be created with `SameSite=None`. * * SameSite=None - INCOMPATIBILITIES and EXCEPTIONS: * * There are known problems with older browsers where cookies created * with `SameSite=None` are `dropped` or created with `SameSite=Strict`. * Reference: https://www.chromium.org/updates/same-site/incompatible-clients * * 1. If we detect a browser that can't create safely a cookie with `SameSite=None` * then Boomerang will create a cookie without the `SameSite` attribute. * 2. A cookie with `SameSite=None` can be created only over `HTTPS` connection. * If current connection is `HTTP` then a cookie will be created * without the `SameSite` attribute. * * * @returns {Array} of cookie attributes used for