sitespeed.io/lib/plugins/webpagetest/index.js

257 lines
7.8 KiB
JavaScript

'use strict';
const urlParser = require('url');
const analyzer = require('./analyzer');
const Aggregator = require('./aggregator');
const forEach = require('lodash.foreach');
const merge = require('lodash.merge');
const get = require('lodash.get');
const path = require('path');
const WebPageTest = require('webpagetest');
const fs = require('fs');
// These are the metrics we want to save in
// the time series database per pageSummary
const DEFAULT_PAGE_SUMMARY_METRICS = [
'data.median.*.SpeedIndex',
'data.median.*.render',
'data.median.*.TTFB',
'data.median.*.loadTime',
'data.median.*.fullyLoaded',
'data.median.*.userTimes.*',
// Use bytesIn to collect data for Opera Mini & UC Mini
'data.median.*.bytesIn',
'data.median.*.breakdown.*.requests',
'data.median.*.breakdown.*.bytes',
'data.median.*.requestsFull',
'data.median.*.custom.*',
'data.median.*.domContentLoadedEventEnd',
'data.median.*.fullyLoadedCPUms',
'data.median.*.docCPUms',
'data.median.*.score_cache',
'data.median.*.score_gzip',
'data.median.*.score_combine',
'data.median.*.score_minify',
'data.median.*.domElements',
'data.median.*.lastVisualChange',
'data.median.*.visualComplete85',
'data.median.*.visualComplete90',
'data.median.*.visualComplete95',
'data.median.*.visualComplete99',
'data.median.*.FirstInteractive',
'data.median.*.LastInteractive',
'data.median.*.TimeToInteractive',
// available only when --timeline option is required for chrome
'data.median.*.chromeUserTiming.*',
'data.median.*.cpuTimes.*'
];
// These are the metrics we want to save in
// the time series database per summary (per domain/test/group)
const DEFAULT_SUMMARY_METRICS = [
'timing.*.SpeedIndex',
'timing.*.render',
'timing.*.TTFB',
'timing.*.fullyLoaded',
'asset.*.breakdown.*.requests',
'asset.*.breakdown.*.bytes',
'custom.*.custom.*'
];
function addCustomMetric(result, filterRegistry) {
const customMetrics = get(result, 'data.median.firstView.custom');
if (customMetrics) {
for (const customMetric of customMetrics) {
filterRegistry.addFilterForType(
'data.median.*.' + customMetric,
'webpagetest.pageSummary'
);
}
}
}
const defaultConfig = {
host: WebPageTest.defaultServer,
location: 'Dulles:Chrome',
connectivity: 'Cable',
runs: 3,
pollResults: 10,
timeout: 600,
includeRepeatView: false,
private: true,
aftRenderingTime: true,
video: true,
timeline: false
};
function isPublicWptHost(address) {
const host = /^(https?:\/\/)?([^/]*)/i.exec(address);
return host && host[2] === urlParser.parse(WebPageTest.defaultServer).host;
}
module.exports = {
open(context, options) {
// The context holds help methods to setup what we need in plugin
// Get a log specificfor this plugin
this.log = context.intel.getLogger('sitespeedio.plugin.webpagetest');
// Make will help you create messages that you will send on the queue
this.make = context.messageMaker('webpagetest').make;
// The aggregator helps you aggregate metrics per URL and/or domain
this.aggregator = new Aggregator(context.statsHelpers, this.log);
// The storagemanager helps you save file to disk
this.storageManager = context.storageManager;
// The filter registry decides which metrics that will be stored in the time/series db
this.filterRegistry = context.filterRegistry;
this.options = merge({}, defaultConfig, options.webpagetest);
if (get(this.options, 'ssio.domainsDashboard')) {
// that adds a lot of disk space need into graphite, so we keep it hidden for now
DEFAULT_PAGE_SUMMARY_METRICS.push(
'data.median.firstView.domains.*.bytes',
'data.median.firstView.domains.*.requests'
);
}
if (!this.options.key && isPublicWptHost(this.options.host)) {
throw new Error(
'webpagetest.key needs to be specified when using the public WebPageTest server.'
);
}
// Register the type of metrics we want to have in the db
this.filterRegistry.registerFilterForType(
DEFAULT_PAGE_SUMMARY_METRICS,
'webpagetest.pageSummary'
);
this.filterRegistry.registerFilterForType(
DEFAULT_SUMMARY_METRICS,
'webpagetest.summary'
);
this.pug = fs.readFileSync(
path.resolve(__dirname, 'pug', 'index.pug'),
'utf8'
);
},
processMessage(message, queue) {
const filterRegistry = this.filterRegistry;
const make = this.make;
const wptOptions = this.options;
switch (message.type) {
// In the setup phase, register our pug file(s) in the HTML plugin
// by sending a message. This plugin uses the same pug for data
// per run and per page summary.
case 'sitespeedio.setup': {
// Tell other plugins that webpagetest will run
queue.postMessage(make('webpagetest.setup'));
// Add the HTML pugs
queue.postMessage(
make('html.pug', {
id: 'webpagetest',
name: 'WebPageTest',
pug: this.pug,
type: 'pageSummary'
})
);
queue.postMessage(
make('html.pug', {
id: 'webpagetest',
name: 'WebPageTest',
pug: this.pug,
type: 'run'
})
);
break;
}
// We got a URL that we should test
case 'url': {
const url = message.url;
const group = message.group;
return analyzer
.analyzeUrl(url, this.storageManager, this.log, wptOptions)
.then(result => {
addCustomMetric(result, filterRegistry);
if (result.trace) {
forEach(result.trace, (value, key) => {
queue.postMessage(
make('webpagetest.chrometrace', value, {
url,
group,
name: key + '.json'
})
);
});
}
queue.postMessage(
make('webpagetest.har', result.har, { url, group })
);
forEach(result.data.runs, (run, runKey) =>
queue.postMessage(
make('webpagetest.run', run, {
url,
group,
runIndex: parseInt(runKey) - 1
})
)
);
const location = result.data.location
.replace(':', '-')
.replace(' ', '-')
.toLowerCase();
// There's no connectivity setup in the default config for WPT, make sure we catch that
const connectivity = get(
result,
'data.connectivity',
'native'
).toLowerCase();
queue.postMessage(
make('webpagetest.pageSummary', result, {
url,
group,
location,
connectivity
})
);
this.aggregator.addToAggregate(
group,
result,
connectivity,
location,
wptOptions
);
})
.catch(err => {
this.log.error('Error creating WebPageTest result ', err);
queue.postMessage(
make('error', err, {
url
})
);
});
}
// All URLs are tested, now create summaries per page and domain/group
case 'sitespeedio.summarize': {
let summary = this.aggregator.summarize();
if (summary && Object.keys(summary.groups).length > 0) {
for (let group of Object.keys(summary.groups)) {
queue.postMessage(
make('webpagetest.summary', summary.groups[group], {
connectivity: this.aggregator.connectivity,
location: this.aggregator.location,
group
})
);
}
}
}
}
},
config: defaultConfig
};