Last active
January 10, 2024 09:01
-
-
Save herablog/1bdd1523de7862ab3f5114a98f3bfb62 to your computer and use it in GitHub Desktop.
CWV (Core Web Vitals) Report to Slack with CrUX API
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
/** | |
* This is an example code for Google App Script | |
* You can run this script with time-driven triggers | |
* @see https://developers.google.com/apps-script/guides/triggers/installable#time-driven_triggers | |
*/ | |
// Settings | |
const CRUX_ORIGINS = []; // e.g. https://example.com | |
const CRUX_METRICS = ['first_input_delay', 'largest_contentful_paint', 'cumulative_layout_shift']; // first_contentful_paint, first_input_delay, largest_contentful_paint, cumulative_layout_shift | |
const CRUX_FORM_FACTOR = ['PHONE', 'DESKTOP']; // ALL_FORM_FACTORS, PHONE, DESKTOP, TABLET | |
const SLACK_WEBHOOK_URL = ''; | |
/** | |
* @typedef {Map<string, {histogram: Array<{ start: number, end?: number, density: number}>}>} CrUXMetrics | |
* @typedef {{record: {key: Object, metrics: CrUXMetrics, urlNormalizationDetails: Object}} CrUXResult | |
*/ | |
/** | |
* @see https://developers.google.com/web/tools/chrome-user-experience-report/api/guides/getting-started | |
*/ | |
const CrUXUtil = { | |
/** | |
* @param {{origin: string, formFactor?: string, metrics?: Array<string>}} payload | |
* @param {string} apiKey | |
* @param {object} headers | |
* @return {CrUXResult} | |
*/ | |
query: (payload, apiKey, headers) => { | |
const url = 'https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=' + apiKey; | |
const options = { | |
method: 'POST', | |
contentType: 'application/json', | |
payload: JSON.stringify(payload) | |
}; | |
if (headers) { | |
options.headers = headers; | |
} | |
const response = UrlFetchApp.fetch(url, options); | |
return JSON.parse(response); | |
} | |
}; | |
const slackUtil = { | |
/** | |
* Send message to Slack via webhook | |
* @param {object} message - We can check the message format at https://api.slack.com/docs/messages/builder | |
* @return {void} | |
*/ | |
send: (message) => { | |
const options = { | |
method: 'POST', | |
contentType: 'application/json', | |
payload: JSON.stringify(message) | |
}; | |
UrlFetchApp.fetch(SLACK_WEBHOOK_URL, options); | |
} | |
}; | |
/** | |
* Collect CrUX data and send it to Slack | |
*/ | |
function collect() { | |
const apiKey = PropertiesService.getScriptProperties().getProperty('API_KEY'); | |
// Comment out if you want to protect API key with HTTP referer | |
// You can set this restriction in GCP console | |
// const referer = PropertiesService.getScriptProperties().getProperty('API_REFERER'); | |
CRUX_ORIGINS.forEach(function(origin) { | |
CRUX_FORM_FACTOR.forEach(function (formFactor) { | |
const params = { | |
origin, | |
metrics: CRUX_METRICS, | |
formFactor | |
}; | |
const headers = { | |
// referer | |
} | |
const result = CrUXUtil.query(params, apiKey, headers); | |
const message = createMessage(result); | |
slackUtil.send(message); | |
}); | |
}); | |
} | |
/** | |
* Create message for Slack incoming webhooks | |
* @param {CrUXResult} CrUXResult | |
* @return {{ username: string, icon_emoji: string, text: string, attachments: Array }} message | |
*/ | |
function createMessage(CrUXResult) { | |
const labeledMetrics = labelMetricData(CrUXResult.record.metrics); | |
const attachments = labeledMetrics.map(function (data) { | |
return { | |
fallback: data.acronym + ' value', | |
color: data.labeledBins[0].percentage >= 75 ? '#237b31' : '#d91c0b', | |
title: data.acronym, | |
text: data.labeledBins.map(bin => bin.label + ': ' + bin.percentage.toFixed(2) + '%').join(', ') | |
}; | |
}); | |
const heading = [ | |
Object.entries(CrUXResult.record.key).map(entry => entry.join(': ')).join(', '), | |
'The field data is collected over the last 28 days.' | |
].join('\n'); | |
const message = { | |
username: 'Core Web Vitals', | |
'icon_emoji': ':google:', | |
text: heading, | |
attachments | |
}; | |
return message; | |
} | |
/** | |
* Label CrUX metrics | |
* @param {CrUXMetrics} metrics | |
* @return {{acronym: string, name: string, labelsBins: Array<{label: 'good'|'needs improvement'|'poor', percentage: number, start: number, end?: number, density: number}>}} | |
*/ | |
function labelMetricData(metrics) { | |
const nameToAcronymMap = { | |
first_contentful_paint: 'FCP', | |
largest_contentful_paint: 'LCP', | |
first_input_delay: 'FID', | |
cumulative_layout_shift: 'CLS', | |
}; | |
return Object.entries(metrics).map(function ([metricName, metricData]) { | |
const standardBinLabels = ['good', 'needs improvement', 'poor']; | |
const metricBins = metricData.histogram; | |
// We assume there are 3 histogram bins and they're in order of: good => poor | |
const labeledBins = metricBins.map((bin, i) => { | |
// Assign a good/poor label, calculate a percentage, and add retain all existing bin properties | |
return { | |
label: standardBinLabels[i], | |
percentage: bin.density * 100, | |
...bin, | |
}; | |
}); | |
return { | |
acronym: nameToAcronymMap[metricName], | |
name: metricName, | |
labeledBins, | |
}; | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment