Files
demo-contact/ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/js/boomerang.js
2026-02-03 10:44:31 +08:00

5361 lines
159 KiB
JavaScript

/**
* @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: <MutationObserver>, timer: <Timeout Timer if any> }`
* - 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 span h1`
* * `<div id="test"> <span class="first">` -> `div#test span.first`
* * `<span class="second"> <div id="test"> <span class="first">` -> `div#test span.first`
* * `<span class="second"> <div id="test"> <span class="first second">` -> `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} [<plugins>] 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