Skip to content

Commit

Permalink
Plugin to beacon User Timing API mark and measure entries (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
querymetrics authored and nicjansma committed May 2, 2017
1 parent 62ab83c commit a302e7e
Show file tree
Hide file tree
Showing 14 changed files with 413 additions and 3 deletions.
3 changes: 2 additions & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"json3": "~3.3.2",
"lodash": "~3.0.0",
"mocha": "~1.21.5",
"resourcetiming-compression": "^0.3.3"
"resourcetiming-compression": "^0.3.3",
"usertiming-compression": "~0.1.4"
},
"resolutions": {
"angular": "1.4.6"
Expand Down
55 changes: 55 additions & 0 deletions doc/api/usertiming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# UserTiming Plugin

## Collect W3C UserTiming API performance marks and measures


This plugin collects all W3C UserTiming API performance marks and measures that were added since navigation start or since the last beacon fired for the current navigation. The data is added to the beacon as the `usertiming` parameter. The value is a compressed string using Nic Jansma's [usertiming-compression.js](https://github.com/nicjansma/usertiming-compression.js) library. A decompression function is also available in the library.

Timing data is rounded to the nearest millisecond.

Please see the [W3C UserTiming API Reference](https://www.w3.org/TR/user-timing/) for details on how to use the UserTiming API.

### Configuring Boomerang

You can enable the `UserTiming` plugin with:
```js
BOOMR.init({
UserTiming: {
'enabled': true
}
})
```

### Example

```js
performance.mark('mark1'); //mark current timestamp as mark1
performance.mark('mark2');
performance.measure('measure1', 'mark1', 'mark2'); //measure1 will be the delta between mark1 and mark2 timestamps
performance.measure('measure2', 'mark2'); //measure2 will be the delta between the mark2 timestamp and the current time
```

The compressed 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:
```json
[{"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"}]
```

### Compatibility and Browser Support


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](https://github.com/nicjansma/usertiming.js) polyfill library to add UserTiming API support for browsers that don't implement it natively.
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"doc": "doc",
"test": "tests"
},
"dependencies": {
"usertiming-compression": "^0.1.4"
},
"devDependencies": {
"async": "^0.9.0",
"bower": "*",
Expand Down Expand Up @@ -178,6 +181,10 @@
{
"name": "Ben Ripkens",
"email": "[email protected]"
},
{
"name": "Nigel Heron",
"email": "[email protected]"
}
],
"license": "BSD-3-Clause",
Expand Down
4 changes: 3 additions & 1 deletion plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"plugins/md5.js",
"plugins/compression.js",
"plugins/errors.js",
"plugins/third-party-analytics.js"
"plugins/third-party-analytics.js",
"node_modules/usertiming-compression/src/usertiming-compression.js",
"plugins/usertiming.js"
]
}
158 changes: 158 additions & 0 deletions plugins/usertiming.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* @module UserTiming
* @desc
* Plugin to collect metrics from the W3C User Timing API.
* For more information about User Timing,
* see: http://www.w3.org/TR/user-timing/
*
* This plugin is dependent on the UserTimingCompression library
* see: https://github.com/nicjansma/usertiming-compression.js
* UserTimingCompression must be loaded before this plugin's init is called.
*/

/*global UserTimingCompression*/

(function() {

BOOMR = BOOMR || {};
BOOMR.plugins = BOOMR.plugins || {};
if (BOOMR.plugins.UserTiming) {
return;
}

var impl = {
complete: false,
initialized: false,
supported: false,
options: {"from": 0, "window": BOOMR.window},

/**
* Calls the UserTimingCompression library to get the compressed user timing data
* that occurred since the last call
*
* @returns {string} compressed user timing data
*/
getUserTiming: function() {
var timings, res, now = BOOMR.now();
var utc = window.UserTimingCompression || BOOMR.window.UserTimingCompression;

timings = utc.getCompressedUserTiming(impl.options);
res = utc.compressForUri(timings);
this.options.from = now;

return res;
},

/**
* Callback for `before_beacon` boomerang event
* Adds the `usertiming` param to the beacon
*/
addEntriesToBeacon: function() {
var r;

if (this.complete) {
return;
}

BOOMR.removeVar("usertiming");
r = this.getUserTiming();
if (r) {
BOOMR.addVar({
"usertiming": r
});
}

this.complete = true;
},

/**
* Callback for `onbeacon` boomerang event
* Clears the `usertiming` beacon param
*/
clearMetrics: function(vars) {
if (vars.hasOwnProperty("usertiming")) {
BOOMR.removeVar("usertiming");
}
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("onbeacon", 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 User Timing is available
*/
pageReady: function() {
if (this.checkSupport()) {
this.subscribe();
}
},

/**
* Checks if the browser supports the User Timing API and that the UserTimingCompression library is available
*
* @returns {boolean} true if supported, false if not
*/
checkSupport: function() {
if (this.supported) {
return true;
}

// Check that the required UserTimingCompression library is available
var utc = window.UserTimingCompression || BOOMR.window.UserTimingCompression;
if (typeof utc === "undefined") {
BOOMR.warn("UserTimingCompression library not found", "usertiming");
return false;
}

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 User Timing API", "usertiming");
this.supported = true;
return true;
}
}
return false;
}
};

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;
},
is_complete: function() {
return true;
},
is_supported: function() {
return impl.initialized && impl.supported;
}
};

}());
2 changes: 1 addition & 1 deletion tests/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ module.exports = function() {
grunt.file.write(rootIndexFile, rootIndexHtml);

// test definitions
grunt.file.write(e2eJsonPath, JSON.stringify(testDefinitions));
grunt.file.write(e2eJsonPath, JSON.stringify(testDefinitions, null, 2));

cb(err, opts);
});
Expand Down
12 changes: 12 additions & 0 deletions tests/page-templates/18-usertiming/00-usertiming-none.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<%= header %>
<%= boomerangSnippet %>
<script src="00-usertiming-none.js" type="text/javascript"></script>
<script>
BOOMR_test.init({
testAfterOnBeacon: true,
UserTiming: {
enabled: true
}
});
</script>
<%= footer %>
18 changes: 18 additions & 0 deletions tests/page-templates/18-usertiming/00-usertiming-none.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*eslint-env mocha*/
/*global BOOMR_test,assert*/

describe("e2e/17-usertiming/00-usertiming-none", function() {
var t = BOOMR_test;
var tf = BOOMR.plugins.TestFramework;

it("Should pass basic beacon validation", function(done) {
t.validateBeaconWasSent(done);
});

it("Should not have usertiming", function() {
if (t.isUserTimingSupported()) {
var b = tf.beacons[0];
assert.equal(b.usertiming, undefined);
}
});
});
24 changes: 24 additions & 0 deletions tests/page-templates/18-usertiming/01-usertiming-basic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<%= header %>
<%= boomerangSnippet %>
<script src="01-usertiming-basic.js" type="text/javascript"></script>
<script src="../../vendor/usertiming-compression/src/usertiming-decompression.js" type="text/javascript"></script>
<script>
if (BOOMR_test.isUserTimingSupported()) {
window.performance.mark("mark1");
setTimeout(function() {
window.performance.mark("mark2");
window.performance.measure("measure1", "mark1", "mark2");
}, 500);
}
</script>
<script>
setTimeout(function() {
BOOMR_test.init({
testAfterOnBeacon: true,
UserTiming: {
enabled: true
}
});
}, 1000);
</script>
<%= footer %>
27 changes: 27 additions & 0 deletions tests/page-templates/18-usertiming/01-usertiming-basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*eslint-env mocha*/
/*global BOOMR_test,assert*/

describe("e2e/17-usertiming/01-usertiming-basic", function() {
var t = BOOMR_test;
var tf = BOOMR.plugins.TestFramework;

it("Should pass basic beacon validation", function(done) {
t.validateBeaconWasSent(done);
});

it("Should have usertiming (if UserTiming is supported)", function() {
if (t.isUserTimingSupported()) {
var b = tf.beacons[0];
assert.isString(b.usertiming);
var data = UserTimingDecompression.decompressUserTiming(b.usertiming);
var usertiming = {};
assert.equal(data.length, 3);
for (var i = 0; i < data.length; i++) {
usertiming[data[i].name] = data[i];
}
assert.isTrue("mark1" in usertiming);
assert.isTrue("mark2" in usertiming);
assert.isTrue("measure1" in usertiming);
}
});
});
37 changes: 37 additions & 0 deletions tests/page-templates/18-usertiming/02-usertiming-polyfill.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<%= header %>
<%= boomerangSnippet %>
<script src="02-usertiming-polyfill.js" type="text/javascript"></script>
<script src="../../vendor/usertiming-compression/src/usertiming-decompression.js" type="text/javascript"></script>
<script>
//
// Not really polyfill, we'll hide window.performance.getEntriesByType
// then bring it back later
//
if (BOOMR_test.isUserTimingSupported()) {
window.getEntriesByTypeCopy = window.performance.getEntriesByType;
window.performance.getEntriesByType = undefined;

BOOMR_test.init({
testAfterOnBeacon: true,
UserTiming: {
enabled: true
},
onBoomerangLoaded: function() {
window.performance.getEntriesByType = window.getEntriesByTypeCopy;
window.performance.mark("mark1");
window.performance.mark("mark2");
window.performance.measure("measure1", "mark1", "mark2");
}
});
}
else {
BOOMR_test.init({
testAfterOnBeacon: true,
UserTiming: {
enabled: true
}
});
}
</script>
<div id="content"></div>
<%= footer %>
Loading

0 comments on commit a302e7e

Please sign in to comment.