diff --git a/lib/plugins/html/htmlBuilder.js b/lib/plugins/html/htmlBuilder.js index 90297d863..30e713f53 100644 --- a/lib/plugins/html/htmlBuilder.js +++ b/lib/plugins/html/htmlBuilder.js @@ -342,7 +342,6 @@ class HTMLBuilder { const pageRuns = this.pageRuns.filter( run => !!get(pageInfo.data, [run.id, 'run']) ); - let rootPath = this.storageManager.rootPathFromUrl(url, daurlAlias); let data = { daurl: url, @@ -380,6 +379,7 @@ class HTMLBuilder { headers: this.summary, version: packageInfo.version, timestamp: runTimestamp, + friendlyNames, context: this.context, pageRuns }; @@ -387,10 +387,18 @@ class HTMLBuilder { for (const run of pageRuns) { pugs[run.id] = renderer.renderTemplate(run.id, data); } + data.pugs = pugs; urlPageRenders.push( this._renderUrlRunPage(url, parseInt(runIndex) + 1, data, daurlAlias) ); + + // Do only once per URL + if (parseInt(runIndex) === 0) { + urlPageRenders.push( + this._renderMetricSummaryPage(url, 'metrics', data, daurlAlias) + ); + } } } @@ -486,6 +494,16 @@ class HTMLBuilder { ); } + async _renderMetricSummaryPage(url, name, locals, alias) { + log.debug('Render URL metric page %s', name); + return this.storageManager.writeHtmlForUrl( + renderer.renderTemplate('url/summary/metrics/index', locals), + name + '.html', + url, + alias + ); + } + async _renderSummaryPage(name, locals) { log.debug('Render summary page %s', name); diff --git a/lib/plugins/html/templates/url/summary/index.pug b/lib/plugins/html/templates/url/summary/index.pug index b8e822234..287c95a98 100644 --- a/lib/plugins/html/templates/url/summary/index.pug +++ b/lib/plugins/html/templates/url/summary/index.pug @@ -27,9 +27,12 @@ block content each val, index in runPages - value = Number(index) + 1 a(href='./' + value + '.html') #{value} - if (value !== Object.keys(runPages).length) + if (value === Object.keys(runPages).length) |  - - | + a(href='metrics.html') (side by side) + else + |  - + | if pageInfo.errors .errors diff --git a/lib/plugins/html/templates/url/summary/metrics/index.pug b/lib/plugins/html/templates/url/summary/metrics/index.pug new file mode 100644 index 000000000..40e6295da --- /dev/null +++ b/lib/plugins/html/templates/url/summary/metrics/index.pug @@ -0,0 +1,39 @@ +extends ../layout.pug + +block content + h1 Metrics per run + - const daTitle = daurlAlias ? daurlAlias : daurl + h5.url + a(href=daurl) #{decodeURIComponent(daTitle)} + + include ../../includes/pageRunInfo + + - const tools = ['browsertime', 'pagexray'] + - const metrics = {}; + - for (let tool of Object.keys(friendlyNames)) { + - if (tools.indexOf(tool) > -1) { + - metrics[tool] = friendlyNames[tool] + - } + - } + + h3 Side by side + .responsive + table + tr + - let run = 1 + th Metric + each page in runPages + th + b #{run} + - run++ + each tool in Object.keys(metrics) + each metricType in Object.keys(metrics[tool]) + each metric in Object.keys(metrics[tool][metricType]) + - const friendly = metrics[tool][metricType][metric] + - const m = get (runPages[0], 'data.' + tool + '.run.' + (friendly.runPath || friendly.path), 'hepp') + if (m !== 'hepp') + tr + td + b #{friendly.name} + each page in runPages + td #{friendly.format(get (page, 'data.' + tool + '.run.' + (friendly.runPath || friendly.path)))} diff --git a/lib/support/friendlynames.js b/lib/support/friendlynames.js index 14ef4df3c..52b1cd41d 100644 --- a/lib/support/friendlynames.js +++ b/lib/support/friendlynames.js @@ -1,5 +1,5 @@ 'use strict'; -const { noop, size, time, co2 } = require('./helpers'); +const { noop, size, time, co2, httpErrors, decimals } = require('./helpers'); module.exports = { browsertime: { @@ -7,128 +7,149 @@ module.exports = { firstContentfulPaint: { path: "statistics.timings.paintTiming['first-contentful-paint'].median", summaryPath: "paintTiming['first-contentful-paint']", + runPath: "timings.paintTiming['first-contentful-paint']", name: 'First Contentful Paint', format: time.ms }, largestContentfulPaint: { path: 'statistics.timings.largestContentfulPaint.renderTime.median', summaryPath: 'timings.largestContentfulPaint', + runPath: 'timings.largestContentfulPaint.renderTime', name: 'Largest Contentful Paint', format: time.ms }, totalBlockingTime: { path: 'statistics.cpu.longTasks.totalBlockingTime.median', summaryPath: 'cpu.longTasks.totalBlockingTime', + runPath: 'cpu.longTasks.totalBlockingTime', name: 'Total Blocking Time', format: time.ms }, cumulativeLayoutShift: { path: 'statistics.pageinfo.cumulativeLayoutShift.median', + runPath: 'pageinfo.cumulativeLayoutShift', summaryPath: 'pageinfo.cumulativeLayoutShift', name: 'Cumulative Layout Shift', - format: noop + format: decimals } }, timings: { firstPaint: { path: 'statistics.timings.firstPaint.median', summaryPath: 'firstPaint', + runPath: 'timings.firstPaint', name: 'First Paint', format: time.ms }, firstContentfulPaint: { path: "statistics.timings.paintTiming['first-contentful-paint'].median", summaryPath: "paintTiming['first-contentful-paint']", + runPath: "timings.paintTiming['first-contentful-paint']", name: 'First Contentful Paint', format: time.ms }, largestContentfulPaint: { path: 'statistics.timings.largestContentfulPaint.renderTime.median', summaryPath: 'timings.largestContentfulPaint', + runPath: 'timings.largestContentfulPaint.renderTime', name: 'Largest Contentful Paint', format: time.ms }, loadEventEnd: { path: 'statistics.timings.loadEventEnd.median', summaryPath: 'loadEventEnd', + runPath: 'timings.loadEventEnd', name: 'Load Event End', format: time.ms }, fullyLoaded: { path: 'statistics.timings.fullyLoaded.median', summaryPath: 'timings.fullyLoaded', + runPath: 'timings.fullyLoaded', name: 'Fully Loaded', format: time.ms }, serverResponseTime: { path: 'statistics.timings.pageTimings.serverResponseTime.median', summaryPath: 'pageTimings.serverResponseTime', + runPath: 'timings.pageTimings.serverResponseTime', name: 'Server Response Time', format: time.ms }, backEndTime: { path: 'statistics.timings.pageTimings.backEndTime.median', summaryPath: 'pageTimings.backEndTime', + runPath: 'timings.pageTimings.backEndTime', name: 'TTFB', format: time.ms }, pageLoadTime: { path: 'statistics.timings.pageTimings.pageLoadTime.median', summaryPath: 'pageTimings.pageLoadTime', + runPath: 'timings.pageTimings.pageLoadTime', name: 'Page Load Time', format: time.ms }, FirstVisualChange: { path: 'statistics.visualMetrics.FirstVisualChange.median', summaryPath: 'visualMetrics.FirstVisualChange', + runPath: 'visualMetrics.FirstVisualChange', name: 'First Visual Change', format: time.ms }, LastVisualChange: { path: 'statistics.visualMetrics.LastVisualChange.median', - name: 'Last Visual Change', summaryPath: 'visualMetrics.LastVisualChange', + runPath: 'visualMetrics.LastVisualChange', + name: 'Last Visual Change', format: time.ms }, SpeedIndex: { path: 'statistics.visualMetrics.SpeedIndex.median', summaryPath: 'visualMetrics.SpeedIndex', + runPath: 'visualMetrics.SpeedIndex', name: 'Speed Index', format: time.ms }, ContentfulSpeedIndex: { path: 'statistics.visualMetrics.ContentfulSpeedIndex.median', summaryPath: 'visualMetrics.ContentfulSpeedIndex', + runPath: 'visualMetrics.ContentfulSpeedIndex', name: 'Contentful Speed Index', format: time.ms }, PerceptualSpeedIndex: { path: 'statistics.visualMetrics.PerceptualSpeedIndex.median', summaryPath: 'visualMetrics.PerceptualSpeedIndex', + runPath: 'visualMetrics.PerceptualSpeedIndex', name: 'Perceptual Speed Index', format: time.ms }, VisualReadiness: { path: 'statistics.visualMetrics.VisualReadiness.median', summaryPath: 'visualMetrics.VisualReadiness', + runPath: 'visualMetrics.VisualReadiness', name: 'Visual Readiness', format: time.ms }, VisualComplete95: { path: 'statistics.visualMetrics.VisualComplete95.median', summaryPath: 'visualMetrics.VisualComplete95', + runPath: 'visualMetrics.VisualComplete95', name: 'Visual Complete 95', format: time.ms }, VisualComplete99: { path: 'statistics.visualMetrics.VisualComplete99.median', summaryPath: 'visualMetrics.VisualComplete99', + runPath: 'visualMetrics.VisualComplete99', name: 'Visual Complete 99', format: time.ms }, VisualComplete: { path: 'statistics.visualMetrics.VisualComplete.median', summaryPath: 'visualMetrics.VisualComplete', + runPath: 'visualMetrics.VisualComplete', name: 'Visual Complete', format: time.ms } @@ -137,33 +158,61 @@ module.exports = { totalBlockingTime: { path: 'statistics.cpu.longTasks.totalBlockingTime.median', summaryPath: 'cpu.longTasks.totalBlockingTime', + runPath: 'cpu.longTasks.totalBlockingTime', name: 'Total Blocking Time', format: time.ms }, maxPotentialFid: { path: 'statistics.cpu.longTasks.maxPotentialFid.median', summaryPath: 'cpu.longTasks.maxPotentialFid', + runPath: 'cpu.longTasks.maxPotentialFid', name: 'Max Potential FID', format: time.ms }, longTasks: { path: 'statistics.cpu.longTasks.tasks.median', summaryPath: 'cpu.longTasks.tasks', + runPath: 'cpu.longTasks.tasks', name: 'Number of Long Tasks', format: noop }, longTasksTotalDuration: { path: 'statistics.cpu.longTasks.totalDuration.median', summaryPath: 'cpu.longTasks.totalDuration', + runPath: 'cpu.longTasks.totalDuration', name: 'Total Duration of Long Tasks', format: time.ms } }, + browser: { + cpuBenchmark: { + path: 'statistics.browser.cpuBenchmark.median', + summaryPath: 'browser.cpuBenchmark', + runPath: 'browser.cpuBenchmark', + name: 'CPU benchmark score', + format: time.ms + } + }, pageinfo: { cumulativeLayoutShift: { path: 'statistics.pageinfo.cumulativeLayoutShift.median', summaryPath: 'pageinfo.cumulativeLayoutShift', + runPath: 'pageinfo.cumulativeLayoutShift', name: 'Cumulative Layout Shift', + format: decimals + }, + domElements: { + path: 'statistics.pageinfo.domElements.median', + summaryPath: 'pageinfo.domElements', + runPath: 'pageinfo.domElements', + name: 'DOM elements', + format: noop + }, + documentHeight: { + path: 'statistics.pageinfo.documentHeight.median', + summaryPath: 'pageinfo.documentHeight', + runPath: 'pageinfo.documentHeight', + name: 'Document height', format: noop } } @@ -196,7 +245,11 @@ module.exports = { name: 'Font Requests', format: noop }, - httpErrors: { path: 'responseCodes', name: 'HTTP Errors', format: noop } + httpErrors: { + path: 'responseCodes', + name: 'HTTP Errors', + format: httpErrors + } }, transferSize: { total: { diff --git a/lib/support/helpers/decimals.js b/lib/support/helpers/decimals.js new file mode 100644 index 000000000..68bb93d54 --- /dev/null +++ b/lib/support/helpers/decimals.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function(decimals) { + let number = Number(decimals).toFixed(3); + if (number === '0.000') { + return 0; + } else return number; +}; diff --git a/lib/support/helpers/httpErrors.js b/lib/support/helpers/httpErrors.js new file mode 100644 index 000000000..6ea80d58d --- /dev/null +++ b/lib/support/helpers/httpErrors.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = function(httpCodes) { + let data = ''; + for (let code of Object.keys(httpCodes)) { + if (Number(code) > 399) { + data += `${code}: ${httpCodes[code]} `; + } + } + return data === '' ? '0' : data; +}; diff --git a/lib/support/helpers/index.js b/lib/support/helpers/index.js index fc565c2fb..4221ae756 100644 --- a/lib/support/helpers/index.js +++ b/lib/support/helpers/index.js @@ -12,5 +12,7 @@ module.exports = { shortAsset: require('./shortAsset'), co2: require('./co2'), noop: require('./noop'), - percent: require('./percent') + percent: require('./percent'), + httpErrors: require('./httpErrors'), + decimals: require('./decimals') };