diff --git a/package.json b/package.json index d048c5dc0..d40626027 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build": "hedy -v --test-bundle", "deploy": "hedy -v --deploy --aws-deploy-bucket=spacecat-prod-deploy --pkgVersion=latest", "deploy-stage": "hedy -v --deploy --aws-deploy-bucket=spacecat-stage-deploy --pkgVersion=latest", - "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l latest --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h", + "deploy-dev": "hedy -v --deploy --pkgVersion=latest$CI_BUILD_NUM -l sandsinh --aws-deploy-bucket=spacecat-dev-deploy --cleanup-ci=24h", "deploy-secrets": "hedy --aws-update-secrets --params-file=secrets/secrets.env", "docs": "npm run docs:lint && npm run docs:build", "docs:build": "npx @redocly/cli build-docs -o ./docs/index.html --config docs/openapi/redocly-config.yaml", diff --git a/src/controllers/preflight.js b/src/controllers/preflight.js index f8d7630f5..a129d7fb4 100644 --- a/src/controllers/preflight.js +++ b/src/controllers/preflight.js @@ -332,7 +332,8 @@ function PreflightController(ctx, log, env) { if (!isDev) { return badRequest('mystiqueUrl override is only allowed in dev'); } - if (!isValidUrl(normalizedMystiqueUrl)) { + if (!isValidUrl(normalizedMystiqueUrl) + || !new URL(normalizedMystiqueUrl).hostname.includes('.')) { return badRequest('Invalid request: mystiqueUrl must be a valid URL'); } if (!(/\.stage\.cloud\.adobe\.io$/).test(new URL(normalizedMystiqueUrl).hostname)) { diff --git a/src/support/grant-suggestions-handler.js b/src/support/grant-suggestions-handler.js index 91ce5b388..135bdfda8 100644 --- a/src/support/grant-suggestions-handler.js +++ b/src/support/grant-suggestions-handler.js @@ -98,6 +98,25 @@ const OPPORTUNITY_STRATEGIES = { ); }, }, + + // Sorts CWV suggestions by total pageviews descending so high-traffic + // pages are granted first. Ties are broken by id ascending. + cwv: { + sortFn: (groupA, groupB) => { + const getPageviews = (group) => { + const s = group.items[0]; + const data = typeof s?.getData === 'function' ? s.getData() : s?.data; + return data?.pageviews ?? 0; + }; + const pvDiff = getPageviews(groupB) - getPageviews(groupA); + if (pvDiff !== 0) return pvDiff; + const a = groupA.items[0]; + const b = groupB.items[0]; + const idA = typeof a?.getId === 'function' ? a.getId() : (a?.id ?? ''); + const idB = typeof b?.getId === 'function' ? b.getId() : (b?.id ?? ''); + return idA.localeCompare(idB); + }, + }, }; /** diff --git a/test/support/grant-suggestions-handler.test.js b/test/support/grant-suggestions-handler.test.js index fa3566082..17a47a90b 100644 --- a/test/support/grant-suggestions-handler.test.js +++ b/test/support/grant-suggestions-handler.test.js @@ -200,6 +200,63 @@ describe('grant-suggestions-handler', () => { const groups = getTopSuggestions([s1, s2]); expect(groups).to.have.lengthOf(2); }); + + it('sorts cwv suggestions by pageviews descending using getData()', () => { + const mk = (id, pageviews) => ({ + getId: () => id, + getRank: () => 0, + getData: () => ({ url: `https://example.com/${id}`, pageviews }), + }); + const s1 = mk('id-1', 5220); + const s2 = mk('id-2', 3800); + const s3 = mk('id-3', 900); + const s4 = mk('id-4', 2000); + const groups = getTopSuggestions([s1, s2, s3, s4], 'cwv'); + expect(groups).to.have.lengthOf(4); + expect(groups[0].items[0]).to.equal(s1); // 5220 first + expect(groups[1].items[0]).to.equal(s2); // 3800 + expect(groups[2].items[0]).to.equal(s4); // 2000 + expect(groups[3].items[0]).to.equal(s3); // 900 last + }); + + it('sorts cwv suggestions by pageviews descending using plain objects', () => { + const s1 = { id: 'id-1', rank: 0, data: { pageviews: 5220 } }; + const s2 = { id: 'id-2', rank: 0, data: { pageviews: 2000 } }; + const groups = getTopSuggestions([s2, s1], 'cwv'); + expect(groups[0].items[0]).to.equal(s1); + expect(groups[1].items[0]).to.equal(s2); + }); + + it('cwv tie-breaks by id ascending when pageviews are equal', () => { + const s1 = { getId: () => 'id-b', getRank: () => 0, getData: () => ({ pageviews: 1000 }) }; + const s2 = { getId: () => 'id-a', getRank: () => 0, getData: () => ({ pageviews: 1000 }) }; + const groups = getTopSuggestions([s1, s2], 'cwv'); + expect(groups[0].items[0]).to.equal(s2); // id-a before id-b + expect(groups[1].items[0]).to.equal(s1); + }); + + it('cwv tie-breaks by id ascending using plain objects when pageviews are equal', () => { + const s1 = { id: 'id-b', rank: 0, data: { pageviews: 1000 } }; + const s2 = { id: 'id-a', rank: 0, data: { pageviews: 1000 } }; + const groups = getTopSuggestions([s1, s2], 'cwv'); + expect(groups[0].items[0]).to.equal(s2); // id-a before id-b + expect(groups[1].items[0]).to.equal(s1); + }); + + it('cwv tie-breaks fall back to empty string when plain objects have no id', () => { + const s1 = { rank: 0, data: { pageviews: 1000 } }; + const s2 = { rank: 0, data: { pageviews: 1000 } }; + const groups = getTopSuggestions([s1, s2], 'cwv'); + expect(groups).to.have.lengthOf(2); + }); + + it('cwv treats missing pageviews as 0', () => { + const s1 = { getId: () => 'id-1', getRank: () => 0, getData: () => ({}) }; + const s2 = { getId: () => 'id-2', getRank: () => 0, getData: () => ({ pageviews: 500 }) }; + const groups = getTopSuggestions([s1, s2], 'cwv'); + expect(groups[0].items[0]).to.equal(s2); // 500 before 0 + expect(groups[1].items[0]).to.equal(s1); + }); }); describe('grantSuggestionsForOpportunity', () => {