UNPKG

boomerangjs

Version:

boomerang always comes back, except when it hits something

323 lines (279 loc) 9.54 kB
/* global UserTimingCompression */ /** * The UserTiming plugin to collect metrics from the W3C * [UserTiming]{@link http://www.w3.org/TR/user-timing/} API. * * This plugin can make use of the * [UserTimingCompression library]{@link https://github.com/nicjansma/usertiming-compression.js}. * In order to use compression, `UserTimingCompression` must be loaded before this plugin's `init()` is called. * * This plugin collects all marks and measures that were added since * navigation start or since the last beacon fired for the current navigation. * * For information on how to include this plugin, see the {@tutorial building} tutorial. * * ## Beacon Parameters * * This plugin adds the following parameters to the beacon: * * * `usertiming`: UserTiming data * * The value is either plain json or a compressed string using * [UserTimingCompression library]{@link https://github.com/nicjansma/usertiming-compression.js}. * A decompression function is also available in the library. * * Timing data is rounded to the nearest millisecond. * * ## Example * * // mark current timestamp as mark1 * performance.mark('mark1'); * // mark current timestamp as mark2 * performance.mark('mark2'); * // measure1 will be the delta between mark1 and mark2 timestamps * performance.measure('measure1', 'mark1', 'mark2'); * //measure2 will be the delta between the mark2 timestamp and the current time * performance.measure('measure2', 'mark2'); * * When not using compression, data added to the beacon will look similar to the following: * * usertiming={"mark":{"mark1":100,"mark2":200},"measure":{"measure1":100,"measure2":200}} * * When using compression, data added to the beacon will look similar to the following: * * usertiming=~(m~(ark~(1~'2s~2~'5k)~easure~(1~'2s_2s~2~'5k_5k))) * * Decompressing the above value will give us the original data for the marks * and measures collected: * * [{"name":"mark1","startTime":100,"duration":0,"entryType":"mark"}, * {"name":"measure1","startTime":100,"duration":100,"entryType":"measure"}, * {"name":"mark2","startTime":200,"duration":0,"entryType":"mark"}, * {"name":"measure2","startTime":200,"duration":200,"entryType":"measure"}] * * Uncompressed measures only send their duration in contrast to compressed measures, which also send their startTime. * * ## Compatibility * * Many browsers [support](http://caniuse.com/#feat=user-timing) the UserTiming * API, e.g.: * * * Chrome 25+ * * Edge * * Firefox 38+ * * IE 10+ * * Opera 15+ * * See Nic Jansma's [usertiming.js]{@link https://github.com/nicjansma/usertiming.js} * polyfill library to add UserTiming API support for browsers that don't * implement it natively. * * @see {@link http://www.w3.org/TR/user-timing/} * @class BOOMR.plugins.UserTiming */ (function() { BOOMR = window.BOOMR || {}; BOOMR.plugins = BOOMR.plugins || {}; if (BOOMR.plugins.UserTiming) { return; } var impl = { // Whether or not this plugin is complete. complete: false, // Whether or not this plugin is initialized initialized: false, // Whether or not UserTiming is supported by this browser supported: false, // Options options: {"from": 0, "window": BOOMR.window}, /* * Gets the user timings, filters out those that have already been sent. * Calls the UserTimingCompression library to get the compressed UserTiming * data that occurred since the last call. * * @returns {string} UserTiming data */ getUserTiming: function() { var entries = this.findUserTimingForFrame(impl.options.window); // 'from' minimum time if (impl.options.from) { entries = entries.filter(function(e) { return e.startTime + e.duration >= impl.options.from; }); } var utc = window.UserTimingCompression || BOOMR.window.UserTimingCompression; if (typeof utc === "undefined") { if (entries.length === 0) { return null; } else { var res = {}; for (var i = 0, l = entries.length; i < l; i++) { var entry = entries[i]; res[entry.entryType] = res[entry.entryType] || {}; if (entry.entryType === "mark") { res[entry.entryType][entry.name] = entry.startTime; } else if (entry.entryType === "measure") { res[entry.entryType][entry.name] = entry.duration; } } return JSON.stringify(res); } } else { var timings, res; timings = utc.compressUserTiming(entries); return utc.compressForUri(timings); } }, /** * Callback for `before_beacon` boomerang event. * * Adds the `usertiming` param to the beacon. */ addEntriesToBeacon: function(vars) { var r, now = BOOMR.hrNow(); // Add entries to all beacon types except early beacons if (this.complete || (vars && typeof vars.early !== "undefined")) { return; } r = this.getUserTiming(); if (r) { BOOMR.addVar("usertiming", r, true); } this.options.from = now; this.complete = true; }, /** * Gets all of the UserTiming entries for a frame * @param {object} frame Window or frame DOM object * * @returns {PerformanceEntry[]} UserTiming entries */ findUserTimingForFrame: function(frame) { var entries; if (!frame) { return []; } try { // Try to access location.href first to trigger any Cross-Origin // warnings. There's also a bug in Chrome ~48 that might cause // the browser to crash if accessing X-O frame.performance. // https://code.google.com/p/chromium/issues/detail?id=585871 // This variable is not otherwise used. /* eslint-disable no-unused-vars */ var frameLoc = frame.location && frame.location.href; /* eslint-enable no-unused-vars */ if (!("performance" in frame) || !frame.performance || !frame.performance.getEntriesByType) { return entries; } // gather marks and measures for this frame // TODO do we need to offset startTime? entries = frame.performance.getEntriesByType("mark"); entries = entries.concat(frame.performance.getEntriesByType("measure")); } catch (e) { return entries; } return entries; }, /** * Callback for `beacon` boomerang event. * * Clears the `usertiming` beacon param. */ clearMetrics: function(vars) { this.complete = false; }, /** * Subscribe to boomerang events that will handle the `usertiming` * beacon param. */ subscribe: function() { BOOMR.subscribe("before_beacon", this.addEntriesToBeacon, null, this); BOOMR.subscribe("beacon", this.clearMetrics, null, this); }, /** * Callback for boomerang page_ready event. * * At page_ready, all javascript should be loaded. We'll call `checkSupport` * again to see if a polyfill for UserTiming is available. */ pageReady: function() { if (this.checkSupport()) { this.subscribe(); } }, /** * Checks if the browser supports the UserTiming API and that the * UserTimingCompression library is available. * * @returns {boolean} true if supported, false if not */ checkSupport: function() { if (this.supported) { return true; } var p = BOOMR.getPerformance(); // Check that we have getEntriesByType if (p && typeof p.getEntriesByType === "function") { var marks = p.getEntriesByType("mark"); var measures = p.getEntriesByType("measure"); // Check that the results of getEntriesByType for marks and measures are Arrays // Some polyfill libraries may incorrectly implement this if (BOOMR.utils.isArray(marks) && BOOMR.utils.isArray(measures)) { BOOMR.info("Client supports UserTiming API", "usertiming"); this.supported = true; return true; } } return false; } }; BOOMR.plugins.UserTiming = { /** * Initializes the plugin. * * @returns {@link BOOMR.plugins.UserTiming} The UserTiming plugin for chaining * @memberof BOOMR.plugins.UserTiming */ init: function(config) { if (impl.initialized) { return this; } if (impl.checkSupport()) { impl.subscribe(); } else { // UserTiming isn't supported by the browser or the UserTimingCompression // library isn't loaded. Let's check again when the page is // ready to see if a polyfill was loaded. BOOMR.subscribe("page_ready", impl.pageReady, null, impl); } impl.initialized = true; return this; }, /** * Whether or not this plugin is complete * * @returns {boolean} `true` if the plugin is complete * @memberof BOOMR.plugins.UserTiming */ is_complete: function() { return true; }, /** * Whether or not UserTiming is supported in this browser. * * @returns {boolean} `true` if UserTiming is supported. * @memberof BOOMR.plugins.UserTiming */ is_supported: function() { return impl.initialized && impl.supported; } }; }());