boomerangjs
Version:
boomerang always comes back, except when it hits something
378 lines (324 loc) • 10.8 kB
JavaScript
/**
* This plugin delays the Page Load beacon until all specified IFRAMEs have
* also signalled they are also fully loaded.
*
* For information on how to include this plugin, see the {@tutorial building} tutorial.
*
* ## Usage
* In the parent page the following configuration should be used:
*
* BOOMR.init({
* ...
* autorun: false,
* IFrameDelay: {
* enabled: true,
* monitoredCount: 1
* }
* });
*
* And in the child IFRAME:
*
* BOOMR.init({
* ...
* IFrameDelay: {
* enabled: true,
* registerParent: true
* }
* });
*
* See the {@link BOOMR.plugins.IFrameDelay.init init()} function for more
* details on each parameter.
*
* Once all registered IFRAMEs have finished loading, the Page Load time
* is set with the load time of the final IFRAME.
*
* ## Beacon Parameters
*
* This plugin adds the following parameters to the beacon:
*
* * `ifdl.done`: When all of the IFRAMEs have finished loading
* * `ifdl.ct`: Number of finished IFRAMEs
* * `ifdl.r`: Number of still-running IFRAMEs
* * `ifdl.mon`: Total number of monitored IFRAMEs
*
* @class BOOMR.plugins.IFrameDelay
*/
(function() {
var w;
// postMessage retry delay in ms
var MSG_RETRY_DELAY = 250;
BOOMR = window.BOOMR || {};
BOOMR.plugins = BOOMR.plugins || {};
if (BOOMR.plugins.IFrameDelay) {
return;
}
w = BOOMR.window;
/* BEGIN_DEBUG */
function debugLog(message) {
BOOMR.debug(
"(url: " + w.location.href + "): " + message,
"IFrameDelay");
}
/* END_DEBUG */
var impl = {
initialized: false,
/**
* If true, send postMessage `boomrIframeLoading` and wait for the current
* frame to send the beacon. When Boomerang sends a beacon, it will
* also postMessage `boomrIframeLoaded`.
*/
registerParent: false,
/**
* If set, count down the amount of times `boomrIframeLoaded` has been sent
*/
monitoredCount: 0,
/**
* Number of frames that went from `boomrIframeLoading` to `boomrIframeLoaded`
*/
finishedCount: 0,
/**
* Number of registered child IFRAMEs currently waiting to finish
*/
runningCount: 0,
/**
* Array of child frame Page IDs
*/
runningFrames: {},
/**
* Loading setInterval ID (used to know if one is set, and to clear later)
*/
loadingIntervalID: undefined,
/**
* Loaded setInterval ID (used to know if one is set, and to clear later)
*/
loadedIntervalID: undefined,
/**
* load timestamp of slowest frame
*/
loadEnd: 0,
/**
* postMessage names
*/
messages: {
start: "boomrIframeLoading",
done: "boomrIframeLoaded",
startACK: "boomrIframeLoadingACK",
doneACK: "boomrIframeLoadedACK"
},
/**
* Parent window postMessage `onmessage` callback listening for messages
* from the child IFRAMEs
*
* @param {Event} event postMessage Event
*/
onIFrameMessageAsParent: function(event) {
var data;
if (event &&
event.data && typeof event.data === "string" && event.data.charAt(0) === "{" &&
event.source) {
try {
data = JSON.parse(event.data);
}
catch (e) {
return;
}
if (data.msg === impl.messages.start) {
debugLog("Received start message from child IFrame");
if (impl.runningFrames[data.pid]) {
// already got a start message from this frame
return;
}
// respond to the frame
event.source.postMessage(JSON.stringify({"msg": impl.messages.startACK}), event.origin);
// track that we're monitoring this frame
impl.runningCount += 1;
impl.runningFrames[data.pid] = 1;
}
else if (data.msg === impl.messages.done) {
debugLog("Received done message from child IFrame");
// respond to the frame
event.source.postMessage(JSON.stringify({"msg": impl.messages.doneACK}), event.origin);
// book-keeping
impl.runningCount -= 1;
impl.finishedCount += 1;
// increment our loadEnd if this is larger
if (data.loadEnd > impl.loadEnd) {
impl.loadEnd = data.loadEnd;
}
// check if we're done!
impl.checkCompleteness();
}
}
},
/**
* Child IFRAME postMessage `onmessage` callback listening for messages
* from parent window
*
* @param {Event} event postMessage Event
*/
onIFrameMessageAsChild: function(event) {
var data;
if (event &&
event.data && typeof event.data === "string" && event.data.charAt(0) === "{" &&
event.source) {
try {
data = JSON.parse(event.data);
}
catch (e) {
return;
}
if (data.msg === impl.messages.startACK) {
debugLog("Received start message ACK from parent");
clearInterval(impl.loadingIntervalID);
impl.loadingIntervalID = undefined;
}
else if (data.msg === impl.messages.doneACK) {
debugLog("Received done message ACK from parent");
clearInterval(impl.loadedIntervalID);
impl.loadedIntervalID = undefined;
}
}
},
/**
* If we are done with all our monitored frames we will tell
* Boomerang that the page is ready and send out a beacon with
* our information.
*/
checkCompleteness: function() {
if (impl.is_complete()) {
// Add time IFrameDelay was done running.
BOOMR.addVar("ifdl.done", BOOMR.now());
// Add number for finished/beaconed IFRAMEs.
BOOMR.addVar("ifdl.ct", impl.finishedCount);
// Add number of "still" running IFRAMEs - used for diagnostics
// if we canceled waiting too long for the child page to send a
// beacon.
BOOMR.addVar("ifdl.r", impl.runningCount);
// Add number of monitored IFRAMEs - if configuration did not
// dictate number of monitored IFRAMEs we should give this
// number here to tell how many boomerang saw.
BOOMR.addVar("ifdl.mon", impl.monitoredCount);
if (BOOMR.hasBrowserOnloadFired()) {
// if the iframe load time is later than the parent then use it
BOOMR.page_ready(impl.loadEnd > 0 ? impl.loadEnd : undefined);
}
else {
// if the parent onload hasn't fired yet, then we'll wait for
// it instead
BOOMR.attach_page_ready(function() {
BOOMR.page_ready(undefined, true);
});
}
}
},
/**
* Whether or not this plugin is complete
*
* @returns {boolean} True when the plugin is complete
*/
is_complete: function() {
return impl.enabled && !impl.registerParent ?
impl.finishedCount >= impl.monitoredCount && impl.runningCount === 0 :
true;
}
};
BOOMR.plugins.IFrameDelay = {
/**
* Initializes the plugin.
*
* @param {object} config Configuration
* @param {boolean} [config.IFrameDelay.registerParent] Should be set to
* `true` for child IFRAMEs. If `true`, the parent frame will wait on
* this child IFRAME.
* @param {number} [config.IFrameDelay.monitoredCount] Should be set by
* the parent frame to indiciate the number of child IFRAMEs it expects
* to wait on.
* @returns {@link BOOMR.plugins.IFrameDelay} The IFrameDelay plugin for chaining
* @memberof BOOMR.plugins.IFrameDelay
*/
init: function(config) {
BOOMR.utils.pluginConfig(
impl,
config,
"IFrameDelay",
["enabled", "registerParent", "monitoredCount"]);
if (impl.initialized) {
return this;
}
impl.initialized = true;
// only run important bits if we're getting the actual configuration
if (this.is_supported()) {
if (impl.registerParent) {
debugLog("Running as Child. Found registerParent=true");
// listen on this window since it will be the source of postMessage calls
BOOMR.utils.addListener(window, "message", impl.onIFrameMessageAsChild);
function postStart() {
debugLog("Trying to notify parent window of load start");
w.parent.postMessage(JSON.stringify({"msg": impl.messages.start, "pid": BOOMR.pageId}), "*");
}
postStart();
// keep retrying until we get an ACK
impl.loadingIntervalID = setInterval(postStart, MSG_RETRY_DELAY);
BOOMR.subscribe("page_load_beacon", function(vars) {
var loadEnd;
if (vars && vars["rt.end"]) {
loadEnd = vars["rt.end"];
}
else {
loadEnd = BOOMR.now();
}
function postEnd() {
// make sure start message was sent first
if (!impl.loadingIntervalID) {
debugLog("Trying to notify parent window of load end");
w.parent.postMessage(JSON.stringify({
"msg": impl.messages.done,
"pid": BOOMR.pageId,
"loadEnd": loadEnd
}), "*");
}
}
postEnd();
// keep retrying until we get an ACK
impl.loadedIntervalID = setInterval(postEnd, MSG_RETRY_DELAY);
});
}
else if (!impl.registerParent && impl.monitoredCount && impl.monitoredCount > 0) {
/* BEGIN_DEBUG */
debugLog("Running as Parent. Found monitoredCount=" +
impl.monitoredCount +
", listening for messages from child windows");
/* END_DEBUG */
BOOMR.utils.addListener(w, "message", impl.onIFrameMessageAsParent);
}
else {
debugLog("Missing configuration. Setting monitored, finished and running to 0 and closing this plugin");
impl.finishedCount = impl.monitoredCount = impl.runningCount = 0;
impl.enabled = false;
}
}
else {
impl.enabled = false;
}
return this;
},
/**
* Whether or not this plugin is complete
*
* @returns {boolean} `true` if the plugin is complete
* @memberof BOOMR.plugins.IFrameDelay
*/
is_complete: function() {
return impl.is_complete();
},
/**
* Whether or not this plugin is supported
*
* @returns {boolean} `true` if the plugin has postMessage and JSON support.
* @memberof BOOMR.plugins.IFrameDelay
*/
is_supported: function() {
return BOOMR.utils.hasPostMessageSupport() && window.JSON;
}
};
}());