/** * @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 setting a cookie with SameSite attribute * * @memberof BOOMR.utils */ getSameSiteAttributeParts: function() { var sameSiteMode = impl.same_site_cookie.toUpperCase(); if (impl.forced_same_site_cookie_none) { sameSiteMode = "NONE"; } if (sameSiteMode === "LAX") { return ["SameSite=Lax"]; } if (sameSiteMode === "NONE") { if (location.protocol === "https:" && this.isCurrentUASameSiteNoneCompatible()) { return ["SameSite=None", "Secure"]; } // Fallback to browser's default return []; } if (sameSiteMode === "STRICT") { return ["SameSite=Strict"]; } return ["SameSite=Lax"]; }, /** * Retrieve items from localStorage * * @param {string} name Name of storage * * @returns {object|null} Returns object retrieved from localStorage. * Returns undefined if not found or expired. * Returns null if parameters are invalid or an error occured * * @memberof BOOMR.utils */ getLocalStorage: function(name) { var value, data; if (!name || !impl.localStorageSupported) { return null; } /* BEGIN_DEBUG */ BOOMR.utils.mark("get_local_storage"); /* END_DEBUG */ try { value = w.localStorage.getItem(impl.LOCAL_STORAGE_PREFIX + name); if (value === null) { return undefined; } data = w.JSON.parse(value); } catch (e) { BOOMR.warn(e); return null; } if (!data || typeof data.items !== "object") { // Items are invalid this.removeLocalStorage(name); return null; } if (typeof data.expires === "number") { if (BOOMR.now() >= data.expires) { // Items are expired this.removeLocalStorage(name); return undefined; } } return data.items; }, /** * Saves items in localStorage * The value stored in localStorage will be a JSON string representation of {"items": items, "expiry": expiry} * where items is the object we're saving and expiry is an optional epoch number of when the data is to be * considered expired * * @param {string} name Name of storage * @param {object} items Items to be saved * @param {number} max_age Age in seconds before items are to be considered expired * * @returns {boolean} True if the localStorage was set successfully * * @memberof BOOMR.utils */ setLocalStorage: function(name, items, max_age) { var data, value, savedval; if (!name || !impl.localStorageSupported || typeof items !== "object") { return false; } /* BEGIN_DEBUG */ BOOMR.utils.mark("set_local_storage"); /* END_DEBUG */ data = {"items": items}; if (typeof max_age === "number") { data.expires = BOOMR.now() + (max_age * 1000); } value = w.JSON.stringify(data); if (value.length < 50000) { try { w.localStorage.setItem(impl.LOCAL_STORAGE_PREFIX + name, value); // confirm storage was set (could be blocked by user's settings, etc.) savedval = w.localStorage.getItem(impl.LOCAL_STORAGE_PREFIX + name); if (value === savedval) { return true; } } catch (ignore) { // Empty } BOOMR.warn("Saved storage value doesn't match what we tried to set:\n" + value + "\n" + savedval); } else { BOOMR.warn("Storage items too large: " + value.length + " " + value); } return false; }, /** * Remove items from localStorage * * @param {string} name Name of storage * * @returns {boolean} True if item was removed from localStorage. * * @memberof BOOMR.utils */ removeLocalStorage: function(name) { if (!name || !impl.localStorageSupported) { return false; } try { w.localStorage.removeItem(impl.LOCAL_STORAGE_PREFIX + name); return true; } catch (ignore) { // Empty } return false; }, /** * Cleans up a URL by removing the query string (if configured), and * limits the URL to the specified size. * * @param {string} url URL to clean * @param {number} urlLimit Maximum size, in characters, of the URL * * @returns {string} Cleaned up URL * * @memberof BOOMR.utils */ cleanupURL: function(url, urlLimit) { if (!url || BOOMR.utils.isArray(url)) { return ""; } if (impl.strip_query_string) { url = url.replace(/\?.*/, "?qs-redacted"); } if (typeof urlLimit !== "undefined" && url && url.length > urlLimit) { // We need to break this URL up. Try at the query string first. var qsStart = url.indexOf("?"); if (qsStart !== -1 && qsStart < urlLimit) { url = url.substr(0, qsStart) + "?..."; } else { // No query string, just stop at the limit url = url.substr(0, urlLimit - 3) + "..."; } } return url; }, /** * Gets the URL with the query string replaced with a hash of its contents. * * @param {string} url URL * @param {boolean} stripHash Whether or not to strip the hash * * @returns {string} URL with query string hashed * * @memberof BOOMR.utils */ hashQueryString: function(url, stripHash) { if (!url) { return url; } if (!url.match) { BOOMR.addError("TypeError: Not a string", "hashQueryString", typeof url); return ""; } if (url.match(/^\/\//)) { url = location.protocol + url; } if (!url.match(/^(https?|file):/)) { BOOMR.error("Passed in URL is invalid: " + url); return ""; } if (stripHash) { url = url.replace(/#.*/, ""); } return url.replace(/\?([^#]*)/, function(m0, m1) { return "?" + (m1.length > 10 ? BOOMR.utils.hashString(m1) : m1); }); }, /** * Sets the object's properties if anything in config matches * one of the property names. * * @param {object} o The plugin's `impl` object within which it stores * all its configuration and private properties * @param {object} config The config object passed in to the plugin's * `init()` method. * @param {string} plugin_name The plugin's name in the {@link BOOMR.plugins} object. * @param {string[]} properties An array containing a list of all configurable * properties that this plugin has. * * @returns {boolean} True if a property was set * * @memberof BOOMR.utils */ pluginConfig: function(o, config, plugin_name, properties) { var i, props = 0; if (!config || !config[plugin_name]) { return false; } for (i = 0; i < properties.length; i++) { if (config[plugin_name][properties[i]] !== undefined) { o[properties[i]] = config[plugin_name][properties[i]]; props++; } } return (props > 0); }, /** * `filter` for arrays * * @param {Array} array The array to iterate over. * @param {Function} predicate The function invoked per iteration. * * @returns {Array} Returns the new filtered array. * * @memberof BOOMR.utils */ arrayFilter: function(array, predicate) { var result = []; if (!(this.isArray(array) || (array && typeof array.length === "number")) || typeof predicate !== "function") { return result; } if (typeof array.filter === "function") { result = array.filter(predicate); } else { var index = -1, length = array.length, value; while (++index < length) { value = array[index]; if (predicate(value, index, array)) { result[result.length] = value; } } } return result; }, /** * `find` for Arrays * * @param {Array} array The array to iterate over * @param {Function} predicate The function invoked per iteration * * @returns {Array} Returns the value of first element that satisfies * the predicate * * @memberof BOOMR.utils */ arrayFind: function(array, predicate) { if (!(this.isArray(array) || (array && typeof array.length === "number")) || typeof predicate !== "function") { return undefined; } if (typeof array.find === "function") { return array.find(predicate); } else { var index = -1, length = array.length, value; while (++index < length) { value = array[index]; if (predicate(value, index, array)) { return value; } } return undefined; } }, /** * MutationObserver feature detection * * Always returns false for IE 11 due several bugs in it's implementation that MS flagged as Won't Fix. * In IE11, XHR responseXML might be malformed if MO is enabled (where extra newlines get added in nodes * with UTF-8 content). * * Another IE 11 MO bug can cause the process to crash when certain mutations occur. * * For the process crash issue, see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8137215/ * and * https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/15167323/ * * @returns {boolean} Returns true if MutationObserver is supported. * * @memberof BOOMR.utils */ isMutationObserverSupported: function() { // We can only detect IE 11 bugs by UA sniffing. var ie11 = (w && w.navigator && !w.navigator.userAgentData && w.navigator.userAgent && w.navigator.userAgent.match(/Trident.*rv[ :]*11\./)); return (!ie11 && w && w.MutationObserver && typeof w.MutationObserver === "function"); }, /** * The callback function may return a falsy value to disconnect the * observer after it returns, or a truthy value to keep watching for * mutations. If the return value is numeric and greater than 0, then * this will be the new timeout. If it is boolean instead, then the * timeout will not fire any more so the caller MUST call disconnect() * at some point. * * @callback BOOMR~addObserverCallback * @param {object[]} mutations List of mutations detected by the observer or `undefined` if the observer timed out * @param {object} callback_data Is the passed in `callback_data` parameter without modifications */ /** * Add a MutationObserver for a given element and terminate after `timeout`ms. * * @param {DOMElement} el DOM element to watch for mutations * @param {MutationObserverInit} config MutationObserverInit object * (https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationObserverInit) * @param {number} timeout Number of milliseconds of no mutations after which the observer should be * automatically disconnected. * If set to a falsy value, the observer will wait indefinitely for Mutations. * @param {BOOMR~addObserverCallback} callback Callback function to call either on timeout or if mutations * are detected. * @param {object} callback_data Any data to be passed to the callback function as its second parameter. * @param {object} callback_ctx An object that represents the `this` object of the `callback` method. * Leave unset the callback function is not a method of an object. * * @returns {object|null} * - `null` if a MutationObserver could not be created OR * - An object containing the observer and the timer object: * `{ observer: , timer: }` * - The caller can use this to disconnect the observer at any point * by calling `retval.observer.disconnect()` * - Note that the caller should first check to see if `retval.observer` * is set before calling `disconnect()` as it may have been cleared automatically. * * @memberof BOOMR.utils */ addObserver: function(el, config, timeout, callback, callback_data, callback_ctx) { var MO, zs, o = {observer: null, timer: null}; /* BEGIN_DEBUG */ BOOMR.utils.mark("add_observer"); /* END_DEBUG */ if (!this.isMutationObserverSupported() || !callback || !el) { return null; } function done(mutations) { var run_again = false; /* BEGIN_DEBUG */ BOOMR.utils.mark("mutation_observer_callback"); /* END_DEBUG */ if (o.timer) { clearTimeout(o.timer); o.timer = null; } if (callback) { run_again = callback.call(callback_ctx, mutations, callback_data); if (!run_again) { callback = null; } } if (!run_again && o.observer) { o.observer.disconnect(); o.observer = null; } if (typeof run_again === "number" && run_again > 0) { o.timer = setTimeout(done, run_again); } } MO = w.MutationObserver; // if the site uses Zone.js then get the native MutationObserver if (w.Zone && typeof w.Zone.__symbol__ === "function") { zs = w.Zone.__symbol__("MutationObserver"); if (zs && typeof zs === "string" && w.hasOwnProperty(zs) && typeof w[zs] === "function") { BOOMR.debug("Detected Zone.js, using native MutationObserver"); MO = w[zs]; } } o.observer = new MO(done); if (timeout) { o.timer = setTimeout(done, o.timeout); } o.observer.observe(el, config); return o; }, /** * Adds an event listener. * * @param {DOMElement} el DOM element * @param {string} type Event name * @param {function} fn Callback function * @param {boolean|object} passiveOrOpts Passive mode or Options object * * @memberof BOOMR.utils */ addListener: function(el, type, fn, passiveOrOpts) { var opts = false; /* BEGIN_DEBUG */ BOOMR.utils.mark("add_listener"); /* END_DEBUG */ if (el.addEventListener) { if (typeof passiveOrOpts === "object") { opts = passiveOrOpts; } else if (typeof passiveOrOpts === "boolean" && passiveOrOpts && BOOMR.browser.supportsPassive()) { opts = { capture: false, passive: true }; } el.addEventListener(type, fn, opts); } else if (el.attachEvent) { el.attachEvent("on" + type, fn); } // ensure the type arry exists impl.listenerCallbacks[type] = impl.listenerCallbacks[type] || []; // save a reference to the target object and function impl.listenerCallbacks[type].push({ el: el, fn: fn}); }, /** * Removes an event listener. * * @param {DOMElement} el DOM element * @param {string} type Event name * @param {function} fn Callback function * * @memberof BOOMR.utils */ removeListener: function(el, type, fn) { var i; /* BEGIN_DEBUG */ BOOMR.utils.mark("remove_listener"); /* END_DEBUG */ if (el.removeEventListener) { // NOTE: We don't need to match any other options (e.g. passive) // from addEventListener, as removeEventListener only cares // about captive. el.removeEventListener(type, fn, false); } else if (el.detachEvent) { el.detachEvent("on" + type, fn); } if (impl.listenerCallbacks.hasOwnProperty(type)) { for (var i = 0; i < impl.listenerCallbacks[type].length; i++) { if (fn === impl.listenerCallbacks[type][i].fn && el === impl.listenerCallbacks[type][i].el) { impl.listenerCallbacks[type].splice(i, 1); return; } } } }, /** * Determines if the specified object is an `Array` or not * * @param {object} ary Object in question * * @returns {boolean} True if the object is an `Array` * * @memberof BOOMR.utils */ isArray: function(ary) { return Object.prototype.toString.call(ary) === "[object Array]"; }, /** * Determines if the specified value is in the array * * @param {object} val Value to check * @param {object} ary Object in question * * @returns {boolean} True if the value is in the Array * * @memberof BOOMR.utils */ inArray: function(val, ary) { var i; if (typeof val === "undefined" || typeof ary === "undefined" || !ary.length) { return false; } for (i = 0; i < ary.length; i++) { if (ary[i] === val) { return true; } } return false; }, /** * Get a query parameter value from a URL's query string * * @param {string} param Query parameter name * @param {string|Object} [url] URL containing the query string, or a link object. * Defaults to `BOOMR.window.location` * * @returns {string|null} URI decoded value or null if param isn't a query parameter * * @memberof BOOMR.utils */ getQueryParamValue: function(param, url) { var l, params, i, kv; if (!param) { return null; } if (typeof url === "string") { l = BOOMR.window.document.createElement("a"); l.href = url; } else if (typeof url === "object" && typeof url.search === "string") { l = url; } else { l = BOOMR.window.location; } // Now that we match, pull out all query string parameters params = l.search.slice(1).split(/&/); for (i = 0; i < params.length; i++) { if (params[i]) { kv = params[i].split("="); if (kv.length && kv[0] === param) { try { return kv.length > 1 ? decodeURIComponent(kv.splice(1).join("=").replace(/\+/g, " ")) : ""; } catch (e) { /** * We have different messages for the same error in different browsers but * we can look at the error name because it looks more consistent. * * Examples: * - URIError: The URI to be encoded contains invalid character (Edge) * - URIError: malformed URI sequence (Firefox) * - URIError: URI malformed (Chrome) * - URIError: URI error (Safari 13.0) / Missing on MDN but this is the result of my local tests. * * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Malformed_URI#Message */ if (e && typeof e.name === "string" && e.name.indexOf("URIError") !== -1) { // NOP } else { throw e; } } } } } return null; }, /** * Generates a pseudo-random UUID (Version 4): * https://en.wikipedia.org/wiki/Universally_unique_identifier * * @returns {string} UUID * * @memberof BOOMR.utils */ generateUUID: function() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0; var v = c === "x" ? r : (r & 0x3 | 0x8); return v.toString(16); }); }, /** * Generates a random ID based on the specified number of characters. Uses * characters a-z0-9. * * @param {number} chars Number of characters (max 40) * * @returns {string} Random ID * * @memberof BOOMR.utils */ generateId: function(chars) { return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".substr(0, chars || 40).replace(/x/g, function(c) { var c = (Math.random() || 0.01).toString(36); // some implementations may return "0" for small numbers if (c === "0") { return "0"; } else { return c.substr(2, 1); } }); }, /** * Attempt to serialize an object, preferring JSURL over JSON.stringify * * @param {Object} value Object to serialize * @returns {string} serialized version of value, empty-string if not possible */ serializeForUrl: function(value) { if (BOOMR.utils.Compression && BOOMR.utils.Compression.jsUrl) { return BOOMR.utils.Compression.jsUrl(value); } if (window.JSON) { return JSON.stringify(value); } // not supported BOOMR.debug("JSON is not supported"); return ""; }, /* BEGIN_DEBUG */ /** * Attempt to deserialize an object, preferring JSURL over JSON.parse * * @param {string} value String to deserialize * * @returns {object|undefined} Deserialized version of value, undefined if not possible */ deserializeForUrl: function(value) { if (BOOMR.utils.Compression && BOOMR.utils.Compression.jsUrlDecompress) { return BOOMR.utils.Compression.jsUrlDecompress(value); } if (window.JSON) { return JSON.parse(value); } // not supported BOOMR.debug("JSON is not supported"); return; }, /* END_DEBUG */ /** * Attempt to identify the URL of boomerang itself using multiple methods for cross-browser support * * This method uses document.currentScript (which cannot be called from an event handler), * script.readyState (IE6-10), * and the stack property of a caught Error object. * * @returns {string} The URL of the currently executing boomerang script. */ getMyURL: function() { var stack; // document.currentScript works in all browsers except for IE: https://caniuse.com/#feat=document-currentscript // #boomr-if-as works in all browsers if the page uses our standard iframe loader // #boomr-scr-as works in all browsers if the page uses our preloader loader // BOOMR_script will be undefined on IE for pages that do not use our standard loaders // Note that we do not use `w.document` or `d` here because we need the current execution context var BOOMR_script = (document.currentScript || document.getElementById("boomr-if-as") || document.getElementById("boomr-scr-as")); if (BOOMR_script) { return BOOMR_script.src; } // For IE 6-10 users on pages not using the standard loader, we iterate through all scripts backwards var scripts = document.getElementsByTagName("script"), i; // i-- is both a decrement as well as a condition, ie, the loop will terminate when i goes from 0 to -1 for (i = scripts.length; i--;) { // We stop at the first script that has its readyState set to interactive indicating that it is // currently executing if (scripts[i].readyState === "interactive") { return scripts[i].src; } } // For IE 11, we throw an Error and inspect its stack property in the catch block // This also works on IE10, but throwing is disruptive so we try to avoid it and use // the less disruptive script iterator above try { throw new Error(); } catch (e) { if ("stack" in e) { stack = this.arrayFilter(e.stack.split(/\n/), function(l) { return l.match(/https?:\/\//); }); if (stack && stack.length) { return stack[0].replace(/.*(https?:\/\/.+?)(:\d+)+\D*$/m, "$1"); } } // FWIW, on IE 8 & 9, the Error object does not contain a stack property, but if you have an uncaught error, // and a `window.onerror` handler (not using addEventListener), then the second argument to that handler is // the URL of the script that threw. The handler needs to `return true;` to prevent the default error handler // This flow is asynchronous though (due to the event handler), so won't work in a function return scenario // like this (we can't use promises because we would only need this hack in browsers that don't support // promises). } return ""; }, /* * Gets the Scroll x and y (rounded) for a page * * @returns {object} Scroll x and y coordinates */ scroll: function() { // Adapted from: // https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY var supportPageOffset = w.pageXOffset !== undefined; var isCSS1Compat = ((w.document.compatMode || "") === "CSS1Compat"); var ret = { x: 0, y: 0 }; if (supportPageOffset) { if (typeof w.pageXOffset === "function") { ret.x = w.pageXOffset(); ret.y = w.pageYOffset(); } else { ret.x = w.pageXOffset; ret.y = w.pageYOffset; } } else if (isCSS1Compat) { ret.x = w.document.documentElement.scrollLeft; ret.y = w.document.documentElement.scrollTop; } else { ret.x = w.document.body.scrollLeft; ret.y = w.document.body.scrollTop; } // round to full numbers if (typeof ret.sx === "number") { ret.sx = Math.round(ret.sx); } if (typeof ret.sy === "number") { ret.sy = Math.round(ret.sy); } return ret; }, /** * Gets the window height * * @returns {number} Window height */ windowHeight: function() { return w.innerHeight || w.document.documentElement.clientHeight || w.document.body.clientHeight; }, /** * Gets the window width * * @returns {number} Window width */ windowWidth: function() { return w.innerWidth || w.document.documentElement.clientWidth || w.document.body.clientWidth; }, /** * Determines if the function is native or not * * @param {function} fn Function * * @returns {boolean} True when the function is native */ isNative: function(fn) { return !!fn && fn.toString && !fn.hasOwnProperty("toString") && /\[native code\]/.test(String(fn)); }, /** * Overwrites a function on the specified object. * * When the Boomerang IFRAME unloads, it will swap the old * function back in, so calls to the functions are successful. * * If this isn't done, callers of the overwritten functions may still * call into freed Boomerang code or the IFRAME that is partially unloaded, * leading to "Freed script" errors or exceptions from accessing * unloaded DOM properties. * * This tracking isn't needed if Boomerang is loaded in the root * document, as everthing will be cleaned up along with Boomerang * on unload. * * @param {object} obj Object whose property will be overwritten * @param {string} functionName Function name * @param {function} newFn New function */ overwriteNative: function(obj, functionName, newFn) { // bail if the object doesn't exist if (!obj || !newFn) { return; } // we only need to keep track if we're running Boomerang in // an IFRAME if (BOOMR.boomerang_frame !== BOOMR.window) { // note we overwrote this impl.nativeOverwrites.push({ obj: obj, functionName: functionName, origFn: obj[functionName] }); } obj[functionName] = newFn; }, /** * Determines if the given input is an Integer. * Relies on standard Number.isInteger() function that available * is most browsers except IE. For IE, this relies on the polyfill * provided by MDN: * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill * * @param {number} input dat * * @returns {string} Random ID * * @memberof BOOMR.utils * */ isInteger: function(data) { var isInt = Number.isInteger || function(value) { return typeof value === "number" && isFinite(value) && Math.floor(value) === value; }; return isInt(data); }, /** * Determines whether or not an Object is empty * * @param {object} data Data object * * @returns {boolean} True if the object has no properties */ isObjectEmpty: function(data) { for (var propName in data) { if (data.hasOwnProperty(propName)) { return false; } } return true; }, /** * Calculates the FNV hash of the specified string. * * @param {string} string Input string * * @returns {string} FNV hash of the input string * */ hashString: function(string) { string = encodeURIComponent(string); var hval = 0x811c9dc5; for (var i = 0; i < string.length; i++) { hval = hval ^ string.charCodeAt(i); hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24); } var hash = (hval >>> 0).toString() + string.length; return parseInt(hash).toString(36); }, /** * Wrapper of isUASameSiteNoneCompatible() that ensures that we pass correct User Agent string * * @returns {boolean} True if a browser can safely create SameSite=None cookie * * @memberof BOOMR.utils */ isCurrentUASameSiteNoneCompatible: function() { if (w && w.navigator && !w.navigator.userAgentData) { if (w.navigator.userAgent && typeof w.navigator.userAgent === "string") { return this.isUASameSiteNoneCompatible(w.navigator.userAgent); } } return true; }, /** * @param {string} uaString User agent string * * @returns {boolean} True if a browser can safely create SameSite=None cookie * * @memberof BOOMR.utils */ isUASameSiteNoneCompatible: function(uaString) { /** * 1. UCBrowser lower than 12.13.2 */ var result = uaString.match(/(UCBrowser)\/(\d+\.\d+)\.(\d+)/); if (result) { var ucMajorMinorPart = parseFloat(result[2]); var ucPatch = result[3]; if (ucMajorMinorPart === 12.13) { if (ucPatch <= 2) { return false; } return true; } if (ucMajorMinorPart < 12.13) { return false; } return true; } /** * 2. Chrome and Chromium version between 51 and 66 * * This the regex covers both because a Chromium AU contains "Chromium/65.0.3325.181 Chrome/65.0.3325.181" */ result = uaString.match(/(Chrome)\/(\d+)\.(\d+)\.(\d+)\.(\d+)/); if (result) { var chromeMajor = result[2]; if (chromeMajor >= 51 && chromeMajor <= 66) { return false; } return true; } /** * 3. Mac OS 10.14.* check */ result = uaString.match(/(Macintosh;.*Mac OS X 10_14[_\d]*.*) AppleWebKit\//); if (result) { // 3.2 Safari check result = uaString.match(/Version\/.* Safari\//); if (result) { // 3.2.1 Not Chrome based check result = uaString.match(/Chrom(?:e|ium)/); if (result === null) { return false; } } // 3.3 Mac OS embeded browser // eslint-disable-next-line max-len result = uaString.match(/^Mozilla\/\d+(?:\.\d+)* \(Macintosh;.*Mac OS X \d+(?:_\d+)*\) AppleWebKit\/\d+(?:\.\d+)* \(KHTML, like Gecko\)$/); if (result) { return false; } return true; } /** * 4. iOS and iPad OS 12 for all browsers */ result = uaString.match(/(iP.+; CPU .*OS 12(?:_\d+)*.*)/); if (result) { return false; } return true; }, /** * Given a HTML node, returns a Pseudo-CSS selector. * * Algorithm: * * Starting at the node itself, gather its selector: `elementName#elementId.elementClasses` * * If the ID or classes are missing, skip them, e.g. just `elementName` or `elementName.elementClasses` * * Going up the parent tree via the `.parentNode`, stop at the first of: * * The closest node (or itself) with an ID * * The BODY node * * For each node found, prepend the node's selector to the full selector string * * Limit the number of selectors added to 5 * * If it takes more than 5 to get to an element with an ID or BODY, collapse to a `*` until they are reached * * For example: * * `

` -> `div span h1` * * `
` -> `div#test span.first` * * `
` -> `div#test span.first` * * `
` -> `div#test span.first.second` * * @param {Node} elementNode Node to generate a Pseudo-CSS selector for * * @return {string} Pseudo-CSS selector * * @memberof BOOMR.utils */ makeSelector: function(elementNode) { var cssSelectors = []; var node = elementNode; // checks to see if a node is valid (element type, not null) // NOTE: Also sets the funtion-local `node` as the next node to iterate over function validateAndSetNode(nodeToCheck, isParent) { var isValid = true; // node invalid if null if (!nodeToCheck) { isValid = false; } // if node is not valid, see if there's a parent that is else { // nodeType 1 == Node.ELEMENT_NODE while (nodeToCheck && nodeToCheck.nodeType !== 1) { nodeToCheck = nodeToCheck.parentNode || nodeToCheck.parentElement; } // BODY is also invalid if (!nodeToCheck || nodeToCheck.tagName === "BODY") { isValid = false; } } // // If we're not just checking to see if a parent exists, // set node to the nearest valid parent if there is one. // // If node itself is valid, it will stay itself // (only changes if invalid but has a valid parent). if (!isParent) { node = nodeToCheck; } return isValid; } while (node) { // if node isn't null but is the wrong type, will set it to nearest valid parent var nodeIsValid = validateAndSetNode(node, false); if (!nodeIsValid || !node) { // if node is invalid and no valid parent, stop iterating break; } // add tagname and ID to selector if ID exists if (node.hasAttribute("id")) { var nodeId = node.tagName.toLowerCase() + "#" + node.getAttribute("id"); cssSelectors.unshift(nodeId); // selector ends if an ID is found break; } // check if parent of this node is valid var parentIsValid = validateAndSetNode(node.parentNode, true); // if we don't have three tagnames in the selector yet, // or we do but this is the last valid one, add class if exists if (cssSelectors.length < 3 || (cssSelectors.length >= 3 && !parentIsValid)) { var nodeClass = node.tagName.toLowerCase(); if (node.hasAttribute("class")) { nodeClass += "." + node.getAttribute("class").replace(/ +/g, "."); } cssSelectors.unshift(nodeClass); } // if > 3 tagnames in selector and parent is valid, // add an asterick if there is not one already else if (cssSelectors[0] !== "*") { cssSelectors.unshift("*"); } // go to next node in sequence node = node.parentNode || node.parentElement; } // return CSS selector return cssSelectors.join(" "); } /* BEGIN_DEBUG */ /** * DEBUG ONLY * * Loops over an array, running a function for each item * * @param {Array} array Array to iterate over * @param {function} fn Function to execute * @param {object} thisArg 'this' argument */ , forEach: function(array, fn, thisArg) { if (!BOOMR.utils.isArray(array) || typeof fn !== "function") { return; } var length = array.length; for (var i = 0; i < length; i++) { if (array.hasOwnProperty(i)) { fn.call(thisArg, array[i], i, array); } } }, /** * DEBUG ONLY * * Logs a UserTiming mark * * @param {string} name Mark name (prefixed by boomr:) */ mark: function(name) { var p = BOOMR.getPerformance(); if (p && typeof p.mark === "function" && !BOOMR.window.BOOMR_no_mark) { p.mark("boomr:" + name); } }, /** * DEBUG ONLY * * Logs a UserTiming measure * * @param {string} name Mark name (prefixed by boomr:) */ measure: function(measureName, startMarkName, endMarkName) { var p = BOOMR.getPerformance(); if (p && typeof p.measure === "function" && !BOOMR.window.BOOMR_no_mark) { p.measure("boomr:" + measureName, startMarkName ? "boomr:" + startMarkName : undefined, endMarkName ? "boomr:" + endMarkName : undefined); } } /* END_DEBUG */ // closes `utils` }, /** * Browser feature detection flags. * * @class BOOMR.browser */ browser: { results: {}, /** * Whether or not the browser supports 'passive' mode for event * listeners * * @returns {boolean} True if the browser supports passive mode */ supportsPassive: function() { if (typeof BOOMR.browser.results.supportsPassive === "undefined") { BOOMR.browser.results.supportsPassive = false; if (!Object.defineProperty) { return false; } try { var opts = Object.defineProperty({}, "passive", { get: function() { BOOMR.browser.results.supportsPassive = true; } }); window.addEventListener("test", null, opts); } catch (e) { // NOP } } return BOOMR.browser.results.supportsPassive; } }, /** * Initializes Boomerang by applying the specified configuration. * * All plugins' `init()` functions will be called with the same config as well. * * @param {object} config Configuration object * @param {boolean} [config.autorun] By default, boomerang runs automatically * and attaches its `page_ready` handler to the `window.onload` event. * If you set `autorun` to `false`, this will not happen and you will * need to call {@link BOOMR.page_ready} yourself. * @param {string} config.beacon_auth_key Beacon authorization key value * @param {string} config.beacon_auth_token Beacon authorization token. * @param {boolean} config.beacon_with_credentials Sends beacon with credentials * @param {boolean} config.beacon_disable_sendbeacon Disables `navigator.sendBeacon()` support * @param {string} config.beacon_url The URL to beacon results back to. * If not set, no beacon will be sent. * @param {boolean} config.beacon_url_force_https Forces protocol-relative Beacon URLs to HTTPS * @param {string} config.beacon_type `GET`, `POST` or `AUTO` * @param {string} [config.site_domain] The domain that all cookies should be set on * Boomerang will try to auto-detect this, but unless your site is of the * `foo.com` format, it will probably get it wrong. It's a good idea * to set this to whatever part of your domain you'd like to share * bandwidth and performance measurements across. * Set this to a falsy value to disable all cookies. * @param {boolean} [config.strip_query_string] Whether or not to strip query strings from all URLs * (e.g. `u`, `pgu`, etc.) * @param {string} [config.user_ip] Despite its name, this is really a free-form * string used to uniquely identify the user's current internet * connection. It's used primarily by the bandwidth test to determine * whether it should re-measure the user's bandwidth or just use the * value stored in the cookie. You may use IPv4, IPv6 or anything else * that you think can be used to identify the user's network connection. * @param {string} [config.same_site_cookie] Used for creating cookies with `SameSite` with one * of the following values: `None`, `Lax` or `Strict`. * @param {boolean} [config.secure_cookie] When `true` all cookies will be created with `Secure` flag. * @param {boolean} [config.request_client_hints] When `true`, gather high entropy values for Architecture, * Model and Platform data from navigator.userAgentData. * @param {boolean} [config.no_unload] Disables all unload handlers and the Unload beacons * @param {function} [config.log] Logger to use. Set to `null` to disable logging. * @param {function} [] Each plugin has its own section * * @returns {BOOMR} Boomerang object * * @memberof BOOMR */ init: function(config) { var i, k, properties = [ "autorun", "beacon_auth_key", "beacon_auth_token", "beacon_with_credentials", "beacon_disable_sendbeacon", "beacon_url", "beacon_url_force_https", "beacon_type", "site_domain", "strip_query_string", "user_ip", "same_site_cookie", "secure_cookie", "request_client_hints", "no_unload" ]; /* BEGIN_DEBUG */ BOOMR.utils.mark("init:start"); /* END_DEBUG */ BOOMR_check_doc_domain(); if (!config) { config = {}; } // ensure logging is setup properly (or null'd out for production) if (config.log !== undefined) { this.log = config.log; } if (!this.log) { this.log = function() {}; } if (!this.pageId) { // generate a random page ID for this page's lifetime this.pageId = BOOMR.utils.generateId(8); BOOMR.debug("Generated PageID: " + this.pageId); } if (config.primary && impl.handlers_attached) { return this; } if (typeof config.site_domain !== "undefined") { if (/:/.test(config.site_domain)) { // domains with : are not valid, fall back to the current hostname config.site_domain = w.location.hostname.toLowerCase(); } this.session.domain = config.site_domain; } if (BOOMR.session.enabled && typeof BOOMR.session.ID === "undefined") { BOOMR.session.ID = BOOMR.utils.generateUUID(); } // Set autorun if in config right now, as plugins that listen for page_ready // event may fire when they .init() if onload has already fired, and whether // or not we should fire page_ready depends on config.autorun. if (typeof config.autorun !== "undefined") { impl.autorun = config.autorun; } /* BEGIN_DEBUG */ BOOMR.utils.mark("init:plugins:start"); /* END_DEBUG */ for (k in this.plugins) { if (this.plugins.hasOwnProperty(k)) { // config[plugin].enabled has been set to false if (config[k] && config[k].hasOwnProperty("enabled") && config[k].enabled === false) { impl.disabled_plugins[k] = 1; if (typeof this.plugins[k].disable === "function") { this.plugins[k].disable(); } continue; } // plugin was previously disabled if (impl.disabled_plugins[k]) { // and has not been explicitly re-enabled if (!config[k] || !config[k].hasOwnProperty("enabled") || config[k].enabled !== true) { continue; } if (typeof this.plugins[k].enable === "function") { this.plugins[k].enable(); } // plugin is now enabled delete impl.disabled_plugins[k]; } // plugin exists and has an init method if (typeof this.plugins[k].init === "function") { try { /* BEGIN_DEBUG */ BOOMR.utils.mark("init:plugins:" + k + ":start"); /* END_DEBUG */ this.plugins[k].init(config); /* BEGIN_DEBUG */ BOOMR.utils.mark("init:plugins:" + k + ":end"); BOOMR.utils.measure( "init:plugins:" + k, "init:plugins:" + k + ":start", "init:plugins:" + k + ":end"); /* END_DEBUG */ } catch (err) { BOOMR.addError(err, k + ".init"); } } } } /* BEGIN_DEBUG */ BOOMR.utils.mark("init:plugins:end"); BOOMR.utils.measure( "init:plugins", "init:plugins:start", "init:plugins:end"); /* END_DEBUG */ for (i = 0; i < properties.length; i++) { if (config[properties[i]] !== undefined) { impl[properties[i]] = config[properties[i]]; } } // if client hints are requested, then squirrel away user agent data of architecture, model, and platform // version on impl for later use on the beacon if (impl.request_client_hints === true && w && w.navigator && w.navigator.userAgentData && typeof w.navigator.userAgentData.getHighEntropyValues === "function") { w.navigator.userAgentData.getHighEntropyValues(["architecture", "model", "platformVersion"]).then(function(ua) { impl.userAgentData = ua; }); } // if it's the first call to init (handlers aren't attached) and we're not asked to wait OR // it's the second init call (handlers are attached) and we were previously waiting // then we set up the page ready autorun functionality if ((!impl.handlers_attached && !config.wait) || (impl.handlers_attached && impl.waiting_for_config)) { // The developer can override onload by setting autorun to false if (!impl.onloadfired && (impl.autorun === undefined || impl.autorun !== false)) { if (BOOMR.hasBrowserOnloadFired()) { BOOMR.loadedLate = true; } BOOMR.attach_page_ready(BOOMR.page_ready_autorun); } impl.waiting_for_config = false; } // only attach handlers once if (impl.handlers_attached) { /* BEGIN_DEBUG */ BOOMR.utils.mark("init:end"); BOOMR.utils.measure( "init", "init:start", "init:end"); /* END_DEBUG */ return this; } if (config.wait) { impl.waiting_for_config = true; } // // Page handlers to attach once // // Listen for pageshow/load for the main Page Load BOOMR.attach_page_ready(function() { // if we're not using the loader snippet, save the onload time for // browsers that do not support NavigationTiming. // This will be later than onload if boomerang arrives late on the // page but it's the best we can do if (!BOOMR.t_onload) { BOOMR.t_onload = BOOMR.now(); } }); // Listen for DOMContentLoaded to fire the internal dom_loaded event BOOMR.utils.addListener(w, "DOMContentLoaded", function() { impl.fireEvent("dom_loaded"); }); // Fire and Listen for config events to refresh config for us and plugins BOOMR.fireEvent("config", config); BOOMR.subscribe("config", function(beaconConfig) { if (beaconConfig.beacon_url) { impl.beacon_url = beaconConfig.beacon_url; } }); // Listen for SPA navigations BOOMR.subscribe("spa_navigation", impl.spaNavigation, null, impl); // Listen for Visibility Change notifications (function() { var forms, iterator; if (visibilityChange !== undefined) { BOOMR.utils.addListener(d, visibilityChange, function() { impl.fireEvent("visibility_changed"); }); // save the current visibility state impl.lastVisibilityState = BOOMR.visibilityState(); BOOMR.subscribe("visibility_changed", function() { var visState = BOOMR.visibilityState(); // record the last time each visibility state occurred BOOMR.lastVisibilityEvent[visState] = BOOMR.now(); BOOMR.debug("Visibility changed from " + impl.lastVisibilityState + " to " + visState); // if we transitioned from prerender to hidden or visible, fire the prerender_to_visible event if (impl.lastVisibilityState === "prerender" && visState !== "prerender") { // note that we transitioned from prerender on the beacon for debugging BOOMR.addVar("vis.pre", "1"); // let all listeners know impl.fireEvent("prerender_to_visible"); } impl.lastVisibilityState = visState; }); } // Listen for mouseup events BOOMR.utils.addListener(d, "mouseup", impl.createCallbackHandler("click")); // Listen for FORM submissions forms = d.getElementsByTagName("form"); for (iterator = 0; iterator < forms.length; iterator++) { BOOMR.utils.addListener(forms[iterator], "submit", impl.createCallbackHandler("form_submit")); } // Listen for pagehide if (!w.onpagehide && w.onpagehide !== null) { // This must be the last one to fire // We only clear w on browsers that don't support onpagehide because // those that do are new enough to not have memory leak problems of // some older browsers BOOMR.utils.addListener(w, "unload", function() { BOOMR.window = w = null; }); } // if we were loaded in an IFRAME, try to keep track if the IFRAME was unloaded if (BOOMR.boomerang_frame !== BOOMR.window) { BOOMR.utils.addListener(BOOMR.boomerang_frame, "unload", impl.onFrameUnloaded); } }()); impl.handlers_attached = true; /* BEGIN_DEBUG */ BOOMR.utils.mark("init:end"); BOOMR.utils.measure( "init", "init:start", "init:end"); /* END_DEBUG */ return this; }, /** * Attach a callback to the `pageshow` or `onload` event if `onload` has not * been fired otherwise queue it to run immediately * * @param {function} cb Callback to run when `onload` fires or page is visible (`pageshow`) * * @memberof BOOMR */ attach_page_ready: function(cb) { if (BOOMR.hasBrowserOnloadFired()) { this.setImmediate(cb, null, null, BOOMR); } else { // Use `pageshow` if available since it will fire even if page came from a back-forward page cache. // Browsers that support `pageshow` will not fire `onload` if navigation was through a back/forward button // and the page was retrieved from back-forward cache. if (w.onpagehide || w.onpagehide === null) { BOOMR.utils.addListener(w, "pageshow", cb); } else { BOOMR.utils.addListener(w, "load", cb); } } }, /** * Sends the `page_ready` event only if `autorun` is still true after * {@link BOOMR.init} is called. * * @param {Event} ev Event * * @memberof BOOMR */ page_ready_autorun: function(ev) { if (impl.autorun) { BOOMR.page_ready(ev, true); } }, /** * Method that fires the {@link BOOMR#event:page_ready} event. Call this * only if you've set `autorun` to `false` when calling the {@link BOOMR.init} * method. You should call this method when you determine that your page * is ready to be used by your user. This will be the end-time used in * the page load time measurement. Optionally, you can pass a Unix Epoch * timestamp as a parameter or set the global `BOOMR_page_ready` var that will * be used as the end-time instead. * * @param {Event|number} [ev] Ready event or optional load event end timestamp if called manually * @param {boolean} auto True if called by `page_ready_autorun` * * @returns {BOOMR} Boomerang object * * @example * BOOMR.init({ autorun: false, ... }); * // wait until the page is ready, i.e. your view has loaded * BOOMR.page_ready(); * * @memberof BOOMR */ page_ready: function(ev, auto) { var tm_page_ready; // a number can be passed as the first argument if called manually which // will be used as the loadEventEnd time if (!auto && typeof ev === "number") { tm_page_ready = ev; ev = null; } if (!ev) { ev = w.event; } if (!ev) { ev = { name: "load" }; } // if we were called manually or global BOOMR_page_ready was set then // add loadEventEnd and note this was 'pr' on the beacon if (!auto) { ev.timing = ev.timing || {}; // use timestamp parameter or global BOOMR_page_ready if set, otherwise use // the current timestamp if (tm_page_ready) { ev.timing.loadEventEnd = tm_page_ready; } else if (typeof w.BOOMR_page_ready === "number") { ev.timing.loadEventEnd = w.BOOMR_page_ready; } else { ev.timing.loadEventEnd = BOOMR.now(); } BOOMR.addVar("pr", 1, true); } else if (typeof w.BOOMR_page_ready === "number") { ev.timing = ev.timing || {}; // the global BOOMR_page_ready will override our loadEventEnd ev.timing.loadEventEnd = w.BOOMR_page_ready; BOOMR.addVar("pr", 1, true); } if (impl.onloadfired) { return this; } // if we're prerendering, wait until complete before firing if (d.prerendering) { BOOMR.utils.addListener(d, "prerenderingchange", function() { // wait 500ms for other events (like LCP) to fire before we send the beacon setTimeout(function() { impl.fireEvent("page_ready", ev); }, 500); impl.onloadfired = true; }); } else { // not prerendering impl.fireEvent("page_ready", ev); impl.onloadfired = true; } return this; }, /** * Determines whether or not the page's `onload` event has fired * * @returns {boolean} True if page's onload was called */ hasBrowserOnloadFired: function() { var p = BOOMR.getPerformance(); // if the document is `complete` then the `onload` event has already occurred, we'll fire the callback // immediately. // // When `document.write` is used to replace the contents of the page and inject boomerang, the document // `readyState` will go from `complete` back to `loading` and then to `complete` again. The second transition // to `complete` doesn't fire a second `pageshow` event in some browsers (e.g. Safari). We need to check if // `performance.timing.loadEventStart` or `BOOMR_onload` has occurred to detect this scenario. Will not work for // older Safari that doesn't have NavTiming return ((d.readyState && d.readyState === "complete") || (p && p.timing && p.timing.loadEventStart > 0) || w.BOOMR_onload > 0); }, /** * Determines whether or not the page's `onload` event has fired, or * if `autorun` is false, whether {@link BOOMR.page_ready} was called. * * @returns {boolean} True if `onload` or {@link BOOMR.page_ready} were called * * @memberof BOOMR */ onloadFired: function() { return impl.onloadfired; }, /** * The callback function may return a falsy value to disconnect the observer * after it returns, or a truthy value to keep watching for mutations. If * the return value is numeric and greater than 0, then this will be the new timeout. * If it is boolean instead, then the timeout will not fire any more so * the caller MUST call disconnect() at some point * * @callback BOOMR~setImmediateCallback * @param {object} data The passed in `data` object * @param {object} cb_data The passed in `cb_data` object * @param {Error} callstack An Error object that holds the callstack for * when `setImmediate` was called, used to determine what called the callback */ /** * Defer the function `fn` until the next instant the browser is free from * user tasks. * * @param {BOOMR~setImmediateCallback} fn The callback function. * @param {object} [data] Any data to pass to the callback function * @param {object} [cb_data] Any passthrough data for the callback function. * This differs from `data` when `setImmediate` is called via an event * handler and `data` is the Event object * @param {object} [cb_scope] The scope of the callback function if it is a method of an object * * @returns nothing * * @memberof BOOMR */ setImmediate: function(fn, data, cb_data, cb_scope) { var cb, cstack; /* BEGIN_DEBUG */ // DEBUG: This is to help debugging, we'll see where setImmediate calls were made from if (typeof Error !== "undefined") { cstack = new Error(); cstack = cstack.stack ? cstack.stack.replace(/^Error/, "Called") : undefined; } /* END_DEBUG */ cb = function() { fn.call(cb_scope || null, data, cb_data || {}, cstack); cb = null; }; if (w.requestIdleCallback) { // set a timeout since rIC doesn't get called reliably in chrome headless w.requestIdleCallback(cb, {timeout: 1000}); } else if (w.setImmediate) { w.setImmediate(cb); } else { setTimeout(cb, 10); } }, /** * Gets the current time in milliseconds since the Unix Epoch (Jan 1 1970). * * In browsers that support `DOMHighResTimeStamp`, this will be replaced * by a function that adds `performance.now()` to `navigationStart` * (with milliseconds.microseconds resolution). * * @function * * @returns {TimeStamp} Milliseconds since Unix Epoch * * @memberof BOOMR */ now: (function() { return Date.now || function() { return new Date().getTime(); }; }()), /** * Gets the `window.performance` object of the root window. * * Checks vendor prefixes for older browsers (e.g. IE9). * * @returns {Performance|undefined} `window.performance` if it exists * * @memberof BOOMR */ getPerformance: function() { try { if (BOOMR.window) { if ("performance" in BOOMR.window && BOOMR.window.performance) { return BOOMR.window.performance; } // vendor-prefixed fallbacks return BOOMR.window.msPerformance || BOOMR.window.webkitPerformance || BOOMR.window.mozPerformance; } } catch (ignore) { // empty } }, /** * Allows us to force SameSite=None from a Boomerang plugin or a third party code. * * When this function is called then Boomerang won't honor "same_site_cookie" * configuration key and won't attempt to return the default value of SameSite=Lax . * * @memberof BOOMR */ forceSameSiteCookieNone: function() { impl.forced_same_site_cookie_none = true; }, /** * Get high resolution delta timestamp from time origin * * This function needs to approximate the time since the performance timeOrigin * or Navigation Timing API's `navigationStart` time. * If available, `performance.now()` can provide this value. * If not we either get the navigation start time from the RT plugin or * from `t_lstart` or `t_start`. Those values are subtracted from the current * time to derive a time since `navigationStart` value. * * @returns {float} Exact or approximate time since the time origin. */ hrNow: function() { var now, navigationStart, p = BOOMR.getPerformance(); if (p && p.now) { now = p.now(); } else { navigationStart = (BOOMR.plugins.RT && BOOMR.plugins.RT.navigationStart && BOOMR.plugins.RT.navigationStart()) || BOOMR.t_lstart || BOOMR.t_start; // if navigationStart is undefined, we'll be returning NaN now = BOOMR.now() - navigationStart; } return now; }, /** * Gets the `document.visibilityState`, or `visible` if Page Visibility * is not supported. * * @function * * @returns {string} Visibility state * * @memberof BOOMR */ visibilityState: (visibilityState === undefined ? function() { return "visible"; } : function() { return d[visibilityState]; }), /** * An mapping of visibliity event states to the latest time they happened * * @type {object} * * @memberof BOOMR */ lastVisibilityEvent: {}, /** * Registers a Boomerang event. * * @param {string} e_name Event name * * @returns {BOOMR} Boomerang object * * @memberof BOOMR */ registerEvent: function(e_name) { if (impl.events.hasOwnProperty(e_name)) { // already registered return this; } // create a new queue of handlers impl.events[e_name] = []; return this; }, /** * Disables boomerang from doing anything further: * 1. Clears event handlers (such as onload) * 2. Clears all event listeners * * @memberof BOOMR */ disable: function() { impl.clearEvents(); impl.clearListeners(); }, /** * Fires a Boomerang event * * @param {string} e_name Event name * @param {object} data Event payload * * @returns {BOOMR} Boomerang object * * @memberof BOOMR */ fireEvent: function(e_name, data) { return impl.fireEvent(e_name, data); }, /** * @callback BOOMR~subscribeCallback * @param {object} eventData Event data * @param {object} cb_data Callback data */ /** * Subscribes to a Boomerang event * * @param {string} e_name Event name, i.e. {@link BOOMR#event:page_ready}. * @param {BOOMR~subscribeCallback} fn Callback function * @param {object} cb_data Callback data, passed as the second parameter to the callback function * @param {object} cb_scope Callback scope. If set to an object, then the * callback function is called as a method of this object, and all * references to `this` within the callback function will refer to `cb_scope`. * @param {boolean} once Whether or not this callback should only be run once * * @returns {BOOMR} Boomerang object * * @memberof BOOMR */ subscribe: function(e_name, fn, cb_data, cb_scope, once) { var i, handler, ev; e_name = e_name.toLowerCase(); /* BEGIN_DEBUG */ BOOMR.utils.mark("subscribe"); BOOMR.utils.mark("subscribe:" + e_name); /* END_DEBUG */ // translate old names if (impl.translate_events[e_name]) { e_name = impl.translate_events[e_name]; } if (!impl.events.hasOwnProperty(e_name)) { // allow subscriptions before they're registered impl.events[e_name] = []; } ev = impl.events[e_name]; // don't allow a handler to be attached more than once to the same event for (i = 0; i < ev.length; i++) { handler = ev[i]; if (handler && handler.fn === fn && handler.cb_data === cb_data && handler.scope === cb_scope) { return this; } } ev.push({ fn: fn, cb_data: cb_data || {}, scope: cb_scope || null, once: once || false }); // attaching to page_ready after onload fires, so call soon if (e_name === "page_ready" && impl.onloadfired && impl.autorun) { this.setImmediate(fn, null, cb_data, cb_scope); } // Note: If no_unload is set, don't listen to any unload-style events. if (!impl.no_unload && (e_name === "page_unload" || e_name === "before_unload")) { // Keep track of how many pagehide/unload/beforeunload handlers we're registering impl.unloadEventsCount++; (function() { var unloadHandler = function boomerangUnloadHandler(evt) { if (impl.no_unload) { // may have been set after the initial load return; } if (fn) { fn.call(cb_scope, evt || w.event, cb_data); } // clear so this doesn't run twice fn = null; // If this was the last pagehide/unload/beforeunload handler, // we'll try to send the beacon immediately after it is done. // The beacon will only be sent if one of the handlers has queued it. if (++impl.unloadEventCalled === impl.unloadEventsCount) { BOOMR.real_sendBeacon(); } }; // // For modern browsers that support pagehide, listen to that event, // and do not listen to beforeunload/unload as they can break BFCache navigations. // if (w.onpagehide || w.onpagehide === null) { BOOMR.utils.addListener(w, "pagehide", unloadHandler); } else { // // For before_unload event in older browsers, attach handlers directly // to the unload and beforeunload events. Not all older browsers support // beforeunload. The first of the two to fire will clear so that the // second doesn't fire. // if (e_name === "page_unload") { BOOMR.utils.addListener(w, "unload", unloadHandler); } BOOMR.utils.addListener(w, "beforeunload", unloadHandler); } }()); } return this; }, /** * Logs an internal Boomerang error. * * If the {@link BOOMR.plugins.Errors} plugin is enabled, this data will * be compressed on the `err` beacon parameter. If not, it will be included * in uncompressed form on the `errors` parameter. * * @param {string|object} err Error * @param {string} [src] Source * @param {object} [extra] Extra data * * @memberof BOOMR */ addError: function BOOMR_addError(err, src, extra) { var str, E = BOOMR.plugins.Errors; /* BEGIN_DEBUG */ BOOMR.utils.mark("add_error"); /* END_DEBUG */ BOOMR.error("Boomerang caught error: " + err + ", src: " + src + ", extra: " + extra); // // Use the Errors plugin if it's enabled // if (E && E.is_supported()) { if (typeof err === "string") { E.send({ message: err, extra: extra, functionName: src, noStack: true }, E.VIA_APP, E.SOURCE_BOOMERANG); } else { if (typeof src === "string") { err.functionName = src; } if (typeof extra !== "undefined") { err.extra = extra; } E.send(err, E.VIA_APP, E.SOURCE_BOOMERANG); } return; } if (typeof err !== "string") { str = String(err); if (str.match(/^\[object/)) { str = err.name + ": " + (err.description || err.message).replace(/\r\n$/, ""); } err = str; } if (src !== undefined) { err = "[" + src + ":" + BOOMR.now() + "] " + err; } if (extra) { err += ":: " + extra; } if (impl.errors[err]) { impl.errors[err]++; } else { impl.errors[err] = 1; } }, /** * Determines if the specified Error is a Cross-Origin error. * * @param {string|object} err Error * * @returns {boolean} True if the Error is a Cross-Origin error. * * @memberof BOOMR */ isCrossOriginError: function(err) { // These are expected for cross-origin iframe access. // For IE and Edge, we'll also check the error number for non-English browsers return err.name === "SecurityError" || (err.name === "TypeError" && err.message === "Permission denied") || (err.name === "Error" && err.message && err.message.match(/^(Permission|Access is) denied/)) || // IE/Edge error number for "Permission Denied" err.number === -2146828218; }, /** * Add one or more parameters to the beacon. * * This method may either be called with a single object containing * key/value pairs, or with two parameters, the first is the variable * name and the second is its value. * * All names should be strings usable in a URL's query string. * * We recommend only using alphanumeric characters and underscores, but you * can use anything you like. * * Values should be strings (or numbers), and have the same restrictions * as names. * * Parameters will be on all subsequent beacons unless `singleBeacon` is * set. Early beacons will not clear vars that were set with `singleBeacon`. * * @param {string|object} name Variable name * @param {string|object} [val] Value. If the first parameter is an object, this * becomes the singleBeacon parameter. * @param {boolean} [singleBeacon=false] Whether or not to add to a single beacon * or all beacons. * * @returns {BOOMR} Boomerang object * * @example * BOOMR.addVar("page_id", 123); * BOOMR.addVar({"page_id": 123, "user_id": "Person1"}); * * @memberof BOOMR */ addVar: function(name, value, singleBeacon) { /* BEGIN_DEBUG */ BOOMR.utils.mark("add_var"); /* END_DEBUG */ if (typeof name === "string") { impl.vars[name] = value; if (singleBeacon) { impl.singleBeaconVars[name] = 1; } } else if (typeof name === "object") { var o = name, k; for (k in o) { if (o.hasOwnProperty(k)) { impl.vars[k] = o[k]; // For object-set, the second parameter (or third) can be // true to force singleBeacon. If so, remove this // after the first beacon if (value || singleBeacon) { impl.singleBeaconVars[k] = 1; } } } } return this; }, /** * Appends data to a beacon. * * If the value already exists, a comma is added and the new data is applied. * * @param {string} name Variable name * @param {string} val Value * * @returns {BOOMR} Boomerang object * * @memberof BOOMR */ appendVar: function(name, value) { var existing = BOOMR.getVar(name) || ""; if (existing) { existing += ","; } BOOMR.addVar(name, existing + value); return this; }, /** * Removes one or more variables from the beacon URL. This is useful within * a plugin to reset the values of parameters that it is about to set. * * Plugins can also use this in the {@link BOOMR#event:beacon} event to clear * any variables that should only live on a single beacon. * * This method accepts either a list of variable names, or a single * array containing a list of variable names. * * @param {string[]|string} name Variable name or list * * @returns {BOOMR} Boomerang object * * @memberof BOOMR */ removeVar: function(arg0) { var i, params; if (!arguments.length) { return this; } if (arguments.length === 1 && BOOMR.utils.isArray(arg0)) { params = arg0; } else { params = arguments; } for (i = 0; i < params.length; i++) { if (impl.vars.hasOwnProperty(params[i])) { delete impl.vars[params[i]]; } } return this; }, /** * Determines whether or not the beacon has the specified variable. * * @param {string} name Variable name * * @returns {boolean} True if the variable is set. * * @memberof BOOMR */ hasVar: function(name) { return impl.vars.hasOwnProperty(name); }, /** * Gets the specified variable. * * @param {string} name Variable name * * @returns {object|undefined} Variable, or undefined if it isn't set * * @memberof BOOMR */ getVar: function(name) { return impl.vars[name]; }, /** * Sets a variable's priority in the beacon URL. * -1 = beginning of the URL * 0 = middle of the URL (default) * 1 = end of the URL * * @param {string} name Variable name * @param {number} pri Priority (-1 or 1) * * @returns {BOOMR} Boomerang object * * @memberof BOOMR */ setVarPriority: function(name, pri) { if (typeof pri !== "number" || Math.abs(pri) !== 1) { return this; } impl.varPriority[pri][name] = 1; return this; }, /** * Sets the Referrers variable. * * @param {string} r Referrer from the document.referrer * * @memberof BOOMR */ setReferrer: function(r) { // document.referrer impl.r = r; }, /** * Starts a timer for a dynamic request. * * Once the named request has completed, call `loaded()` to send a beacon * with the duration. * * @example * var timer = BOOMR.requestStart("my-timer"); * // do stuff * timer.loaded(); * * @param {string} name Timer name * * @returns {object} An object with a `.loaded()` function that you can call * when the dynamic timer is complete. * * @memberof BOOMR */ requestStart: function(name) { var t_start = BOOMR.now(); BOOMR.plugins.RT.startTimer("xhr_" + name, t_start); return { loaded: function(data) { BOOMR.responseEnd(name, t_start, data); } }; }, /** * Determines if Boomerang can send a beacon. * * Queryies all plugins to see if they implement `readyToSend()`, * and if so, that they return `true`. * * If not, the beacon cannot be sent. * * @returns {boolean} True if Boomerang can send a beacon * * @memberof BOOMR */ readyToSend: function() { var plugin; for (plugin in this.plugins) { if (this.plugins.hasOwnProperty(plugin)) { if (impl.disabled_plugins[plugin]) { continue; } if (typeof this.plugins[plugin].readyToSend === "function" && this.plugins[plugin].readyToSend() === false) { BOOMR.debug("Plugin " + plugin + " is not ready to send"); return false; } } } return true; }, /** * Sends a beacon for a dynamic request. * * @param {string|object} name Timer name or timer object data. * @param {string} [name.initiator] Initiator, such as `xhr` or `spa` * @param {string} [name.url] URL of the request * @param {TimeStamp} t_start Start time * @param {object} data Request data * @param {TimeStamp} t_end End time * * @memberof BOOMR */ responseEnd: function(name, t_start, data, t_end) { // take the now timestamp for start and end, if unspecified, in case we delay this beacon t_start = typeof t_start === "number" ? t_start : BOOMR.now(); t_end = typeof t_end === "number" ? t_end : BOOMR.now(); // wait until all plugins are ready to send if (!BOOMR.readyToSend()) { BOOMR.debug("Attempted to call responseEnd before all plugins were Ready to Send, trying again..."); // try again later setTimeout(function() { BOOMR.responseEnd(name, t_start, data, t_end); }, 1000); return; } // Wait until we've sent the Page Load beacon first if (!BOOMR.hasSentPageLoadBeacon() && !BOOMR.utils.inArray(name.initiator, BOOMR.constants.BEACON_TYPE_SPAS)) { // wait for a beacon, then try again BOOMR.subscribe("page_load_beacon", function() { BOOMR.responseEnd(name, t_start, data, t_end); }, null, BOOMR, true); return; } // Ensure we don't have two beacons trying to send data at the same time if (impl.beaconInQueue) { // wait until the beacon is sent, then try again BOOMR.subscribe("beacon", function() { BOOMR.responseEnd(name, t_start, data, t_end); }, null, BOOMR, true); return; } // Lock the beacon queue impl.beaconInQueue = true; if (typeof name === "object") { if (!name.url) { BOOMR.debug("BOOMR.responseEnd: First argument must have a url property if it's an object"); return; } impl.fireEvent("xhr_load", name); } else { // flush out any queue'd beacons before we set the Page Group // and timers BOOMR.real_sendBeacon(); BOOMR.addVar("xhr.pg", name, true); BOOMR.plugins.RT.startTimer("xhr_" + name, t_start); impl.fireEvent("xhr_load", { name: "xhr_" + name, data: data, timing: { loadEventEnd: t_end } }); } }, // // uninstrumentXHR, instrumentXHR, uninstrumentFetch and instrumentFetch // are stubs that will be replaced by auto-xhr.js if active. // /** * Undo XMLHttpRequest instrumentation and reset the original `XMLHttpRequest` * object * * This is implemented in `plugins/auto-xhr.js` {@link BOOMR.plugins.AutoXHR}. * * @memberof BOOMR */ uninstrumentXHR: function() { }, /** * Instrument all requests made via XMLHttpRequest to send beacons. * * This is implemented in `plugins/auto-xhr.js` {@link BOOMR.plugins.AutoXHR}. * * @memberof BOOMR */ instrumentXHR: function() { }, /** * Undo fetch instrumentation and reset the original `fetch` * function * * This is implemented in `plugins/auto-xhr.js` {@link BOOMR.plugins.AutoXHR}. * * @memberof BOOMR */ uninstrumentFetch: function() { }, /** * Instrument all requests made via fetch to send beacons. * * This is implemented in `plugins/auto-xhr.js` {@link BOOMR.plugins.AutoXHR}. * * @memberof BOOMR */ instrumentFetch: function() { }, /** * Request boomerang to send its beacon with all queued beacon data * (via {@link BOOMR.addVar}). * * Boomerang may ignore this request. * * When this method is called, boomerang checks all plugins. If any * plugin has not completed its checks (ie, the plugin's `is_complete()` * method returns `false`, then this method does nothing. * * If all plugins have completed, then this method fires the * {@link BOOMR#event:before_beacon} event with all variables that will be * sent on the beacon. * * After all {@link BOOMR#event:before_beacon} handlers return, this method * checks if a `beacon_url` has been configured and if there are any * beacon parameters to be sent. If both are true, it fires the beacon. * * The {@link BOOMR#event:beacon} event is then fired. * * `sendBeacon()` should be called any time a plugin goes from * `is_complete() = false` to `is_complete = true` so the beacon is * sent. * * The actual beaconing is handled in {@link BOOMR.real_sendBeacon} after * a short delay (via {@link BOOMR.setImmediate}). If other calls to * `sendBeacon` happen before {@link BOOMR.real_sendBeacon} is called, * those calls will be discarded (so it's OK to call this in quick * succession). * * @param {string} [beacon_url_override] Beacon URL override * * @memberof BOOMR */ sendBeacon: function(beacon_url_override) { // This plugin wants the beacon to go somewhere else, // so update the location if (beacon_url_override) { impl.beacon_url_override = beacon_url_override; } if (!impl.beaconQueued) { impl.beaconQueued = true; BOOMR.setImmediate(BOOMR.real_sendBeacon, null, null, BOOMR); } return true; }, /** * Sends a beacon when the beacon queue is empty. * * @param {object} params Beacon parameters to set * @param {function} callback Callback to run when the queue is ready * @param {object} that Function to apply callback to */ sendBeaconWhenReady: function(params, callback, that) { // If we're already sending a beacon, wait until the queue is empty if (impl.beaconInQueue) { // wait until the beacon is sent, then try again BOOMR.subscribe("beacon", function() { BOOMR.sendBeaconWhenReady(params, callback, that); }, null, BOOMR, true); return; } // Lock the beacon queue impl.beaconInQueue = true; // add all parameters for (var paramName in params) { if (params.hasOwnProperty(paramName)) { // add this data to a single beacon BOOMR.addVar(paramName, params[paramName], true); } } // run the callback if (typeof callback === "function" && typeof that !== "undefined") { callback.apply(that); } // send the beacon BOOMR.sendBeacon(); }, /** * Sends all beacon data. * * This function should be called directly any time a "new" beacon is about * to be constructed. For example, if you're creating a new XHR or other * custom beacon, you should ensure the existing beacon data is flushed * by calling `BOOMR.real_sendBeacon();` first. * * @memberof BOOMR */ real_sendBeacon: function() { var k, form, url, errors = [], params = [], paramsJoined, varsSent = {}; if (!impl.beaconQueued) { return false; } /* BEGIN_DEBUG */ BOOMR.utils.mark("send_beacon:start"); /* END_DEBUG */ impl.beaconQueued = false; BOOMR.debug("Checking if we can send beacon"); // At this point someone is ready to send the beacon. We send // the beacon only if all plugins have finished doing what they // wanted to do for (k in this.plugins) { if (this.plugins.hasOwnProperty(k)) { if (impl.disabled_plugins[k]) { continue; } if (!this.plugins[k].is_complete(impl.vars)) { BOOMR.debug("Plugin " + k + " is not complete, deferring beacon send"); // if an Early beacon is blocked, then we'll cancel it. // By removing the `early` param, the beacon params will be merged // with the following load beacon. delete impl.vars.early; return false; } } } // Sanity test that the browser is still available (and not shutting down) if (!window || !window.Image || !window.navigator || !BOOMR.window) { BOOMR.debug("DOM not fully available, not sending a beacon"); return false; } // For SPA apps, don't strip hashtags as some SPA frameworks use #s for tracking routes // instead of History pushState() APIs. Use d.URL instead of location.href because of a // Safari bug. var isSPA = BOOMR.utils.inArray(impl.vars["http.initiator"], BOOMR.constants.BEACON_TYPE_SPAS); var isPageLoad = typeof impl.vars["http.initiator"] === "undefined" || isSPA; if (!impl.vars.pgu) { impl.vars.pgu = isSPA ? d.URL : d.URL.replace(/#.*/, ""); } impl.vars.pgu = BOOMR.utils.cleanupURL(impl.vars.pgu); // Use the current document.URL if it hasn't already been set, or for SPA apps, // on each new beacon (since each SPA soft navigation might change the URL) if (!impl.vars.u || isSPA) { impl.vars.u = impl.vars.pgu; } if (impl.vars.pgu === impl.vars.u) { delete impl.vars.pgu; } // Add cleaned-up referrer URLs to the beacon, if available if (impl.r) { impl.vars.r = BOOMR.utils.cleanupURL(impl.r); } else { delete impl.vars.r; } impl.vars.v = BOOMR.version; // Snippet version, if available if (BOOMR.snippetVersion) { impl.vars.sv = BOOMR.snippetVersion; } // Snippet method is IFRAME if not specified (pre-v12 snippets) impl.vars.sm = BOOMR.snippetMethod || "i"; if (BOOMR.session.enabled) { impl.vars["rt.si"] = BOOMR.session.ID + "-" + Math.round(BOOMR.session.start / 1000).toString(36); impl.vars["rt.ss"] = BOOMR.session.start; if (typeof impl.vars.early === "undefined") { // make sure Session Length is always at least 1 for non-Early beacons impl.vars["rt.sl"] = BOOMR.session.length >= 1 ? BOOMR.session.length : 1; } else { impl.vars["rt.sl"] = BOOMR.session.length; } } else { BOOMR.removeVar("rt.si", "rt.ss", "rt.sl"); } if (BOOMR.visibilityState()) { impl.vars["vis.st"] = BOOMR.visibilityState(); if (BOOMR.lastVisibilityEvent.visible) { impl.vars["vis.lv"] = BOOMR.now() - BOOMR.lastVisibilityEvent.visible; } if (BOOMR.lastVisibilityEvent.hidden) { impl.vars["vis.lh"] = BOOMR.now() - BOOMR.lastVisibilityEvent.hidden; } } var platform = ""; if (navigator.userAgentData && typeof navigator.userAgentData.platform === "string") { platform = navigator.userAgentData.platform; } else { platform = navigator.platform; } impl.vars["ua.plt"] = platform; impl.vars["ua.vnd"] = navigator.vendor; // if userAgentData exists, then store on the beacon if (impl.userAgentData) { impl.vars["ua.arch"] = impl.userAgentData.architecture; impl.vars["ua.model"] = impl.userAgentData.model; impl.vars["ua.pltv"] = impl.userAgentData.platformVersion; } if (this.pageId) { impl.vars.pid = this.pageId; } // add beacon number impl.vars.n = ++this.beaconsSent; if (w !== window) { impl.vars["if"] = ""; } for (k in impl.errors) { if (impl.errors.hasOwnProperty(k)) { errors.push(k + (impl.errors[k] > 1 ? " (*" + impl.errors[k] + ")" : "")); } } if (errors.length > 0) { impl.vars.errors = errors.join("\n"); } impl.errors = {}; // If we reach here, all plugins have completed impl.fireEvent("before_beacon", impl.vars); // clone the vars object for two reasons: first, so all listeners of // 'beacon' get an exact clone (in case listeners are doing // BOOMR.removeVar), and second, to help build our priority list of vars. for (k in impl.vars) { if (impl.vars.hasOwnProperty(k)) { varsSent[k] = impl.vars[k]; } } BOOMR.removeVar(["qt", "pgu"]); if (typeof impl.vars.early === "undefined") { // remove any vars that should only be on a single beacon. // Early beacons don't clear vars even if flagged as `singleBeacon` so // that they can be re-sent on the next normal beacon for (var singleVarName in impl.singleBeaconVars) { if (impl.singleBeaconVars.hasOwnProperty(singleVarName)) { BOOMR.removeVar(singleVarName); } } // clear single beacon vars list impl.singleBeaconVars = {}; // keep track of page load beacons if (!impl.hasSentPageLoadBeacon && isPageLoad) { impl.hasSentPageLoadBeacon = true; // let this beacon go out first BOOMR.setImmediate(function() { impl.fireEvent("page_load_beacon", varsSent); }); } } // Stop at this point if we are rate limited if (BOOMR.session.rate_limited) { BOOMR.debug("Skipping because we're rate limited"); return false; } // mark that we're no longer sending a beacon now, as those // paying attention to this will trigger at the beacon event impl.beaconInQueue = false; // send the beacon data BOOMR.sendBeaconData(varsSent); /* BEGIN_DEBUG */ BOOMR.utils.mark("send_beacon:end"); BOOMR.utils.measure( "send_beacon", "send_beacon:start", "send_beacon:end"); /* END_DEBUG */ return true; }, /** * Sends beacon data via the Beacon API, XHR or Image * * @param {object} data Data */ sendBeaconData: function(data) { var urlFirst = [], urlLast = [], params, paramsJoined, url, img, useImg = true, xhr, ret; BOOMR.debug("Ready to send beacon: " + BOOMR.utils.objectToString(data)); // Use the override URL if given impl.beacon_url = impl.beacon_url_override || impl.beacon_url; // Check that the beacon_url was set first if (!impl.beacon_url) { BOOMR.debug("No beacon URL, so skipping."); return false; } if (!impl.beaconUrlAllowed(impl.beacon_url)) { BOOMR.debug("Beacon URL not allowed: " + impl.beacon_url); return false; } // Check that we have data to send if (BOOMR.utils.isObjectEmpty(data)) { return false; } // If we reach here, we've figured out all of the beacon data we'll send. impl.fireEvent("beacon", data); // get high- and low-priority variables first, which remove any of // those vars from data urlFirst = this.getVarsOfPriority(data, -1); urlLast = this.getVarsOfPriority(data, 1); // merge the 3 lists params = urlFirst.concat(this.getVarsOfPriority(data, 0), urlLast); paramsJoined = params.join("&"); // If beacon_url is protocol relative, make it https only if (impl.beacon_url_force_https && impl.beacon_url.match(/^\/\//)) { impl.beacon_url = "https:" + impl.beacon_url; } // if there are already url parameters in the beacon url, // change the first parameter prefix for the boomerang url parameters to & url = impl.beacon_url + ((impl.beacon_url.indexOf("?") > -1) ? "&" : "?") + paramsJoined; // // Try to send an IMG beacon if possible (which is the most compatible), // otherwise send an XHR beacon if the URL length is longer than 2,000 bytes. // if (impl.beacon_type === "GET") { useImg = true; if (url.length > BOOMR.constants.MAX_GET_LENGTH) { ((window.console && (console.warn || console.log)) || function() {})( "Boomerang: Warning: Beacon may not be sent via GET due to payload size > 2000 bytes" ); } } else if (impl.beacon_type === "POST" || url.length > BOOMR.constants.MAX_GET_LENGTH) { // switch to a XHR beacon if the the user has specified a POST OR GET length is too long useImg = false; } // // Try the sendBeacon API first. // But if beacon_type is set to "GET", dont attempt // sendBeacon API call // if (w && w.navigator && typeof w.navigator.sendBeacon === "function" && BOOMR.utils.isNative(w.navigator.sendBeacon) && typeof w.Blob === "function" && impl.beacon_type !== "GET" && // As per W3C, The sendBeacon method does not provide ability to pass any // header other than 'Content-Type'. So if we need to send data with // 'Authorization' header, we need to fallback to good old xhr. typeof impl.beacon_auth_token === "undefined" && !impl.beacon_disable_sendbeacon) { // note we're using sendBeacon with &sb=1 var blobData = new w.Blob([paramsJoined + "&sb=1"], { type: "application/x-www-form-urlencoded" }); if (w.navigator.sendBeacon(impl.beacon_url, blobData)) { return true; } // sendBeacon was not successful, try Image or XHR beacons } // If we don't have XHR available, force an image beacon and hope // for the best if (!BOOMR.orig_XMLHttpRequest && (!w || !w.XMLHttpRequest)) { useImg = true; } if (useImg) { // // Image beacon // // just in case Image isn't a valid constructor try { img = new Image(); } catch (e) { BOOMR.debug("Image is not a constructor, not sending a beacon"); return false; } img.src = url; } else { // // XHR beacon // // Send a form-encoded XHR POST beacon xhr = new (BOOMR.window.orig_XMLHttpRequest || BOOMR.orig_XMLHttpRequest || BOOMR.window.XMLHttpRequest)(); try { this.sendXhrPostBeacon(xhr, paramsJoined); } catch (e) { // if we had an exception with the window XHR object, try our IFRAME XHR xhr = new BOOMR.boomerang_frame.XMLHttpRequest(); this.sendXhrPostBeacon(xhr, paramsJoined); } } return true; }, /** * Determines whether or not a Page Load beacon has been sent. * * @returns {boolean} True if a Page Load beacon has been sent. * * @memberof BOOMR */ hasSentPageLoadBeacon: function() { return impl.hasSentPageLoadBeacon; }, /** * Sends a beacon via XMLHttpRequest * * @param {object} xhr XMLHttpRequest object * @param {object} [paramsJoined] XMLHttpRequest.send() argument * * @memberof BOOMR */ sendXhrPostBeacon: function(xhr, paramsJoined) { xhr.open("POST", impl.beacon_url); xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); if (typeof impl.beacon_auth_token !== "undefined") { if (typeof impl.beacon_auth_key === "undefined") { impl.beacon_auth_key = "Authorization"; } xhr.setRequestHeader(impl.beacon_auth_key, impl.beacon_auth_token); } if (impl.beacon_with_credentials) { xhr.withCredentials = true; } xhr.send(paramsJoined); }, /** * Gets all variables of the specified priority * * @param {object} vars Variables (will be modified for pri -1 and 1) * @param {number} pri Priority (-1, 0, or 1) * * @return {string[]} Array of URI-encoded vars * * @memberof BOOMR */ getVarsOfPriority: function(vars, pri) { var name, url = [], // if we were given a priority, iterate over that list // else iterate over vars iterVars = (pri !== 0 ? impl.varPriority[pri] : vars); for (name in iterVars) { // if this var is set, add it to our URL array if (iterVars.hasOwnProperty(name) && vars.hasOwnProperty(name)) { url.push(this.getUriEncodedVar(name, typeof vars[name] === "undefined" ? "" : vars[name])); // remove this name from vars so it isn't also added // to the non-prioritized list when pri=0 is called if (pri !== 0) { delete vars[name]; } } } return url; }, /** * Gets a URI-encoded name/value pair. * * @param {string} name Name * @param {string} value Value * * @returns {string} URI-encoded string * * @memberof BOOMR */ getUriEncodedVar: function(name, value) { if (value === undefined || value === null) { value = ""; } if (typeof value === "object") { value = BOOMR.utils.serializeForUrl(value); } var result = encodeURIComponent(name) + "=" + encodeURIComponent(value); return result; }, /** * Gets the latest ResourceTiming entry for the specified URL. * * Default sort order is chronological startTime. * * @param {string} url Resource URL * @param {function} [sort] Sort the entries before returning the last one * @param {function} [filter] Filter the entries. Will be applied before sorting * * @returns {PerformanceEntry|undefined} Entry, or undefined if ResourceTiming is not * supported or if the entry doesn't exist * * @memberof BOOMR */ getResourceTiming: function(url, sort, filter) { var entries, p = BOOMR.getPerformance(); try { if (p && typeof p.getEntriesByName === "function") { entries = p.getEntriesByName(url); if (!entries || !entries.length) { return; } if (typeof filter === "function") { entries = BOOMR.utils.arrayFilter(entries, filter); if (!entries || !entries.length) { return; } } if (entries.length > 1 && typeof sort === "function") { entries.sort(sort); } return entries[entries.length - 1]; } } catch (e) { BOOMR.warn("getResourceTiming:" + e); } }, /** * Determines whether beacon data is for a Page Load beacon, or not. * * Page Load beacons include regular Page Loads, SPA Hard or SPA Soft beacons. * * We also consider an Aborted Load beacon a Page Load. * * @param {object} data Beacon Data * @returns {boolean} True if beacon data is for a Page Load beacon */ isPageLoadBeacon: function(data) { return (typeof data["rt.quit"] === "undefined" || typeof data["rt.abld"] !== "undefined") && (typeof data["http.initiator"] === "undefined" || BOOMR.utils.inArray(data["http.initiator"], BOOMR.constants.BEACON_TYPE_SPAS)); }, /** * Returns the timestamp offset by the Prerendered value, if it happened * * @param {number} ts Timestamp * * @returns {number} Offset timestamp if Prerendered, original timestamp if not */ getPrerenderedOffset: function(ts) { var actSt = BOOMR.getActivationStart(); ts = Math.floor(ts); if (actSt === false) { // Prerender not supported or did not happen, return original timestamp return ts; } else if (actSt !== null) { // integer offset, return the difference var newTs = ts - actSt; // return the offset (at least 1ms) return newTs >= 0 ? Math.max(1, newTs) : ts; } }, /** * Gets the Activation Start time for Prerendered navigations. * * @returns {false|number} false if Prerender isn't supported, false if there was no Prerender, or a timestamp * of the Activation if there was one */ getActivationStart: function() { if (impl.prerenderedOffset !== null) { // we've previously calculated, return the value return impl.prerenderedOffset; } // we're going to now check/set it, default to false (not supported or didn't happen) impl.prerenderedOffset = false; // check if prerendering is supported first if (typeof d.prerendering !== "boolean") { // not supported, return false return impl.prerenderedOffset; } // check if there was an activation via NavigationTiming var p = BOOMR.getPerformance(); if (p && typeof p.getEntriesByType === "function") { // get the navigation entry var navEntry = p.getEntriesByType("navigation")[0]; if (navEntry && navEntry.activationStart) { impl.prerenderedOffset = Math.floor(navEntry.activationStart); } } return impl.prerenderedOffset; } /* BEGIN_DEBUG */, /** * Sets the list of allowed Beacon URLs * * @param {string[]} urls List of string regular expressions */ setBeaconUrlsAllowed: function(urls) { impl.beacon_urls_allowed = urls; } /* END_DEBUG */ }; // if not already set already on BOOMR, determine the URL if (!BOOMR.url) { boomr.url = boomr.utils.getMyURL(); } else { // canonicalize the URL var a = BOOMR.window.document.createElement("a"); a.href = BOOMR.url; boomr.url = a.href; } delete BOOMR_start; /** * @global * @type {TimeStamp} * @name BOOMR_lstart * @desc * This variable is added to the global scope (`window`) until Boomerang loads, * at which point it is removed. * * Time the loader script started fetching boomerang.js (if the asynchronous * loader snippet is used). */ if (typeof BOOMR_lstart === "number") { /** * Time the loader script started fetching boomerang.js (if using the * asynchronous loader snippet) (`BOOMR_lstart`) * @type {TimeStamp} * * @memberof BOOMR */ boomr.t_lstart = BOOMR_lstart; delete BOOMR_lstart; } else if (typeof BOOMR.window.BOOMR_lstart === "number") { boomr.t_lstart = BOOMR.window.BOOMR_lstart; } /** * This variable is added to the global scope (`window`). * * Time the `window.onload` event fired (if using the asynchronous loader snippet). * * This timestamp is logged in the case boomerang.js loads after the onload event * for browsers that don't support NavigationTiming. * * @global * @name BOOMR_onload * @type {TimeStamp} */ if (typeof BOOMR.window.BOOMR_onload === "number") { /** * Time the `window.onload` event fired (if using the asynchronous loader snippet). * * This timestamp is logged in the case boomerang.js loads after the onload event * for browsers that don't support NavigationTiming. * * @type {TimeStamp} * @memberof BOOMR */ boomr.t_onload = BOOMR.window.BOOMR_onload; } (function() { var make_logger; if (typeof console === "object" && console.log !== undefined) { /** * Logs the message to the console * * @param {string} m Message * @param {string} l Log level * @param {string} [s] Source * * @function log * * @memberof BOOMR */ boomr.log = function(m, l, s) { console.log("(" + BOOMR.now() + ") " + "{" + BOOMR.pageId + "}" + ": " + s + ": [" + l + "] " + m); }; } else { // NOP for browsers that don't support it boomr.log = function() {}; } make_logger = function(l) { return function(m, s) { this.log(m, l, "boomerang" + (s ? "." + s : "")); return this; }; }; /** * Logs debug messages to the console * * Debug messages are stripped out of production builds. * * @param {string} m Message * @param {string} [s] Source * * @function debug * * @memberof BOOMR */ boomr.debug = make_logger("debug"); /** * Logs info messages to the console * * @param {string} m Message * @param {string} [s] Source * * @function info * * @memberof BOOMR */ boomr.info = make_logger("info"); /** * Logs warning messages to the console * * @param {string} m Message * @param {string} [s] Source * * @function warn * * @memberof BOOMR */ boomr.warn = make_logger("warn"); /** * Logs error messages to the console * * @param {string} m Message * @param {string} [s] Source * * @function error * * @memberof BOOMR */ boomr.error = make_logger("error"); }()); // If the browser supports performance.now(), swap that in for BOOMR.now try { var p = boomr.getPerformance(); if (p && typeof p.now === "function" && // #545 handle bogus performance.now from broken shims /\[native code\]/.test(String(p.now)) && p.timing && p.timing.navigationStart) { boomr.now = function() { return Math.round(p.now() + p.timing.navigationStart); }; } } catch (ignore) { // empty } impl.checkLocalStorageSupport(); (function() { var ident; for (ident in boomr) { if (boomr.hasOwnProperty(ident)) { BOOMR[ident] = boomr[ident]; } } if (!BOOMR.xhr_excludes) { /** * URLs to exclude from automatic `XMLHttpRequest` instrumentation. * * You can put any of the following in it: * * A full URL * * A hostname * * A path * * @example * BOOMR = window.BOOMR || {}; * BOOMR.xhr_excludes = { * "mysite.com": true, * "/dashboard/": true, * "https://mysite.com/dashboard/": true * }; * * @memberof BOOMR */ BOOMR.xhr_excludes = {}; } }()); /* BEGIN_DEBUG */ /* * This block reports on overridden functions on `window` and properties on `document` using `BOOMR.warn()`. * To enable, add `overridden` with a value of `true` to the query string. */ (function() { /** * Checks a window for overridden functions. * * @param {Window} win The window object under test * * @returns {Array} Array of overridden function names */ BOOMR.checkWindowOverrides = function(win) { if (!Object.getOwnPropertyNames) { return []; } var freshWindow, objects, overridden = []; function setup() { var iframe = d.createElement("iframe"); iframe.style.display = "none"; iframe.src = "javascript:false"; // eslint-disable-line no-script-url d.getElementsByTagName("script")[0].parentNode.appendChild(iframe); freshWindow = iframe.contentWindow; objects = Object.getOwnPropertyNames(freshWindow); } function teardown() { iframe.parentNode.removeChild(iframe); } function checkWindowObject(objectKey) { if (isNonNative(objectKey)) { overridden.push(objectKey); } } function isNonNative(key) { var split = key.split("."), fn = win, results = []; while (fn && split.length) { try { fn = fn[split.shift()]; } catch (e) { return false; } } return typeof fn === "function" && !isNativeFunction(fn, key); } function isNativeFunction(fn, str) { if (str === "console.assert" || str === "Function.prototype" || str.indexOf("onload") >= 0 || str.indexOf("onbeforeunload") >= 0 || str.indexOf("onerror") >= 0 || str.indexOf("onload") >= 0 || str.indexOf("NodeFilter") >= 0) { return true; } return fn.toString && !fn.hasOwnProperty("toString") && /\[native code\]/.test(String(fn)); } setup(); for (var objectIndex = 0; objectIndex < objects.length; objectIndex++) { var objectKey = objects[objectIndex]; if (objectKey === "window" || objectKey === "self" || objectKey === "top" || objectKey === "parent" || objectKey === "frames") { continue; } if (freshWindow[objectKey] && (typeof freshWindow[objectKey] === "object" || typeof freshWindow[objectKey] === "function")) { checkWindowObject(objectKey); var propertyNames = []; try { propertyNames = Object.getOwnPropertyNames(freshWindow[objectKey]); } catch (e) { ; } for (var i = 0; i < propertyNames.length; i++) { checkWindowObject([objectKey, propertyNames[i]].join(".")); } if (freshWindow[objectKey].prototype) { propertyNames = Object.getOwnPropertyNames(freshWindow[objectKey].prototype); for (var i = 0; i < propertyNames.length; i++) { checkWindowObject([objectKey, "prototype", propertyNames[i]].join(".")); } } } } return overridden; }; /** * Checks a document for overridden properties. * * @param {HTMLDocument} doc The document object under test * * @returns {Array} Array of overridden properties names */ BOOMR.checkDocumentOverrides = function(doc) { return BOOMR.utils.arrayFilter(["readyState", "domain", "hidden", "URL", "cookie"], function(key) { return doc.hasOwnProperty(key); }); }; if (BOOMR.utils.getQueryParamValue("overridden") === "true" && w && w.Object && Object.getOwnPropertyNames) { var overridden = [] .concat(BOOMR.checkWindowOverrides(w)) .concat(BOOMR.checkDocumentOverrides(d)); if (overridden.length > 0) { BOOMR.warn("overridden: " + overridden.sort()); } } })(); /* END_DEBUG */ dispatchEvent("onBoomerangLoaded", { "BOOMR": BOOMR }, true); }(window)); // end of boomerang beaconing section