diff --git a/.gitignore b/.gitignore index 3dba12e4a2..6aab85ca45 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ coverage oom_1 **/__diff_output__/** tests/__reports__/*.html +lighthouse +.lighthouseci # OS generated files # ###################### diff --git a/README.md b/README.md index d01ebd7f16..ee4a5c331f 100644 --- a/README.md +++ b/README.md @@ -38,44 +38,55 @@ npm start ## Scripts -- `build` - - runs [semantic-release/assets.sh](./scripts/semantic-release/assets.sh) to build assets for all environments in `dist/` +- `build` -- `build:` where `` is `stage`, `sandbox`, or `production` - - runs webpack with `NODE_ENV=` + - runs [semantic-release/assets.sh](./scripts/semantic-release/assets.sh) to build assets for all environments in `dist/` -- `build:analyze` - - runs [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) in static gzip mode +- `build:` where `` is `stage`, `sandbox`, or `production` -- `build:demo` - - runs webpack with `env.demo` set + - runs webpack with `NODE_ENV=` -- `dev` - - runs webpack-dev-server with `TARGET=sdk`, `NODE_ENV=local`, `STAGE_TAG=local` +- `build:analyze` -- `dev:` where `` is `standalone`, `modal`, or `lander` - - runs webpack-dev-server with `TARGET=`, `NODE_ENV=local`, `STAGE_TAG=local` - - note: `modal` uses `TARGET=standalone-modal` + - runs [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) in static gzip mode -- `dev:` where `` is `stage`, `sandbox`, or `production` - - runs webpack-dev-server with `TARGET=standalone` and `NODE_ENV=` +- `build:demo` -- `lint` - - checks our codebase for style issues + - runs webpack with `env.demo` set -- `preinstall` - - runs automatically on `npm install` and removes `node_modules/` +- `dev` -- `start` - - runs `npm run dev` + - runs webpack-dev-server with `TARGET=sdk`, `NODE_ENV=local`, `STAGE_TAG=local` -- `test` - - runs all unit tests +- `dev:` where `` is `standalone`, `modal`, or `lander` -- `test:` where `` is `func`, `func:nosnaps` or `func:ciupdate` - - `func` runs all snapshot functional tests - - `func:nosnaps` runs all non-snapshot functional tests - - `func:ciupdate` updates all snapshots generated by functional tests + - runs webpack-dev-server with `TARGET=`, `NODE_ENV=local`, `STAGE_TAG=local` + - note: `modal` uses `TARGET=standalone-modal` + +- `dev:` where `` is `stage`, `sandbox`, or `production` + + - runs webpack-dev-server with `TARGET=standalone` and `NODE_ENV=` + +- `lint` + + - checks our codebase for style issues + +- `preinstall` + + - runs automatically on `npm install` and removes `node_modules/` + +- `start` + + - runs `npm run dev` + +- `test` + + - runs all unit tests + +- `test:` where `` is `func`, `func:nosnaps` or `func:ciupdate` + - `func` runs all snapshot functional tests + - `func:nosnaps` runs all non-snapshot functional tests + - `func:ciupdate` updates all snapshots generated by functional tests ## Testing @@ -95,12 +106,16 @@ Example CONFIG_PATH=US/DEV_US_MULTI npm run test:func:snapshots -- --testPathPattern sdk ``` -Alternatively, you can remove ` -- --testPathPattern {integrationType}` and just run the following to run tests on an account for all integration types. +Alternatively, you can remove `-- --testPathPattern {integrationType}` and just run the following to run tests on an account for all integration types. ``` CONFIG_PATH={locale}/{account} npm run test:func:snapshots ``` +## Performance Benchmark + +To run performance benchmark, first run `npm run dev:standalone` in one command line instance and `npm run benchmark` in a second command line instance. The `dev:standalone` command creates static pages that the functional tests are run on. To use lighthouse benchmarking install the lighthouse cli via `npm install -g @lhci/cli`. Prior to running benchmark run `LIGHTHOUSE_URL={URL_WITH_NO_BRACKETS} lhci autorun --config=./tools/performance/config/lighthouserc-desktop.js` and `LIGHTHOUSE_URL={URL_WITH_NO_BRACKETS} lhci autorun --config=./tools/performance/config/lighthouserc-mobile.js` + ## Releasing This package is published weekly, **Every Wednesday**. Please [view our Changelog](CHANGELOG.md) to stay updated with bug fixes and new features. diff --git a/package.json b/package.json index bdea33c118..9a9cf65d8c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "build:sandbox": "webpack --env.NODE_ENV=sandbox", "build:production": "webpack --env.NODE_ENV=production", "build:analyze": "webpack --env.analyze", + "build:analyzeComponents": "webpack --env.analyzeComponents", + "build:analyzeMComponents": "webpack --env.analyzeMessageComponents", "build:demo": "webpack --env.demo", "dev": "webpack-dev-server --config webpack.config.dev.js --env.TARGET=sdk --env.NODE_ENV=local --env.STAGE_TAG=local", "dev:ci": "webpack-dev-server --config webpack.config.dev.js --env.TARGET=ci --env.NODE_ENV=local --env.STAGE_TAG=local", diff --git a/tools/performance/compile.js b/tools/performance/compile.js new file mode 100644 index 0000000000..a649a5f0af --- /dev/null +++ b/tools/performance/compile.js @@ -0,0 +1,60 @@ +const fs = require('fs'); + +const { getComponentHtml } = require(`./components.js`); +const { createLighthouseHtml } = require(`./lighthouse.js`); +const { createMetricsHtml } = require(`./metrics.js`); +const basePath = process.cwd(); + +/** + * Write html to file + * @param {string} html - html + */ +const writeHtmlToFile = html => + fs.writeFile(`${basePath}/dist/performanceData${new Date().toISOString()}.html`, html, err => { + if (err) { + console.log('Error occured when writting performanceData.html'); + } else { + console.log('performanceData.html created'); + } + }); + +/** + * Take data and create html string + * @param {object} data - contains all variables needed for final document + * @returns {string} - html + */ +const html = data => ` + + Performance Benchmark + + + +
${new Date().toDateString()}
+ ${createLighthouseHtml(data)} + ${getComponentHtml(data)} + ${createMetricsHtml(data)} + +`; + +try { + const htmlJsonData = { + ...JSON.parse(fs.readFileSync(`${basePath}/dist/lighthouseScores.json`)), + ...JSON.parse(fs.readFileSync(`${basePath}/dist/components.json`)), + ...JSON.parse(fs.readFileSync(`${basePath}/dist/metrics.json`)) + }; + // create html and write to file + writeHtmlToFile(html(htmlJsonData)); +} catch (err) { + console.log(err); +} diff --git a/tools/performance/components.js b/tools/performance/components.js new file mode 100644 index 0000000000..7a5b9a06bc --- /dev/null +++ b/tools/performance/components.js @@ -0,0 +1,99 @@ +const fs = require('fs'); + +const basePath = process.cwd(); + +/** + * Get the json data for the analyzed bundles + * @param {string} file - file name + * @returns {promise} - array of objects + */ +const getComponentFileData = file => + new Promise(resolve => { + fs.readFile(`${basePath}/dist/${file}.json`, { encoding: 'utf-8' }, (err, data) => { + if (err) { + console.log(err); + throw err; + } + + resolve( + JSON.parse(data).map(row => ({ + label: row.label, + parsedSize: row.parsedSize, + gzipSize: row.gzipSize, + groups: row.groups + })) + ); + }); + }); + +/** + * Create an array of promises for the bundle data + * @returns {array} - array of promises + */ +const getComponentJson = () => { + return [getComponentFileData('messagesReport'), getComponentFileData('componentsReport')]; +}; + +/** + * Create HTML table row + * @param {array} jsonDataArray - each row is a file object + * @returns {string} - HTML table rows + */ +const getFileSizeTableRowHtml = jsonDataArray => { + return jsonDataArray + .map(row => `${row.label}${row.parsedSize} bytes${row.gzipSize} bytes`) + .join(''); +}; + +/** + * Create HTML for all components + * @param {array} messagesReport - bundle data for messages + * @param {array} componentsReport - bundle data for components + * @returns {string} - html for all components + */ +const getComponentHtml = ({ messagesReport, componentsReport }) => { + // Messaging Size + const messaging = getFileSizeTableRowHtml(messagesReport); + + // Modals Sizes + const modals = getFileSizeTableRowHtml(componentsReport); + + // Largest file sizes + const largestFiles = getFileSizeTableRowHtml( + [...componentsReport, ...messagesReport].sort((a, b) => b.gzipSize - a.gzipSize).splice(0, 3) + ); + + const headings = `NameUnzippedGzipped`; + + return `

NPM Modules

+
Largest${messagesReport[0].groups[0].groups[0].label}${messagesReport[0].groups[0].groups[0].parsedSize} bytes (unzipped)
+

File Sizes

+ ${headings}${messaging}${modals}
+

Largest Files

+ ${headings}${largestFiles}
`; +}; + +/** + * Save the html to a json file + * @param {*} json + */ +const outputLighthouseJson = json => { + // save html to json file to be used in compile.js + fs.writeFile('dist/components.json', JSON.stringify({ ...json }), err => { + if (err) { + console.log('components.json failed to save'); + console.log(err); + } else { + console.log('components.json saved'); + } + }); +}; + +if (process.env.BENCHMARK === 'true') { + Promise.all(getComponentJson()).then(reports => { + const [messagesReport, componentsReport] = reports; + outputLighthouseJson({ messagesReport, componentsReport }); + }); +} + +module.exports = { getComponentHtml }; diff --git a/tools/performance/config/lighthouserc-desktop.js b/tools/performance/config/lighthouserc-desktop.js new file mode 100644 index 0000000000..0552824482 --- /dev/null +++ b/tools/performance/config/lighthouserc-desktop.js @@ -0,0 +1,38 @@ +const process = require('process'); + +if (!process.env.ARG) process.env.ARG = process.argv.pop().replace('--', ''); + +module.exports = { + ci: { + collect: { + numberOfRuns: 2, + url: [ + `${process.env.LIGHTHOUSE_URL}/${process.env.STAGE_TAG ? `?stage_tag=${process.env.STAGE_TAG}` : ''}`, + `${process.env.LIGHTHOUSE_URL}/category/jewelry${ + process.env.STAGE_TAG ? `?stage_tag=${process.env.STAGE_TAG}` : '' + }`, + `${process.env.LIGHTHOUSE_URL}/product/7${ + process.env.STAGE_TAG ? `?stage_tag=${process.env.STAGE_TAG}` : '' + }`, + `${process.env.LIGHTHOUSE_URL}/cart${ + process.env.STAGE_TAG ? `?stage_tag=${process.env.STAGE_TAG}` : '' + }`, + `${process.env.LIGHTHOUSE_URL}/checkout${ + process.env.STAGE_TAG ? `?stage_tag=${process.env.STAGE_TAG}` : '' + }` + ], + settings: { + preset: 'desktop', + output: 'json', + maxWaitForLoad: 10000, + chromeFlags: '--no-sandbox --disable-storage-reset --disable-dev-shm-usage --in-process-gpu' + }, + psiStrategy: 'desktop' + }, + upload: { + target: 'filesystem', + outputDir: 'lighthouse', + reportFilenamePattern: 'desktop-report-%%PATHNAME%%-%%DATETIME%%.%%EXTENSION%%' + } + } +}; diff --git a/tools/performance/config/lighthouserc-mobile.js b/tools/performance/config/lighthouserc-mobile.js new file mode 100644 index 0000000000..fa76606018 --- /dev/null +++ b/tools/performance/config/lighthouserc-mobile.js @@ -0,0 +1,37 @@ +const process = require('process'); + +if (!process.env.ARG) process.env.ARG = process.argv.pop().replace('--', ''); + +module.exports = { + ci: { + collect: { + numberOfRuns: 2, + url: [ + `${process.env.LIGHTHOUSE_URL}/${process.env.STAGE_TAG ? `?stage_tag=${process.env.STAGE_TAG}` : ''}`, + `${process.env.LIGHTHOUSE_URL}/category/jewelry${ + process.env.STAGE_TAG ? `?stage_tag=${process.env.STAGE_TAG}` : '' + }`, + `${process.env.LIGHTHOUSE_URL}/product/7${ + process.env.STAGE_TAG ? `?stage_tag=${process.env.STAGE_TAG}` : '' + }`, + `${process.env.LIGHTHOUSE_URL}/cart${ + process.env.STAGE_TAG ? `?stage_tag=${process.env.STAGE_TAG}` : '' + }`, + `${process.env.LIGHTHOUSE_URL}/checkout${ + process.env.STAGE_TAG ? `?stage_tag=${process.env.STAGE_TAG}` : '' + }` + ], + settings: { + output: 'json', + maxWaitForLoad: 10000, + chromeFlags: '--no-sandbox --disable-storage-reset --disable-dev-shm-usage --in-process-gpu' + }, + psiStrategy: 'mobile' + }, + upload: { + target: 'filesystem', + outputDir: 'lighthouse', + reportFilenamePattern: 'mobile-report-%%PATHNAME%%-%%DATETIME%%.%%EXTENSION%%' + } + } +}; diff --git a/tools/performance/lighthouse.js b/tools/performance/lighthouse.js new file mode 100644 index 0000000000..5989b5f2b7 --- /dev/null +++ b/tools/performance/lighthouse.js @@ -0,0 +1,185 @@ +const fs = require('fs'); + +const basePath = process.cwd(); + +/** + * Get lighthouse files inside of lighthouse directory if it exists + * @returns {Promise} - array of lighthouse files names + */ +const checkDirectory = () => { + return new Promise(resolve => { + fs.readdir(`${basePath}/lighthouse`, { encoding: 'utf8' }, (err, file) => { + if (err) { + resolve([]); + } + resolve(file); + }); + }); +}; + +/** + * Get json from file + * @param {string} file - name of file + * @returns {json} + */ +const getLighthouseReport = file => JSON.parse(fs.readFileSync(`${basePath}/lighthouse/${file}`, { encoding: 'utf8' })); + +/** + * Return score object for 1 run + * @param {string} file - name of file + * @returns {object} + */ +const formatScoreObject = file => { + const lighthouseReport = getLighthouseReport(file); + return { + url: lighthouseReport.requestedUrl, + fetchTime: lighthouseReport.fetchTime, + performance: lighthouseReport.categories.performance.score, + accessibility: lighthouseReport.categories.accessibility.score, + bestPractices: lighthouseReport.categories['best-practices'].score, + seo: lighthouseReport.categories.seo.score + }; +}; + +/** + * Read raw lighthouse json files and organize by url and seperate desktop and mobile + * @param {array} files - array of file names + * @returns {object} - desktop and mobile scores grouped by url + */ +const groupScores = files => { + const scores = { desktopScores: {}, mobileScores: {} }; + files.forEach(file => { + let pageLayout = ''; + if (file.indexOf('desktop-report') !== -1) { + pageLayout = 'desktopScores'; + } else if (file.indexOf('mobile-report') !== -1) { + pageLayout = 'mobileScores'; + } + // There are other files besides json is output by lighthouse + if (file.indexOf('json') !== -1 && pageLayout) { + const score = formatScoreObject(file); + if (!scores[pageLayout][score.url]) { + scores[pageLayout][score.url] = []; + } + scores[pageLayout][score.url].push(score); + } + }); + + return scores; +}; + +/** + * Get average score of most recent lighthouse scores (excludes the warm up run) + * @param {array} scores - array of site score objects + * @param {string} page - url + * @returns {object} - scores for page + */ +const getSortedAverageScores = (scores, page) => { + const timeSorted = scores.sort((a, b) => a.fetchTime - b.fetchTime).slice(1); + + const averageTimes = { + performance: 0, + accessibility: 0, + bestPractices: 0, + seo: 0 + }; + + timeSorted.forEach(run => { + averageTimes.performance += run.performance; + averageTimes.accessibility += run.accessibility; + averageTimes.bestPractices += run.bestPractices; + averageTimes.seo += run.seo; + }); + + const count = timeSorted.length; + return { + url: page, + performance: averageTimes.performance / count, + accessibility: averageTimes.accessibility / count, + bestPractices: averageTimes.bestPractices / count, + seo: averageTimes.seo / count + }; +}; + +/** + * Turns array of page scores into one averaged score per page + * @param {array} desktopLighthouseScores - all scores for each desktop page + * @param {array} mobileLighthouseScores - all scores for each mobile page + * @returns {object} - desktop and mobile averaged score for each page + */ +const getScores = (desktopLighthouseScores, mobileLighthouseScores) => { + const desktopAverageScores = {}; + const mobileAverageScores = {}; + + Object.entries(desktopLighthouseScores).forEach(page => { + const url = page[0]; + desktopAverageScores[url] = getSortedAverageScores(desktopLighthouseScores[url], url); + mobileAverageScores[url] = getSortedAverageScores(mobileLighthouseScores[url], url); + }); + + return { desktopAverageScores, mobileAverageScores }; +}; + +/** + * Get scores in table rows + * @param {array} scoresArr desktop or mobile scores + * @returns {array} - html table rows of scores + */ +const scores = scoresArr => + Object.keys(scoresArr) + .map( + score => + ` + ${score} + ${scoresArr[score].performance.toFixed(3)} + ${scoresArr[score].accessibility.toFixed(3)} + ${scoresArr[score].bestPractices.toFixed(3)} + ${scoresArr[score].seo.toFixed(3)} + ` + ) + .join(''); + +/** + * Create the html from the scores + * @param {object} desktopAverageScores - pages with averaged scores + * @param {object} mobileAverageScores - pages with averaged scores + * @returns {string} - html + */ +const createLighthouseHtml = ({ desktopAverageScores, mobileAverageScores }) => { + const desktopLighthouse = scores(desktopAverageScores); + const mobileLighthouse = scores(mobileAverageScores); + const lighthouseHeadings = `URLPerformanceAccessibilityBest PracticesSEO`; + + return `

Lighthouse Scores

+

Desktop

+ ${lighthouseHeadings}${desktopLighthouse}
+

Mobile

+ ${lighthouseHeadings}${mobileLighthouse}
`; +}; + +/** + * Save the html to a json file + * @param {object} json + */ +const outputLighthouseJson = json => { + fs.writeFile('dist/lighthouseScores.json', JSON.stringify({ ...json }), err => { + if (err) { + console.log('lighthouseScores.json failed to save'); + console.log(err); + } else { + console.log('lighthouseScores.json saved'); + } + }); +}; + +// Create Lighthouse Json Output for Report Document +if (process.env.BENCHMARK === 'true') { + checkDirectory().then(files => { + const { desktopScores, mobileScores } = groupScores(files); + const { desktopAverageScores, mobileAverageScores } = getScores(desktopScores, mobileScores); + // create html for lighthouse scores and save to json file for compile.js + outputLighthouseJson({ desktopAverageScores, mobileAverageScores }); + }); +} + +module.exports = { createLighthouseHtml }; diff --git a/tools/performance/metrics.js b/tools/performance/metrics.js new file mode 100644 index 0000000000..e9e6b7c44c --- /dev/null +++ b/tools/performance/metrics.js @@ -0,0 +1,254 @@ +const puppeteer = require('puppeteer'); +const fs = require('fs'); +const { PerformanceObserver, performance } = require('perf_hooks'); + +/** + * Generate html from the metrics + * @param {object} metricsReport - stats + * @returns {string} - html + */ +const createMetricsHtml = ({ + totalRequests, + totalDownloadGzipped, + totalDownloadUnzipped, + totalUpload, + firstRenderDelay, + renderDuration, + largestRequests, + networkRequests +}) => { + // create table row for largest requests + const largestRequestsRow = largestRequests + .map(files => `${files.url}${files.encoding}${files.size} bytes`) + .join(''); + + // create table row for network requests + const networkRequestsRow = networkRequests + .map(files => `${files.url.split('?')[0]}${files.speed.toFixed(4)}`) + .join(''); + + return `

Network Requests

+ + + + + + + +
Total Requests${totalRequests}
Total Download Gzipped${totalDownloadGzipped} bytes
Total Download Unzipped${totalDownloadUnzipped} bytes
Total Upload${totalUpload} bytes
First Render Delay${firstRenderDelay} ms
Render Duration${renderDuration} ms
+

Largest Requests

+ + + ${largestRequestsRow} +
URLEncodingSize
+

All Network Requests

+ + + ${networkRequestsRow} +
URLTime in Seconds
`; +}; + +/** + * Generate json from the metrics + * @param {object} metricsReport - stats + */ +const createMetricsJson = metricsReport => { + fs.writeFile('dist/metrics.json', JSON.stringify({ ...metricsReport }), err => { + if (err) { + console.log('metrics.json failed to save'); + console.log(err); + } else { + console.log('metrics.json saved'); + } + }); +}; + +// start puppeteer. timeout allows for message library to start running in browser on jenkins +const getMetrics = () => + setTimeout( + async () => { + const networkRequests = []; + const stats = { + totalRequests: 0, + totalDownloadGzipped: 0, + totalDownloadUnzipped: 0, + totalUpload: 0, + firstRenderDelay: null, + renderDuration: null + }; + const responseTimes = {}; + + const browser = await puppeteer.launch({ + headless: true, + ignoreHTTPSErrors: true, + devtools: false, + args: ['--no-sandbox'] + }); + + const page = await browser.newPage(); + + await page.setDefaultNavigationTimeout(0); + + // Listen to the network requests + page.setRequestInterception(true); + + // Listen for requests and get request url and requestId to help match sizes + page.on('request', async interceptedRequest => { + performance.mark(JSON.stringify({ start: interceptedRequest.url() })); + // Get log request with firstRenderDelay and renderDuration + if (interceptedRequest.url().includes('credit-presentment/log') && interceptedRequest._postData) { + const { tracking } = JSON.parse(interceptedRequest._postData); + // look through each tracking request and fnd first render delay and renderDuration + tracking.forEach(row => { + if (row.first_render_delay) { + stats.firstRenderDelay = Number(row.first_render_delay); + } + if (row.render_duration) { + stats.renderDuration = Number(row.render_duration); + } + }); + } + + interceptedRequest.continue(); + }); + + // Listen for responses and get speed + page.on('response', async response => { + // start tracking performance based on url + performance.mark(JSON.stringify({ end: response.url() })); + const obs = new PerformanceObserver((perfObserverList, observer) => { + perfObserverList.getEntries().forEach(row => { + // get name object and startTimes + const { name, startTime } = row; + // get name of mark for start and end + const { start, end } = JSON.parse(name); + // add the start and end times to an object with matching key names + if (start) { + if (!responseTimes[start]) { + responseTimes[start] = {}; + } + responseTimes[start].start = startTime; + } else { + if (!responseTimes[end]) { + responseTimes[end] = {}; + } + responseTimes[end].end = startTime; + } + }); + + observer.disconnect(); + }); + obs.observe({ entryTypes: ['mark'], buffered: true }); + }); + + // Listen for when response received and get info about requests + page._client.on('Network.responseReceived', requestReceived => { + // requestReceived.response.encodedDataLength is inaccurate for gzipped but fine for upload size. Getting gzip size from performance function data instead + // get base64 image size + const stringLength = requestReceived.response.url.length - 'data:image/png;base64,'.length; + const sizeInBytes = 4 * Math.ceil(stringLength / 3) * 0.5624896334383812; + const sizeInKb = sizeInBytes / 1000; + + // remove gatsby build files + if (requestReceived.response.url.indexOf('herokuapp') === -1) { + // get network requests + networkRequests.push({ + url: requestReceived.response.url, + speed: requestReceived.response.timing + ? requestReceived.timestamp - requestReceived.response.timing.requestTime + : 0, + encoding: + (requestReceived.response.headersText && + requestReceived.response.headersText.includes('Content-Encoding')) || + (requestReceived.response.headersText && + requestReceived.response.headersText.includes('content-encoding')) || + (requestReceived.response.headers && + requestReceived.response.headers['Content-Encoding']) || + (requestReceived.response.headers && requestReceived.response.headers['content-encoding']) + ? 'gzip' + : 'unzipped', + size: + Number(requestReceived.response.headers['content-length']) || + Number(requestReceived.response.headers['Content-Length']) || + requestReceived.response.encodedDataLength || + sizeInKb, + download: + (requestReceived.response.headersText && + requestReceived.response.headersText.includes('Content-Encoding: gzip')) || + Boolean(Number(requestReceived.response.headers['content-length'])) || + Boolean(Number(requestReceived.response.headers['Content-Length'])) + }); + } + }); + + // go to localhost standalone or requested metrics_url with the supplied stage tag + await page.goto( + `https://${process.env.METRICS_URL || 'localhost.paypal.com:8080/standalone.html'}?env=stage${ + process.env.STAGE_TAG ? `&stageTag=${process.env.STAGE_TAG}` : '' + }`, + { + waitUntil: 'networkidle2' + } + ); + + // look for performance entries that the resource type + const resourceMetrics = await page.evaluate(() => { + return { + parentDocument: JSON.stringify(performance.getEntriesByType('resource')) + }; + }); + + // Wait for long enough for log request for fire + await page.waitFor(10000).then(() => { + const parentDocument = JSON.parse(resourceMetrics.parentDocument); + networkRequests.forEach((requests, index) => { + // count total requests + stats.totalRequests += 1; + // calculate the speed + if ( + !requests.speed && + responseTimes[requests.url] && + responseTimes[requests.url].start && + responseTimes[requests.url].end + ) { + const { start, end } = responseTimes[requests.url]; + networkRequests[index].speed = end - start; + } + // get size of gzip and unzipped downloads + if (requests.download) { + if (requests.encoding === 'gzip') { + [...parentDocument].forEach(performanceRequestData => { + if (requests.url === performanceRequestData.name) { + networkRequests[index].size = performanceRequestData.encodedBodySize; + } + }); + stats.totalDownloadGzipped += requests.size; + } else { + stats.totalDownloadUnzipped += requests.size; + } + } else { + stats.totalUpload += requests.size; + } + }); + // sort largest requests + const largestRequests = [...networkRequests].sort((a, b) => { + return b.size - a.size; + }); + // take only top 3 largest requests + stats.largestRequests = largestRequests.splice(0, 3); + stats.networkRequests = [...networkRequests]; + // create html out of stats + createMetricsJson(stats); + }); + + await browser.close(); + await process.exit(); + }, + process.env.METRICS_URL ? 0 : 30000 // need to wait for server to start in jenkins + ); + +if (process.env.BENCHMARK === 'true') { + getMetrics(); +} + +module.exports = { createMetricsHtml }; diff --git a/webpack.config.js b/webpack.config.js index 94a0d6cf6e..dab34e868f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -87,6 +87,45 @@ module.exports = (env = {}) => { }) }); + const MESSAGING_COMPONENTS_CONFIG = getWebpackConfig({ + libraryTarget: 'window', + modulename: 'crc', + web: true, + minify: true, + debug: false, + analyze: env.analyzeMessageComponents, + entry: './src/components/message/index.js', + filename: 'smart-credit-message.js', + env: env.NODE_ENV, + vars: globals({ + ...env, + TARGET: 'messagingComponent' + }) + }); + + if (process.env.BENCHMARK === 'true') { + COMPONENTS_CONFIG.plugins.forEach((plugin, index) => { + if (plugin.constructor.name === 'BundleAnalyzerPlugin') { + COMPONENTS_CONFIG.plugins[index].opts.analyzerMode = 'json'; + COMPONENTS_CONFIG.plugins[index].opts.reportFilename = 'componentsReport.json'; + } + }); + + MESSAGING_JS_CONFIG.plugins.forEach((plugin, index) => { + if (plugin.constructor.name === 'BundleAnalyzerPlugin') { + MESSAGING_JS_CONFIG.plugins[index].opts.analyzerMode = 'json'; + MESSAGING_JS_CONFIG.plugins[index].opts.reportFilename = 'messagesReport.json'; + } + }); + + MESSAGING_COMPONENTS_CONFIG.plugins.forEach((plugin, index) => { + if (plugin.constructor.name === 'BundleAnalyzerPlugin') { + MESSAGING_COMPONENTS_CONFIG.plugins[index].opts.analyzerMode = 'json'; + MESSAGING_COMPONENTS_CONFIG.plugins[index].opts.reportFilename = 'messagingCompReport.json'; + } + }); + } + const RENDERING_CONFIG = getWebpackConfig({ entry: { renderMessage: './src/server/index.js'