boomerangjs
Version:
boomerang always comes back, except when it hits something
1,694 lines (1,469 loc) • 163 kB
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