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