diff --git a/package-lock.json b/package-lock.json index 9634aaf1..3aafb53c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@adobe/helix-shared-wrap": "2.0.0", "@adobe/helix-status": "10.0.10", "@adobe/helix-universal-logger": "3.0.11", + "@adobe/spacecat-shared-utils": "^1.1.0", "comma-number": "2.1.0", "human-format": "1.2.0" }, @@ -266,6 +267,11 @@ "cookie": "0.5.0" } }, + "node_modules/@adobe/spacecat-shared-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-utils/-/spacecat-shared-utils-1.1.0.tgz", + "integrity": "sha512-yq6o47d6IuMApgvlGWdIfUOkqYNwcEm4IzO5WHnrTpWXPK4vvssfAfFtoeki7NQ+A4nFoc7/esqOfX42Q452nA==" + }, "node_modules/@arr/every": { "version": "1.0.1", "resolved": "https://artifactory.corp.adobe.com/artifactory/api/npm/npm-adobe-release/@arr/every/-/every-1.0.1.tgz", diff --git a/package.json b/package.json index 3d447d46..b5360bde 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "start": "nodemon", - "test": "c8 mocha -i -g 'Post-Deploy' --spec=test/**/*.test.js", + "test": "c8 mocha --spec=test/**/*.test.js", "test-postdeploy": "mocha -g 'Post-Deploy' --spec=test/**/*.test.js", "lint": "eslint .", "semantic-release": "semantic-release", @@ -58,6 +58,7 @@ "@adobe/helix-shared-wrap": "2.0.0", "@adobe/helix-status": "10.0.10", "@adobe/helix-universal-logger": "3.0.11", + "@adobe/spacecat-shared-utils": "^1.1.0", "comma-number": "2.1.0", "human-format": "1.2.0" }, @@ -94,4 +95,4 @@ "*.js": "eslint", "*.cjs": "eslint" } -} \ No newline at end of file +} diff --git a/src/cwv/handler.js b/src/cwv/handler.js index 136b74ba..8e71dc51 100644 --- a/src/cwv/handler.js +++ b/src/cwv/handler.js @@ -15,6 +15,7 @@ import commaNumber from 'comma-number'; import { postSlackMessage, markdown, section } from '../support/slack.js'; import { generateDomainKey } from '../support/rumapi.js'; +const RUM_API_URL = 'https://main--franklin-dashboard--adobe.hlx.live/views/rum-dashboard'; const COLOR_EMOJIS = { gray: ':gray-circle:', green: ':green:', @@ -68,7 +69,7 @@ export function getColorEmoji(type, value) { async function createBacklink(rumApiKey, finalUrl, log) { try { const domainkey = await generateDomainKey(rumApiKey, finalUrl); - return `https://main--franklin-dashboard--adobe.hlx.live/views/rum-dashboard?interval=7&offset=0&limit=100&url=${finalUrl}&domainkey=${domainkey}`; + return `${RUM_API_URL}?interval=7&offset=0&limit=100&url=${finalUrl}&domainkey=${domainkey}`; } catch (e) { log.info('Could not generate domain key. Will not add backlink to result'); return null; @@ -82,7 +83,11 @@ async function buildSlackMessage(url, finalUrl, overThreshold, rumApiKey, log) { text: markdown(`For *${url}*, ${overThreshold.length} page(s) had CWV over threshold in the *last week* for the real users.\n More information is below (up to three pages):`), })); - for (let i = 0; i < Math.min(3, overThreshold.length); i += 1) { + const backlinks = await Promise.all( + overThreshold.slice(0, 3).map(async (ot) => createBacklink(rumApiKey, ot.url, log)), + ); + + overThreshold.slice(0, 3).forEach((ot, i) => { const topLine = section({ text: markdown(`:arrow-green: *<${overThreshold[i].url}|${overThreshold[i].url}>*`), }); @@ -95,43 +100,44 @@ async function buildSlackMessage(url, finalUrl, overThreshold, rumApiKey, log) { markdown(`${getColorEmoji('inp', overThreshold[i].avginp)} *INP:* ${overThreshold[i].avginp === null ? 0 : overThreshold[i].avginp} ms`), ], }); - - blocks.push(topLine); - blocks.push(stats); - } - - const backlink = await createBacklink(rumApiKey, finalUrl, log); - - if (backlink) { - blocks.push(section({ - text: markdown(`*To access the full report <${backlink}|click here> :link:* _(expires in 7 days)_`), - })); - } + blocks.push(topLine, stats); + if (backlinks[i]) { + blocks.push(section({ + text: markdown(`*To access the full report <${backlinks[i]}|click here> :link:* _(expires in 7 days)_`), + })); + } + }); return blocks; } export default async function cwvHandler(message, context) { - const { url, auditResult, auditContext } = message; - const { log, env: { RUM_API_UBER_KEY: rumApiKey, SLACK_BOT_TOKEN: token } } = context; + const { log } = context; + try { + const { url, auditResult, auditContext } = message; + const { env: { RUM_API_UBER_KEY: rumApiKey, SLACK_BOT_TOKEN: token } } = context; - verifyParameters(message, context); + verifyParameters(message, context); - const { finalUrl, slackContext: { channel, ts } } = auditContext; + const { finalUrl, slackContext: { channel, ts } } = auditContext; - const overThreshold = auditResult - .filter((result) => result.avglcp > 2500 || result.avgcls > 0.1 || result.avginp > 200); + const overThreshold = auditResult + .filter((result) => result.avglcp > 2500 || result.avgcls > 0.1 || result.avginp > 200); - if (overThreshold.length === 0) { - log.info(`All CWV values are below threshold for ${url}`); - return new Response(200); - } + if (overThreshold.length === 0) { + log.info(`All CWV values are below threshold for ${url}`); + return new Response(200); + } - const blocks = await buildSlackMessage(url, finalUrl, overThreshold, rumApiKey, log); + const blocks = await buildSlackMessage(url, finalUrl, overThreshold, rumApiKey, log); - await postSlackMessage(token, { blocks, channel, ts }); + await postSlackMessage(token, { blocks, channel, ts }); - log.info(`Slack notification sent for ${url}`); + log.info(`Slack notification sent for ${url}`); - return new Response(200); + return new Response(200); + } catch (error) { + log.error(`Error in cwvHandler: ${error.message}`); + return new Response(500); + } } diff --git a/src/support/rumapi.js b/src/support/rumapi.js index 06e13d87..f5991729 100644 --- a/src/support/rumapi.js +++ b/src/support/rumapi.js @@ -11,6 +11,7 @@ */ import { createUrl } from '@adobe/fetch'; +import { hasText } from '@adobe/spacecat-shared-utils'; import { fetch } from './utils.js'; const API = 'https://helix-pages.anywhere.run/helix-services/run-query@v3/rotate-domainkeys'; @@ -22,6 +23,9 @@ function getExpirationDate() { } export async function generateDomainKey(rumApiKey, finalUrl) { + if (!hasText(rumApiKey) || !hasText(finalUrl)) { + throw new Error('Invalid input: rumApiKey and finalUrl are required'); + } const params = { domainkey: rumApiKey, url: finalUrl, @@ -32,7 +36,10 @@ export async function generateDomainKey(rumApiKey, finalUrl) { let respJson; try { - const resp = await fetch(createUrl(API, params)); + const resp = await fetch(createUrl(API, params), { method: 'POST' }); + if (!resp.ok) { + throw new Error(`Request failed with status ${resp.status}: ${resp.statusText}`); + } respJson = await resp.json(); } catch (e) { throw new Error(`Error during rum/rotate-domainkeys api call: ${e.message}`); diff --git a/test/cwv/handler.test.js b/test/cwv/handler.test.js index 4c4bd300..3d776b22 100644 --- a/test/cwv/handler.test.js +++ b/test/cwv/handler.test.js @@ -163,7 +163,7 @@ describe('cwv handler', () => { const { channel, ts } = message.auditContext.slackContext; nock('https://helix-pages.anywhere.run') - .get('/helix-services/run-query@v3/rotate-domainkeys') + .post('/helix-services/run-query@v3/rotate-domainkeys') .query(true) .reply(200, successKeyResponse); nock('https://slack.com', { @@ -190,7 +190,7 @@ describe('cwv handler', () => { const { channel, ts } = message.auditContext.slackContext; nock('https://helix-pages.anywhere.run') - .get('/helix-services/run-query@v3/rotate-domainkeys') + .post('/helix-services/run-query@v3/rotate-domainkeys') .query(true) .reply(200, wrongKeyResponse); nock('https://slack.com', { diff --git a/test/support/rumapi.test.js b/test/support/rumapi.test.js index 6a8d49c2..65d71310 100644 --- a/test/support/rumapi.test.js +++ b/test/support/rumapi.test.js @@ -58,7 +58,7 @@ describe('rum api', () => { it('rejects when rum api returns 500', async () => { nock('https://helix-pages.anywhere.run') - .get('/helix-services/run-query@v3/rotate-domainkeys') + .post('/helix-services/run-query@v3/rotate-domainkeys') .query(params) .reply(500); @@ -68,7 +68,7 @@ describe('rum api', () => { it('rejects when rum api returns invalid json', async () => { nock('https://helix-pages.anywhere.run') - .get('/helix-services/run-query@v3/rotate-domainkeys') + .post('/helix-services/run-query@v3/rotate-domainkeys') .query(params) .reply(200, 'invalid-json'); @@ -78,7 +78,7 @@ describe('rum api', () => { it('rejects when rum api returns unexpected format', async () => { nock('https://helix-pages.anywhere.run') - .get('/helix-services/run-query@v3/rotate-domainkeys') + .post('/helix-services/run-query@v3/rotate-domainkeys') .query(params) .reply(200, '{ "key": "value" }'); @@ -88,7 +88,7 @@ describe('rum api', () => { it('rejects when rum api returns unsuccessful repsonse', async () => { nock('https://helix-pages.anywhere.run') - .get('/helix-services/run-query@v3/rotate-domainkeys') + .post('/helix-services/run-query@v3/rotate-domainkeys') .query(params) .reply(200, wrongKeyResponse); @@ -98,7 +98,7 @@ describe('rum api', () => { it('rejects when rum api returns null key', async () => { nock('https://helix-pages.anywhere.run') - .get('/helix-services/run-query@v3/rotate-domainkeys') + .post('/helix-services/run-query@v3/rotate-domainkeys') .query(params) .reply(200, nullKeyResponse); @@ -108,7 +108,7 @@ describe('rum api', () => { it('returns scoped domain key when successful', async () => { nock('https://helix-pages.anywhere.run') - .get('/helix-services/run-query@v3/rotate-domainkeys') + .post('/helix-services/run-query@v3/rotate-domainkeys') .query(params) .reply(200, successKeyResponse);