Move the WebPageTest plugin out of sitespeed.io (#3205)
* Move the WebPageTest plugin out of sitespeed.io You can find the plugin at: https://github.com/sitespeedio/plugin-webpagetest
This commit is contained in:
parent
25600ac69f
commit
f37e0b9d89
|
|
@ -50,6 +50,4 @@ jobs:
|
|||
run: xvfb-run node test/runWithoutCli.js
|
||||
- name: Run tests with CruX
|
||||
run: bin/sitespeed.js -b chrome -n 1 --crux.key ${{ secrets.CRUX_KEY }} --xvfb https://www.sitespeed.io
|
||||
- name: Run tests on WebPageTest
|
||||
run: bin/sitespeed.js -b chrome -n 2 --summaryDetail --browsertime.chrome.timeline https://www.sitespeed.io/ --webpagetest.key ${{ secrets.WPT_KEY }} --xvfb
|
||||
|
||||
|
|
@ -19,7 +19,6 @@ const matrixPlugin = require('../plugins/matrix/index');
|
|||
|
||||
const browsertimeConfig = require('../plugins/browsertime/index').config;
|
||||
const metricsConfig = require('../plugins/metrics/index').config;
|
||||
const webPageTestConfig = require('../plugins/webpagetest/index').config;
|
||||
const slackConfig = require('../plugins/slack/index').config;
|
||||
const htmlConfig = require('../plugins/html/index').config;
|
||||
|
||||
|
|
@ -1058,65 +1057,6 @@ module.exports.parseCommandLine = function parseCommandLine() {
|
|||
'Add/change/remove filters for metrics. If you want to send all metrics, use: *+ . If you want to remove all current metrics and send only the coach score: *- coach.summary.score.*',
|
||||
group: 'Metrics'
|
||||
})
|
||||
/*
|
||||
WebPageTest cli options
|
||||
*/
|
||||
.option('webpagetest.host', {
|
||||
default: webPageTestConfig.host,
|
||||
describe: 'The domain of your WebPageTest instance.',
|
||||
group: 'WebPageTest'
|
||||
})
|
||||
.option('webpagetest.key', {
|
||||
describe: 'The API key for you WebPageTest instance.',
|
||||
group: 'WebPageTest'
|
||||
})
|
||||
.option('webpagetest.location', {
|
||||
describe: 'The location for the test',
|
||||
default: webPageTestConfig.location,
|
||||
group: 'WebPageTest'
|
||||
})
|
||||
.option('webpagetest.connectivity', {
|
||||
describe: 'The connectivity for the test.',
|
||||
default: webPageTestConfig.connectivity,
|
||||
group: 'WebPageTest'
|
||||
})
|
||||
.option('webpagetest.runs', {
|
||||
describe: 'The number of runs per URL.',
|
||||
default: webPageTestConfig.runs,
|
||||
group: 'WebPageTest'
|
||||
})
|
||||
.option('webpagetest.custom', {
|
||||
describe:
|
||||
'Execute arbitrary Javascript at the end of a test to collect custom metrics.',
|
||||
group: 'WebPageTest'
|
||||
})
|
||||
.option('webpagetest.file', {
|
||||
describe: 'Path to a script file',
|
||||
group: 'WebPageTest'
|
||||
})
|
||||
.option('webpagetest.script', {
|
||||
describe: 'The WebPageTest script as a string.',
|
||||
group: 'WebPageTest'
|
||||
})
|
||||
.option('webpagetest.includeRepeatView', {
|
||||
describe: 'Do repeat or single views',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
group: 'WebPageTest'
|
||||
})
|
||||
.option('webpagetest.private', {
|
||||
describe: 'Wanna keep the runs private or not',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
group: 'WebPageTest'
|
||||
})
|
||||
.option('webpagetest.timeline', {
|
||||
describe:
|
||||
'Activates Chrome tracing and get the devtools.timeline (only works for Chrome).',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
group: 'WebPageTest'
|
||||
})
|
||||
/**
|
||||
Slack options
|
||||
*/
|
||||
|
|
@ -1534,7 +1474,7 @@ module.exports.parseCommandLine = function parseCommandLine() {
|
|||
explicitOptions = merge(explicitOptions, config);
|
||||
}
|
||||
|
||||
if (argv.webpagetest.custom) {
|
||||
if (argv.webpagetest && argv.webpagetest.custom) {
|
||||
argv.webpagetest.custom = fs.readFileSync(
|
||||
path.resolve(argv.webpagetest.custom),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,179 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const forEach = require('lodash.foreach');
|
||||
|
||||
const metrics = ['TTFB', 'render', 'fullyLoaded', 'SpeedIndex'];
|
||||
|
||||
class Aggregator {
|
||||
constructor(statsHelpers, log) {
|
||||
this.statsHelpers = statsHelpers;
|
||||
this.log = log;
|
||||
this.timingStats = {};
|
||||
this.assetStats = {};
|
||||
this.customStats = {};
|
||||
this.assetGroups = {};
|
||||
this.timingGroups = {};
|
||||
this.customGroups = {};
|
||||
this.connectivity;
|
||||
this.location;
|
||||
}
|
||||
|
||||
addToAggregate(group, wptData, connectivity, location, wptOptions) {
|
||||
const log = this.log;
|
||||
const statsHelpers = this.statsHelpers;
|
||||
// TODO this will break if we run multiple locations/connectivity per run
|
||||
this.location = location;
|
||||
this.connectivity = connectivity;
|
||||
|
||||
if (this.assetGroups[group] === undefined) {
|
||||
this.assetGroups[group] = {};
|
||||
this.timingGroups[group] = {};
|
||||
this.customGroups[group] = {};
|
||||
}
|
||||
|
||||
forEach(wptData.data.runs, (run, index) => {
|
||||
// TODO remove this if check once issue with 0 stats, but 200 response is fixed upstream.
|
||||
// It seems to be cases when users tries to navigate away before fullyLoaded has happened
|
||||
const speedIndex = run.firstView.SpeedIndex || 0;
|
||||
if (wptOptions && wptOptions.video && speedIndex <= 0) {
|
||||
log.error(
|
||||
`Incomplete first view data for WPT test ${
|
||||
wptData.data.id
|
||||
}, run ${index}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
forEach(run, (viewData, viewName) => {
|
||||
forEach(metrics, metric =>
|
||||
statsHelpers.pushGroupStats(
|
||||
this.timingStats,
|
||||
this.timingGroups[group],
|
||||
[viewName, metric],
|
||||
viewData[metric]
|
||||
)
|
||||
);
|
||||
|
||||
forEach(viewData.userTimes, (timingData, timingName) =>
|
||||
statsHelpers.pushGroupStats(
|
||||
this.timingStats,
|
||||
this.timingGroups[group],
|
||||
[viewName, timingName],
|
||||
timingData
|
||||
)
|
||||
);
|
||||
|
||||
forEach(viewData.breakdown, (contentType, typeName) =>
|
||||
forEach(['requests', 'bytes'], property =>
|
||||
statsHelpers.pushGroupStats(
|
||||
this.assetStats,
|
||||
this.assetGroups[group],
|
||||
[viewName, typeName, property],
|
||||
contentType[property]
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
forEach(viewData.custom, metricName => {
|
||||
if (!isNaN(viewData[metricName]) && viewData[metricName] !== null) {
|
||||
// https://github.com/sitespeedio/sitespeed.io/issues/2985
|
||||
if (
|
||||
Array.isArray(viewData[metricName]) &&
|
||||
viewData[metricName].length === 0
|
||||
) {
|
||||
log.debug('Empty array for ' + metricName);
|
||||
} else {
|
||||
statsHelpers.pushGroupStats(
|
||||
this.customStats,
|
||||
this.customGroups[group],
|
||||
[viewName, 'custom', metricName],
|
||||
viewData[metricName]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
summarize() {
|
||||
const summary = {
|
||||
groups: {
|
||||
total: {
|
||||
timing: this.summarizePerTimingType(this.timingStats),
|
||||
asset: this.summarizePerAssetType(this.assetStats),
|
||||
custom: this.summarizePerCustomType(this.customStats)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (let group of Object.keys(this.timingGroups)) {
|
||||
if (!summary.groups[group]) summary.groups[group] = {};
|
||||
summary.groups[group].timing = this.summarizePerTimingType(
|
||||
this.timingGroups[group]
|
||||
);
|
||||
}
|
||||
for (let group of Object.keys(this.assetGroups)) {
|
||||
if (!summary.groups[group]) summary.groups[group] = {};
|
||||
summary.groups[group].asset = this.summarizePerAssetType(
|
||||
this.assetGroups[group]
|
||||
);
|
||||
}
|
||||
if (this.customGroups) {
|
||||
for (let group of Object.keys(this.customGroups)) {
|
||||
if (!summary.groups[group]) summary.groups[group] = {};
|
||||
summary.groups[group].custom = this.summarizePerCustomType(
|
||||
this.customGroups[group]
|
||||
);
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
summarizePerAssetType(type) {
|
||||
const statsHelpers = this.statsHelpers;
|
||||
const summary = {};
|
||||
forEach(type, (view, viewName) =>
|
||||
forEach(view, (contentType, contentTypeName) =>
|
||||
forEach(contentType, (stats, propertyName) =>
|
||||
statsHelpers.setStatsSummary(
|
||||
summary,
|
||||
[viewName, 'breakdown', contentTypeName, propertyName],
|
||||
stats
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
return summary;
|
||||
}
|
||||
|
||||
summarizePerTimingType(type) {
|
||||
const statsHelpers = this.statsHelpers;
|
||||
const summary = {};
|
||||
forEach(type, (view, viewName) =>
|
||||
forEach(view, (stats, name) =>
|
||||
statsHelpers.setStatsSummary(summary, [viewName, name], stats)
|
||||
)
|
||||
);
|
||||
return summary;
|
||||
}
|
||||
|
||||
summarizePerCustomType(type) {
|
||||
const statsHelpers = this.statsHelpers;
|
||||
const summary = {};
|
||||
forEach(type, (view, viewName) =>
|
||||
forEach(view, (metricName, name) =>
|
||||
forEach(metricName, (stats, propertyName) => {
|
||||
statsHelpers.setStatsSummary(
|
||||
summary,
|
||||
[viewName, name, propertyName],
|
||||
stats
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Aggregator;
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const clone = require('lodash.clonedeep');
|
||||
const get = require('lodash.get');
|
||||
const WebPageTest = require('webpagetest');
|
||||
const { promisify } = require('util');
|
||||
|
||||
module.exports = {
|
||||
async analyzeUrl(url, storageManager, log, wptOptions) {
|
||||
const wptClient = new WebPageTest(wptOptions.host, wptOptions.key);
|
||||
wptOptions.firstViewOnly = !wptOptions.includeRepeatView;
|
||||
let urlOrScript = url;
|
||||
|
||||
log.info('Sending url ' + url + ' to test on ' + wptOptions.host);
|
||||
if (wptOptions.script) {
|
||||
urlOrScript = wptOptions.script.split('{{{URL}}}').join(url);
|
||||
}
|
||||
|
||||
// Setup WebPageTest methods
|
||||
const runTest = promisify(wptClient.runTest.bind(wptClient));
|
||||
const getHARData = promisify(wptClient.getHARData.bind(wptClient));
|
||||
const getScreenshotImage = promisify(
|
||||
wptClient.getScreenshotImage.bind(wptClient)
|
||||
);
|
||||
const getWaterfallImage = promisify(
|
||||
wptClient.getWaterfallImage.bind(wptClient)
|
||||
);
|
||||
const getChromeTraceData = promisify(
|
||||
wptClient.getChromeTraceData.bind(wptClient)
|
||||
);
|
||||
|
||||
// See https://github.com/sitespeedio/sitespeed.io/issues/1367
|
||||
const options = clone(wptOptions);
|
||||
|
||||
try {
|
||||
const data = await runTest(urlOrScript, options);
|
||||
const id = data.data.id;
|
||||
log.info('Got %s analysed with id %s from %s', url, id, options.host);
|
||||
log.verbose('Got JSON from WebPageTest :%:2j', data);
|
||||
|
||||
// Something failed with WebPageTest but how should we handle that?
|
||||
// Also, this doesn't contain every failure case e.g. successfulFV/RVRuns=0 we should include it
|
||||
if (data.statusCode !== 200) {
|
||||
log.error(
|
||||
'The test got status code %s from WebPageTest with %s. Checkout %s to try to find the original reason.',
|
||||
data.statusCode,
|
||||
data.statusText,
|
||||
get(data, 'data.summary')
|
||||
);
|
||||
} else {
|
||||
log.info('WebPageTest result at: ' + data.data.summary);
|
||||
}
|
||||
|
||||
if (
|
||||
data.data.median &&
|
||||
data.data.median.firstView &&
|
||||
data.data.median.firstView.numSteps &&
|
||||
data.data.median.firstView.numSteps > 1
|
||||
) {
|
||||
// MULTISTEP
|
||||
log.info(
|
||||
"Sitespeed.io doesn't support multi step WebPageTest scripting at the moment. Either use sitespeed.io scripting (https://www.sitespeed.io/documentation/sitespeed.io/scripting/) or help us implement support for WebPageTest in https://github.com/sitespeedio/sitespeed.io/issues/2620"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if WPT test has been required with the timeline and chrome, gather additional infos
|
||||
// /!\ this mutates data
|
||||
if (
|
||||
data.statusCode === 200 &&
|
||||
data.data.median.firstView &&
|
||||
'chromeUserTiming' in data.data.median.firstView
|
||||
) {
|
||||
let chromeUserTiming = {};
|
||||
// from "chromeUserTiming": [{"name": "unloadEventStart","time": 0}, …]
|
||||
// to "chromeUserTiming":{unloadEventStart:0, …}
|
||||
data.data.median.firstView.chromeUserTiming.forEach(measure => {
|
||||
chromeUserTiming[measure.name] = measure.time;
|
||||
});
|
||||
data.data.median.firstView.chromeUserTiming = chromeUserTiming;
|
||||
log.verbose(
|
||||
'detected chromeUserTiming and restructured them to :%:2j',
|
||||
chromeUserTiming
|
||||
);
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
let har;
|
||||
promises.push(
|
||||
getHARData(id, {})
|
||||
.then(theHar => (har = theHar))
|
||||
.catch(e =>
|
||||
log.warn(
|
||||
`Couldn't get HAR for id ${id} ${e.message} (url = ${url})`
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const traces = {};
|
||||
const views = ['firstView'];
|
||||
if (!wptOptions.firstViewOnly) {
|
||||
views.push('repeatView');
|
||||
}
|
||||
|
||||
for (const view of views) {
|
||||
for (let run = 1; run <= wptOptions.runs; run++) {
|
||||
// The WPT API wrapper mutates the options object, why ohh why?!?!?!
|
||||
const repeatView = view === 'repeatView';
|
||||
|
||||
promises.push(
|
||||
getScreenshotImage(id, {
|
||||
run,
|
||||
repeatView
|
||||
})
|
||||
.then(img => {
|
||||
return storageManager.writeDataForUrl(
|
||||
img,
|
||||
'wpt-' + run + '-' + view + '.png',
|
||||
url,
|
||||
'screenshots'
|
||||
);
|
||||
})
|
||||
.catch(e => {
|
||||
log.warn(
|
||||
`Couldn't get screenshot for id ${id}, run ${run} from the WebPageTest API with the error ${
|
||||
e.message
|
||||
} (url = ${url})`
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
promises.push(
|
||||
getWaterfallImage(id, {
|
||||
run,
|
||||
repeatView
|
||||
})
|
||||
.then(img =>
|
||||
storageManager.writeDataForUrl(
|
||||
img,
|
||||
'wpt-waterfall-' + run + '-' + view + '.png',
|
||||
url,
|
||||
'waterfall'
|
||||
)
|
||||
)
|
||||
.catch(e =>
|
||||
log.warn(
|
||||
`Couldn't get waterfall for id ${id}, run ${run} from the WebPageTest API with the error: ${
|
||||
e.message
|
||||
} (url = ${url})`
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
promises.push(
|
||||
getWaterfallImage(id, {
|
||||
run,
|
||||
chartType: 'connection',
|
||||
repeatView
|
||||
})
|
||||
.then(img =>
|
||||
storageManager.writeDataForUrl(
|
||||
img,
|
||||
'wpt-waterfall-connection' + run + '-' + view + '.png',
|
||||
url,
|
||||
'waterfall'
|
||||
)
|
||||
)
|
||||
.catch(e =>
|
||||
log.warn(
|
||||
`Couldn't get connection waterfall for id ${id}, run ${run} from the WebPageTest API with the error: ${
|
||||
e.message
|
||||
} (url = ${url})`
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (wptOptions.timeline) {
|
||||
promises.push(
|
||||
getChromeTraceData(id, {
|
||||
run,
|
||||
repeatView
|
||||
})
|
||||
.then(
|
||||
trace => (traces['trace-' + run + '-wpt-' + view] = trace)
|
||||
)
|
||||
.catch(e =>
|
||||
log.warn(
|
||||
`Couldn't get chrome trace for id ${id}, run ${run} from the WebPageTest API with the error: ${
|
||||
e.message
|
||||
} (url = ${url})`
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
const myResult = {
|
||||
data: data.data,
|
||||
har
|
||||
};
|
||||
myResult.trace = traces;
|
||||
return myResult;
|
||||
} catch (e) {
|
||||
if (e.error && e.error.code === 'TIMEOUT') {
|
||||
log.error(
|
||||
'The test for WebPageTest timed out. Is your WebPageTest agent overloaded with work? You can try to increase how long time to wait for tests to finish by configuring --webpagetest.timeout to a higher value (default is 600 and is in seconds). ',
|
||||
e
|
||||
);
|
||||
} else {
|
||||
log.error('Could not run test for WebPageTest', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,297 +0,0 @@
|
|||
'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',
|
||||
// hero timings
|
||||
'data.median.*.heroElementTimes.*',
|
||||
// available only when --timeline option is required for chrome
|
||||
'data.median.*.chromeUserTiming.*',
|
||||
'data.median.*.cpuTimes.*',
|
||||
// Cherry picked metrics for standard deviation
|
||||
'data.standardDeviation.*.SpeedIndex',
|
||||
'data.standardDeviation.*.render',
|
||||
'data.standardDeviation.*.TTFB',
|
||||
'data.standardDeviation.*.loadTime',
|
||||
'data.standardDeviation.*.fullyLoaded',
|
||||
'data.standardDeviation.*.userTimes.*',
|
||||
'data.standardDeviation.*.lastVisualChange',
|
||||
'data.standardDeviation.*.visualComplete85',
|
||||
'data.standardDeviation.*.visualComplete90',
|
||||
'data.standardDeviation.*.visualComplete95',
|
||||
'data.standardDeviation.*.visualComplete99',
|
||||
'data.standardDeviation.*.FirstInteractive',
|
||||
'data.standardDeviation.*.LastInteractive',
|
||||
'data.standardDeviation.*.TimeToInteractive',
|
||||
'data.standardDeviation.*.heroElementTimes.*'
|
||||
];
|
||||
|
||||
// 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);
|
||||
this.allOptions = options;
|
||||
|
||||
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.filterRegistry.registerFilterForType(
|
||||
DEFAULT_SUMMARY_METRICS,
|
||||
'webpagetest.run'
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
case 'browsertime.navigationScripts': {
|
||||
this.log.info(
|
||||
'WebPageTest can only be used with URLs and not with scripting/multiple pages at the moment'
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// We got a URL that we should test
|
||||
case 'url': {
|
||||
const url = message.url;
|
||||
let group = message.group;
|
||||
return analyzer
|
||||
.analyzeUrl(url, this.storageManager, this.log, wptOptions)
|
||||
.then(result => {
|
||||
addCustomMetric(result, filterRegistry);
|
||||
if (result && result.trace) {
|
||||
forEach(result.trace, (value, key) => {
|
||||
queue.postMessage(
|
||||
make('webpagetest.chrometrace', value, {
|
||||
url,
|
||||
group,
|
||||
name: key + '.json'
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (result && result.har) {
|
||||
queue.postMessage(
|
||||
make('webpagetest.har', result.har, { url, group })
|
||||
);
|
||||
|
||||
queue.postMessage(
|
||||
make('webpagetest.browser', {
|
||||
browser: result.har.log.browser
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (result && result.data) {
|
||||
forEach(result.data.runs, (run, runKey) =>
|
||||
queue.postMessage(
|
||||
make('webpagetest.run', run, {
|
||||
url,
|
||||
group,
|
||||
runIndex: parseInt(runKey) - 1
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
if (result && result.data) {
|
||||
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
|
||||
};
|
||||
|
|
@ -1,289 +0,0 @@
|
|||
mixin rowHeading(items)
|
||||
thead
|
||||
tr
|
||||
each item in items
|
||||
th= item
|
||||
|
||||
mixin numberCell(title, number)
|
||||
td.number(data-title=title)= number
|
||||
|
||||
mixin sizeCell(title, size)
|
||||
td.number(data-title=title, data-value= size)= h.size.format(size)
|
||||
|
||||
- const wpt = pageInfo.data.webpagetest.run ? pageInfo.data.webpagetest.run : pageInfo.data.webpagetest.pageSummary.data.median
|
||||
- const wptRoot = pageInfo.data.webpagetest.run ? pageInfo.data.webpagetest : pageInfo.data.webpagetest.pageSummary.data;
|
||||
- const wptRun = runNumber? runNumber : 1
|
||||
|
||||
a
|
||||
h2 WebPageTest
|
||||
p.small Metrics and data collected using #{options.webpagetest.host}.
|
||||
if wptRoot.summary
|
||||
a(href= wptRoot.summary) Check
|
||||
| the result page on WebPageTest.
|
||||
else
|
||||
a(href= pageInfo.data.webpagetest.run.firstView.pages.details) Check
|
||||
| the individual result page on WebPageTest.
|
||||
|
||||
.row
|
||||
.one-half.column
|
||||
table
|
||||
tr
|
||||
th Metric
|
||||
th Value
|
||||
if wptRoot.from
|
||||
tr
|
||||
td From:
|
||||
td !{wptRoot.from}
|
||||
if wptRoot.id
|
||||
tr
|
||||
td Id:
|
||||
td
|
||||
a(href= wptRoot.summary) #{wptRoot.id}
|
||||
if wptRoot.tester
|
||||
tr
|
||||
td Tester:
|
||||
td #{wptRoot.tester}
|
||||
if wpt.firstView
|
||||
tr
|
||||
td Browser:
|
||||
td #{wpt.firstView.browser_name}
|
||||
tr
|
||||
td Version:
|
||||
td #{wpt.firstView.browser_version}
|
||||
tr
|
||||
td Render (first view):
|
||||
td #{wpt.firstView.render}
|
||||
tr
|
||||
td Speed Index (first view):
|
||||
td #{wpt.firstView.SpeedIndex}
|
||||
tr
|
||||
td Last Visual Change (first view):
|
||||
td #{wpt.firstView.lastVisualChange}
|
||||
if wpt.repeatView
|
||||
tr
|
||||
td Render (repeat view):
|
||||
td #{wpt.repeatView.render}
|
||||
tr
|
||||
td SpeedIndex (repeat view):
|
||||
td #{wpt.repeatView.SpeedIndex}
|
||||
tr
|
||||
td Last Visual Change (repeat view):
|
||||
td #{wpt.repeatView.lastVisualChange}
|
||||
|
||||
.one-half.column
|
||||
img.u-max-full-width(src='data/screenshots/wpt-' + wptRun + '-firstView.png', alt='Screenshot')
|
||||
|
||||
.downloads
|
||||
- const harEnding = options.gzipHAR ? '.har.gz' : '.har'
|
||||
- const harName = 'data/webpagetest' + harEnding
|
||||
- const harDownloadName = downloadName + '-webpagetest-' + harEnding
|
||||
|
||||
a.button.button-download(href=harName, download=downloadName) Download HAR
|
||||
if options.webpagetest.timeline
|
||||
- const tracePath = 'data/trace-' + wptRun + '-wpt-firstView.json.gz'
|
||||
a.button.button-download(href=tracePath, download=downloadName + 'trace-' + wptRun + '-wpt-firstView.json.gz') Download first view timeline
|
||||
if wpt.repeatView
|
||||
- const tracePathRepeat = 'data/trace-' + wptRun + '-wpt-repeatView.json.gz'
|
||||
a.button.button-download(href=tracePathRepeat, download=downloadName + 'trace-' + wptRun + '-wpt-repeatView.json.gz') Download repeat view timeline
|
||||
|
||||
each view in ['firstView', 'repeatView']
|
||||
- const median = wpt[view];
|
||||
if median
|
||||
h2 #{view === 'firstView' ? 'First View': 'Repeat View'}
|
||||
if options.webpagetest.video
|
||||
a#visualmetrics
|
||||
h3 Visual Metrics
|
||||
.row
|
||||
.one-half.column
|
||||
table
|
||||
tr
|
||||
th(colspan='2') Visual Metrics
|
||||
tr
|
||||
td Render
|
||||
+numberCell('Render', median.render)
|
||||
tr
|
||||
td Speed Index
|
||||
+numberCell('SpeedIndex', median.SpeedIndex)
|
||||
tr
|
||||
td Visual Complete 85%
|
||||
+numberCell('Visual Complete 85%', median.visualComplete85)
|
||||
tr
|
||||
td Last Visual Change
|
||||
+numberCell('Last Visual Change', median.lastVisualChange)
|
||||
if median.FirstInteractive
|
||||
tr
|
||||
td First Interactive
|
||||
+numberCell('First Interactive', median.FirstInteractive)
|
||||
if median.TimeToInteractive
|
||||
tr
|
||||
td Time To Interactive
|
||||
+numberCell('Time To Interactive', median.TimeToInteractive)
|
||||
if median.LastInteractive
|
||||
tr
|
||||
td Last Interactive
|
||||
+numberCell('Last Interactive', median.LastInteractive)
|
||||
|
||||
.one-half.column
|
||||
if median.videoFrames
|
||||
table
|
||||
tr
|
||||
th(colspan='2') Visual Progress
|
||||
- let lastProgress = -1
|
||||
each frame in median.videoFrames
|
||||
if lastProgress !== frame.VisuallyComplete
|
||||
- lastProgress = frame.VisuallyComplete
|
||||
tr
|
||||
td #{frame.VisuallyComplete}
|
||||
td #{frame.time} ms
|
||||
else
|
||||
p Missing WebPageTest visual metrics
|
||||
|
||||
a#timingsandpagemetrics
|
||||
h3 Timing and page metrics
|
||||
.row
|
||||
.one-half.column
|
||||
table
|
||||
th(colspan='2') Timings
|
||||
tr
|
||||
td TTFB
|
||||
+numberCell('TTFB', median.TTFB)
|
||||
if median.firstPaint
|
||||
tr
|
||||
td First paint
|
||||
+numberCell('First Paint', median.firstPaint.toFixed(0))
|
||||
tr
|
||||
td DOM loading
|
||||
+numberCell('DOM loading', median.domLoading)
|
||||
tr
|
||||
td DOM interactive
|
||||
+numberCell('DOM interactive', median.domInteractive)
|
||||
tr
|
||||
td Load Time
|
||||
+numberCell('Load Time', median.loadTime)
|
||||
tr
|
||||
td Fully Loaded
|
||||
+numberCell('Fully Loaded', median.fullyLoaded)
|
||||
if (median.userTimes)
|
||||
each value, key in median.userTimes
|
||||
tr
|
||||
td #{key}
|
||||
+numberCell(key, value)
|
||||
.one-half.column
|
||||
table
|
||||
tr
|
||||
th(colspan='2') Page metrics
|
||||
tr
|
||||
td Requests
|
||||
+numberCell('Requests', median.requestsFull)
|
||||
tr
|
||||
td Connections
|
||||
+numberCell('Connections', median.connections)
|
||||
tr
|
||||
td DOM Elements
|
||||
+numberCell('DOM Elements', median.domElements)
|
||||
tr
|
||||
td Total image size
|
||||
+sizeCell('Image total', median.image_total)
|
||||
tr
|
||||
td bytesOut
|
||||
+sizeCell('bytesOut', median.bytesOut)
|
||||
tr
|
||||
td bytesOutDoc
|
||||
+sizeCell('bytesOutDoc', median.bytesOutDoc)
|
||||
tr
|
||||
td bytesIn
|
||||
+sizeCell('bytesIn', median.bytesIn)
|
||||
tr
|
||||
td bytesInDoc
|
||||
+sizeCell('bytesInDoc', median.bytesInDoc)
|
||||
if median.certificate_bytes
|
||||
tr
|
||||
td Certificates size
|
||||
+sizeCell('certificate_bytes', median.certificate_bytes)
|
||||
|
||||
//- You need to have timeline or right trace categories to get the timings
|
||||
//- that is default now on WebPageTest.org
|
||||
if (median.chromeUserTiming)
|
||||
.row
|
||||
.column
|
||||
table
|
||||
tr
|
||||
th(colspan='2') Chrome User Timings
|
||||
//- The WebPageTest APi isn't concistent
|
||||
//- Per run it is an array
|
||||
if Array.isArray(median.chromeUserTiming)
|
||||
each value in median.chromeUserTiming
|
||||
tr
|
||||
td #{value.name}
|
||||
td #{value.time}
|
||||
else
|
||||
each value, key in median.chromeUserTiming
|
||||
tr
|
||||
td #{key}
|
||||
td #{value}
|
||||
h3 Waterfall
|
||||
img.u-max-full-width(src='data/waterfall/wpt-waterfall-' + wptRun + '-' + view + '.png', alt='Waterfall view')
|
||||
h3 Connection view
|
||||
img.u-max-full-width(src='data/waterfall/wpt-waterfall-connection' + wptRun + '-' + view + '.png', alt='Connection view')
|
||||
|
||||
h3 Request per content type
|
||||
.responsive
|
||||
if median.breakdown
|
||||
table(data-sortable, id='contentSize')
|
||||
+rowHeading(['Type', 'size', 'size uncompressed', 'requests'])
|
||||
each value, contentType in median.breakdown
|
||||
tr
|
||||
td(data-title='Content Type') #{contentType}
|
||||
+sizeCell('Size', median.breakdown[contentType].bytes)
|
||||
+sizeCell('Size uncompressed', median.breakdown[contentType].bytesUncompressed)
|
||||
+numberCell('Requests', median.breakdown[contentType].requests)
|
||||
else
|
||||
p Missing size data
|
||||
|
||||
h3 Request and size per domain
|
||||
.responsive
|
||||
if median.domains
|
||||
table(data-sortable, id='contentSizePerDomain')
|
||||
+rowHeading(['Domain', 'size', 'requests', 'connections'])
|
||||
each value, domain in median.domains
|
||||
tr
|
||||
td(data-title='Domain') #{domain}
|
||||
+sizeCell('Size', median.domains[domain].bytes)
|
||||
+numberCell('Requests', median.domains[domain].requests)
|
||||
+numberCell('Connections', median.domains[domain].connections)
|
||||
else
|
||||
p Missing domain data
|
||||
//- WebPageTest.org has custom metrics by default
|
||||
if (median.custom)
|
||||
h3 Custom metrics
|
||||
.row
|
||||
.column
|
||||
table
|
||||
each key in median.custom
|
||||
tr
|
||||
td #{key}
|
||||
td.break #{median[key]}
|
||||
//- Not a great fan of JSON with dots ...
|
||||
if (median['lighthouse.BestPractices'])
|
||||
h3 Lighthouse
|
||||
.row
|
||||
.column
|
||||
table
|
||||
tr
|
||||
td Best Practices Score
|
||||
td #{median['lighthouse.BestPractices']}
|
||||
tr
|
||||
td Estimated Input Latency
|
||||
td #{median['lighthouse.Performance.estimated-input-latency']}
|
||||
tr
|
||||
td First Interactive
|
||||
td #{median['lighthouse.Performance.first-interactive']}
|
||||
tr
|
||||
td Accessibility Score
|
||||
td #{median['lighthouse.Accessibility']}
|
||||
tr
|
||||
td SEO Score
|
||||
td #{median['lighthouse.SEO']}
|
||||
tr
|
||||
td Progressive Web App Score
|
||||
td #{median['lighthouse.ProgressiveWebApp']}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,56 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const plugin = require('../lib/plugins/webpagetest');
|
||||
const Aggregator = require('../lib/plugins/webpagetest/aggregator');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const expect = require('chai').expect;
|
||||
const intel = require('intel');
|
||||
const messageMaker = require('../lib/support/messageMaker');
|
||||
const filterRegistry = require('../lib/support/filterRegistry');
|
||||
const statsHelpers = require('../lib/support/statsHelpers');
|
||||
|
||||
const wptResultPath = path.resolve(
|
||||
__dirname,
|
||||
'fixtures',
|
||||
'webpagetest.data.json'
|
||||
);
|
||||
const wptResult = JSON.parse(fs.readFileSync(wptResultPath, 'utf8'));
|
||||
|
||||
describe('webpagetest', () => {
|
||||
const context = { messageMaker, filterRegistry, intel, statsHelpers };
|
||||
|
||||
describe('plugin', () => {
|
||||
it('should require key for default server', () => {
|
||||
expect(() => plugin.open(context, {})).to.throw();
|
||||
});
|
||||
it('should require key for public server', () => {
|
||||
expect(() =>
|
||||
plugin.open(context, { webpagetest: { host: 'www.webpagetest.org' } })
|
||||
).to.throw();
|
||||
});
|
||||
it('should not require key for private server', () => {
|
||||
expect(() =>
|
||||
plugin.open(context, { webpagetest: { host: 'http://myserver.foo' } })
|
||||
).to.not.throw();
|
||||
});
|
||||
});
|
||||
|
||||
const aggregator = new Aggregator(
|
||||
statsHelpers,
|
||||
intel.getLogger('sitespeedio.plugin.webpagetest')
|
||||
);
|
||||
describe('aggregator', () => {
|
||||
it('should summarize data', () => {
|
||||
aggregator.addToAggregate(
|
||||
'www.sitespeed.io',
|
||||
wptResult,
|
||||
'native',
|
||||
'Test',
|
||||
{ video: true }
|
||||
);
|
||||
|
||||
expect(aggregator.summarize()).to.not.be.empty;
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue