diff --git a/package-lock.json b/package-lock.json index bdb16095..e4bb095b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5551,7 +5551,6 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-rum-api-client/-/spacecat-shared-rum-api-client-2.9.0.tgz", "integrity": "sha512-VbtFFeQP/+/DJ9W+z09oMqLQfbGizmInScKv1xJs9di8lXRQislKofR/rC8Aak2Bu0d4odfdhjyywpxwLBggBg==", - "license": "Apache-2.0", "dependencies": { "@adobe/fetch": "4.1.8", "@adobe/helix-shared-wrap": "2.0.2", @@ -5666,8 +5665,7 @@ "node_modules/@adobe/spacecat-shared-rum-api-client/node_modules/aws4": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", - "license": "MIT" + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" }, "node_modules/@adobe/spacecat-shared-utils": { "version": "1.19.6", @@ -20369,7 +20367,6 @@ "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-1.22.1.tgz", "integrity": "sha512-fGBj3KH32JH9Kg4Y3DQ2iF5jfpjT7T5ypA3JOpJcwGZTI/krDzrSxOb7ArX0HCATCBCobkWzDynHBF03j9oPvw==", "dev": true, - "license": "MIT", "dependencies": { "@redocly/openapi-core": "1.22.1", "abort-controller": "^3.0.0", @@ -20538,15 +20535,13 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.10.1.tgz", "integrity": "sha512-H3LnKVGzOaxskwJu8pmJYwBOWjP61qOK7TuTrbafqArDVckE06fhA6l0nO4KvBbjLPjy1Al7UnlxOu23V4Nl0w==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@redocly/openapi-core": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.22.1.tgz", "integrity": "sha512-pjr456gJzrbwPvBjFKlWOd9nZEcr/dp6XSH0c8ijWGWv5K+V0vpUUy2zaTAVsFAoCydx1miEoOcbLhWQ9WC2nw==", "dev": true, - "license": "MIT", "dependencies": { "@redocly/ajv": "^8.11.0", "@redocly/config": "^0.10.1", @@ -22555,7 +22550,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.3.0", @@ -22589,7 +22583,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "8.3.0", "@typescript-eslint/types": "8.3.0", @@ -22618,7 +22611,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.3.0", "@typescript-eslint/visitor-keys": "8.3.0" @@ -22636,7 +22628,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "8.3.0", "@typescript-eslint/utils": "8.3.0", @@ -22661,7 +22652,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -22675,7 +22665,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "8.3.0", "@typescript-eslint/visitor-keys": "8.3.0", @@ -22704,7 +22693,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -22720,7 +22708,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.3.0", @@ -22743,7 +22730,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" 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 2034eb04..3d6be775 100644 --- a/src/cwv/handler.js +++ b/src/cwv/handler.js @@ -11,25 +11,18 @@ */ import RUMAPIClient from '@adobe/spacecat-shared-rum-api-client'; -import URI from 'urijs'; -import { hasText } from '@adobe/spacecat-shared-utils'; 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 function getAuditUrl(baseURL) { - const uri = new URI(baseURL); - return hasText(uri.subdomain()) ? baseURL.replace(/https?:\/\//, '') : baseURL.replace(/https?:\/\//, 'www.'); -} - export async function CWVRunner(auditUrl, context, site) { const rumAPIClient = RUMAPIClient.createFrom(context); const domainkey = await getRUMDomainkey(site.getBaseURL(), context); - const finalUrl = getAuditUrl(auditUrl); const options = { - domain: finalUrl, + domain: auditUrl, domainkey, interval: INTERVAL, granularity: 'hourly', @@ -44,11 +37,11 @@ export async function CWVRunner(auditUrl, context, site) { return { auditResult, - fullAuditRef: finalUrl, + fullAuditRef: auditUrl, }; } export default new AuditBuilder() - .withUrlResolver((site) => site.getBaseURL()) + .withUrlResolver(wwwUrlResolver) .withRunner(CWVRunner) .build(); diff --git a/src/opportunities/opportunities.js b/src/experimentation-opportunities/experimentation-opportunities.js similarity index 56% rename from src/opportunities/opportunities.js rename to src/experimentation-opportunities/experimentation-opportunities.js index 4eff2b43..2c44b31e 100644 --- a/src/opportunities/opportunities.js +++ b/src/experimentation-opportunities/experimentation-opportunities.js @@ -10,14 +10,17 @@ * governing permissions and limitations under the License. */ -/* c8 ignore start */ 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; - -let log = console; +const OPPTY_QUERIES = [ + 'rageclick', + 'high-inorganic-high-bounce-rate', + 'high-organic-low-ctr', +]; /** * Audit handler container for all the opportunities @@ -27,10 +30,8 @@ let log = console; * @returns */ -export async function opportunitiesHandler(auditUrl, context, site) { - log = context.log; - log.info(`Received Opportunities audit request for ${auditUrl}`); - const startTime = process.hrtime(); +export async function handler(auditUrl, context, site) { + const { log } = context; const rumAPIClient = RUMAPIClient.createFrom(context); const domainkey = await getRUMDomainkey(site.getBaseURL(), context); @@ -40,30 +41,21 @@ export async function opportunitiesHandler(auditUrl, context, site) { interval: DAYS, granularity: 'hourly', }; - const experimentationHandlers = ['rageclick']; - const queryResults = await rumAPIClient.queryMulti(experimentationHandlers, options); - const auditData = { - experimentationOpportunities: [], - }; - for (const queryResult of Object.keys(queryResults)) { - if (experimentationHandlers.includes(queryResult)) { - auditData.experimentationOpportunities.push(...queryResults[queryResult]); - } - } - const endTime = process.hrtime(startTime); - const elapsedSeconds = endTime[0] + endTime[1] / 1e9; - const formattedElapsed = elapsedSeconds.toFixed(2); + const queryResults = await rumAPIClient.queryMulti(OPPTY_QUERIES, options); + const experimentationOpportunities = Object.values(queryResults).flatMap((oppty) => oppty); - log.info(`Opportunities Audit is completed in ${formattedElapsed} seconds for ${auditUrl}`); + log.info(`Found ${experimentationOpportunities.length} many experimentation opportunites for ${auditUrl}`); return { - auditResult: auditData, + auditResult: { + experimentationOpportunities, + }, fullAuditRef: auditUrl, }; } export default new AuditBuilder() - .withRunner(opportunitiesHandler) + .withRunner(handler) + .withUrlResolver(wwwUrlResolver) .build(); -/* c8 ignore stop */ diff --git a/src/index.js b/src/index.js index a99c7086..19b7cfd2 100644 --- a/src/index.js +++ b/src/index.js @@ -29,7 +29,7 @@ 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 opportunities from './opportunities/opportunities.js'; +import experimentationOpportunities from './experimentation-opportunities/experimentation-opportunities.js'; import costs from './costs/handler.js'; import structuredData from './structured-data/handler.js'; @@ -46,7 +46,7 @@ const HANDLERS = { conversion, 'experimentation-ess-daily': essExperimentationDaily, 'experimentation-ess-all': essExperimentationAll, - opportunities, + 'experimentation-opportunities': experimentationOpportunities, costs, 'structured-data': structuredData, dummy: (message) => ok(message), @@ -68,7 +68,7 @@ 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) { @@ -82,11 +82,11 @@ 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); + log.error(`${type} audit for ${url} failed after ${getElapsedSeconds(startTime)} seconds`, e); return internalServerError(); } } diff --git a/test/audits/cwv.test.js b/test/audits/cwv.test.js index 664cef80..28080c1c 100644 --- a/test/audits/cwv.test.js +++ b/test/audits/cwv.test.js @@ -17,7 +17,7 @@ import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import nock from 'nock'; import { createSite } from '@adobe/spacecat-shared-data-access/src/models/site.js'; -import { CWVRunner, getAuditUrl } from '../../src/cwv/handler.js'; +import { CWVRunner } from '../../src/cwv/handler.js'; import { rumData } from '../fixtures/rum-data.js'; use(sinonChai); @@ -59,7 +59,7 @@ describe('Index Tests', () => { }); it('cwv audit runs rum api client cwv query', async () => { - const result = await CWVRunner('https://spacecat.com', context, site); + const result = await CWVRunner('www.spacecat.com', context, site); expect(result).to.deep.equal({ auditResult: { cwv: rumData.filter((data) => data.pageviews >= 7000), @@ -70,13 +70,4 @@ describe('Index Tests', () => { fullAuditRef: auditUrl, }); }); - - it('audit url calculated correctly', async () => { - expect(getAuditUrl('http://spacecat.com')).to.equal('www.spacecat.com'); - expect(getAuditUrl('https://spacecat.com')).to.equal('www.spacecat.com'); - expect(getAuditUrl('http://www.spacecat.com')).to.equal('www.spacecat.com'); - expect(getAuditUrl('https://www.spacecat.com')).to.equal('www.spacecat.com'); - expect(getAuditUrl('http://blog.spacecat.com')).to.equal('blog.spacecat.com'); - expect(getAuditUrl('https://blog.spacecat.com')).to.equal('blog.spacecat.com'); - }); }); diff --git a/test/audits/opportunities.test.js b/test/audits/experimentation-opportunities.test.js similarity index 84% rename from test/audits/opportunities.test.js rename to test/audits/experimentation-opportunities.test.js index 22905dfb..dfd17478 100644 --- a/test/audits/opportunities.test.js +++ b/test/audits/experimentation-opportunities.test.js @@ -16,7 +16,7 @@ import { expect, use } from 'chai'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import nock from 'nock'; -import { opportunitiesHandler } from '../../src/opportunities/opportunities.js'; +import { handler } from '../../src/experimentation-opportunities/experimentation-opportunities.js'; import { MockContextBuilder } from '../shared.js'; import opportunitiesData from '../fixtures/opportunitiesdata.json' assert { type: 'json' }; @@ -91,19 +91,22 @@ describe('Opportunities Tests', () => { const site = { getBaseURL: () => 'https://abc.com', }; - const auditData = await opportunitiesHandler(url, context, site); + const auditData = await handler(url, context, site); - expect(context.rumApiClient.queryMulti).calledWith( - ['rageclick'], - { - domain: 'https://abc.com', - domainkey: 'abc_dummy_key', - interval: 30, - granularity: 'hourly', - }, - ); + 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(opportunitiesData.rageclick); + ).to.deep.equal(expected); }); }); 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 index fde23dbf..f738a5ec 100644 --- a/test/fixtures/opportunitiesdata.json +++ b/test/fixtures/opportunitiesdata.json @@ -61,5 +61,25 @@ } ] } - ] + ], + "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": [] + }] }