Merge b192d3c495 into 4894c06607
This commit is contained in:
commit
a89bbb44fd
|
|
@ -0,0 +1,20 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
url: {
|
||||||
|
describe: 'The URL where to send the webhook.',
|
||||||
|
group: 'WebHook'
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
describe: 'Choose what type of message to send',
|
||||||
|
choices: ['budget', 'errors', 'summary'],
|
||||||
|
default: 'summary',
|
||||||
|
group: 'WebHook'
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
describe: 'How to format the content of the webhook.',
|
||||||
|
choices: ['html', 'markdown', 'text'],
|
||||||
|
default: 'text',
|
||||||
|
group: 'WebHook'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const newLine = '\n';
|
||||||
|
class Format {
|
||||||
|
constructor(style) {
|
||||||
|
this.style = style;
|
||||||
|
}
|
||||||
|
|
||||||
|
link(url, name) {
|
||||||
|
switch (this.style) {
|
||||||
|
case 'html':
|
||||||
|
return `<a href="${url}">${name ? name : url}</a>`;
|
||||||
|
case 'markdown':
|
||||||
|
return `[${name ? name : url}](${url})`;
|
||||||
|
default:
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
heading(text) {
|
||||||
|
switch (this.style) {
|
||||||
|
case 'html':
|
||||||
|
return `<h1>${text}</h1>`;
|
||||||
|
case 'markdown':
|
||||||
|
return `# ${text}`;
|
||||||
|
default:
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
image(url, altText) {
|
||||||
|
switch (this.style) {
|
||||||
|
case 'html':
|
||||||
|
return `<img src="${url}"></img>`;
|
||||||
|
case 'markdown':
|
||||||
|
return ``;
|
||||||
|
default:
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bold(text) {
|
||||||
|
switch (this.style) {
|
||||||
|
case 'html':
|
||||||
|
return `<b>${text}"</b>`;
|
||||||
|
case 'markdown':
|
||||||
|
return `**${text})**`;
|
||||||
|
default:
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre(text) {
|
||||||
|
switch (this.style) {
|
||||||
|
case 'html':
|
||||||
|
return `<pre>${text}"</pre>`;
|
||||||
|
case 'markdown':
|
||||||
|
return `${text})`;
|
||||||
|
default:
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p(text) {
|
||||||
|
switch (this.style) {
|
||||||
|
case 'html':
|
||||||
|
return `<p>${text}"</p>`;
|
||||||
|
case 'markdown':
|
||||||
|
return `${newLine}${newLine}${text}`;
|
||||||
|
default:
|
||||||
|
return `${newLine}${newLine}${text}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list(text) {
|
||||||
|
switch (this.style) {
|
||||||
|
case 'html':
|
||||||
|
return `<ul>${text}"</ul>`;
|
||||||
|
default:
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listItem(text) {
|
||||||
|
switch (this.style) {
|
||||||
|
case 'html':
|
||||||
|
return `<li>${text}"</li>`;
|
||||||
|
case 'markdown':
|
||||||
|
return `* ${text})`;
|
||||||
|
default:
|
||||||
|
return `* ${text} ${newLine})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr() {
|
||||||
|
switch (this.style) {
|
||||||
|
case 'html':
|
||||||
|
return `<hr>`;
|
||||||
|
case 'markdown':
|
||||||
|
return `---`;
|
||||||
|
default:
|
||||||
|
return `${newLine}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Format;
|
||||||
|
|
@ -0,0 +1,300 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const throwIfMissing = require('../../support/util').throwIfMissing;
|
||||||
|
const log = require('intel').getLogger('sitespeedio.plugin.webhook');
|
||||||
|
const path = require('path');
|
||||||
|
const get = require('lodash.get');
|
||||||
|
const cliUtil = require('../../cli/util');
|
||||||
|
const send = require('./send');
|
||||||
|
const Format = require('./format');
|
||||||
|
const friendlynames = require('../../support/friendlynames');
|
||||||
|
|
||||||
|
function getPageSummary(data, format, resultUrls, alias, screenshotType) {
|
||||||
|
let text = format.heading(
|
||||||
|
'Tested data for ' +
|
||||||
|
data.info.url +
|
||||||
|
(resultUrls.hasBaseUrl()
|
||||||
|
? format.link(
|
||||||
|
resultUrls.absoluteSummaryPagePath(
|
||||||
|
data.info.url,
|
||||||
|
alias[data.info.url]
|
||||||
|
),
|
||||||
|
'(result)'
|
||||||
|
)
|
||||||
|
: '')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resultUrls.hasBaseUrl()) {
|
||||||
|
text += format.image(
|
||||||
|
resultUrls.absoluteSummaryPagePath(data.info.url, alias[data.info.url]) +
|
||||||
|
'data/screenshots/1/afterPageCompleteCheck.' +
|
||||||
|
screenshotType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.statistics.visualMetrics) {
|
||||||
|
let f = friendlynames['browsertime']['timnings']['FirstVisualChange'];
|
||||||
|
text += format.p(
|
||||||
|
f.name +
|
||||||
|
' ' +
|
||||||
|
f.format(data.statistics.visualMetrics['FirstVisualChange'].median)
|
||||||
|
);
|
||||||
|
f = friendlynames['browsertime']['timnings']['SpeedIndex'];
|
||||||
|
text += format.p(
|
||||||
|
f.name +
|
||||||
|
' ' +
|
||||||
|
f.format(data.statistics.visualMetrics['SpeedIndex'].median)
|
||||||
|
);
|
||||||
|
|
||||||
|
f = friendlynames['browsertime']['timnings']['LastVisualChange'];
|
||||||
|
text += format.p(
|
||||||
|
f.name +
|
||||||
|
' ' +
|
||||||
|
f.format(data.statistics.visualMetrics['LastVisualChange'].median)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.statistics.googleWebVitals) {
|
||||||
|
for (let metric of Object.keys(data.statistics.googleWebVitals)) {
|
||||||
|
let f =
|
||||||
|
friendlynames.browsertime.timings[metric] ||
|
||||||
|
friendlynames.browsertime.cpu[metric] ||
|
||||||
|
friendlynames.browsertime.pageinfo[metric];
|
||||||
|
|
||||||
|
if (f) {
|
||||||
|
text += format.p(
|
||||||
|
f.name +
|
||||||
|
' ' +
|
||||||
|
f.format(data.statistics.googleWebVitals[metric].median)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// We do not have a mapping for FID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBrowserData(data) {
|
||||||
|
if (data && data.browser) {
|
||||||
|
return `${data.browser.name} ${data.browser.version} ${get(
|
||||||
|
data,
|
||||||
|
'android.model',
|
||||||
|
''
|
||||||
|
)} ${get(data, 'android.androidVersion', '')} ${get(
|
||||||
|
data,
|
||||||
|
'android.id',
|
||||||
|
''
|
||||||
|
)} `;
|
||||||
|
} else return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name() {
|
||||||
|
return path.basename(__dirname);
|
||||||
|
},
|
||||||
|
get cliOptions() {
|
||||||
|
return require(path.resolve(__dirname, 'cli.js'));
|
||||||
|
},
|
||||||
|
open(context, options = {}) {
|
||||||
|
this.webHookOptions = options.webhook || {};
|
||||||
|
this.options = options;
|
||||||
|
log.info('Starting the webhook plugin');
|
||||||
|
throwIfMissing(options.webhook, ['url'], 'webhook');
|
||||||
|
this.format = new Format(this.webHookOptions.style);
|
||||||
|
this.resultUrls = context.resultUrls;
|
||||||
|
this.waitForUpload = false;
|
||||||
|
this.alias = {};
|
||||||
|
this.data = {};
|
||||||
|
this.errorTexts = {};
|
||||||
|
this.message = { text: '' };
|
||||||
|
if (options.webhook) {
|
||||||
|
for (let key of Object.keys(options.webhook)) {
|
||||||
|
if (key !== 'url' && key !== 'messages' && key !== 'style') {
|
||||||
|
this.message[key] = options.webhook[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async processMessage(message) {
|
||||||
|
const options = this.webHookOptions;
|
||||||
|
const format = this.format;
|
||||||
|
switch (message.type) {
|
||||||
|
case 'browsertime.browser': {
|
||||||
|
this.browserData = message.data;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'gcs.setup':
|
||||||
|
case 'ftp.setup':
|
||||||
|
case 's3.setup': {
|
||||||
|
this.waitForUpload = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'browsertime.alias': {
|
||||||
|
this.alias[message.url] = message.data;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'browsertime.config': {
|
||||||
|
if (message.data.screenshot === true) {
|
||||||
|
this.screenshotType = message.data.screenshotType;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'browsertime.pageSummary': {
|
||||||
|
if (this.waitForUpload && options.messages.indexOf('pageSumary') > -1) {
|
||||||
|
this.data[message.url] = message.data;
|
||||||
|
} else if (options.messages.indexOf('pageSumary') > -1) {
|
||||||
|
await send(options.url, {
|
||||||
|
text: `Test finished ${format.link(message.data.info.url)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'error': {
|
||||||
|
// We can send too many messages to Matrix and get 429 so instead
|
||||||
|
// we bulk send them all one time
|
||||||
|
if (options.messages.indexOf('error') > -1) {
|
||||||
|
this.errorTexts += `${format.hr()} ⚠️ Error from ${format.bold(
|
||||||
|
message.source
|
||||||
|
)} testing ${
|
||||||
|
message.url ? format.link(message.url) : ''
|
||||||
|
} ${format.pre(message.data)}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'budget.result': {
|
||||||
|
if (options.messages.indexOf('budget') > -1) {
|
||||||
|
let text = '';
|
||||||
|
// We have failing URLs in the budget
|
||||||
|
if (Object.keys(message.data.failing).length > 0) {
|
||||||
|
const failingURLs = Object.keys(message.data.failing);
|
||||||
|
text += format.heading(
|
||||||
|
`${'⚠️ Budget failing (' +
|
||||||
|
failingURLs.length +
|
||||||
|
' URLs)'}`
|
||||||
|
);
|
||||||
|
text += format.p(
|
||||||
|
`${get(this.options, 'name', '') +
|
||||||
|
' ' +
|
||||||
|
getBrowserData(this.browserData)}`
|
||||||
|
);
|
||||||
|
for (let url of failingURLs) {
|
||||||
|
text += format.bold(
|
||||||
|
`❌ ${url}` +
|
||||||
|
(this.resultUrls.hasBaseUrl()
|
||||||
|
? ` (${format.link(
|
||||||
|
this.resultUrls.absoluteSummaryPagePath(
|
||||||
|
url,
|
||||||
|
this.alias[url]
|
||||||
|
) + 'index.html',
|
||||||
|
'result'
|
||||||
|
)} - ${format.link(
|
||||||
|
this.resultUrls.absoluteSummaryPagePath(
|
||||||
|
url,
|
||||||
|
this.alias[url]
|
||||||
|
) +
|
||||||
|
'data/screenshots/1/afterPageCompleteCheck.' +
|
||||||
|
this.screenshotType,
|
||||||
|
'screenshot'
|
||||||
|
)}`
|
||||||
|
: '')
|
||||||
|
);
|
||||||
|
|
||||||
|
let items = '';
|
||||||
|
for (let failing of message.data.failing[url]) {
|
||||||
|
items += format.listItem(
|
||||||
|
`${failing.metric} : ${failing.friendlyValue} (${
|
||||||
|
failing.friendlyLimit
|
||||||
|
})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
text += format.list(items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(message.data.error).length > 0) {
|
||||||
|
const errorURLs = Object.keys(message.data.error);
|
||||||
|
text += format.heading(
|
||||||
|
`⚠️ Budget errors testing ${errorURLs.length} URLs`
|
||||||
|
);
|
||||||
|
for (let url of errorURLs) {
|
||||||
|
text += format.p(`❌ ${url}`);
|
||||||
|
text += format.pre(`${message.data.error[url]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
Object.keys(message.data.error).length === 0 &&
|
||||||
|
Object.keys(message.data.failing).length === 0
|
||||||
|
) {
|
||||||
|
text += format.p(
|
||||||
|
`🎉 All (${
|
||||||
|
Object.keys(message.data.working).length
|
||||||
|
}) URL(s) passed the budget using ${get(
|
||||||
|
this.options,
|
||||||
|
'name',
|
||||||
|
''
|
||||||
|
)} ${getBrowserData(this.browserData)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!this.waitForUpload) {
|
||||||
|
await send(options.url, { text });
|
||||||
|
} else {
|
||||||
|
this.budgetText = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'gcs.finished':
|
||||||
|
case 'ftp.finished':
|
||||||
|
case 's3.finished': {
|
||||||
|
if (
|
||||||
|
this.waitForUpload &&
|
||||||
|
options.messages.indexOf('pageSummary') > -1
|
||||||
|
) {
|
||||||
|
const message = {
|
||||||
|
text: ''
|
||||||
|
};
|
||||||
|
for (let url of Object.keys(this.data)) {
|
||||||
|
message.text += getPageSummary(
|
||||||
|
this.data[url],
|
||||||
|
format,
|
||||||
|
this.resultUrls,
|
||||||
|
this.alias,
|
||||||
|
this.screenshotType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.resultUrls.reportSummaryUrl()) {
|
||||||
|
message.text += format.p(
|
||||||
|
format.link(
|
||||||
|
this.resultUrls.reportSummaryUrl() + '/index.html',
|
||||||
|
'Summary'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await send(options.url, message);
|
||||||
|
} else if (
|
||||||
|
this.waitForUpload &&
|
||||||
|
options.messages.indexOf('budget') > -1
|
||||||
|
) {
|
||||||
|
await send(options.url, { text: this.budgetText });
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'sitespeedio.render': {
|
||||||
|
if (this.errorTexts !== '') {
|
||||||
|
await send(options.url, { message: this.errorTexts });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
get config() {
|
||||||
|
return cliUtil.pluginDefaults(this.cliOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
const log = require('intel').getLogger('sitespeedio.plugin.webhook');
|
||||||
|
|
||||||
|
function send(url, message, retries = 3, backoff = 5000) {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
const send = parsedUrl.protocol === 'https' ? https : http;
|
||||||
|
|
||||||
|
const retryCodes = [408, 429, 500, 503];
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = send.request(
|
||||||
|
{
|
||||||
|
host: parsedUrl.hostname,
|
||||||
|
port: parsedUrl.port,
|
||||||
|
path: parsedUrl.pathname,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(JSON.stringify(message), 'utf8')
|
||||||
|
},
|
||||||
|
method: 'POST'
|
||||||
|
},
|
||||||
|
res => {
|
||||||
|
const { statusCode } = res;
|
||||||
|
if (statusCode < 200 || statusCode > 299) {
|
||||||
|
if (retries > 0 && retryCodes.includes(statusCode)) {
|
||||||
|
setTimeout(() => {
|
||||||
|
return send(url, message, retries - 1, backoff * 2);
|
||||||
|
}, backoff);
|
||||||
|
} else {
|
||||||
|
log.error(
|
||||||
|
`Got error from the webhook server. Error Code: ${
|
||||||
|
res.statusCode
|
||||||
|
} Message: ${res.statusMessage}`
|
||||||
|
);
|
||||||
|
reject(new Error(`Status Code: ${res.statusCode}`));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const data = [];
|
||||||
|
res.on('data', chunk => {
|
||||||
|
data.push(chunk);
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
|
resolve(Buffer.concat(data).toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
req.write(JSON.stringify(message));
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (url, message) => {
|
||||||
|
return send(url, message);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue