diff --git a/CHANGELOG.md b/CHANGELOG.md index 4521dfd9..28db63b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,99 @@ +## [1.30.4](https://github.com/adobe/spacecat-audit-worker/compare/v1.30.3...v1.30.4) (2024-09-05) + + +### Bug Fixes + +* **broken-backlinks:** introduce auto-suggest flag for broken backlinks suggestions ([#396](https://github.com/adobe/spacecat-audit-worker/issues/396)) ([4601b00](https://github.com/adobe/spacecat-audit-worker/commit/4601b0084c8373bfda1af58f93d1bdecb382c986)) + +## [1.30.3](https://github.com/adobe/spacecat-audit-worker/compare/v1.30.2...v1.30.3) (2024-09-04) + + +### Bug Fixes + +* **deps:** update dependency @adobe/spacecat-shared-google-client to v1.2.3 ([#395](https://github.com/adobe/spacecat-audit-worker/issues/395)) ([e92d4cd](https://github.com/adobe/spacecat-audit-worker/commit/e92d4cd9223d35f959a000134e1660c6b2d15a21)) + +## [1.30.2](https://github.com/adobe/spacecat-audit-worker/compare/v1.30.1...v1.30.2) (2024-09-04) + + +### Bug Fixes + +* **experimentation-ess:** audit url calculating for rum bundler API ([#389](https://github.com/adobe/spacecat-audit-worker/issues/389)) ([488a9a7](https://github.com/adobe/spacecat-audit-worker/commit/488a9a7fdcc248a8f49e30b211f6c36d3aa68091)) + +## [1.30.1](https://github.com/adobe/spacecat-audit-worker/compare/v1.30.0...v1.30.1) (2024-09-04) + + +### Bug Fixes + +* **deps:** update dependency @adobe/spacecat-shared-ahrefs-client to v1.5.2 ([#394](https://github.com/adobe/spacecat-audit-worker/issues/394)) ([6e684ff](https://github.com/adobe/spacecat-audit-worker/commit/6e684ffebf30ab4fdd633f2a4419c646675e80e7)) + +# [1.30.0](https://github.com/adobe/spacecat-audit-worker/compare/v1.29.2...v1.30.0) (2024-09-03) + + +### Bug Fixes + +* **deps:** update dependency @adobe/spacecat-shared-rum-api-client to v2.9.0 ([#388](https://github.com/adobe/spacecat-audit-worker/issues/388)) ([b55e4fb](https://github.com/adobe/spacecat-audit-worker/commit/b55e4fb993a7d3f782e4e2109ce63c46e2df74bf)) + + +### Features + +* high bounce rate and low ctr opportunities ([#384](https://github.com/adobe/spacecat-audit-worker/issues/384)) ([33aa025](https://github.com/adobe/spacecat-audit-worker/commit/33aa0257031ddc92c47718aaf100a30cf7662a61)) + +## [1.29.3](https://github.com/adobe/spacecat-audit-worker/compare/v1.29.2...v1.29.3) (2024-09-03) + + +### Bug Fixes + +* **deps:** update dependency @adobe/spacecat-shared-rum-api-client to v2.9.0 ([#388](https://github.com/adobe/spacecat-audit-worker/issues/388)) ([b55e4fb](https://github.com/adobe/spacecat-audit-worker/commit/b55e4fb993a7d3f782e4e2109ce63c46e2df74bf)) + +## [1.29.2](https://github.com/adobe/spacecat-audit-worker/compare/v1.29.1...v1.29.2) (2024-09-02) + + +### Bug Fixes + +* **cwv:** audit url calculating for rum bundler API ([#386](https://github.com/adobe/spacecat-audit-worker/issues/386)) ([1ac8123](https://github.com/adobe/spacecat-audit-worker/commit/1ac8123b08d8e2fbb62c10addf9893ba3124019c)) + +## [1.29.1](https://github.com/adobe/spacecat-audit-worker/compare/v1.29.0...v1.29.1) (2024-08-28) + + +### Bug Fixes + +* audit context includes interval info ([#383](https://github.com/adobe/spacecat-audit-worker/issues/383)) ([c0add98](https://github.com/adobe/spacecat-audit-worker/commit/c0add98e5b1a794284bcb72624c284f0aed6593d)) + +# [1.29.0](https://github.com/adobe/spacecat-audit-worker/compare/v1.28.1...v1.29.0) (2024-08-27) + + +### Features + +* introduce structured data audit ([#367](https://github.com/adobe/spacecat-audit-worker/issues/367)) ([3e6901d](https://github.com/adobe/spacecat-audit-worker/commit/3e6901d5c51c2ade763aeae7607ba8f476d55dce)) + +## [1.28.1](https://github.com/adobe/spacecat-audit-worker/compare/v1.28.0...v1.28.1) (2024-08-27) + + +### Bug Fixes + +* **deps:** update dependency jsdom to v25 ([#379](https://github.com/adobe/spacecat-audit-worker/issues/379)) ([d785019](https://github.com/adobe/spacecat-audit-worker/commit/d7850193c94d6f6bc03134c1cfb00d28dd16f807)) + +# [1.28.0](https://github.com/adobe/spacecat-audit-worker/compare/v1.27.0...v1.28.0) (2024-08-26) + + +### Features + +* opportunities audit ([#360](https://github.com/adobe/spacecat-audit-worker/issues/360)) ([22128fd](https://github.com/adobe/spacecat-audit-worker/commit/22128fd0c311d65e0a5b60e4d229205feb2bf219)) + +# [1.27.0](https://github.com/adobe/spacecat-audit-worker/compare/v1.26.5...v1.27.0) (2024-08-26) + + +### Features + +* cwv audit revival ([#378](https://github.com/adobe/spacecat-audit-worker/issues/378)) ([3a27bde](https://github.com/adobe/spacecat-audit-worker/commit/3a27bded17a6efe7354b5533a217e47fc14c2285)) + +## [1.26.5](https://github.com/adobe/spacecat-audit-worker/compare/v1.26.4...v1.26.5) (2024-08-24) + + +### Bug Fixes + +* **deps:** update external fixes ([#376](https://github.com/adobe/spacecat-audit-worker/issues/376)) ([923ad80](https://github.com/adobe/spacecat-audit-worker/commit/923ad80f45b2ea2462857c997c377260349629f7)) + ## [1.26.4](https://github.com/adobe/spacecat-audit-worker/compare/v1.26.3...v1.26.4) (2024-08-23) diff --git a/default.json b/default.json index 107732fd..c889c400 100644 --- a/default.json +++ b/default.json @@ -46,8 +46,5 @@ "updateTypes": ["patch", "minor"], "automerge": true } - ], - "encrypted": { - "npmToken": "wcFMA/xDdHCJBTolAQ//cBnUY2GbbFUjxEW/JyLas0ZYKaS9XCOrP4FA7r43SwRV44ynLM0XIWB+ceGUFrTN8YWfvkbbMVFcxqKYFnjl2l1S/8G9jvvS7bJLZCd1WNk7oVkIXnpb76FE6FQe0giudOiaWo21QVORVEoN/G6du3EOEiCghLMSCMtjg9+0M9Qg9dFJFoUeAHxLhGMjU3kQFO89ljYjaEYA4dvY2Id4pQOx2l0K2T70hSvGhrDGnDyGzOdA3rtz5as+/LgASvOOsTvn9JRndN9G1vnWqHR8xJBibPOqRyJ82IH+pe/+fRex/4Dr1HxlVa9BPhLslAFgUZqTrmwVhZutwURd5sRYNbYiMCBNNjmuVSfIIv+3/y4yEimaQWoh5RVGNxfYddryOSL/FKGeIybuKpskhzTzjtohvu71EjBinyx0qw/O92gZ+UHIZqhOWfmIoGV/pLPjGqkboXYjeQmSauVl8e9by1QXDiGmKe0T16UblNAhU+sQJdneFE4UBflK61jnReAUOsRVFvn9vuVrOuF8zwdXPs8xb7vuE77wUg9M/2N7I+y3tzVtb4uHvoVjtzIlkBe37xkzunKCDbk/Hdgk4lGdlFeTpQYL8Q/rU0itnYHZwfUThgPq1BhP+rWxj6Qkv1DR1983yowG9Zv54bO42oGO6apVAMZQ4q8Bz15BK0TCQFzSdAHRSJq9bALOc/HK5GbEQQ7sZ1JU76jjSFXqeBMZs7A2jrm1yNJAJ7WW+zLgXLjm9s1e1epn4PXerD50ypBKZnKfhmnnBB67IlO4IVfOLIllq3N1dJT2q5zqGdfgMN1qbyCg2JlgVSM8k83wwYtdLXC+mfl+" - } + ] } diff --git a/src/backlinks/handler.js b/src/backlinks/handler.js index 1060ea6e..515d0f57 100644 --- a/src/backlinks/handler.js +++ b/src/backlinks/handler.js @@ -64,18 +64,21 @@ export default async function auditBrokenBacklinks(message, context) { (backlink) => !excludedURLs?.includes(backlink.url_to), ); let brokenBacklinks = await filterOutValidBacklinks(filteredBacklinks, log); - try { - const topPages = await dataAccess.getTopPagesForSite(siteId, 'ahrefs', 'global'); - const keywords = topPages.map( - (page) => ({ - url: page.getURL(), - keyword: page.getTopKeyword(), - traffic: page.getTraffic(), - }), - ); - brokenBacklinks = enhanceBacklinksWithFixes(brokenBacklinks, keywords, log); - } catch (e) { - log.error(`Enhancing backlinks with fixes for siteId ${siteId} failed with error: ${e.message}`, e); + + if (configuration.isHandlerEnabledForSite(`${type}-auto-suggest`, site)) { + try { + const topPages = await dataAccess.getTopPagesForSite(siteId, 'ahrefs', 'global'); + const keywords = topPages.map( + (page) => ({ + url: page.getURL(), + keyword: page.getTopKeyword(), + traffic: page.getTraffic(), + }), + ); + brokenBacklinks = enhanceBacklinksWithFixes(brokenBacklinks, keywords, log); + } catch (e) { + log.error(`Enhancing backlinks with fixes for siteId ${siteId} failed with error: ${e.message}`, e); + } } auditResult = { diff --git a/src/common/audit.js b/src/common/audit.js index 546f0ecf..fbc21e41 100644 --- a/src/common/audit.js +++ b/src/common/audit.js @@ -10,8 +10,9 @@ * governing permissions and limitations under the License. */ -import { composeAuditURL } from '@adobe/spacecat-shared-utils'; +import { composeAuditURL, hasText } from '@adobe/spacecat-shared-utils'; import { ok } from '@adobe/spacecat-shared-http-utils'; +import URI from 'urijs'; import { retrieveSiteBySiteId } from '../utils/data-access.js'; export async function defaultMessageSender(resultMessage, context) { @@ -52,6 +53,12 @@ export async function defaultUrlResolver(site) { return composeAuditURL(site.getBaseURL()); } +export function wwwUrlResolver(site) { + const baseURL = site.getBaseURL(); + const uri = new URI(baseURL); + return hasText(uri.subdomain()) ? baseURL.replace(/https?:\/\//, '') : baseURL.replace(/https?:\/\//, 'www.'); +} + export async function noopUrlResolver(site) { return site.getBaseURL(); } diff --git a/src/cwv/handler.js b/src/cwv/handler.js index fb842512..3d6be775 100644 --- a/src/cwv/handler.js +++ b/src/cwv/handler.js @@ -1,5 +1,5 @@ /* - * Copyright 2023 Adobe. All rights reserved. + * Copyright 2024 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -10,83 +10,38 @@ * governing permissions and limitations under the License. */ -import RUMAPIClient, { createRUMURL } from '@adobe/spacecat-shared-rum-api-client-v1'; -import { internalServerError, noContent } from '@adobe/spacecat-shared-http-utils'; -import { composeAuditURL } from '@adobe/spacecat-shared-utils'; -import { retrieveSiteBySiteId } from '../utils/data-access.js'; - -const PAGEVIEW_THRESHOLD = 35000; - -export function filterRUMData(data) { - return data.pageviews > PAGEVIEW_THRESHOLD // ignore the pages with low pageviews - && data.url.toLowerCase() !== 'other'; // ignore the combined result +import RUMAPIClient from '@adobe/spacecat-shared-rum-api-client'; +import { getRUMDomainkey } from '../support/utils.js'; +import { AuditBuilder } from '../common/audit-builder.js'; +import { wwwUrlResolver } from '../common/audit.js'; + +const DAILY_THRESHOLD = 1000; +const INTERVAL = 7; // days + +export async function CWVRunner(auditUrl, context, site) { + const rumAPIClient = RUMAPIClient.createFrom(context); + const domainkey = await getRUMDomainkey(site.getBaseURL(), context); + const options = { + domain: auditUrl, + domainkey, + interval: INTERVAL, + granularity: 'hourly', + }; + const cwvData = await rumAPIClient.query('cwv', options); + const auditResult = { + cwv: cwvData.filter((data) => data.pageviews >= DAILY_THRESHOLD * INTERVAL), + auditContext: { + interval: INTERVAL, + }, + }; + + return { + auditResult, + fullAuditRef: auditUrl, + }; } -/** - * url param in run-query@v3/rum-dashboard works in a 'startsWith' fashion. url=domain.com returns - * an empty result whereas url=www.domain.com/ returns the desired result. To catch the redirects - * to subdomains we issue a GET call to the domain, then use the final url after redirects - * @param url - * @returns finalUrl {Promise} - */ - -function processRUMResponse(data) { - return data - .filter(filterRUMData) - .map((row) => ({ - url: row.url, - pageviews: row.pageviews, - CLS: row.avgcls, - INP: row.avginp, - LCP: row.avglcp, - })); -} -export default async function auditCWV(message, context) { - const { type, url: siteId, auditContext = {} } = message; - const { dataAccess, log, sqs } = context; - const { - AUDIT_RESULTS_QUEUE_URL: queueUrl, - } = context.env; - try { - const site = await retrieveSiteBySiteId(dataAccess, siteId, log); - const url = site.getBaseURL(); - - log.info(`Received audit req for domain: ${url}`); - - const rumAPIClient = RUMAPIClient.createFrom(context); - const finalUrl = await composeAuditURL(url); - auditContext.finalUrl = finalUrl; - - const params = { - url: finalUrl, - }; - - const data = await rumAPIClient.getRUMDashboard(params); - const auditResult = processRUMResponse(data); - const fullAuditRef = createRUMURL({ ...params, domainkey: '' }); - - const auditData = { - siteId: site.getId(), - isLive: site.isLive(), - auditedAt: new Date().toISOString(), - auditType: type, - fullAuditRef, - auditResult, - }; - - await dataAccess.addAudit(auditData); - - await sqs.sendMessage(queueUrl, { - type, - url, - auditContext, - auditResult, - }); - - log.info(`Successfully audited ${url} for ${type} type audit`); - return noContent(); - } catch (e) { - log.info(`CWV audit failed for ${siteId} failed due to ${e.message}`); - return internalServerError(`Internal server error: ${e.message}`); - } -} +export default new AuditBuilder() + .withUrlResolver(wwwUrlResolver) + .withRunner(CWVRunner) + .build(); diff --git a/src/experimentation-ess/all.js b/src/experimentation-ess/all.js index b387afc8..8531914b 100644 --- a/src/experimentation-ess/all.js +++ b/src/experimentation-ess/all.js @@ -13,6 +13,7 @@ /* c8 ignore start */ import { AuditBuilder } from '../common/audit-builder.js'; import { processAudit, postProcessor } from './common.js'; +import { wwwUrlResolver } from '../common/audit.js'; const DAYS = 180; @@ -79,6 +80,7 @@ export async function essExperimentationAllAuditRunner(auditUrl, context, site) export default new AuditBuilder() .withRunner(essExperimentationAllAuditRunner) + .withUrlResolver(wwwUrlResolver) .withPostProcessors([postProcessor]) .withPersister(persistOnlyMetadata) .withMessageSender(() => true) diff --git a/src/experimentation-ess/daily.js b/src/experimentation-ess/daily.js index 8f94f441..5766a8b1 100644 --- a/src/experimentation-ess/daily.js +++ b/src/experimentation-ess/daily.js @@ -13,6 +13,7 @@ /* c8 ignore start */ import { AuditBuilder } from '../common/audit-builder.js'; import { processAudit } from './common.js'; +import { wwwUrlResolver } from '../common/audit.js'; const DAYS = 1; @@ -44,5 +45,6 @@ export async function essExperimentationDailyAuditRunner(auditUrl, context, site export default new AuditBuilder() .withRunner(essExperimentationDailyAuditRunner) + .withUrlResolver(wwwUrlResolver) .build(); /* c8 ignore stop */ diff --git a/src/experimentation-opportunities/experimentation-opportunities.js b/src/experimentation-opportunities/experimentation-opportunities.js new file mode 100644 index 00000000..2c44b31e --- /dev/null +++ b/src/experimentation-opportunities/experimentation-opportunities.js @@ -0,0 +1,61 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import RUMAPIClient from '@adobe/spacecat-shared-rum-api-client'; +import { AuditBuilder } from '../common/audit-builder.js'; +import { getRUMDomainkey } from '../support/utils.js'; +import { wwwUrlResolver } from '../common/audit.js'; + +const DAYS = 30; +const OPPTY_QUERIES = [ + 'rageclick', + 'high-inorganic-high-bounce-rate', + 'high-organic-low-ctr', +]; + +/** + * Audit handler container for all the opportunities + * @param {*} auditUrl + * @param {*} context + * @param {*} site + * @returns + */ + +export async function handler(auditUrl, context, site) { + const { log } = context; + + const rumAPIClient = RUMAPIClient.createFrom(context); + const domainkey = await getRUMDomainkey(site.getBaseURL(), context); + const options = { + domain: auditUrl, + domainkey, + interval: DAYS, + granularity: 'hourly', + }; + + const queryResults = await rumAPIClient.queryMulti(OPPTY_QUERIES, options); + const experimentationOpportunities = Object.values(queryResults).flatMap((oppty) => oppty); + + log.info(`Found ${experimentationOpportunities.length} many experimentation opportunites for ${auditUrl}`); + + return { + auditResult: { + experimentationOpportunities, + }, + fullAuditRef: auditUrl, + }; +} + +export default new AuditBuilder() + .withRunner(handler) + .withUrlResolver(wwwUrlResolver) + .build(); diff --git a/src/index.js b/src/index.js index ca3a21e3..19b7cfd2 100644 --- a/src/index.js +++ b/src/index.js @@ -11,10 +11,10 @@ */ import wrap from '@adobe/helix-shared-wrap'; import { helixStatus } from '@adobe/helix-status'; -import { Response } from '@adobe/fetch'; import secrets from '@adobe/helix-shared-secrets'; import dataAccess from '@adobe/spacecat-shared-data-access'; import { resolveSecretsName, sqsEventAdapter } from '@adobe/spacecat-shared-utils'; +import { internalServerError, notFound, ok } from '@adobe/spacecat-shared-http-utils'; import sqs from './support/sqs.js'; import apex from './apex/handler.js'; @@ -29,7 +29,9 @@ import experimentation from './experimentation/handler.js'; import conversion from './conversion/handler.js'; import essExperimentationDaily from './experimentation-ess/daily.js'; import essExperimentationAll from './experimentation-ess/all.js'; +import experimentationOpportunities from './experimentation-opportunities/experimentation-opportunities.js'; import costs from './costs/handler.js'; +import structuredData from './structured-data/handler.js'; const HANDLERS = { apex, @@ -44,7 +46,10 @@ const HANDLERS = { conversion, 'experimentation-ess-daily': essExperimentationDaily, 'experimentation-ess-all': essExperimentationAll, + 'experimentation-opportunities': experimentationOpportunities, costs, + 'structured-data': structuredData, + dummy: (message) => ok(message), }; function getElapsedSeconds(startTime) { @@ -63,13 +68,13 @@ async function run(message, context) { const { log } = context; const { type, url } = message; - log.info(`Audit req received for url: ${url}`); + log.info(`Received ${type} audit request for: ${url}`); const handler = HANDLERS[type]; if (!handler) { const msg = `no such audit type: ${type}`; log.error(msg); - return new Response('', { status: 404 }); + return notFound(); } const startTime = process.hrtime(); @@ -77,17 +82,12 @@ async function run(message, context) { try { const result = await (typeof handler.run === 'function' ? handler.run(message, context) : handler(message, context)); - log.info(`Audit for ${type} completed in ${getElapsedSeconds(startTime)} seconds`); + log.info(`${type} audit for ${url} completed in ${getElapsedSeconds(startTime)} seconds`); return result; } catch (e) { - log.error(`Audit failed after ${getElapsedSeconds(startTime)} seconds`, e); - return new Response('', { - status: e.statusCode || 500, - headers: { - 'x-error': 'internal server error', - }, - }); + log.error(`${type} audit for ${url} failed after ${getElapsedSeconds(startTime)} seconds`, e); + return internalServerError(); } } diff --git a/src/structured-data/handler.js b/src/structured-data/handler.js new file mode 100644 index 00000000..21484a15 --- /dev/null +++ b/src/structured-data/handler.js @@ -0,0 +1,135 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import GoogleClient from '@adobe/spacecat-shared-google-client'; +import { isArray } from '@adobe/spacecat-shared-utils'; +import { AuditBuilder } from '../common/audit-builder.js'; + +/** + * Processes an audit of a set of pages from a site using Google's URL inspection tool. + * + * @async + * @function + * @param {string} baseURL - The base URL for the audit. + * @param {Object} context - The context object. + * @param {Array} pages - An array of page URLs to be audited. + * + * @returns {Promise>} - A promise that resolves to an array of objects, + * each containing the inspection URL, filtered index status result, and filtered rich results. + * If an error occurs during the inspection of a URL, the object will include an error message. + * + * @throws {Error} - Throws an error if the audit process fails. + */ +export async function processStructuredData(baseURL, context, pages) { + const { log } = context; + + let google; + try { + google = await GoogleClient.createFrom(context, baseURL); + } catch (error) { + log.error(`Failed to create Google client. Site was probably not onboarded to GSC yet. Error: ${error.message}`); + throw new Error(`Failed to create Google client. Site was probably not onboarded to GSC yet. Error: ${error.message}`); + } + + const urlInspectionResult = pages.map(async (page) => { + try { + const { inspectionResult } = await google.urlInspect(page); + log.info(`Successfully inspected URL: ${page}`); + + const filteredIndexStatusResult = { + verdict: inspectionResult?.indexStatusResult?.verdict, + lastCrawlTime: inspectionResult?.indexStatusResult?.lastCrawlTime, + }; + + const detectedItemTypes = []; + const filteredRichResults = inspectionResult?.richResultsResult?.detectedItems?.map( + (item) => { + detectedItemTypes.push(item?.richResultType); + const filteredItems = item?.items?.filter( + (issueItem) => issueItem?.issues?.some( + (issue) => issue?.severity === 'ERROR', + ), + )?.map((issueItem) => ({ + name: issueItem?.name, + issues: issueItem?.issues?.filter((issue) => issue?.severity === 'ERROR'), + })); + + return { + richResultType: item?.richResultType, + items: filteredItems, + }; + }, + )?.filter((item) => item.items.length > 0) ?? []; + + if (filteredRichResults.length > 0) { + filteredRichResults.verdict = inspectionResult?.richResultsResult?.verdict; + log.info(`Found ${filteredRichResults.length} rich results issues for URL: ${page}`); + } else { + log.info(`No rich results issues found for URL: ${page}`); + } + return { + inspectionUrl: page, + indexStatusResult: filteredIndexStatusResult, + richResults: inspectionResult?.richResultsResult + ? { + verdict: inspectionResult.richResultsResult.verdict, + detectedItemTypes, + detectedIssues: filteredRichResults, + } + : {}, + }; + } catch (error) { + log.error(`Failed to inspect URL: ${page}. Error: ${error.message}`); + return { + inspectionUrl: page, + error: error.message, + }; + } + }); + + const results = await Promise.allSettled(urlInspectionResult); + return results + .filter((result) => result.status === 'fulfilled') + .map((result) => result.value); +} + +export async function structuredDataHandler(baseURL, context, site) { + const { log } = context; + log.info(`Received structured data audit request for ${baseURL}`); + const startTime = process.hrtime(); + + const siteId = site.getId(); + + const structuredDataURLs = await site.getConfig().getIncludedURLs('structured-data'); + if (isArray(structuredDataURLs) && structuredDataURLs.length === 0) { + log.error(`No product detail pages found for site ID: ${siteId}`); + throw new Error(`No product detail pages found for site: ${baseURL}`); + } + + const auditResult = await processStructuredData(baseURL, context, structuredDataURLs); + + const endTime = process.hrtime(startTime); + const elapsedSeconds = endTime[0] + endTime[1] / 1e9; + const formattedElapsed = elapsedSeconds.toFixed(2); + + log.info(`Structured data audit completed in ${formattedElapsed} seconds for ${baseURL}`); + + return { + fullAuditRef: baseURL, + auditResult, + }; +} + +export default new AuditBuilder() + .withRunner(structuredDataHandler) + .withUrlResolver((site) => site.getBaseURL()) + .build(); diff --git a/test/audits/backlinks.test.js b/test/audits/backlinks.test.js index 5d43a4cb..f7a5a766 100644 --- a/test/audits/backlinks.test.js +++ b/test/audits/backlinks.test.js @@ -56,6 +56,14 @@ describe('Backlinks Tests', function () { enabledByDefault: false, dependencies: [], }, + 'broken-backlinks-auto-suggest': { + enabled: { + sites: ['site1', 'site2', 'site3', 'site'], + orgs: ['org1', 'org2', 'org3'], + }, + enabledByDefault: false, + dependencies: [], + }, }, jobs: [], }; @@ -303,7 +311,7 @@ describe('Backlinks Tests', function () { auditResult: { finalUrl: 'bar.foo.com', brokenBacklinks: auditResult.backlinks, - fullAuditRef: 'https://ahrefs.com/site-explorer/broken-backlinks?select=title%2Curl_from%2Curl_to%2Ctraffic_domain&limit=50&mode=prefix&order_by=domain_rating_source%3Adesc%2Ctraffic_domain%3Adesc&target=bar.foo.com&output=json&where=%7B%22and%22%3A%5B%7B%22field%22%3A%22is_dofollow%22%2C%22is%22%3A%5B%22eq%22%2C1%5D%7D%2C%7B%22field%22%3A%22is_content%22%2C%22is%22%3A%5B%22eq%22%2C1%5D%7D%2C%7B%22field%22%3A%22domain_rating_source%22%2C%22is%22%3A%5B%22gte%22%2C29.5%5D%7D%2C%7B%22field%22%3A%22traffic_domain%22%2C%22is%22%3A%5B%22gte%22%2C500%5D%7D%2C%7B%22field%22%3A%22links_external%22%2C%22is%22%3A%5B%22lte%22%2C300%5D%7D%5D%7D', + fullAuditRef: 'https://ahrefs.com/site-explorer/broken-backlinks?select=title%2Curl_from%2Curl_to%2Ctraffic_domain&limit=50&mode=prefix&order_by=domain_rating_source%3Adesc%2Ctraffic_domain%3Adesc&target=bar.foo.com&output=json&where=%7B%22and%22%3A%5B%7B%22field%22%3A%22domain_rating_source%22%2C%22is%22%3A%5B%22gte%22%2C29.5%5D%7D%2C%7B%22field%22%3A%22traffic_domain%22%2C%22is%22%3A%5B%22gte%22%2C500%5D%7D%2C%7B%22field%22%3A%22links_external%22%2C%22is%22%3A%5B%22lte%22%2C300%5D%7D%5D%7D', }, }; @@ -317,7 +325,8 @@ describe('Backlinks Tests', function () { expect(context.log.info).to.have.been.calledWith('Successfully audited site1 for broken-backlinks type audit'); }); - it('should successfully perform an audit to detect broken backlinks based on keywords from top pages', async () => { + it('should successfully perform an audit to detect broken backlinks and suggest fixes based on keywords from top pages if auto-suggest' + + ' enabled', async () => { mockDataAccess.getSiteByID = sinon.stub().withArgs('site1').resolves(site); mockDataAccess.getTopPagesForSite.resolves([siteTopPage, siteTopPage2]); mockDataAccess.getConfiguration = sinon.stub().resolves(configuration); @@ -343,7 +352,7 @@ describe('Backlinks Tests', function () { auditResult: { finalUrl: 'bar.foo.com', brokenBacklinks: auditResult.backlinks, - fullAuditRef: 'https://ahrefs.com/site-explorer/broken-backlinks?select=title%2Curl_from%2Curl_to%2Ctraffic_domain&limit=50&mode=prefix&order_by=domain_rating_source%3Adesc%2Ctraffic_domain%3Adesc&target=bar.foo.com&output=json&where=%7B%22and%22%3A%5B%7B%22field%22%3A%22is_dofollow%22%2C%22is%22%3A%5B%22eq%22%2C1%5D%7D%2C%7B%22field%22%3A%22is_content%22%2C%22is%22%3A%5B%22eq%22%2C1%5D%7D%2C%7B%22field%22%3A%22domain_rating_source%22%2C%22is%22%3A%5B%22gte%22%2C29.5%5D%7D%2C%7B%22field%22%3A%22traffic_domain%22%2C%22is%22%3A%5B%22gte%22%2C500%5D%7D%2C%7B%22field%22%3A%22links_external%22%2C%22is%22%3A%5B%22lte%22%2C300%5D%7D%5D%7D', + fullAuditRef: 'https://ahrefs.com/site-explorer/broken-backlinks?select=title%2Curl_from%2Curl_to%2Ctraffic_domain&limit=50&mode=prefix&order_by=domain_rating_source%3Adesc%2Ctraffic_domain%3Adesc&target=bar.foo.com&output=json&where=%7B%22and%22%3A%5B%7B%22field%22%3A%22domain_rating_source%22%2C%22is%22%3A%5B%22gte%22%2C29.5%5D%7D%2C%7B%22field%22%3A%22traffic_domain%22%2C%22is%22%3A%5B%22gte%22%2C500%5D%7D%2C%7B%22field%22%3A%22links_external%22%2C%22is%22%3A%5B%22lte%22%2C300%5D%7D%5D%7D', }, }; @@ -356,6 +365,49 @@ describe('Backlinks Tests', function () { .calledWith(context.env.AUDIT_RESULTS_QUEUE_URL, expectedMessage); }); + it('should successfully perform an audit to detect broken backlinks and not suggest fixes if auto-suggest disabled', async () => { + mockDataAccess.getSiteByID = sinon.stub().withArgs('site2').resolves(site2); + configuration.disableHandlerForSite('broken-backlinks-auto-suggest', { getId: () => site2.getId(), getOrganizationId: () => org.getId() }); + mockDataAccess.getConfiguration = sinon.stub().resolves(configuration); + + nock(site2.getBaseURL()) + .get(/.*/) + .reply(301, undefined, { location: 'https://www.foo.com' }); + + nock('https://www.foo.com') + .get(/.*/) + .reply(200); + + nock('https://ahrefs.com') + .get(/.*/) + .reply(200, auditResult); + + const expectedMessage = { + type: message.type, + url: site2.getBaseURL(), + auditContext: { + finalUrl: 'www.foo.com', + }, + auditResult: { + finalUrl: 'www.foo.com', + brokenBacklinks: auditResult.backlinks, + fullAuditRef: 'https://ahrefs.com/site-explorer/broken-backlinks?select=title%2Curl_from%2Curl_to%2Ctraffic_domain&limit=50&mode=prefix&order_by=domain_rating_source%3Adesc%2Ctraffic_domain%3Adesc&target=www.foo.com&output=json&where=%7B%22and%22%3A%5B%7B%22field%22%3A%22domain_rating_source%22%2C%22is%22%3A%5B%22gte%22%2C29.5%5D%7D%2C%7B%22field%22%3A%22traffic_domain%22%2C%22is%22%3A%5B%22gte%22%2C500%5D%7D%2C%7B%22field%22%3A%22links_external%22%2C%22is%22%3A%5B%22lte%22%2C300%5D%7D%5D%7D', + }, + }; + + const response = await auditBrokenBacklinks({ + url: site2.getId(), type: 'broken-backlinks', + }, context); + + expect(response.status).to.equal(204); + expect(mockDataAccess.getTopPagesForSite).to.not.have.been.called; + expect(mockDataAccess.addAudit).to.have.been.calledOnce; + expect(context.sqs.sendMessage).to.have.been.calledOnce; + expect(context.sqs.sendMessage).to.have.been + .calledWith(context.env.AUDIT_RESULTS_QUEUE_URL, expectedMessage); + expect(context.log.info).to.have.been.calledWith('Successfully audited site2 for broken-backlinks type audit'); + }); + it('should detect broken backlinks and save the proper audit result, even if the suggested fix fails', async () => { mockDataAccess.getSiteByID = sinon.stub().withArgs('site1').resolves(site); mockDataAccess.getTopPagesForSite.resolves([createSiteTopPage({ @@ -394,7 +446,7 @@ describe('Backlinks Tests', function () { auditResult: { finalUrl: 'bar.foo.com', brokenBacklinks: brokenBacklink.backlinks, - fullAuditRef: 'https://ahrefs.com/site-explorer/broken-backlinks?select=title%2Curl_from%2Curl_to%2Ctraffic_domain&limit=50&mode=prefix&order_by=domain_rating_source%3Adesc%2Ctraffic_domain%3Adesc&target=bar.foo.com&output=json&where=%7B%22and%22%3A%5B%7B%22field%22%3A%22is_dofollow%22%2C%22is%22%3A%5B%22eq%22%2C1%5D%7D%2C%7B%22field%22%3A%22is_content%22%2C%22is%22%3A%5B%22eq%22%2C1%5D%7D%2C%7B%22field%22%3A%22domain_rating_source%22%2C%22is%22%3A%5B%22gte%22%2C29.5%5D%7D%2C%7B%22field%22%3A%22traffic_domain%22%2C%22is%22%3A%5B%22gte%22%2C500%5D%7D%2C%7B%22field%22%3A%22links_external%22%2C%22is%22%3A%5B%22lte%22%2C300%5D%7D%5D%7D', + fullAuditRef: 'https://ahrefs.com/site-explorer/broken-backlinks?select=title%2Curl_from%2Curl_to%2Ctraffic_domain&limit=50&mode=prefix&order_by=domain_rating_source%3Adesc%2Ctraffic_domain%3Adesc&target=bar.foo.com&output=json&where=%7B%22and%22%3A%5B%7B%22field%22%3A%22domain_rating_source%22%2C%22is%22%3A%5B%22gte%22%2C29.5%5D%7D%2C%7B%22field%22%3A%22traffic_domain%22%2C%22is%22%3A%5B%22gte%22%2C500%5D%7D%2C%7B%22field%22%3A%22links_external%22%2C%22is%22%3A%5B%22lte%22%2C300%5D%7D%5D%7D', }, }; const response = await auditBrokenBacklinks(message, context); @@ -432,7 +484,7 @@ describe('Backlinks Tests', function () { auditResult: { finalUrl: 'www.foo.com', brokenBacklinks: auditResult.backlinks, - fullAuditRef: 'https://ahrefs.com/site-explorer/broken-backlinks?select=title%2Curl_from%2Curl_to%2Ctraffic_domain&limit=50&mode=prefix&order_by=domain_rating_source%3Adesc%2Ctraffic_domain%3Adesc&target=www.foo.com&output=json&where=%7B%22and%22%3A%5B%7B%22field%22%3A%22is_dofollow%22%2C%22is%22%3A%5B%22eq%22%2C1%5D%7D%2C%7B%22field%22%3A%22is_content%22%2C%22is%22%3A%5B%22eq%22%2C1%5D%7D%2C%7B%22field%22%3A%22domain_rating_source%22%2C%22is%22%3A%5B%22gte%22%2C29.5%5D%7D%2C%7B%22field%22%3A%22traffic_domain%22%2C%22is%22%3A%5B%22gte%22%2C500%5D%7D%2C%7B%22field%22%3A%22links_external%22%2C%22is%22%3A%5B%22lte%22%2C300%5D%7D%5D%7D', + fullAuditRef: 'https://ahrefs.com/site-explorer/broken-backlinks?select=title%2Curl_from%2Curl_to%2Ctraffic_domain&limit=50&mode=prefix&order_by=domain_rating_source%3Adesc%2Ctraffic_domain%3Adesc&target=www.foo.com&output=json&where=%7B%22and%22%3A%5B%7B%22field%22%3A%22domain_rating_source%22%2C%22is%22%3A%5B%22gte%22%2C29.5%5D%7D%2C%7B%22field%22%3A%22traffic_domain%22%2C%22is%22%3A%5B%22gte%22%2C500%5D%7D%2C%7B%22field%22%3A%22links_external%22%2C%22is%22%3A%5B%22lte%22%2C300%5D%7D%5D%7D', }, }; @@ -498,7 +550,7 @@ describe('Backlinks Tests', function () { auditResult: { finalUrl: 'foo.com', brokenBacklinks: auditResult.backlinks.concat(brokenBacklinkWithTimeout), - fullAuditRef: 'https://ahrefs.com/site-explorer/broken-backlinks?select=title%2Curl_from%2Curl_to%2Ctraffic_domain&limit=50&mode=prefix&order_by=domain_rating_source%3Adesc%2Ctraffic_domain%3Adesc&target=foo.com&output=json&where=%7B%22and%22%3A%5B%7B%22field%22%3A%22is_dofollow%22%2C%22is%22%3A%5B%22eq%22%2C1%5D%7D%2C%7B%22field%22%3A%22is_content%22%2C%22is%22%3A%5B%22eq%22%2C1%5D%7D%2C%7B%22field%22%3A%22domain_rating_source%22%2C%22is%22%3A%5B%22gte%22%2C29.5%5D%7D%2C%7B%22field%22%3A%22traffic_domain%22%2C%22is%22%3A%5B%22gte%22%2C500%5D%7D%2C%7B%22field%22%3A%22links_external%22%2C%22is%22%3A%5B%22lte%22%2C300%5D%7D%5D%7D', + fullAuditRef: 'https://ahrefs.com/site-explorer/broken-backlinks?select=title%2Curl_from%2Curl_to%2Ctraffic_domain&limit=50&mode=prefix&order_by=domain_rating_source%3Adesc%2Ctraffic_domain%3Adesc&target=foo.com&output=json&where=%7B%22and%22%3A%5B%7B%22field%22%3A%22domain_rating_source%22%2C%22is%22%3A%5B%22gte%22%2C29.5%5D%7D%2C%7B%22field%22%3A%22traffic_domain%22%2C%22is%22%3A%5B%22gte%22%2C500%5D%7D%2C%7B%22field%22%3A%22links_external%22%2C%22is%22%3A%5B%22lte%22%2C300%5D%7D%5D%7D', }, }; diff --git a/test/audits/cwv.test.js b/test/audits/cwv.test.js index ae5cf8e3..28080c1c 100644 --- a/test/audits/cwv.test.js +++ b/test/audits/cwv.test.js @@ -15,79 +15,42 @@ import { expect, use } from 'chai'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import { Request } from '@adobe/fetch'; import nock from 'nock'; import { createSite } from '@adobe/spacecat-shared-data-access/src/models/site.js'; -import { main } from '../../src/index.js'; -import { getRUMUrl } from '../../src/support/utils.js'; -import { expectedAuditResult, rumData } from '../fixtures/rum-data.js'; +import { CWVRunner } from '../../src/cwv/handler.js'; +import { rumData } from '../fixtures/rum-data.js'; use(sinonChai); const sandbox = sinon.createSandbox(); + +const baseURL = 'https://spacecat.com'; +const auditUrl = 'www.spacecat.com'; const DOMAIN_REQUEST_DEFAULT_PARAMS = { + domain: auditUrl, + domainkey: 42, interval: 7, - offset: 0, - limit: 101, + granularity: 'hourly', }; -const mockDate = '2023-11-27T12:30:01.124Z'; describe('Index Tests', () => { - const request = new Request('https://space.cat'); - let mockDataAccess; - let context; - let messageBodyJson; - let site; - - before('init', function () { - this.clock = sandbox.useFakeTimers({ - now: new Date(mockDate).getTime(), - }); - }); + const site = createSite({ baseURL }); + const context = { + runtime: { name: 'aws-lambda', region: 'us-east-1' }, + func: { package: 'spacecat-services', version: 'ci', name: 'test' }, + rumApiClient: { + query: sandbox.stub().withArgs('variable-1', sinon.match(DOMAIN_REQUEST_DEFAULT_PARAMS)).resolves(rumData), + }, + }; beforeEach('setup', () => { - site = createSite({ - baseURL: 'https://adobe.com', - }); - - mockDataAccess = { - getSiteByID: sinon.stub(), - addAudit: sinon.stub(), - }; - mockDataAccess.getSiteByID = sinon.stub().withArgs('site-id').resolves(site); - - messageBodyJson = { - type: 'cwv', - url: 'site-id', - auditContext: { - finalUrl: 'adobe.com', - }, - }; - context = { - log: console, - runtime: { - region: 'us-east-1', - }, - dataAccess: mockDataAccess, - env: { - AUDIT_RESULTS_QUEUE_URL: 'queueUrl', - RUM_DOMAIN_KEY: 'domainkey', - }, - invocation: { - event: { - Records: [{ - body: JSON.stringify(messageBodyJson), - }], - }, - }, - sqs: { - sendMessage: sandbox.stub().resolves(), - }, - }; - }); - - after(function () { - this.clock.uninstall(); + nock('https://secretsmanager.us-east-1.amazonaws.com/') + .post('/', (body) => body.SecretId === '/helix-deploy/spacecat-services/customer-secrets/spacecat_com/ci') + .reply(200, { + SecretString: JSON.stringify({ + RUM_DOMAIN_KEY: '42', + }), + }); }); afterEach(() => { @@ -95,76 +58,16 @@ describe('Index Tests', () => { sinon.restore(); }); - it('fetch cwv for base url > process > send results', async () => { - nock('https://adobe.com') - .get('/') - .reply(200); - nock('https://helix-pages.anywhere.run') - .get('/helix-services/run-query@v3/rum-dashboard') - .query({ - ...DOMAIN_REQUEST_DEFAULT_PARAMS, - domainkey: context.env.RUM_DOMAIN_KEY, - url: 'adobe.com', - }) - .reply(200, rumData); - - const resp = await main(request, context); - - const expectedMessage = { - ...messageBodyJson, - url: site.getBaseURL(), - auditResult: expectedAuditResult, - }; - - expect(resp.status).to.equal(204); - expect(mockDataAccess.addAudit).to.have.been.calledOnce; - expect(mockDataAccess.addAudit).to.have.been.calledWith({ - siteId: site.getId(), - isLive: false, - auditedAt: mockDate, - auditType: 'cwv', - fullAuditRef: 'https://helix-pages.anywhere.run/helix-services/run-query@v3/rum-dashboard?interval=7&offset=0&limit=101&url=adobe.com&domainkey=', - auditResult: expectedAuditResult, + it('cwv audit runs rum api client cwv query', async () => { + const result = await CWVRunner('www.spacecat.com', context, site); + expect(result).to.deep.equal({ + auditResult: { + cwv: rumData.filter((data) => data.pageviews >= 7000), + auditContext: { + interval: 7, + }, + }, + fullAuditRef: auditUrl, }); - expect(context.sqs.sendMessage).to.have.been.calledOnce; - expect(context.sqs.sendMessage).to.have.been - .calledWith(context.env.AUDIT_RESULTS_QUEUE_URL, expectedMessage); - }); - - it('fetch cwv for base url for base url > process > reject', async () => { - nock('https://adobe.com') - .get('/') - .reply(200); - nock('https://helix-pages.anywhere.run') - .get('/helix-services/run-query@v3/rum-dashboard') - .query({ - ...DOMAIN_REQUEST_DEFAULT_PARAMS, - domainkey: context.env.RUM_DOMAIN_KEY, - checkpoint: 404, - url: 'adobe.com', - }) - .replyWithError('Bad request'); - - const resp = await main(request, context); - - expect(resp.status).to.equal(500); - }); - - it('getRUMUrl do not add scheme to urls with a scheme already', async () => { - nock('http://space.cat') - .get('/') - .reply(200); - - const finalUrl = await getRUMUrl('http://space.cat'); - expect(finalUrl).to.eql('space.cat'); - }); - - it('getRUMUrl adds scheme to urls without a scheme', async () => { - nock('https://space.cat') - .get('/') - .reply(200); - - const finalUrl = await getRUMUrl('space.cat'); - expect(finalUrl).to.eql('space.cat'); }); }); diff --git a/test/audits/experimentation-opportunities.test.js b/test/audits/experimentation-opportunities.test.js new file mode 100644 index 00000000..dfd17478 --- /dev/null +++ b/test/audits/experimentation-opportunities.test.js @@ -0,0 +1,112 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use } from 'chai'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import nock from 'nock'; +import { handler } from '../../src/experimentation-opportunities/experimentation-opportunities.js'; +import { MockContextBuilder } from '../shared.js'; +import opportunitiesData from '../fixtures/opportunitiesdata.json' assert { type: 'json' }; + +use(sinonChai); + +describe('Opportunities Tests', () => { + const url = 'https://abc.com'; + let context; + let processEnvCopy; + let messageBodyJson; + let sandbox; + before('setup', function () { + sandbox = sinon.createSandbox(); + const mockDate = '2023-11-27T12:30:01.124Z'; + this.clock = sandbox.useFakeTimers({ + now: new Date(mockDate).getTime(), + }); + }); + + beforeEach('setup', () => { + messageBodyJson = { + type: '404', + url: 'https://abc.com', + auditContext: { + finalUrl: 'abc.com', + }, + }; + context = new MockContextBuilder() + .withSandbox(sandbox) + .withOverrides({ + env: { + AUDIT_RESULTS_QUEUE_URL: 'queueUrl', + AWS_REGION: 'us-east-1', + AWS_ACCESS_KEY_ID: 'some-key-id', + AWS_SECRET_ACCESS_KEY: 'some-secret-key', + AWS_SESSION_TOKEN: 'some-secret-token', + }, + runtime: { name: 'aws-lambda', region: 'us-east-1' }, + func: { package: 'spacecat-services', version: 'ci', name: 'test' }, + }) + .build(messageBodyJson); + processEnvCopy = { ...process.env }; + process.env = { + ...process.env, + ...context.env, + }; + }); + after('clean', function () { + this.clock.uninstall(); + }); + + afterEach(() => { + process.env = processEnvCopy; + nock.cleanAll(); + sinon.restore(); + }); + + it('fetch bundles for base url > process > send opportunities', async () => { + nock('https://secretsmanager.us-east-1.amazonaws.com/') + .post('/', (body) => body.SecretId === '/helix-deploy/spacecat-services/customer-secrets/abc_com/ci') + .reply(200, { + SecretString: JSON.stringify({ + RUM_DOMAIN_KEY: 'abc_dummy_key', + }), + }); + nock('https://abc.com') + .get('/') + .reply(200); + context.rumApiClient = { + queryMulti: sinon.stub().resolves(opportunitiesData), + }; + const site = { + getBaseURL: () => 'https://abc.com', + }; + const auditData = await handler(url, context, site); + + const expected = Object.values(opportunitiesData).flatMap((data) => data); + + expect(context.rumApiClient.queryMulti).calledWith([ + 'rageclick', + 'high-inorganic-high-bounce-rate', + 'high-organic-low-ctr', + ], { + domain: 'https://abc.com', + domainkey: 'abc_dummy_key', + interval: 30, + granularity: 'hourly', + }); + expect( + auditData.auditResult.experimentationOpportunities, + ).to.deep.equal(expected); + }); +}); diff --git a/test/audits/structured-data.test.js b/test/audits/structured-data.test.js new file mode 100644 index 00000000..71224293 --- /dev/null +++ b/test/audits/structured-data.test.js @@ -0,0 +1,283 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ +import GoogleClient from '@adobe/spacecat-shared-google-client'; +import { expect, use } from 'chai'; +import sinonChai from 'sinon-chai'; +import sinon from 'sinon'; + +import { structuredDataHandler } from '../../src/structured-data/handler.js'; +import { MockContextBuilder } from '../shared.js'; + +use(sinonChai); + +const sandbox = sinon.createSandbox(); +const message = { + type: 'structured-data', + url: 'https://www.example.com', +}; + +describe('URLInspect Audit', () => { + let context; + let googleClientStub; + let urlInspectStub; + let siteStub; + + let fullUrlInspectionResult; + + beforeEach(() => { + context = new MockContextBuilder() + .withSandbox(sandbox) + .withOverrides({ + log: { + info: sinon.stub(), + warn: sinon.stub(), + error: sinon.stub(), + }, + }) + .build(message); + + googleClientStub = { + urlInspect: sandbox.stub(), + }; + + urlInspectStub = googleClientStub.urlInspect; + siteStub = { + getId: () => '123', + getConfig: () => ({ + getIncludedURLs: () => ['https://example.com/product/1', 'https://example.com/product/2'], + }), + }; + + fullUrlInspectionResult = { + inspectionResult: { + inspectionResultLink: 'https://search.google.com/search-console/inspect?resource_id=https://www.example.com/', + indexStatusResult: { + verdict: 'PASS', + coverageState: 'Submitted and indexed', + robotsTxtState: 'ALLOWED', + indexingState: 'INDEXING_ALLOWED', + lastCrawlTime: '2024-08-13T22:35:22Z', + pageFetchState: 'SUCCESSFUL', + googleCanonical: 'https://www.example.com/foo', + userCanonical: 'https://www.example.com/foo', + referringUrls: [ + 'https://www.example.com/bar', + ], + crawledAs: 'MOBILE', + }, + mobileUsabilityResult: { + verdict: 'VERDICT_UNSPECIFIED', + }, + richResultsResult: { + verdict: 'PASS', + detectedItems: [ + { + richResultType: 'Product snippets', + items: [ + { + name: 'Example Product Name', + issues: [ + { + issueMessage: 'Missing field "image"', + severity: 'ERROR', + }, + ], + }, + ], + }, + { + richResultType: 'Merchant listings', + items: [ + { + name: 'Example Product Name', + issues: [ + { + issueMessage: 'Missing field "hasMerchantReturnPolicy"', + severity: 'WARNING', + }, + { + issueMessage: 'Missing field "shippingDetails"', + severity: 'ERROR', + }, + ], + }, + ], + }, + ], + }, + }, + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should successfully return a filtered result of the url inspection result', async () => { + sandbox.stub(GoogleClient, 'createFrom').returns(googleClientStub); + urlInspectStub.resolves(fullUrlInspectionResult); + + const auditData = await structuredDataHandler('https://www.example.com', context, siteStub); + + expect(auditData.auditResult).to.deep.equal( + [ + { + inspectionUrl: 'https://example.com/product/1', + indexStatusResult: { + verdict: 'PASS', + lastCrawlTime: '2024-08-13T22:35:22Z', + }, + richResults: { + verdict: 'PASS', + detectedItemTypes: [ + 'Product snippets', + 'Merchant listings', + ], + detectedIssues: [ + { + richResultType: 'Product snippets', + items: [ + { + name: 'Example Product Name', + issues: [ + { + issueMessage: 'Missing field "image"', + severity: 'ERROR', + }, + ], + }, + ], + }, + { + richResultType: 'Merchant listings', + items: [ + { + name: 'Example Product Name', + issues: [ + { + issueMessage: 'Missing field "shippingDetails"', + severity: 'ERROR', + }, + ], + }, + ], + }, + ], + }, + }, + { + inspectionUrl: 'https://example.com/product/2', + indexStatusResult: { + verdict: 'PASS', + lastCrawlTime: '2024-08-13T22:35:22Z', + }, + richResults: { + verdict: 'PASS', + detectedItemTypes: [ + 'Product snippets', + 'Merchant listings', + ], + detectedIssues: [ + { + richResultType: 'Product snippets', + items: [ + { + name: 'Example Product Name', + issues: [ + { + issueMessage: 'Missing field "image"', + severity: 'ERROR', + }, + ], + }, + ], + }, + { + richResultType: 'Merchant listings', + items: [ + { + name: 'Example Product Name', + issues: [ + { + issueMessage: 'Missing field "shippingDetails"', + severity: 'ERROR', + }, + ], + }, + ], + }, + ], + }, + }, + ], + ); + }); + + it('returns no rich results when there are no rich results errors', async () => { + sandbox.stub(GoogleClient, 'createFrom').returns(googleClientStub); + delete fullUrlInspectionResult.inspectionResult.richResultsResult; + urlInspectStub.resolves(fullUrlInspectionResult); + + const auditData = await structuredDataHandler('https://www.example.com', context, siteStub); + + expect(auditData.auditResult[0].richResults).to.deep.equal({}); + }); + + it('returns no rich results when there are no errors in rich results', async () => { + sandbox.stub(GoogleClient, 'createFrom').returns(googleClientStub); + fullUrlInspectionResult.inspectionResult + .richResultsResult.detectedItems[0].items[0].issues = []; + delete fullUrlInspectionResult.inspectionResult + .richResultsResult.detectedItems[1].items[0].issues[1]; + urlInspectStub.resolves(fullUrlInspectionResult); + + const auditData = await structuredDataHandler('https://www.example.com', context, siteStub); + + expect(auditData.auditResult[0].richResults.detectedIssues).to.deep.equal([]); + expect(auditData.auditResult[1].richResults.verdict).to.equal('PASS'); + }); + + it('throws error if there are no configured PDPs', async () => { + sandbox.stub(GoogleClient, 'createFrom').returns(googleClientStub); + siteStub.getConfig = () => ({ + getIncludedURLs: () => [], + }); + try { + await structuredDataHandler('https://www.example.com', context, siteStub); + } catch (error) { + expect(error.message).to.equal('No product detail pages found for site: https://www.example.com'); + } + }); + + it('throws error if site is not configured for google search console', async () => { + sandbox.stub(GoogleClient, 'createFrom').throws('No secrets found'); + + try { + await structuredDataHandler('https://www.example.com', context, siteStub); + } catch (error) { + expect(error.message).to.equal('Failed to create Google client. Site was probably not onboarded to GSC yet. Error: Sinon-provided No secrets found'); + } + }); + + it('throws error if google client fails to inspect URL', async () => { + urlInspectStub.rejects(new Error('Failed to inspect URL')); + sandbox.stub(GoogleClient, 'createFrom').returns(googleClientStub); + + try { + await structuredDataHandler('https://www.example.com', context, siteStub); + } catch (error) { + expect(error.message).to.equal('Failed to inspect URL: https://example.com/product/1. Error: Failed to inspect URL'); + } + }); +}); diff --git a/test/common/audit.test.js b/test/common/audit.test.js index a3078ab4..d1296366 100644 --- a/test/common/audit.test.js +++ b/test/common/audit.test.js @@ -21,10 +21,13 @@ import { createOrganization } from '@adobe/spacecat-shared-data-access/src/model import { createConfiguration } from '@adobe/spacecat-shared-data-access/src/models/configuration.js'; import { composeAuditURL, prependSchema } from '@adobe/spacecat-shared-utils'; import { - defaultMessageSender, defaultOrgProvider, + defaultMessageSender, + defaultOrgProvider, defaultPersister, defaultSiteProvider, - defaultUrlResolver, noopUrlResolver, + defaultUrlResolver, + noopUrlResolver, + wwwUrlResolver, } from '../../src/common/audit.js'; import { AuditBuilder } from '../../src/common/audit-builder.js'; import { MockContextBuilder } from '../shared.js'; @@ -326,4 +329,13 @@ describe('Audit tests', () => { expect(context.sqs.sendMessage).to.have.been.calledOnce; expect(context.sqs.sendMessage).to.have.been.calledWith(queueUrl, expectedMessage); }); + + it('wwwUrlResolver calculates audit urls correctly', async () => { + expect(wwwUrlResolver(createSite({ baseURL: 'http://spacecat.com' }))).to.equal('www.spacecat.com'); + expect(wwwUrlResolver(createSite({ baseURL: 'https://spacecat.com' }))).to.equal('www.spacecat.com'); + expect(wwwUrlResolver(createSite({ baseURL: 'http://www.spacecat.com' }))).to.equal('www.spacecat.com'); + expect(wwwUrlResolver(createSite({ baseURL: 'https://www.spacecat.com' }))).to.equal('www.spacecat.com'); + expect(wwwUrlResolver(createSite({ baseURL: 'http://blog.spacecat.com' }))).to.equal('blog.spacecat.com'); + expect(wwwUrlResolver(createSite({ baseURL: 'https://blog.spacecat.com' }))).to.equal('blog.spacecat.com'); + }); }); diff --git a/test/fixtures/opportunitiesdata.json b/test/fixtures/opportunitiesdata.json new file mode 100644 index 00000000..f738a5ec --- /dev/null +++ b/test/fixtures/opportunitiesdata.json @@ -0,0 +1,85 @@ +{ + "rageclick": [ + { + "type": "rageclick", + "page": "https://abc.com/abc-adoption/survey", + "screenshot": "", + "trackedPageKPIName": "The percentage of users who click on the same element lot of times in a short period of time.", + "trackedPageKPIValue": 43.25513196480938, + "pageViews": 34100, + "samples": 341, + "metrics": [ + { + "type": "click", + "selector": ".abc-survey", + "value": 2111, + "targets": { + "https://abc.com/in/products/media_1c2c9e62626b7b4df13882c013438725ae05464d6.jpeg": 577, + "undefined": 1534 + }, + "samples": 138, + "mobileSamples": 106, + "desktopSamples": 32, + "percentage": 40.469208211143695 + }, + { + "type": "click", + "selector": ".abc-survey #abc-survey-next", + "value": 2160, + "targets": { + "https://abc.com/in/images/media_1c2c9e62626b7b4df13882c0134982380923890.jpeg": 141, + "https://abc.com/in/images/media_1c2c9e62626b7b4df13882c013498223987923afe.jpeg": 401, + "undefined": 1618 + }, + "samples": 157, + "mobileSamples": 96, + "desktopSamples": 61, + "percentage": 46.04105571847507 + } + ] + }, + { + "type": "rageclick", + "page": "https://abc.com/abc-adoption/account", + "screenshot": "", + "trackedPageKPIName": "The percentage of users who click on the same element lot of times in a short period of time.", + "trackedPageKPIValue": 8.771929824561402, + "pageViews": 11400, + "samples": 114, + "metrics": [ + { + "type": "click", + "selector": ".account #favorites", + "value": 138, + "targets": { + "undefined": 138 + }, + "samples": 10, + "mobileSamples": 9, + "desktopSamples": 1, + "percentage": 8.771929824561402 + } + ] + } + ], + "high-organic-low-ctr": [{ + "type": "high-organic-low-ctr", + "page": "https://abc.com/abc-adoption/account", + "screenshot": "", + "trackedPageKPIName": "Click Through Rate", + "trackedPageKPIValue": "0.14", + "pageViews": 2145, + "samples": 123, + "metrics": [] + }], + "high-inorganic-high-bounce-rate": [{ + "type": "high-inorganic-high-bounce-rate", + "page": "https://abc.com/abc-adoption/pricing", + "screenshot": "", + "trackedPageKPIName": "Bounce Rate", + "trackedPageKPIValue": 0.76, + "pageViews": 8839, + "samples": 89, + "metrics": [] + }] +} diff --git a/test/fixtures/rum-data.js b/test/fixtures/rum-data.js index e1ceca35..65c18db5 100644 --- a/test/fixtures/rum-data.js +++ b/test/fixtures/rum-data.js @@ -10,499 +10,377 @@ * governing permissions and limitations under the License. */ -export const expectedAuditResult = [ +export const rumData = [ { - url: 'https://www.adobe.com/', - pageviews: '36064271', - CLS: 0.148, - INP: 65, - LCP: 5239, + url: 'https://www.aem.live/home', + lcp: 2099.699999988079, + lcpCount: 9, + cls: 0.020660136604802475, + clsCount: 7, + inp: 12, + inpCount: 3, + ttfb: 520.4500000476837, + ttfbCount: 18, + pageviews: 9620, }, { - url: 'https://www.adobe.com/express/feature/image/remove-background', - pageviews: '2737380', - CLS: 0.129, - INP: 95, - LCP: 1423, + url: 'https://www.aem.live/developer/block-collection', + lcp: 512.1249999403954, + lcpCount: 4, + cls: 0.0005409526209424976, + clsCount: 4, + inp: 20, + inpCount: 2, + ttfb: 122.90000003576279, + ttfbCount: 4, + pageviews: 8000, }, { - url: 'https://www.adobe.com/express/', - pageviews: '2703493', - CLS: 0.028, - INP: 79, - LCP: 1869, + url: 'https://www.aem.live/docs/', + lcp: 711.3499999996275, + lcpCount: 15, + cls: 0.011139588793585503, + clsCount: 10, + inp: 36, + inpCount: 8, + ttfb: 96.97500000149012, + ttfbCount: 16, + pageviews: 7910, }, -]; -export const rumData = { - ':names': [ - 'results', - 'meta', - ], - ':type': 'multi-sheet', - ':version': 3, - results: { - limit: 6, - offset: 1, - total: 6, - data: [ - { - url: 'https://www.adobe.com/', - pageviews: '36064271', - pageviews_1: '3549602', - pageviews_diff: '32514669', - lcpgood: 32, - fidgood: 88, - inpgood: 82, - clsgood: 60, - lcpbad: 37, - fidbad: 0, - inpbad: 0, - clsbad: 18, - avglcp: 5239, - avgfid: 6, - avginp: 65, - avgcls: 0.148, - rumshare: '43.455607462', - lcpgood_1: 25, - fidgood_1: 89, - inpgood_1: 83, - clsgood_1: 89, - lcpbad_1: 45, - fidbad_1: 1, - inpbad_1: 3, - clsbad_1: 0, - avglcp_1: 6376, - avgfid_1: 7, - avginp_1: 72, - avgcls_1: 0.01, - rumshare_1: '6.99597117', - url_1: 'https://www.adobe.com/', - }, - { - url: 'https://www.adobe.com/express/feature/image/remove-background', - pageviews: '2737380', - pageviews_1: '2246720', - pageviews_diff: '490660', - lcpgood: 80, - fidgood: 87, - inpgood: 82, - clsgood: 55, - lcpbad: 4, - fidbad: 1, - inpbad: 0, - clsbad: 20, - avglcp: 1423, - avgfid: 10, - avginp: 95, - avgcls: 0.129, - rumshare: '3.298403308', - lcpgood_1: 81, - fidgood_1: 87, - inpgood_1: 78, - clsgood_1: 44, - lcpbad_1: 4, - fidbad_1: 1, - inpbad_1: 4, - clsbad_1: 33, - avglcp_1: 1452, - avgfid_1: 12, - avginp_1: 116, - avgcls_1: 0.774, - rumshare_1: '4.428098797', - url_1: 'https://www.adobe.com/express/feature/image/remove-background', - }, - { - url: 'https://www.adobe.com/express/', - pageviews: '2703493', - pageviews_1: '2996880', - pageviews_diff: '-293387', - lcpgood: 73, - fidgood: 87, - inpgood: 83, - clsgood: 80, - lcpbad: 7, - fidbad: 1, - inpbad: 0, - clsbad: 3, - avglcp: 1869, - avgfid: 8, - avginp: 79, - avgcls: 0.028, - rumshare: '3.257571201', - lcpgood_1: 76, - fidgood_1: 88, - inpgood_1: 84, - clsgood_1: 84, - lcpbad_1: 6, - fidbad_1: 1, - inpbad_1: 2, - clsbad_1: 1, - avglcp_1: 1718, - avgfid_1: 8, - avginp_1: 73, - avgcls_1: 0.029, - rumshare_1: '5.906601945', - url_1: 'https://www.adobe.com/express/', - }, - { - url: 'https://www.adobe.com/acrobat/online/pdf-to-word.html', - pageviews: '6999', - pageviews_1: '1663400', - pageviews_diff: '-301000', - lcpgood: 58, - fidgood: 98, - inpgood: 86, - clsgood: 47, - lcpbad: 30, - fidbad: 1, - inpbad: 0, - clsbad: 36, - avglcp: 5086, - avgfid: 5, - avginp: 112, - avgcls: 0.686, - rumshare: '1.641622525', - lcpgood_1: 59, - fidgood_1: 98, - inpgood_1: 87, - clsgood_1: 46, - lcpbad_1: 30, - fidbad_1: 1, - inpbad_1: 7, - clsbad_1: 36, - avglcp_1: 5332, - avgfid_1: 5, - avginp_1: 112, - avgcls_1: 0.69, - rumshare_1: '3.278423452', - url_1: 'https://www.adobe.com/acrobat/online/pdf-to-word.html', - }, - { - url: 'https://www.adobe.com/acrobat/online/merge-pdf.html', - pageviews: '1', - pageviews_1: '818200', - pageviews_diff: '-134300', - lcpgood: 81, - fidgood: 98, - inpgood: 83, - clsgood: 55, - lcpbad: 11, - fidbad: 1, - inpbad: 0, - clsbad: 29, - avglcp: 2120, - avgfid: 5, - avginp: 144, - avgcls: 0.412, - rumshare: '0.824064625', - lcpgood_1: 81, - fidgood_1: 98, - inpgood_1: 82, - clsgood_1: 46, - lcpbad_1: 10, - fidbad_1: 1, - inpbad_1: 9, - clsbad_1: 42, - avglcp_1: 2056, - avgfid_1: 4, - avginp_1: 152, - avgcls_1: 0.727, - rumshare_1: '1.612604346', - url_1: 'https://www.adobe.com/acrobat/online/merge-pdf.html', - }, - { - url: 'Other', - pageviews: '38797220', - pageviews_1: '38700170', - pageviews_diff: '97050', - lcpgood: 71, - fidgood: 92, - inpgood: 82, - clsgood: 64, - lcpbad: 13, - fidbad: 1, - inpbad: 0, - clsbad: 21, - avglcp: 2656, - avgfid: 10, - avginp: 131, - avgcls: 0.315, - rumshare: '46.74867164', - lcpgood_1: 70, - fidgood_1: 90, - inpgood_1: 80, - clsgood_1: 56, - lcpbad_1: 13, - fidbad_1: 1, - inpbad_1: 5, - clsbad_1: 30, - avglcp_1: 2533, - avgfid_1: 9, - avginp_1: 124, - avgcls_1: 0.418, - rumshare_1: '76.274825622', - url_1: 'Other', - }, - ], - columns: [ - 'url', - 'pageviews', - 'pageviews_1', - 'pageviews_diff', - 'lcpgood', - 'fidgood', - 'inpgood', - 'clsgood', - 'lcpbad', - 'fidbad', - 'inpbad', - 'clsbad', - 'avglcp', - 'avgfid', - 'avginp', - 'avgcls', - 'rumshare', - 'lcpgood_1', - 'fidgood_1', - 'inpgood_1', - 'clsgood_1', - 'lcpbad_1', - 'fidbad_1', - 'inpbad_1', - 'clsbad_1', - 'avglcp_1', - 'avgfid_1', - 'avginp_1', - 'avgcls_1', - 'rumshare_1', - 'url_1', - ], + { + url: 'https://www.aem.live/tools/rum/explorer.html', + lcp: 1111.8000000715256, + lcpCount: 9, + cls: 0.000996222305285266, + clsCount: 7, + inp: 184, + inpCount: 5, + ttfb: 312.69999980926514, + ttfbCount: 9, + pageviews: 7600, + }, + { + url: 'https://www.aem.live/developer/tutorial', + lcp: 1256.0249999985099, + lcpCount: 6, + cls: 0.3156739291116928, + clsCount: 6, + inp: 36, + inpCount: 3, + ttfb: 399.62500001490116, + ttfbCount: 6, + pageviews: 1600, + }, + { + url: 'https://www.aem.live/developer/anatomy-of-a-franklin-project', + lcp: 1360.6000000005588, + lcpCount: 4, + cls: 0.00013092332403036945, + clsCount: 2, + inp: null, + inpCount: 0, + ttfb: 345.69999999962965, + ttfbCount: 4, + pageviews: 1200, + }, + { + url: 'https://www.aem.live/developer/indexing', + lcp: null, + lcpCount: 0, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: 317, + ttfbCount: 1, + pageviews: 1000, + }, + { + url: 'https://www.aem.live/docs/custom-headers', + lcp: null, + lcpCount: 0, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: 22.69999998807907, + ttfbCount: 1, + pageviews: 900, + }, + { + url: 'https://www.aem.live/docs/dev-collab-and-good-practices', + lcp: 158.60000000149012, + lcpCount: 1, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: 34.70000000298023, + ttfbCount: 1, + pageviews: 700, + }, + { + url: 'https://www.aem.live/docs/go-live-checklist', + lcp: 75.5, + lcpCount: 1, + cls: null, + clsCount: 0, + inp: 8, + inpCount: 1, + ttfb: null, + ttfbCount: 0, + pageviews: 600, + }, + { + url: 'https://www.aem.live/developer/keeping-it-100', + lcp: 282.7250000759959, + lcpCount: 2, + cls: 0.006388621736403153, + clsCount: 2, + inp: 22, + inpCount: 2, + ttfb: 48.374999932944775, + ttfbCount: 2, + pageviews: 500, + }, + { + url: 'https://www.aem.live/tools/sidekick/', + lcp: 2756.899999856949, + lcpCount: 1, + cls: 0.0004926924960464029, + clsCount: 1, + inp: null, + inpCount: 0, + ttfb: 612.7749998904765, + ttfbCount: 2, + pageviews: 410, + }, + { + url: 'https://www.aem.live/docs/placeholders', + lcp: 740.875, + lcpCount: 2, + cls: 0.0029431425237193406, + clsCount: 2, + inp: 24, + inpCount: 1, + ttfb: 330, + ttfbCount: 1, + pageviews: 403, + }, + { + url: 'https://www.aem.live/docs/byo-cdn-cloudflare-worker-setup', + lcp: 483.69999998807907, + lcpCount: 1, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: 155.69999998807907, + ttfbCount: 1, + pageviews: 400, + }, + { + url: 'https://www.aem.live/docs/davidsmodel', + lcp: 1546.9000000059605, + lcpCount: 3, + cls: 0.7502103183538148, + clsCount: 2, + inp: 24, + inpCount: 1, + ttfb: 1016.2000000234693, + ttfbCount: 3, + pageviews: 300, + }, + { + url: 'https://www.aem.live/docs/setup-customer-sharepoint', + lcp: 361.09999999403954, + lcpCount: 1, + cls: 0.00017381700304902785, + clsCount: 1, + inp: null, + inpCount: 0, + ttfb: 12.899999976158142, + ttfbCount: 1, + pageviews: 300, + }, + { + url: 'https://www.aem.live/docs/sidekick', + lcp: null, + lcpCount: 0, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: 18, + ttfbCount: 1, + pageviews: 200, }, - meta: { - limit: 12, - offset: 0, - total: 12, - columns: [ - 'name', - 'value', - 'type', - ], - data: [ - { - name: 'description', - value: 'Get Helix RUM data for a given domain or owner/repo combination', - type: 'query description', - }, - { - name: 'limit', - value: 6, - type: 'request parameter', - }, - { - name: 'interval', - value: 30, - type: 'request parameter', - }, - { - name: 'offset', - value: 1, - type: 'request parameter', - }, - { - name: 'startdate', - value: '2022-02-01', - type: 'request parameter', - }, - { - name: 'enddate', - value: '2022-05-28', - type: 'request parameter', - }, - { - name: 'timezone', - value: 'UTC', - type: 'request parameter', - }, - { - name: 'url', - value: 'www.adobe.com/', - type: 'request parameter', - }, - { - name: 'owner', - value: '-', - type: 'request parameter', - }, - { - name: 'repo', - value: '-', - type: 'request parameter', - }, - { - name: 'device', - value: 'all', - type: 'request parameter', - }, - { - name: 'rising', - value: false, - type: 'request parameter', - }, - { - name: 'avgcls', - value: '75th percentile value of the Cumulative Layout Shift metric in the current period', - type: 'response detail', - }, - { - name: 'avgcls_1', - value: '75th percentile value of the CLS metric in the previous period', - type: 'response detail', - }, - { - name: 'avgfid', - value: '75th percentile value of the First Input Delay metric in milliseconds in the current period', - type: 'response detail', - }, - { - name: 'avgfid_1', - value: '75th percentile value of FID in the previous period', - type: 'response detail', - }, - { - name: 'avginp', - value: '75th percentile value of the Interaction to Next Paint metric in milliseconds in the current period', - type: 'response detail', - }, - { - name: 'avginp_1', - value: '75th percentile of INP in the previous period', - type: 'response detail', - }, - { - name: 'avglcp', - value: '75th percentile of the Largest Contentful Paint metric in milliseconds in the current period', - type: 'response detail', - }, - { - name: 'avglcp_1', - value: '75th percentile of LCP in the previous period', - type: 'response detail', - }, - { - name: 'clsbad', - value: 'percentage of all page views where Cumulative Layout Shift is in the “needs improvement” range in the current period', - type: 'response detail', - }, - { - name: 'clsbad_1', - value: 'percentage of of all page views with bad CLS in the previous period', - type: 'response detail', - }, - { - name: 'clsgood', - value: 'percentage of all page views where the CLS metric is in the “good” range in the current period', - type: 'response detail', - }, - { - name: 'clsgood_1', - value: 'percentage of pageviews with good CLS the the previous period', - type: 'response detail', - }, - { - name: 'fidbad', - value: 'percentage of pageviews with bad FID in the current period', - type: 'response detail', - }, - { - name: 'fidbad_1', - value: 'percentage of pageviews with bad FID in the previous period', - type: 'response detail', - }, - { - name: 'fidgood', - value: 'percentage of pageviews with good FID in the current period', - type: 'response detail', - }, - { - name: 'fidgood_1', - value: 'percentage of pageviews with good FID in the previous period', - type: 'response detail', - }, - { - name: 'inpbad', - value: 'percentage of pageviews with bad INP in the current period', - type: 'response detail', - }, - { - name: 'inpbad_1', - value: 'percentage of pageviews with bad INP in the previous period', - type: 'response detail', - }, - { - name: 'inpgood', - value: 'percentage of pageviews with good INP in the current period', - type: 'response detail', - }, - { - name: 'inpgood_1', - value: 'percentage of pageviews with bad INP in the previous period', - type: 'response detail', - }, - { - name: 'lcpbad', - value: 'percentage of pageviews with bad LCP in the current period', - type: 'response detail', - }, - { - name: 'lcpbad_1', - value: 'percentage of pageviews with bad LCP in the previous period', - type: 'response detail', - }, - { - name: 'lcpgood', - value: 'percentage of pageviews with good LCP in the current period', - type: 'response detail', - }, - { - name: 'lcpgood_1', - value: 'percentage of pageviews with good LCP in the current period', - type: 'response detail', - }, - { - name: 'pageviews', - value: 'estimated number of pageviews in the current period', - type: 'response detail', - }, - { - name: 'pageviews_1', - value: 'estimated number of pageviews in the previous period', - type: 'response detail', - }, - { - name: 'pageviews_diff', - value: 'difference in pageviews between the current and previous period. If the parameter rising is true, then pages will be ranked according to this value', - type: 'response detail', - }, - { - name: 'rumshare', - value: 'percentage of all traffic for the given domain that is going to this url in the current period', - type: 'response detail', - }, - { - name: 'rumshare_1', - value: 'percentage of of all traffic in the previous domain that is going to this url in the previous period', - type: 'response detail', - }, - { - name: 'url', - value: 'the URL of the page that is getting traffic', - type: 'response detail', - }, - { - name: 'url_1', - value: 'the URL of the page that is getting traffic in the previous period (these last two values are always the same)', - type: 'response detail', - }, - ], + { + url: 'https://www.aem.live/developer/web-components', + lcp: 612.2000000476837, + lcpCount: 1, + cls: 0.00007814911374216038, + clsCount: 1, + inp: null, + inpCount: 0, + ttfb: 273.4000000357628, + ttfbCount: 1, + pageviews: 200, + }, + { + url: 'https://www.aem.live/docs/authentication-setup-site', + lcp: 639.1999999284744, + lcpCount: 1, + cls: null, + clsCount: 0, + inp: 8, + inpCount: 1, + ttfb: 2551.824999988079, + ttfbCount: 2, + pageviews: 200, + }, + { + url: 'https://www.aem.live/developer/rum', + lcp: 786.1999999992549, + lcpCount: 2, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: 261.55000000074506, + ttfbCount: 2, + pageviews: 200, + }, + { + url: 'https://www.aem.live/developer/block-collection/section-metadata', + lcp: null, + lcpCount: 0, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: 274.10000002384186, + ttfbCount: 1, + pageviews: 200, + }, + { + url: 'https://www.aem.live/vip/intake', + lcp: 28, + lcpCount: 1, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: null, + ttfbCount: 0, + pageviews: 200, + }, + { + url: 'https://www.aem.live/docs/sidekick-extension', + lcp: 433.70000000298023, + lcpCount: 1, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: 334.30000000447035, + ttfbCount: 1, + pageviews: 110, + }, + { + url: 'https://www.aem.live/docs/rum', + lcp: 78.79999995231628, + lcpCount: 1, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: 12.100000023841858, + ttfbCount: 1, + pageviews: 110, + }, + { + url: 'https://www.aem.live/developer/block-collection/breadcrumbs', + lcp: 45.39999997615814, + lcpCount: 1, + cls: 0.000014899872841139986, + clsCount: 1, + inp: 24, + inpCount: 1, + ttfb: 9.100000023841858, + ttfbCount: 1, + pageviews: 100, + }, + { + url: 'https://www.aem.live/developer/block-collection/headings', + lcp: null, + lcpCount: 0, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: 288.10000002384186, + ttfbCount: 1, + pageviews: 100, + }, + { + url: 'https://www.aem.live/developer/github-actions', + lcp: null, + lcpCount: 0, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: 56.59999999962747, + ttfbCount: 1, + pageviews: 100, + }, + { + url: 'https://www.aem.live/developer/importer', + lcp: 1345.7999999523163, + lcpCount: 1, + cls: 0.07786671987900227, + clsCount: 1, + inp: 56, + inpCount: 1, + ttfb: 118.89999997615814, + ttfbCount: 1, + pageviews: 100, }, -}; + { + url: 'https://www.aem.live/developer/configuring-aem-assets-sidekick-plugin', + lcp: null, + lcpCount: 0, + cls: null, + clsCount: 0, + inp: null, + inpCount: 0, + ttfb: 22, + ttfbCount: 1, + pageviews: 100, + }, + { + url: 'https://www.aem.live/tools/rum/list.html', + lcp: 1182.5, + lcpCount: 1, + cls: null, + clsCount: 0, + inp: 8, + inpCount: 1, + ttfb: 433.2999997138977, + ttfbCount: 1, + pageviews: 100, + }, + { + url: 'https://www.aem.live/developer/forms', + lcp: 954, + lcpCount: 1, + cls: 0.0005954693086687166, + clsCount: 1, + inp: null, + inpCount: 0, + ttfb: 190.09999999403954, + ttfbCount: 1, + pageviews: 100, + }, +]; diff --git a/test/index.test.js b/test/index.test.js index 6f3769dd..01addaa5 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -29,8 +29,8 @@ describe('Index Tests', () => { beforeEach('setup', () => { messageBodyJson = { - type: 'cwv', - url: 'adobe.com', + type: 'dummy', + url: 'site-id', auditContext: { key: 'value', }, @@ -80,10 +80,8 @@ describe('Index Tests', () => { expect(resp.status).to.equal(500); }); - it('fails when missing required env variables', async () => { + it('happy path', async () => { const resp = await main(request, context); - - expect(resp.status).to.equal(500); - expect(resp.headers.plain()['x-error']).to.equal('internal server error'); + expect(resp.status).to.equal(200); }); });