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:
Peter Hedenskog 2020-12-15 08:10:47 +01:00 committed by GitHub
parent 25600ac69f
commit f37e0b9d89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1 additions and 6735 deletions

View File

@ -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

View File

@ -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),
{

View File

@ -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;

View File

@ -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);
}
}
}
};

View File

@ -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
};

View File

@ -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}.&nbsp;
if wptRoot.summary
a(href= wptRoot.summary) Check
| &nbsp;the result page on WebPageTest.
else
a(href= pageInfo.data.webpagetest.run.firstView.pages.details) Check
| &nbsp;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

View File

@ -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;
});
});
});