Last active
April 13, 2020 16:29
-
-
Save SilentImp/691d7f8874361e422391e1ca3034f98a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require('dotenv').config(); | |
const fs = require('fs'); | |
const lighthouse = require('lighthouse'); | |
const puppeteer = require('puppeteer'); | |
const cluster = require('cluster'); | |
const numCPUs = require('os').cpus().length; | |
const DOMAIN = process.env.DOMAIN; | |
const fetch = require('isomorphic-fetch'); | |
const FormData = require('form-data'); | |
const branchName = require('current-git-branch'); | |
// Branch from which we are making merge request | |
// In the pipeline we have environment variable `CI_COMMIT_REF_NAME`, | |
// which contains name of this banch. Function `branchName` | |
// will return something like «HEAD detached» message in the pipeline. | |
// And name of the branch outside of pipeline | |
const CURRENT_BRANCH = process.env.CI_COMMIT_REF_NAME || branchName(); | |
// Merge request target branch, usually it's master | |
const DEFAULT_BRANCH = process.env.CI_DEFAULT_BRANCH || 'master'; | |
// ID of the project | |
const GITLAB_PROJECT_ID = process.env.CI_PROJECT_ID || '18090019'; | |
// Token | |
const TOKEN = process.env.GITLAB_TOKEN; | |
// GitLab domain | |
const GITLAB_DOMAIN = process.env.CI_SERVER_HOST || process.env.GITLAB_DOMAIN || 'gitlab.com'; | |
// User or organization name | |
const NAME_SPACE = process.env.CI_PROJECT_NAMESPACE || process.env.PROJECT_NAMESPACE || 'silentimp'; | |
// Repo name | |
const PROJECT = process.env.CI_PROJECT_NAME || process.env.PROJECT_NAME || 'lighthouse-comments'; | |
// Name of the job, which create an artifact | |
const JOB_NAME = process.env.CI_JOB_NAME || process.env.JOB_NAME || 'lighthouse'; | |
/** | |
* Return part of report for specific page | |
* | |
* @param report {Object} — report | |
* @param subject {String} — subject, that allow find specific page | |
* @return {Object} — page report | |
*/ | |
const getPage = (report, subject) => report.find(item => (item.subject === subject)); | |
/** | |
* Return specific metric for the page | |
* | |
* @param page {Object} — page | |
* @param name {String} — metrics name | |
* @return {Object} — metric | |
*/ | |
const getMetric = (page, name) => page.metrics.find(item => item.name === name); | |
/** | |
* Return table cell for desired metric | |
* | |
* @param branch {Object} - report from feature branch | |
* @param master {Object} - report from master branch | |
* @param name {String} - metrics name | |
*/ | |
const buildCell = (branch, master, name) => { | |
const branchMetric = getMetric(branch, name); | |
const masterMetric = getMetric(master, name); | |
const branchValue = branchMetric.value; | |
const masterValue = masterMetric.value; | |
const desiredLarger = branchMetric.desiredSize === 'larger'; | |
const isChanged = branchValue !== masterValue; | |
const larger = branchValue > masterValue; | |
if (!isChanged) return `${branchValue}`; | |
if (larger) return `${branchValue} ${desiredLarger ? '💚' : '💔' } **+${Math.abs(branchValue - masterValue).toFixed(2)}**`; | |
return `${branchValue} ${!desiredLarger ? '💚' : '💔' } **-${Math.abs(branchValue - masterValue).toFixed(2)}**`; | |
}; | |
/** | |
* Returns text of the comment with table inside | |
* This table contain changes in all metrics | |
* | |
* @param branch {Object} report from feature branch | |
* @param master {Object} report from master branch | |
* @return {String} comment markdown | |
*/ | |
const buildCommentText = (branch, master) =>{ | |
const md = branch.map( page => { | |
const pageAtMaster = getPage(master, page.subject); | |
if (!pageAtMaster) return ''; | |
const md = `|${page.subject}|${buildCell(page, pageAtMaster, 'Performance')}|${buildCell(page, pageAtMaster, 'Accessibility')}|${buildCell(page, pageAtMaster, 'Best Practices')}|${buildCell(page, pageAtMaster, 'SEO')}| | |
`; | |
return md; | |
}).join(''); | |
return ` | |
|Path|Performance|Accessibility|Best Practices|SEO| | |
|--- |--- |--- |--- |--- | | |
${md} | |
`; | |
}; | |
/** | |
* Returns an artifact | |
* | |
* @param name {String} artifact file name | |
* @return {Object} object with performance artifact | |
* @throw {Error} thhrow an error, if artifact contain string, that can't be parsed as a JSON. Or in case of fetch errors. | |
*/ | |
const getArtifact = async name => { | |
const response = await fetch(`https://${GITLAB_DOMAIN}/${NAME_SPACE}/${PROJECT}/-/jobs/artifacts/master/raw/${name}?job=${JOB_NAME}`); | |
if (!response.ok) throw new Error('Artifact not found'); | |
const data = await response.json(); | |
return data; | |
}; | |
/** | |
* Returns iid of the merge request from feature branch to master | |
* @param from {String} — name of the feature branch | |
* @param to {String} — name of the master branch | |
* @return {Number} — iid of the merge request | |
*/ | |
const getMRID = async (from, to) => { | |
const response = await fetch(`https://${GITLAB_DOMAIN}/api/v4/projects/${GITLAB_PROJECT_ID}/merge_requests?target_branch=${to}&source_branch=${from}`, { | |
method: 'GET', | |
headers: { | |
'PRIVATE-TOKEN': TOKEN, | |
} | |
}); | |
if (!response.ok) throw new Error('Merge request not found'); | |
const [{iid}] = await response.json(); | |
return iid; | |
}; | |
/** | |
* Adding comment to merege request | |
* @param md {String} — markdown text of the comment | |
*/ | |
const addComment = async md => { | |
const iid = await getMRID(CURRENT_BRANCH, DEFAULT_BRANCH); | |
const commentPath = `https://${GITLAB_DOMAIN}/api/v4/projects/${GITLAB_PROJECT_ID}/merge_requests/${iid}/notes`; | |
const body = new FormData(); | |
body.append('body', md); | |
await fetch(commentPath, { | |
method: 'POST', | |
headers: { | |
'PRIVATE-TOKEN': TOKEN, | |
}, | |
body, | |
}); | |
}; | |
// Count of measurements we want to make | |
const MEASURES_COUNT = 3; | |
// build report for single url | |
const buildReport = browser => async url => { | |
const data = await lighthouse( | |
`${DOMAIN}${url}`, | |
{ | |
port: new URL(browser.wsEndpoint()).port, | |
output: 'json', | |
}, | |
{ | |
extends: 'lighthouse:full', | |
} | |
); | |
const { report: reportJSON } = data; | |
const report = JSON.parse(reportJSON); | |
const metrics = [ | |
{ | |
name: report.categories.performance.title, | |
value: report.categories.performance.score, | |
desiredSize: 'larger', | |
}, | |
{ | |
name: report.categories.accessibility.title, | |
value: report.categories.accessibility.score, | |
desiredSize: 'larger', | |
}, | |
{ | |
name: report.categories['best-practices'].title, | |
value: report.categories['best-practices'].score, | |
desiredSize: 'larger', | |
}, | |
{ | |
name: report.categories.seo.title, | |
value: report.categories.seo.score, | |
desiredSize: 'larger', | |
}, | |
{ | |
name: report.categories.pwa.title, | |
value: report.categories.pwa.score, | |
desiredSize: 'larger', | |
}, | |
]; | |
return { | |
subject: url, | |
metrics: metrics, | |
}; | |
}; | |
/** | |
* Reducer which will calculate an avarage value of all page measurements | |
* @param pages {Object} — accumulator | |
* @param page {Object} — page | |
* @param page {Object} — page with avarage metrics values | |
*/ | |
const mergeMetrics = (pages, page) => { | |
if (!pages) return page; | |
return { | |
subject: pages.subject, | |
metrics: pages.metrics.map((measure, index) => { | |
let value = (measure.value + page.metrics[index].value)/2; | |
value = +value.toFixed(2); | |
return { | |
...measure, | |
value, | |
} | |
}), | |
} | |
} | |
/** | |
* Returns urls array splited to chunks accordin to cors number | |
* | |
* @param urls {String[]} — URLs array | |
* @param cors {Number} — count of available cors | |
* @return {Array<String[]>} — URLs array splited to chunks | |
*/ | |
function chunkArray(urls, cors) { | |
const chunks = [...Array(cors)].map(() => []); | |
let index = 0; | |
urls.forEach((url) => { | |
if (index > (chunks.length - 1)) { | |
index = 0; | |
} | |
chunks[index].push(url); | |
index += 1; | |
}); | |
return chunks; | |
} | |
const urls = [ | |
'/inloggen', | |
'/wachtwoord-herstellen-otp', | |
'/lp/service', | |
'/send-request-to/ww-tammer', | |
'/post-service-request/binnenschilderwerk', | |
]; | |
(async () => { | |
if (cluster.isMaster) { | |
// Parent proccess | |
const chunks = chunkArray(urls, numCPUs); | |
chunks.map(chunk => { | |
const worker = cluster.fork(); | |
worker.send(chunk); | |
}); | |
let report = []; | |
let reportsCount = 0; | |
cluster.on('message', async (worker, msg) => { | |
report = [...report, ...msg]; | |
worker.disconnect(); | |
reportsCount++; | |
if (reportsCount === chunks.length) { | |
fs.writeFileSync(`./performance.json`, JSON.stringify(report)); | |
if (CURRENT_BRANCH === DEFAULT_BRANCH) process.exit(0); | |
try { | |
const masterReport = await getArtifact('performance.json'); | |
const md = buildCommentText(report, masterReport) | |
await addComment(md); | |
} catch (error) { | |
console.log(error); | |
} | |
process.exit(0); | |
} | |
}); | |
} else { | |
// Child process | |
process.on('message', async (urls) => { | |
const browser = await puppeteer.launch({ | |
args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], | |
}); | |
const builder = buildReport(browser); | |
const report = []; | |
for (let url of urls) { | |
// Let's measure MEASURES_COUNT times and calculate the avarage | |
let measures = []; | |
let index = MEASURES_COUNT; | |
while(index--){ | |
const metric = await builder(url); | |
measures.push(metric); | |
} | |
const measure = measures.reduce(mergeMetrics); | |
report.push(measure); | |
} | |
cluster.worker.send(report); | |
await browser.close(); | |
}); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment