From 98a8293dc44defc9b7fcc7ed1c8258107d58c69c Mon Sep 17 00:00:00 2001 From: Barshat Rai Date: Wed, 16 Apr 2025 16:13:06 +0530 Subject: [PATCH 1/2] feat: forms - high page views, low form views opportunity feat: forms - high page views, low form views opportunity feat: forms - high page views, low form views opportunity - review comments feat: forms - high page views, low form views opportunity - review comments feat: forms - high page views, low form views opportunity feat: forms - high page views, low form views opportunity - refactoring feat: low form views: avoid repetition of same form over multiple opportunities feat: low form views: updated guidance and descriptions --- src/forms-opportunities/constants.js | 1 + src/forms-opportunities/formcalc.js | 5 +- src/forms-opportunities/handler.js | 7 +- .../oppty-handlers/low-conversion-handler.js | 7 +- .../oppty-handlers/low-navigation-handler.js | 14 +- .../oppty-handlers/low-views-handler.js | 96 ++++++++ src/forms-opportunities/utils.js | 26 ++- test/audits/forms/formcalc.test.js | 16 +- .../forms/low-conv-oppoty-handler.test.js | 11 + .../forms/low-nav-oppoty-handler.test.js | 7 + .../forms/low-views-oppoty-handler.test.js | 207 ++++++++++++++++++ .../forms/high-form-views-low-conversions.js | 57 +++++ 12 files changed, 429 insertions(+), 25 deletions(-) create mode 100644 src/forms-opportunities/oppty-handlers/low-views-handler.js create mode 100644 test/audits/forms/low-views-oppoty-handler.test.js diff --git a/src/forms-opportunities/constants.js b/src/forms-opportunities/constants.js index 086d96cf4..57844a32a 100644 --- a/src/forms-opportunities/constants.js +++ b/src/forms-opportunities/constants.js @@ -12,4 +12,5 @@ export const FORM_OPPORTUNITY_TYPES = { LOW_CONVERSION: 'high-form-views-low-conversions', LOW_NAVIGATION: 'high-page-views-low-form-nav', + LOW_VIEWS: 'high-page-views-low-form-views', }; diff --git a/src/forms-opportunities/formcalc.js b/src/forms-opportunities/formcalc.js index 3067bd7da..91af04f73 100644 --- a/src/forms-opportunities/formcalc.js +++ b/src/forms-opportunities/formcalc.js @@ -116,14 +116,11 @@ export function getHighPageViewsLowFormViewsMetrics(formVitalsCollection) { resultMap.forEach((metrics, url) => { const { total: pageViews } = metrics.pageview; const { total: formViews } = metrics.formview; - const { total: formEngagement } = metrics.formengagement; if (hasHighPageViews(pageViews) && hasLowFormViews(pageViews, formViews)) { urls.push({ url, - pageViews, - formViews, - formEngagement, + ...metrics, }); } }); diff --git a/src/forms-opportunities/handler.js b/src/forms-opportunities/handler.js index b87d444c4..6367a6972 100644 --- a/src/forms-opportunities/handler.js +++ b/src/forms-opportunities/handler.js @@ -19,6 +19,7 @@ import { generateOpptyData } from './utils.js'; import { getScrapedDataForSiteId } from '../support/utils.js'; import createLowConversionOpportunities from './oppty-handlers/low-conversion-handler.js'; import createLowNavigationOpportunities from './oppty-handlers/low-navigation-handler.js'; +import createLowViewsOpportunities from './oppty-handlers/low-views-handler.js'; const { AUDIT_STEP_DESTINATIONS } = Audit; const FORMS_OPPTY_QUERIES = [ @@ -100,8 +101,10 @@ export async function processOpportunityStep(context) { log.info(`[Form Opportunity] [Site Id: ${site.getId()}] processing opportunity`); const scrapedData = await getScrapedDataForSiteId(site, context); const latestAudit = await site.getLatestAuditByAuditType('forms-opportunities'); - await createLowConversionOpportunities(finalUrl, latestAudit, scrapedData, context); - await createLowNavigationOpportunities(finalUrl, latestAudit, scrapedData, context); + const excludeUrls = new Set(); + await createLowNavigationOpportunities(finalUrl, latestAudit, scrapedData, context, excludeUrls); + await createLowViewsOpportunities(finalUrl, latestAudit, scrapedData, context, excludeUrls); + await createLowConversionOpportunities(finalUrl, latestAudit, scrapedData, context, excludeUrls); log.info(`[Form Opportunity] [Site Id: ${site.getId()}] opportunity identified`); return { status: 'complete', diff --git a/src/forms-opportunities/oppty-handlers/low-conversion-handler.js b/src/forms-opportunities/oppty-handlers/low-conversion-handler.js index 6b0cca467..503199ef6 100644 --- a/src/forms-opportunities/oppty-handlers/low-conversion-handler.js +++ b/src/forms-opportunities/oppty-handlers/low-conversion-handler.js @@ -82,9 +82,10 @@ function generateDefaultGuidance(scrapedData, oppoty) { * @param auditUrl - The URL of the audit * @param auditData - The audit data containing the audit result and additional details. * @param context - The context object containing the data access and logger objects. + * @param excludeUrls - A set of URLs to exclude from the opportunity creation process. */ // eslint-disable-next-line max-len -export default async function createLowConversionOpportunities(auditUrl, auditDataObject, scrapedData, context) { +export default async function createLowConversionOpportunities(auditUrl, auditDataObject, scrapedData, context, excludeUrls = new Set()) { const { dataAccess, log, sqs, site, env, } = context; @@ -107,9 +108,9 @@ export default async function createLowConversionOpportunities(auditUrl, auditDa // eslint-disable-next-line max-len const formOpportunities = await generateOpptyData(formVitals, context, [FORM_OPPORTUNITY_TYPES.LOW_CONVERSION]); log.debug(`forms opportunities ${JSON.stringify(formOpportunities, null, 2)}`); - const filteredOpportunities = filterForms(formOpportunities, scrapedData, log); + const filteredOpportunities = filterForms(formOpportunities, scrapedData, log, excludeUrls); + filteredOpportunities.forEach((oppty) => excludeUrls.add(oppty.form)); log.info(`filtered opportunties high form views low conversion for form ${JSON.stringify(filteredOpportunities, null, 2)}`); - try { for (const opptyData of filteredOpportunities) { let highFormViewsLowConversionsOppty = opportunities.find( diff --git a/src/forms-opportunities/oppty-handlers/low-navigation-handler.js b/src/forms-opportunities/oppty-handlers/low-navigation-handler.js index f9667d1cf..a5880afdc 100644 --- a/src/forms-opportunities/oppty-handlers/low-navigation-handler.js +++ b/src/forms-opportunities/oppty-handlers/low-navigation-handler.js @@ -18,9 +18,10 @@ const formPathSegments = ['contact', 'newsletter', 'sign', 'enrol', 'subscribe', * @param auditUrl - The URL of the audit * @param auditData - The audit data containing the audit result and additional details. * @param context - The context object containing the data access and logger objects. + * @param excludeUrls - A set of URLs to exclude from the opportunity creation process. */ // eslint-disable-next-line max-len -export default async function createLowNavigationOpportunities(auditUrl, auditDataObject, scrapedData, context) { +export default async function createLowNavigationOpportunities(auditUrl, auditDataObject, scrapedData, context, excludeUrls = new Set()) { const { dataAccess, log } = context; const { Opportunity } = dataAccess; @@ -47,14 +48,19 @@ export default async function createLowNavigationOpportunities(auditUrl, auditDa const filteredOpportunitiesByNavigation = formOpportunities.filter((opportunity) => formPathSegments.some((substring) => opportunity.form?.includes(substring) && !opportunity.formNavigation?.url?.includes('search'))); - const filteredOpportunities = filterForms(filteredOpportunitiesByNavigation, scrapedData, log); + const filteredOpportunities = filterForms( + filteredOpportunitiesByNavigation, + scrapedData, + log, + excludeUrls, + ); + filteredOpportunities.forEach((oppty) => excludeUrls.add(oppty.form)); log.info(`filtered opportunities: high-page-views-low-form-navigations: ${JSON.stringify(filteredOpportunities, null, 2)}`); - try { for (const opptyData of filteredOpportunities) { let highPageViewsLowFormNavOppty = opportunities.find( (oppty) => oppty.getType() === FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION - && oppty.getData().form === opptyData.form, + && oppty.getData().form === opptyData.form, ); const opportunityData = { diff --git a/src/forms-opportunities/oppty-handlers/low-views-handler.js b/src/forms-opportunities/oppty-handlers/low-views-handler.js new file mode 100644 index 000000000..58ff50dfd --- /dev/null +++ b/src/forms-opportunities/oppty-handlers/low-views-handler.js @@ -0,0 +1,96 @@ +/* + * Copyright 2025 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 { FORM_OPPORTUNITY_TYPES } from '../constants.js'; +import { filterForms, generateOpptyData } from '../utils.js'; + +/** + * @param auditUrl - The URL of the audit + * @param auditData - The audit data containing the audit result and additional details. + * @param context - The context object containing the data access and logger objects. + * @param excludeUrls - A set of URLs to exclude from the opportunity creation process. + */ +// eslint-disable-next-line max-len +export default async function createLowViewsOpportunities(auditUrl, auditDataObject, scrapedData, context, excludeUrls = new Set()) { + const { dataAccess, log } = context; + const { Opportunity } = dataAccess; + + const auditData = JSON.parse(JSON.stringify(auditDataObject)); + log.info(`Syncing high page views low form views opportunity for ${auditData.siteId}`); + let opportunities; + + try { + opportunities = await Opportunity.allBySiteIdAndStatus(auditData.siteId, 'NEW'); + } catch (e) { + log.error(`Fetching opportunities for siteId ${auditData.siteId} failed with error: ${e.message}`); + throw new Error(`Failed to fetch opportunities for siteId ${auditData.siteId}: ${e.message}`); + } + + const { formVitals } = auditData.auditResult; + // eslint-disable-next-line max-len + const formOpportunities = await generateOpptyData(formVitals, context, [FORM_OPPORTUNITY_TYPES.LOW_VIEWS]); + log.debug(`forms opportunities high-page-views-low-form-views: ${JSON.stringify(formOpportunities, null, 2)}`); + + const filteredOpportunities = filterForms(formOpportunities, scrapedData, log, excludeUrls); + filteredOpportunities.forEach((oppty) => excludeUrls.add(oppty.form)); + log.info(`filtered opportunities: high-page-views-low-form-views: ${JSON.stringify(filteredOpportunities, null, 2)}`); + try { + for (const opptyData of filteredOpportunities) { + let highPageViewsLowFormViewsOptty = opportunities.find( + (oppty) => oppty.getType() === FORM_OPPORTUNITY_TYPES.LOW_VIEWS + && oppty.getData().form === opptyData.form, + ); + + const opportunityData = { + siteId: auditData.siteId, + auditId: auditData.auditId, + runbook: 'https://adobe.sharepoint.com/:w:/s/AEM_Forms/EeYKNa4HQkRAleWXjC5YZbMBMhveB08F1yTTUQSrP97Eow?e=cZdsnA', + type: FORM_OPPORTUNITY_TYPES.LOW_VIEWS, + origin: 'AUTOMATION', + title: 'The form has low views', + description: 'The form has low views but the page containing the form has higher traffic', + tags: ['Forms Conversion'], + data: { + ...opptyData, + }, + guidance: { + recommendations: [ + { + insight: `The form in the page: ${opptyData.form} has low discoverability and only ${(opptyData.formViews / opptyData.pageViews) * 100}% visitors landing on the page are viewing the form.`, + recommendation: 'Position the form higher up on the page so users see it without scrolling. Consider using clear and compelling CTAs, minimizing distractions, and ensuring strong visibility across devices.', + type: 'guidance', + rationale: 'Forms that are visible above the fold are more likely to be seen and interacted with by users.', + }, + ], + }, + }; + + log.info(`Forms Opportunity created high page views low form views ${JSON.stringify(opportunityData, null, 2)}`); + if (!highPageViewsLowFormViewsOptty) { + // eslint-disable-next-line no-await-in-loop + highPageViewsLowFormViewsOptty = await Opportunity.create(opportunityData); + } else { + highPageViewsLowFormViewsOptty.setAuditId(auditData.auditId); + highPageViewsLowFormViewsOptty.setData({ + ...highPageViewsLowFormViewsOptty.getData(), + ...opportunityData.data, + }); + highPageViewsLowFormViewsOptty.setGuidance(opportunityData.guidance); + // eslint-disable-next-line no-await-in-loop + await highPageViewsLowFormViewsOptty.save(); + } + } + } catch (e) { + log.error(`Creating Forms opportunity for high page views low form views for siteId ${auditData.siteId} failed with error: ${e.message}`, e); + } + log.info(`Successfully synced Opportunity for site: ${auditData.siteId} and high page views low form views audit type.`); +} diff --git a/src/forms-opportunities/utils.js b/src/forms-opportunities/utils.js index ad4fb8ae5..0fc1c9456 100644 --- a/src/forms-opportunities/utils.js +++ b/src/forms-opportunities/utils.js @@ -17,6 +17,7 @@ import { GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { getHighPageViewsLowFormCtrMetrics, getHighFormViewsLowConversionMetrics, + getHighPageViewsLowFormViewsMetrics, } from './formcalc.js'; import { FORM_OPPORTUNITY_TYPES } from './constants.js'; @@ -90,10 +91,10 @@ function getFormMetrics(metricObject) { }); } -function convertToLowNavOpptyData(metricObject) { +function convertToLowViewOpptyData(metricObject) { const { formview: { total: formViews, mobile: formViewsMobile, desktop: formViewsDesktop }, - CTA, trafficacquisition, + trafficacquisition, } = metricObject; return { trackedFormKPIName: 'Form Views', @@ -131,10 +132,18 @@ function convertToLowNavOpptyData(metricObject) { }, }, ], - formNavigation: CTA, }; } +function convertToLowNavOpptyData(metricObject) { + const { + CTA, + } = metricObject; + const opptyData = convertToLowViewOpptyData(metricObject); + opptyData.formNavigation = CTA; + return opptyData; +} + function convertToLowConversionOpptyData(metricObject) { const { trafficacquisition } = metricObject; const deviceWiseMetrics = getFormMetrics(metricObject); @@ -204,6 +213,8 @@ async function convertToOpportunityData(opportunityType, metricObject, context) opportunityData = convertToLowConversionOpptyData(metricObject); } else if (opportunityType === FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION) { opportunityData = convertToLowNavOpptyData(metricObject); + } else if (opportunityType === FORM_OPPORTUNITY_TYPES.LOW_VIEWS) { + opportunityData = convertToLowViewOpptyData(metricObject); } const screenshot = await getPresignedUrl('screenshot-desktop-fullpage.png', context, url, site); @@ -222,7 +233,8 @@ async function convertToOpportunityData(opportunityType, metricObject, context) export async function generateOpptyData( formVitals, context, - opportunityTypes = [FORM_OPPORTUNITY_TYPES.LOW_CONVERSION, FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION], + opportunityTypes = [FORM_OPPORTUNITY_TYPES.LOW_CONVERSION, + FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION, FORM_OPPORTUNITY_TYPES.LOW_VIEWS], ) { const formVitalsCollection = formVitals.filter( (row) => row.formengagement && row.formsubmit && row.formview, @@ -231,6 +243,7 @@ export async function generateOpptyData( Object.entries({ [FORM_OPPORTUNITY_TYPES.LOW_CONVERSION]: getHighFormViewsLowConversionMetrics, [FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION]: getHighPageViewsLowFormCtrMetrics, + [FORM_OPPORTUNITY_TYPES.LOW_VIEWS]: getHighPageViewsLowFormViewsMetrics, }) .filter(([opportunityType]) => opportunityTypes.includes(opportunityType)) .flatMap(([opportunityType, metricsMethod]) => metricsMethod(formVitalsCollection) @@ -249,12 +262,13 @@ export function shouldExcludeForm(scrapedFormData) { * @param formOpportunities * @param scrapedData * @param log + * @param excludeUrls urls to exclude from opportunity creation * @returns {*} */ -export function filterForms(formOpportunities, scrapedData, log) { +export function filterForms(formOpportunities, scrapedData, log, excludeUrls = new Set()) { return formOpportunities.filter((opportunity) => { let urlMatches = false; - if (opportunity.form.includes('search')) { + if (opportunity.form.includes('search') || excludeUrls.has(opportunity.form)) { return false; // exclude search pages } if (isNonEmptyArray(scrapedData?.formData)) { diff --git a/test/audits/forms/formcalc.test.js b/test/audits/forms/formcalc.test.js index 20204707e..e5f0e5f04 100644 --- a/test/audits/forms/formcalc.test.js +++ b/test/audits/forms/formcalc.test.js @@ -48,15 +48,19 @@ describe('Form Calc functions', () => { expect(result).to.eql([ { url: 'https://www.surest.com/info/win', - pageViews: 8670, - formViews: 300, - formEngagement: 4300, + formengagement: { total: 4300, desktop: 4000, mobile: 300 }, + formsubmit: { total: 0, desktop: 0, mobile: 0 }, + formview: { total: 300, desktop: 0, mobile: 300 }, + pageview: { total: 8670, desktop: 4670, mobile: 4000 }, + trafficacquisition: {}, }, { url: 'https://www.surest.com/newsletter', - pageViews: 8670, - formViews: 300, - formEngagement: 300, + formengagement: { total: 300, desktop: 0, mobile: 300 }, + formsubmit: { total: 0, desktop: 0, mobile: 0 }, + formview: { total: 300, desktop: 0, mobile: 300 }, + pageview: { total: 8670, desktop: 4670, mobile: 4000 }, + trafficacquisition: {}, }, ]); }); diff --git a/test/audits/forms/low-conv-oppoty-handler.test.js b/test/audits/forms/low-conv-oppoty-handler.test.js index 5024c0dbf..33765b8d4 100644 --- a/test/audits/forms/low-conv-oppoty-handler.test.js +++ b/test/audits/forms/low-conv-oppoty-handler.test.js @@ -185,4 +185,15 @@ describe('createLowConversionOpportunities handler method', () => { expect(dataAccessStub.Opportunity.create).to.be.calledWith(testData.opportunityData5); expect(logStub.info).to.be.calledWith('Successfully synced Opportunity for site: site-id and high-form-views-low-conversions audit type.'); }); + + it('should not create low conversion opportunity if another opportunity already exists', async () => { + const excludeUrls = new Set(); + excludeUrls.add('https://www.surest.com/newsletter'); + excludeUrls.add('https://www.surest.com/info/win-1'); + await createLowConversionOpportunities(auditUrl, auditData, undefined, context, excludeUrls); + expect(dataAccessStub.Opportunity.create).to.be.callCount(3); + expect(excludeUrls.has('https://www.surest.com/contact-us')).to.be.true; + expect(excludeUrls.has('https://www.surest.com/info/win-2')).to.be.true; + expect(excludeUrls.has('https://www.surest.com/info/win')).to.be.true; + }); }); diff --git a/test/audits/forms/low-nav-oppoty-handler.test.js b/test/audits/forms/low-nav-oppoty-handler.test.js index de832fe4a..fabb31056 100644 --- a/test/audits/forms/low-nav-oppoty-handler.test.js +++ b/test/audits/forms/low-nav-oppoty-handler.test.js @@ -194,4 +194,11 @@ describe('createLowNavigationOpportunities handler method', () => { expect(dataAccessStub.Opportunity.create).to.not.be.called; expect(logStub.info).to.be.calledWith('Successfully synced Opportunity for site: site-id and high page views low form nav audit type.'); }); + + it('should not create low nav opportunity if another opportunity already exists', async () => { + const excludeUrls = new Set(); + excludeUrls.add('https://www.surest.com/newsletter'); + await createLowNavigationOpportunities(auditUrl, auditData, undefined, context, excludeUrls); + expect(dataAccessStub.Opportunity.create).to.not.be.called; + }); }); diff --git a/test/audits/forms/low-views-oppoty-handler.test.js b/test/audits/forms/low-views-oppoty-handler.test.js new file mode 100644 index 000000000..618356854 --- /dev/null +++ b/test/audits/forms/low-views-oppoty-handler.test.js @@ -0,0 +1,207 @@ +/* + * Copyright 2025 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 createLowViewsOpportunities from '../../../src/forms-opportunities/oppty-handlers/low-views-handler.js'; +import { FORM_OPPORTUNITY_TYPES } from '../../../src/forms-opportunities/constants.js'; +import testData from '../../fixtures/forms/high-form-views-low-conversions.js'; + +use(sinonChai); +describe('createLowFormViewsOpportunities handler method', () => { + let logStub; + let dataAccessStub; + let auditData; + let auditUrl; + let highPageViewsLowFormViewsOptty; + let context; + + beforeEach(() => { + sinon.restore(); + auditUrl = 'https://example.com'; + highPageViewsLowFormViewsOptty = { + getId: () => 'opportunity-id', + setAuditId: sinon.stub(), + save: sinon.stub(), + getType: () => FORM_OPPORTUNITY_TYPES.LOW_VIEWS, + setData: sinon.stub(), + setGuidance: sinon.stub(), + getData: sinon.stub().returns({ + form: 'https://www.surest.com/existing-opportunity', + screenshot: '', + trackedFormKPIName: 'Form Views', + trackedFormKPIValue: 100, + formViews: 100, + pageViews: 5000, + samples: 5000, + }), + }; + logStub = { + info: sinon.stub(), + debug: sinon.stub(), + error: sinon.stub(), + }; + dataAccessStub = { + Opportunity: { + allBySiteIdAndStatus: sinon.stub().resolves([]), + create: sinon.stub(), + }, + }; + context = { + log: logStub, + dataAccess: dataAccessStub, + env: { + S3_SCRAPER_BUCKET_NAME: 'test-bucket', + }, + site: { + getId: sinon.stub().returns('test-site-id'), + }, + }; + auditData = testData.lowFormviewsAuditData; + }); + + it('should create new high page views low form views opportunity', async () => { + const expectedOpportunityData = { + siteId: 'site-id', + auditId: 'audit-id', + runbook: 'https://adobe.sharepoint.com/:w:/s/AEM_Forms/EeYKNa4HQkRAleWXjC5YZbMBMhveB08F1yTTUQSrP97Eow?e=cZdsnA', + type: FORM_OPPORTUNITY_TYPES.LOW_VIEWS, + origin: 'AUTOMATION', + title: 'The form has low views', + description: 'The form has low views but the page containing the form has higher traffic', + tags: [ + 'Forms Conversion', + ], + data: { + form: 'https://www.surest.com/high-page-low-form-view', + screenshot: '', + trackedFormKPIName: 'Form Views', + trackedFormKPIValue: 200, + formViews: 200, + pageViews: 6690, + samples: 6690, + scrapedStatus: false, + metrics: [ + { + type: 'formViews', + device: '*', + value: { + page: 200, + }, + }, + { + type: 'formViews', + device: 'mobile', + value: { + page: 0, + }, + }, + { + type: 'formViews', + device: 'desktop', + value: { + page: 200, + }, + }, + { + type: 'traffic', + device: '*', + value: { + paid: 2690, + total: 6690, + earned: 2000, + owned: 2000, + }, + }, + ], + }, + guidance: { + recommendations: [ + { + insight: 'The form in the page: https://www.surest.com/high-page-low-form-view has low discoverability and only 2.9895366218236172% visitors landing on the page are viewing the form.', + recommendation: 'Position the form higher up on the page so users see it without scrolling. Consider using clear and compelling CTAs, minimizing distractions, and ensuring strong visibility across devices.', + type: 'guidance', + rationale: 'Forms that are visible above the fold are more likely to be seen and interacted with by users.', + }, + ], + }, + }; + await createLowViewsOpportunities(auditUrl, auditData, undefined, context); + + const actualCall = dataAccessStub.Opportunity.create.getCall(0).args[0]; + expect(actualCall).to.deep.equal(expectedOpportunityData); + expect(logStub.info).to.be.calledWith('Successfully synced Opportunity for site: site-id and high page views low form views audit type.'); + }); + + it('should not create low views opportunity if another opportunity already exists', async () => { + const excludeUrls = new Set(); + excludeUrls.add('https://www.surest.com/existing-opportunity'); + await createLowViewsOpportunities(auditUrl, auditData, undefined, context, excludeUrls); + expect(dataAccessStub.Opportunity.create).to.be.callCount(1); + expect(excludeUrls.has('https://www.surest.com/high-page-low-form-view')).to.be.true; + }); + + it('should use existing high page views low form view opportunity', async () => { + dataAccessStub.Opportunity.allBySiteIdAndStatus.resolves([highPageViewsLowFormViewsOptty]); + await createLowViewsOpportunities(auditUrl, auditData, undefined, context); + expect(highPageViewsLowFormViewsOptty.save).to.be.calledOnce; + expect(highPageViewsLowFormViewsOptty.setGuidance).to.be.calledWith( + { + recommendations: [ + { + insight: 'The form in the page: https://www.surest.com/existing-opportunity has low discoverability and only 2.9895366218236172% visitors landing on the page are viewing the form.', + recommendation: 'Position the form higher up on the page so users see it without scrolling. Consider using clear and compelling CTAs, minimizing distractions, and ensuring strong visibility across devices.', + type: 'guidance', + rationale: 'Forms that are visible above the fold are more likely to be seen and interacted with by users.', + }, + ], + }, + ); + expect(logStub.info).to.be.calledWith('Successfully synced Opportunity for site: site-id and high page views low form views audit type.'); + }); + + it('should throw error if fetching high page views low form navigation opportunity fails', async () => { + dataAccessStub.Opportunity.allBySiteIdAndStatus.rejects(new Error('some-error')); + + try { + await createLowViewsOpportunities(auditUrl, auditData, undefined, context); + } catch (err) { + expect(err.message).to.equal('Failed to fetch opportunities for siteId site-id: some-error'); + } + + expect(logStub.error).to.be.calledWith('Fetching opportunities for siteId site-id failed with error: some-error'); + }); + + it('should throw error if creating high page views low form navigation opportunity fails', async () => { + dataAccessStub.Opportunity.allBySiteIdAndStatus.returns([]); + dataAccessStub.Opportunity.create = sinon.stub().rejects(new Error('some-error')); + + try { + await createLowViewsOpportunities(auditUrl, auditData, undefined, context); + } catch (err) { + expect(err.message).to.equal('Failed to create Forms opportunity for high page views low form views for siteId site-id: some-error'); + } + + expect(logStub.error).to.be.calledWith('Creating Forms opportunity for high page views low form views for siteId site-id failed with error: some-error'); + }); + + it('should handle empty form vitals data', async () => { + auditData.auditResult.formVitals = []; + + await createLowViewsOpportunities(auditUrl, auditData, undefined, context); + + expect(dataAccessStub.Opportunity.create).to.not.be.called; + expect(logStub.info).to.be.calledWith('Successfully synced Opportunity for site: site-id and high page views low form views audit type.'); + }); +}); diff --git a/test/fixtures/forms/high-form-views-low-conversions.js b/test/fixtures/forms/high-form-views-low-conversions.js index 48cc452ab..9992e738f 100644 --- a/test/fixtures/forms/high-form-views-low-conversions.js +++ b/test/fixtures/forms/high-form-views-low-conversions.js @@ -335,6 +335,63 @@ const testData = { ], }, }, + lowFormviewsAuditData: { + type: 'high-page-views-low-form-nav', + siteId: 'site-id', + auditId: 'audit-id', + auditResult: { + formVitals: [ + { + url: 'https://www.surest.com/high-page-low-form-view', + formsubmit: { + 'desktop:windows': 0, + }, + formview: { + 'desktop:windows': 200, + }, + formengagement: { + 'desktop:windows': 100, + }, + pageview: { + 'desktop:windows': 5690, + 'mobile:ios': 1000, + }, + trafficacquisition: { + paid: 2690, + maxTimeDelta: 3060, + total: 6690, + earned: 2000, + sources: [], + owned: 2000, + }, + }, + { + url: 'https://www.surest.com/existing-opportunity', + formsubmit: { + 'desktop:windows': 0, + }, + formview: { + 'desktop:windows': 200, + }, + formengagement: { + 'desktop:windows': 100, + }, + pageview: { + 'desktop:windows': 5690, + 'mobile:ios': 1000, + }, + trafficacquisition: { + paid: 2690, + maxTimeDelta: 3060, + total: 6690, + earned: 2000, + sources: [], + owned: 2000, + }, + }, + ], + }, + }, auditData3: { type: 'high-form-views-low-conversions', siteId: 'site-id', From edf01fd3fb467fe9315603977f9ac19e4527a9aa Mon Sep 17 00:00:00 2001 From: Barshat Rai Date: Wed, 23 Apr 2025 16:15:11 +0530 Subject: [PATCH 2/2] feat: forms - add formsource param in opportunity, use formurl+formsource to exclude duplicate opportunities --- src/forms-opportunities/formcalc.js | 3 ++- src/forms-opportunities/handler.js | 8 ++++---- .../oppty-handlers/low-conversion-handler.js | 8 ++++---- .../oppty-handlers/low-navigation-handler.js | 8 ++++---- .../oppty-handlers/low-views-handler.js | 8 ++++---- src/forms-opportunities/utils.js | 4 +++- test/audits/forms/formcalc.test.js | 5 +++++ test/audits/forms/low-conv-oppoty-handler.test.js | 4 ++-- test/audits/forms/low-nav-oppoty-handler.test.js | 1 + test/audits/forms/low-views-oppoty-handler.test.js | 1 + test/fixtures/forms/formcalcaudit.js | 1 + test/fixtures/forms/high-form-views-low-conversions.js | 7 +++++++ 12 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/forms-opportunities/formcalc.js b/src/forms-opportunities/formcalc.js index 91af04f73..c2e89eff6 100644 --- a/src/forms-opportunities/formcalc.js +++ b/src/forms-opportunities/formcalc.js @@ -28,7 +28,7 @@ function aggregateFormVitalsByDevice(formVitalsCollection) { formVitalsCollection.forEach((item) => { const { url, formview = {}, formengagement = {}, pageview = {}, formsubmit = {}, - trafficacquisition = {}, + trafficacquisition = {}, formsource = '', } = item; const totals = { @@ -56,6 +56,7 @@ function aggregateFormVitalsByDevice(formVitalsCollection) { totals.pageview = calculateSums(pageview, totals.pageview); totals.formsubmit = calculateSums(formsubmit, totals.formsubmit); totals.trafficacquisition = trafficacquisition; + totals.formsource = formsource; resultMap.set(url, totals); }); diff --git a/src/forms-opportunities/handler.js b/src/forms-opportunities/handler.js index 6367a6972..656489296 100644 --- a/src/forms-opportunities/handler.js +++ b/src/forms-opportunities/handler.js @@ -101,10 +101,10 @@ export async function processOpportunityStep(context) { log.info(`[Form Opportunity] [Site Id: ${site.getId()}] processing opportunity`); const scrapedData = await getScrapedDataForSiteId(site, context); const latestAudit = await site.getLatestAuditByAuditType('forms-opportunities'); - const excludeUrls = new Set(); - await createLowNavigationOpportunities(finalUrl, latestAudit, scrapedData, context, excludeUrls); - await createLowViewsOpportunities(finalUrl, latestAudit, scrapedData, context, excludeUrls); - await createLowConversionOpportunities(finalUrl, latestAudit, scrapedData, context, excludeUrls); + const excludeForms = new Set(); + await createLowNavigationOpportunities(finalUrl, latestAudit, scrapedData, context, excludeForms); + await createLowViewsOpportunities(finalUrl, latestAudit, scrapedData, context, excludeForms); + await createLowConversionOpportunities(finalUrl, latestAudit, scrapedData, context, excludeForms); log.info(`[Form Opportunity] [Site Id: ${site.getId()}] opportunity identified`); return { status: 'complete', diff --git a/src/forms-opportunities/oppty-handlers/low-conversion-handler.js b/src/forms-opportunities/oppty-handlers/low-conversion-handler.js index 503199ef6..3e948357f 100644 --- a/src/forms-opportunities/oppty-handlers/low-conversion-handler.js +++ b/src/forms-opportunities/oppty-handlers/low-conversion-handler.js @@ -82,10 +82,10 @@ function generateDefaultGuidance(scrapedData, oppoty) { * @param auditUrl - The URL of the audit * @param auditData - The audit data containing the audit result and additional details. * @param context - The context object containing the data access and logger objects. - * @param excludeUrls - A set of URLs to exclude from the opportunity creation process. + * @param excludeForms - A set of Forms to exclude from the opportunity creation process. */ // eslint-disable-next-line max-len -export default async function createLowConversionOpportunities(auditUrl, auditDataObject, scrapedData, context, excludeUrls = new Set()) { +export default async function createLowConversionOpportunities(auditUrl, auditDataObject, scrapedData, context, excludeForms = new Set()) { const { dataAccess, log, sqs, site, env, } = context; @@ -108,8 +108,8 @@ export default async function createLowConversionOpportunities(auditUrl, auditDa // eslint-disable-next-line max-len const formOpportunities = await generateOpptyData(formVitals, context, [FORM_OPPORTUNITY_TYPES.LOW_CONVERSION]); log.debug(`forms opportunities ${JSON.stringify(formOpportunities, null, 2)}`); - const filteredOpportunities = filterForms(formOpportunities, scrapedData, log, excludeUrls); - filteredOpportunities.forEach((oppty) => excludeUrls.add(oppty.form)); + const filteredOpportunities = filterForms(formOpportunities, scrapedData, log, excludeForms); + filteredOpportunities.forEach((oppty) => excludeForms.add(oppty.form + oppty.formsource)); log.info(`filtered opportunties high form views low conversion for form ${JSON.stringify(filteredOpportunities, null, 2)}`); try { for (const opptyData of filteredOpportunities) { diff --git a/src/forms-opportunities/oppty-handlers/low-navigation-handler.js b/src/forms-opportunities/oppty-handlers/low-navigation-handler.js index a5880afdc..1cc20a7c7 100644 --- a/src/forms-opportunities/oppty-handlers/low-navigation-handler.js +++ b/src/forms-opportunities/oppty-handlers/low-navigation-handler.js @@ -18,10 +18,10 @@ const formPathSegments = ['contact', 'newsletter', 'sign', 'enrol', 'subscribe', * @param auditUrl - The URL of the audit * @param auditData - The audit data containing the audit result and additional details. * @param context - The context object containing the data access and logger objects. - * @param excludeUrls - A set of URLs to exclude from the opportunity creation process. + * @param excludeForms - A set of Forms to exclude from the opportunity creation process. */ // eslint-disable-next-line max-len -export default async function createLowNavigationOpportunities(auditUrl, auditDataObject, scrapedData, context, excludeUrls = new Set()) { +export default async function createLowNavigationOpportunities(auditUrl, auditDataObject, scrapedData, context, excludeForms = new Set()) { const { dataAccess, log } = context; const { Opportunity } = dataAccess; @@ -52,9 +52,9 @@ export default async function createLowNavigationOpportunities(auditUrl, auditDa filteredOpportunitiesByNavigation, scrapedData, log, - excludeUrls, + excludeForms, ); - filteredOpportunities.forEach((oppty) => excludeUrls.add(oppty.form)); + filteredOpportunities.forEach((oppty) => excludeForms.add(oppty.form + oppty.formsource)); log.info(`filtered opportunities: high-page-views-low-form-navigations: ${JSON.stringify(filteredOpportunities, null, 2)}`); try { for (const opptyData of filteredOpportunities) { diff --git a/src/forms-opportunities/oppty-handlers/low-views-handler.js b/src/forms-opportunities/oppty-handlers/low-views-handler.js index 58ff50dfd..dff35ca8b 100644 --- a/src/forms-opportunities/oppty-handlers/low-views-handler.js +++ b/src/forms-opportunities/oppty-handlers/low-views-handler.js @@ -17,10 +17,10 @@ import { filterForms, generateOpptyData } from '../utils.js'; * @param auditUrl - The URL of the audit * @param auditData - The audit data containing the audit result and additional details. * @param context - The context object containing the data access and logger objects. - * @param excludeUrls - A set of URLs to exclude from the opportunity creation process. + * @param excludeForms - A set of Forms to exclude from the opportunity creation process. */ // eslint-disable-next-line max-len -export default async function createLowViewsOpportunities(auditUrl, auditDataObject, scrapedData, context, excludeUrls = new Set()) { +export default async function createLowViewsOpportunities(auditUrl, auditDataObject, scrapedData, context, excludeForms = new Set()) { const { dataAccess, log } = context; const { Opportunity } = dataAccess; @@ -40,8 +40,8 @@ export default async function createLowViewsOpportunities(auditUrl, auditDataObj const formOpportunities = await generateOpptyData(formVitals, context, [FORM_OPPORTUNITY_TYPES.LOW_VIEWS]); log.debug(`forms opportunities high-page-views-low-form-views: ${JSON.stringify(formOpportunities, null, 2)}`); - const filteredOpportunities = filterForms(formOpportunities, scrapedData, log, excludeUrls); - filteredOpportunities.forEach((oppty) => excludeUrls.add(oppty.form)); + const filteredOpportunities = filterForms(formOpportunities, scrapedData, log, excludeForms); + filteredOpportunities.forEach((oppty) => excludeForms.add(oppty.form + oppty.formsource)); log.info(`filtered opportunities: high-page-views-low-form-views: ${JSON.stringify(filteredOpportunities, null, 2)}`); try { for (const opptyData of filteredOpportunities) { diff --git a/src/forms-opportunities/utils.js b/src/forms-opportunities/utils.js index 0fc1c9456..5c98d5cf3 100644 --- a/src/forms-opportunities/utils.js +++ b/src/forms-opportunities/utils.js @@ -196,6 +196,7 @@ function convertToLowConversionOpptyData(metricObject) { async function convertToOpportunityData(opportunityType, metricObject, context) { const { url, pageview: { total: pageViews }, formview: { total: formViews }, + formsource = '', } = metricObject; const { @@ -221,6 +222,7 @@ async function convertToOpportunityData(opportunityType, metricObject, context) opportunityData = { ...opportunityData, form: url, + formsource, formViews, pageViews, screenshot, @@ -268,7 +270,7 @@ export function shouldExcludeForm(scrapedFormData) { export function filterForms(formOpportunities, scrapedData, log, excludeUrls = new Set()) { return formOpportunities.filter((opportunity) => { let urlMatches = false; - if (opportunity.form.includes('search') || excludeUrls.has(opportunity.form)) { + if (opportunity.form.includes('search') || excludeUrls.has(opportunity.form + opportunity.formsource)) { return false; // exclude search pages } if (isNonEmptyArray(scrapedData?.formData)) { diff --git a/test/audits/forms/formcalc.test.js b/test/audits/forms/formcalc.test.js index e5f0e5f04..a91fc27c2 100644 --- a/test/audits/forms/formcalc.test.js +++ b/test/audits/forms/formcalc.test.js @@ -31,6 +31,7 @@ describe('Form Calc functions', () => { pageview: { total: 8670, desktop: 4670, mobile: 4000 }, url: 'https://www.surest.com/info/win', trafficacquisition: {}, + formsource: '.myform', }, { formengagement: { total: 300, desktop: 0, mobile: 300 }, @@ -39,6 +40,7 @@ describe('Form Calc functions', () => { pageview: { total: 8670, desktop: 4670, mobile: 4000 }, url: 'https://www.surest.com/newsletter', trafficacquisition: {}, + formsource: '', }, ]); }); @@ -53,6 +55,7 @@ describe('Form Calc functions', () => { formview: { total: 300, desktop: 0, mobile: 300 }, pageview: { total: 8670, desktop: 4670, mobile: 4000 }, trafficacquisition: {}, + formsource: '.myform', }, { url: 'https://www.surest.com/newsletter', @@ -61,6 +64,7 @@ describe('Form Calc functions', () => { formview: { total: 300, desktop: 0, mobile: 300 }, pageview: { total: 8670, desktop: 4670, mobile: 4000 }, trafficacquisition: {}, + formsource: '', }, ]); }); @@ -74,6 +78,7 @@ describe('Form Calc functions', () => { formview: { total: 300, desktop: 0, mobile: 300 }, formengagement: { total: 300, desktop: 0, mobile: 300 }, formsubmit: { total: 0, desktop: 0, mobile: 0 }, + formsource: '', trafficacquisition: {}, CTA: { url: 'https://www.surest.com/about-us', diff --git a/test/audits/forms/low-conv-oppoty-handler.test.js b/test/audits/forms/low-conv-oppoty-handler.test.js index 33765b8d4..b4a3c8331 100644 --- a/test/audits/forms/low-conv-oppoty-handler.test.js +++ b/test/audits/forms/low-conv-oppoty-handler.test.js @@ -189,10 +189,10 @@ describe('createLowConversionOpportunities handler method', () => { it('should not create low conversion opportunity if another opportunity already exists', async () => { const excludeUrls = new Set(); excludeUrls.add('https://www.surest.com/newsletter'); - excludeUrls.add('https://www.surest.com/info/win-1'); + excludeUrls.add('https://www.surest.com/info/win-1.form'); await createLowConversionOpportunities(auditUrl, auditData, undefined, context, excludeUrls); expect(dataAccessStub.Opportunity.create).to.be.callCount(3); - expect(excludeUrls.has('https://www.surest.com/contact-us')).to.be.true; + expect(excludeUrls.has('https://www.surest.com/contact-us.mycontact')).to.be.true; expect(excludeUrls.has('https://www.surest.com/info/win-2')).to.be.true; expect(excludeUrls.has('https://www.surest.com/info/win')).to.be.true; }); diff --git a/test/audits/forms/low-nav-oppoty-handler.test.js b/test/audits/forms/low-nav-oppoty-handler.test.js index fabb31056..58760a58b 100644 --- a/test/audits/forms/low-nav-oppoty-handler.test.js +++ b/test/audits/forms/low-nav-oppoty-handler.test.js @@ -90,6 +90,7 @@ describe('createLowNavigationOpportunities handler method', () => { trackedFormKPIValue: 300, formViews: 300, pageViews: 8670, + formsource: '', samples: 8670, scrapedStatus: false, metrics: [ diff --git a/test/audits/forms/low-views-oppoty-handler.test.js b/test/audits/forms/low-views-oppoty-handler.test.js index 618356854..08fcbfff8 100644 --- a/test/audits/forms/low-views-oppoty-handler.test.js +++ b/test/audits/forms/low-views-oppoty-handler.test.js @@ -90,6 +90,7 @@ describe('createLowFormViewsOpportunities handler method', () => { trackedFormKPIValue: 200, formViews: 200, pageViews: 6690, + formsource: '', samples: 6690, scrapedStatus: false, metrics: [ diff --git a/test/fixtures/forms/formcalcaudit.js b/test/fixtures/forms/formcalcaudit.js index 6e95a52e5..d97349e68 100644 --- a/test/fixtures/forms/formcalcaudit.js +++ b/test/fixtures/forms/formcalcaudit.js @@ -38,6 +38,7 @@ export const formVitalsCollection = [ 'desktop:windows': 4670, 'mobile:ios': 4000, }, + formsource: '.myform', }, { url: 'https://www.surest.com/newsletter', diff --git a/test/fixtures/forms/high-form-views-low-conversions.js b/test/fixtures/forms/high-form-views-low-conversions.js index 9992e738f..edc690ac9 100644 --- a/test/fixtures/forms/high-form-views-low-conversions.js +++ b/test/fixtures/forms/high-form-views-low-conversions.js @@ -418,6 +418,7 @@ const testData = { }, { url: 'https://www.surest.com/contact-us', + formsource: '.mycontact', formsubmit: { 'desktop:windows': 100, }, @@ -454,6 +455,7 @@ const testData = { }, { url: 'https://www.surest.com/info/win-1', + formsource: '.form', formsubmit: { 'desktop:windows': 100, }, @@ -819,6 +821,7 @@ const testData = { screenshot: '', formViews: 3000, pageViews: 8670, + formsource: '', samples: 8670, scrapedStatus: false, }, @@ -842,6 +845,7 @@ const testData = { trackedFormKPIValue: 0.014947683109118086, formViews: 6690, pageViews: 6690, + formsource: '', samples: 6690, scrapedStatus: false, metrics: [ @@ -940,6 +944,7 @@ const testData = { trackedFormKPIValue: 0.014947683109118086, formViews: 6690, pageViews: 6690, + formsource: '', samples: 6690, scrapedStatus: false, metrics: [ @@ -1047,6 +1052,7 @@ const testData = { trackedFormKPIValue: 0.014947683109118086, formViews: 6690, pageViews: 6690, + formsource: '', samples: 6690, scrapedStatus: true, metrics: [ @@ -1228,6 +1234,7 @@ const testData = { form: 'https://www.surest.com/contact-us', formViews: 3000, pageViews: 7690, + formsource: '', screenshot: '', samples: 7690, scrapedStatus: false,