This commit is contained in:
Peter Hedenskog 2025-10-31 05:15:03 +03:00 committed by GitHub
commit a89bbb44fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 484 additions and 0 deletions

View File

@ -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'
}
};

View File

@ -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 `![${altText}](${url})`;
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;

View File

@ -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()} &#9888;&#65039; 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(
`${'&#9888;&#65039; Budget failing (' +
failingURLs.length +
' URLs)'}`
);
text += format.p(
`${get(this.options, 'name', '') +
' ' +
getBrowserData(this.browserData)}`
);
for (let url of failingURLs) {
text += format.bold(
`&#10060; ${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(
`&#9888;&#65039; Budget errors testing ${errorURLs.length} URLs`
);
for (let url of errorURLs) {
text += format.p(`&#10060; ${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(
`&#127881; 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);
}
};

View File

@ -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);
};