Skip to content

Commit

Permalink
Merge pull request #32 from Financial-Times/perfume
Browse files Browse the repository at this point in the history
Use Perfume.js for Real User Monitoring of Performance Metrics
  • Loading branch information
Adam Braimbridge authored Jan 2, 2020
2 parents 7ee3417 + 4c49b5f commit ecbf66a
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 53 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ if (flags.get('realUserMonitoringForPerformance')) {
nTracking.trackers.realUserMonitoringForPerformance();
}
```
<div><img width="50%" src="https://user-images.githubusercontent.com/224547/71410882-f2b50980-263e-11ea-86f2-f7e986fc9fad.png" /></div>
<div><img width="70%" src="https://user-images.githubusercontent.com/224547/71626767-c709c480-2be6-11ea-91a5-506972a3b4d7.png" /></div>

_Above: Real-user-monitoring performance metrics are sent to spoor-api._

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@
"node": ">=8.16.0"
},
"dependencies": {
"tti-polyfill": "^0.2.2",
"@financial-times/o-grid": "^5.0.0",
"@financial-times/o-tracking": "^2.0.3",
"@financial-times/o-viewport": "^4.0.0"
"@financial-times/o-viewport": "^4.0.0",
"perfume.js": "^4.6.0"
},
"peerDependencies": {
"react": "^16.9.0"
Expand Down
2 changes: 1 addition & 1 deletion secret-squirrel.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = {
strings: {
deny: [],
denyOverrides: [
'f2b50980-263e-11ea-86f2-f7e986fc9fad' // README.md:57
'c709c480-2be6-11ea-91a5-506972a3b4d7' // README.md:51
]
}
};
119 changes: 70 additions & 49 deletions src/client/trackers/real-user-monitoring-for-performance.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,81 @@
import ttiPolyfill from 'tti-polyfill';
import Perfume from 'perfume.js';
import { broadcast } from '../broadcast';
import { userIsInCohort } from '../utils/userIsInCohort';

// Perfume.js is a web performance package.
// @see https://zizzamia.github.io/perfume/#/default-options/
const options = {
logging: false,
firstPaint: true,
largestContentfulPaint: true,
firstInputDelay: true,
largestContentfulPaint: true,
navigationTiming: true,
};

// @see "Important metrics to measure" https://web.dev/metrics
const requiredMetrics = [
'domInteractive',
'domComplete',
'timeToFirstByte',
'firstPaint',
'largestContentfulPaint',
'firstInputDelay'
];

const cohortPercent = 5;

export const realUserMonitoringForPerformance = () => {
const cohortPercent = 5;

// Check browser support.
// @see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceLongTaskTiming
if (!'PerformanceLongTaskTiming' in window) return;

// Gather metrics for only a cohort of users.
if (!userIsInCohort(cohortPercent)) return;

// For browser compatibility @see: https://mdn.github.io/dom-examples/performance-apis/perf-api-support.html
if (!'PerformanceLongTaskTiming' in window || !'ttiPolyfill' in window) return;
const navigation = performance.getEntriesByType('navigation')[0];
const { type, domInteractive, domComplete } = navigation;

// @see: https://web.dev/lcp/#how-to-measure-lcp (largest-contentful-paint)
let largestContentfulPaint;
const lcpPerformanceObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
largestContentfulPaint = lastEntry.renderTime || lastEntry.loadTime;
});
lcpPerformanceObserver.observe({type: 'largest-contentful-paint', buffered: true});

ttiPolyfill.getFirstConsistentlyInteractive().then(timeToInteractive => {

// Disconnect the observer once it no longer needs to observe the performance data
// @SEE: https://w3c.github.io/performance-timeline/#the-performanceobserver-interface
lcpPerformanceObserver.disconnect();

const navigation = performance.getEntriesByType('navigation')[0];
const { type, domInteractive, domComplete, responseStart, requestStart } = navigation;

// Proceed only if the page load event is a "navigate".
// @see: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/type
if (type !== 'navigate') return;

try {
const timeToFirstByte = responseStart - requestStart;
const firstPaint = performance.getEntriesByName('first-paint')[0].startTime;
const firstContentfulPaint = performance.getEntriesByName('first-contentful-paint')[0].startTime;
const context = {
firstPaint: Math.round(firstPaint),
firstContentfulPaint: Math.round(firstContentfulPaint),
timeToFirstByte: Math.round(timeToFirstByte),
domInteractive: Math.round(domInteractive),
domComplete: Math.round(domComplete),
largestContentfulPaint: Math.round(largestContentfulPaint),
timeToInteractive: Math.round(timeToInteractive),
};

console.log(context); // eslint-disable-line no-console
const data = {
action: 'performance',
category: 'page',
...context
};
broadcast('oTracking.event', data);
// Proceed only if the page load event is a "navigate".
// @see: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/type
if (type !== 'navigate') return;

const context = {
action: 'performance',
category: 'page',
domInteractive: Math.round(domInteractive),
domComplete: Math.round(domComplete),
};

/**
* analyticsTracker()
*
* This function is called every time one of the performance events occurs.
* The "final" event should be `firstInputDelay`, which is triggered by any "input" event (most likely to be a click.)
* Once all the metrics are present, it fires a broadcast() to the Spoor API.
*/
let hasAlreadyBroadcast = false;
options.analyticsTracker = (({ metricName, duration, data }) => {
if (hasAlreadyBroadcast) return;

if (duration) {
// Metrics with "duration":
// firstPaint, firstContentfulPaint, firstInputDelay and largestContentfulPaint
context[metricName] = Math.round(duration);
}
catch (error) {
console.error(error); // eslint-disable-line no-console
else if (metricName === 'navigationTiming') {
context.timeToFirstByte = Math.round(data.timeToFirstByte);
}

// Broadcast only if all the metrics are present
const contextContainsAllRequiredMetrics = requiredMetrics.every(metric => !isNaN(context[metric]));
if (contextContainsAllRequiredMetrics) {
console.log({performanceMetrics:context}); // eslint-disable-line no-console
broadcast('oTracking.event', context);
hasAlreadyBroadcast = true;
}
});

new Perfume(options);
};

0 comments on commit ecbf66a

Please sign in to comment.