Skip to content

Commit 98a8293

Browse files
committed
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
1 parent 420d275 commit 98a8293

12 files changed

+429
-25
lines changed

src/forms-opportunities/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212
export const FORM_OPPORTUNITY_TYPES = {
1313
LOW_CONVERSION: 'high-form-views-low-conversions',
1414
LOW_NAVIGATION: 'high-page-views-low-form-nav',
15+
LOW_VIEWS: 'high-page-views-low-form-views',
1516
};

src/forms-opportunities/formcalc.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,11 @@ export function getHighPageViewsLowFormViewsMetrics(formVitalsCollection) {
116116
resultMap.forEach((metrics, url) => {
117117
const { total: pageViews } = metrics.pageview;
118118
const { total: formViews } = metrics.formview;
119-
const { total: formEngagement } = metrics.formengagement;
120119

121120
if (hasHighPageViews(pageViews) && hasLowFormViews(pageViews, formViews)) {
122121
urls.push({
123122
url,
124-
pageViews,
125-
formViews,
126-
formEngagement,
123+
...metrics,
127124
});
128125
}
129126
});

src/forms-opportunities/handler.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { generateOpptyData } from './utils.js';
1919
import { getScrapedDataForSiteId } from '../support/utils.js';
2020
import createLowConversionOpportunities from './oppty-handlers/low-conversion-handler.js';
2121
import createLowNavigationOpportunities from './oppty-handlers/low-navigation-handler.js';
22+
import createLowViewsOpportunities from './oppty-handlers/low-views-handler.js';
2223

2324
const { AUDIT_STEP_DESTINATIONS } = Audit;
2425
const FORMS_OPPTY_QUERIES = [
@@ -100,8 +101,10 @@ export async function processOpportunityStep(context) {
100101
log.info(`[Form Opportunity] [Site Id: ${site.getId()}] processing opportunity`);
101102
const scrapedData = await getScrapedDataForSiteId(site, context);
102103
const latestAudit = await site.getLatestAuditByAuditType('forms-opportunities');
103-
await createLowConversionOpportunities(finalUrl, latestAudit, scrapedData, context);
104-
await createLowNavigationOpportunities(finalUrl, latestAudit, scrapedData, context);
104+
const excludeUrls = new Set();
105+
await createLowNavigationOpportunities(finalUrl, latestAudit, scrapedData, context, excludeUrls);
106+
await createLowViewsOpportunities(finalUrl, latestAudit, scrapedData, context, excludeUrls);
107+
await createLowConversionOpportunities(finalUrl, latestAudit, scrapedData, context, excludeUrls);
105108
log.info(`[Form Opportunity] [Site Id: ${site.getId()}] opportunity identified`);
106109
return {
107110
status: 'complete',

src/forms-opportunities/oppty-handlers/low-conversion-handler.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,10 @@ function generateDefaultGuidance(scrapedData, oppoty) {
8282
* @param auditUrl - The URL of the audit
8383
* @param auditData - The audit data containing the audit result and additional details.
8484
* @param context - The context object containing the data access and logger objects.
85+
* @param excludeUrls - A set of URLs to exclude from the opportunity creation process.
8586
*/
8687
// eslint-disable-next-line max-len
87-
export default async function createLowConversionOpportunities(auditUrl, auditDataObject, scrapedData, context) {
88+
export default async function createLowConversionOpportunities(auditUrl, auditDataObject, scrapedData, context, excludeUrls = new Set()) {
8889
const {
8990
dataAccess, log, sqs, site, env,
9091
} = context;
@@ -107,9 +108,9 @@ export default async function createLowConversionOpportunities(auditUrl, auditDa
107108
// eslint-disable-next-line max-len
108109
const formOpportunities = await generateOpptyData(formVitals, context, [FORM_OPPORTUNITY_TYPES.LOW_CONVERSION]);
109110
log.debug(`forms opportunities ${JSON.stringify(formOpportunities, null, 2)}`);
110-
const filteredOpportunities = filterForms(formOpportunities, scrapedData, log);
111+
const filteredOpportunities = filterForms(formOpportunities, scrapedData, log, excludeUrls);
112+
filteredOpportunities.forEach((oppty) => excludeUrls.add(oppty.form));
111113
log.info(`filtered opportunties high form views low conversion for form ${JSON.stringify(filteredOpportunities, null, 2)}`);
112-
113114
try {
114115
for (const opptyData of filteredOpportunities) {
115116
let highFormViewsLowConversionsOppty = opportunities.find(

src/forms-opportunities/oppty-handlers/low-navigation-handler.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ const formPathSegments = ['contact', 'newsletter', 'sign', 'enrol', 'subscribe',
1818
* @param auditUrl - The URL of the audit
1919
* @param auditData - The audit data containing the audit result and additional details.
2020
* @param context - The context object containing the data access and logger objects.
21+
* @param excludeUrls - A set of URLs to exclude from the opportunity creation process.
2122
*/
2223
// eslint-disable-next-line max-len
23-
export default async function createLowNavigationOpportunities(auditUrl, auditDataObject, scrapedData, context) {
24+
export default async function createLowNavigationOpportunities(auditUrl, auditDataObject, scrapedData, context, excludeUrls = new Set()) {
2425
const { dataAccess, log } = context;
2526
const { Opportunity } = dataAccess;
2627

@@ -47,14 +48,19 @@ export default async function createLowNavigationOpportunities(auditUrl, auditDa
4748
const filteredOpportunitiesByNavigation = formOpportunities.filter((opportunity) => formPathSegments.some((substring) => opportunity.form?.includes(substring)
4849
&& !opportunity.formNavigation?.url?.includes('search')));
4950

50-
const filteredOpportunities = filterForms(filteredOpportunitiesByNavigation, scrapedData, log);
51+
const filteredOpportunities = filterForms(
52+
filteredOpportunitiesByNavigation,
53+
scrapedData,
54+
log,
55+
excludeUrls,
56+
);
57+
filteredOpportunities.forEach((oppty) => excludeUrls.add(oppty.form));
5158
log.info(`filtered opportunities: high-page-views-low-form-navigations: ${JSON.stringify(filteredOpportunities, null, 2)}`);
52-
5359
try {
5460
for (const opptyData of filteredOpportunities) {
5561
let highPageViewsLowFormNavOppty = opportunities.find(
5662
(oppty) => oppty.getType() === FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION
57-
&& oppty.getData().form === opptyData.form,
63+
&& oppty.getData().form === opptyData.form,
5864
);
5965

6066
const opportunityData = {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2025 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import { FORM_OPPORTUNITY_TYPES } from '../constants.js';
14+
import { filterForms, generateOpptyData } from '../utils.js';
15+
16+
/**
17+
* @param auditUrl - The URL of the audit
18+
* @param auditData - The audit data containing the audit result and additional details.
19+
* @param context - The context object containing the data access and logger objects.
20+
* @param excludeUrls - A set of URLs to exclude from the opportunity creation process.
21+
*/
22+
// eslint-disable-next-line max-len
23+
export default async function createLowViewsOpportunities(auditUrl, auditDataObject, scrapedData, context, excludeUrls = new Set()) {
24+
const { dataAccess, log } = context;
25+
const { Opportunity } = dataAccess;
26+
27+
const auditData = JSON.parse(JSON.stringify(auditDataObject));
28+
log.info(`Syncing high page views low form views opportunity for ${auditData.siteId}`);
29+
let opportunities;
30+
31+
try {
32+
opportunities = await Opportunity.allBySiteIdAndStatus(auditData.siteId, 'NEW');
33+
} catch (e) {
34+
log.error(`Fetching opportunities for siteId ${auditData.siteId} failed with error: ${e.message}`);
35+
throw new Error(`Failed to fetch opportunities for siteId ${auditData.siteId}: ${e.message}`);
36+
}
37+
38+
const { formVitals } = auditData.auditResult;
39+
// eslint-disable-next-line max-len
40+
const formOpportunities = await generateOpptyData(formVitals, context, [FORM_OPPORTUNITY_TYPES.LOW_VIEWS]);
41+
log.debug(`forms opportunities high-page-views-low-form-views: ${JSON.stringify(formOpportunities, null, 2)}`);
42+
43+
const filteredOpportunities = filterForms(formOpportunities, scrapedData, log, excludeUrls);
44+
filteredOpportunities.forEach((oppty) => excludeUrls.add(oppty.form));
45+
log.info(`filtered opportunities: high-page-views-low-form-views: ${JSON.stringify(filteredOpportunities, null, 2)}`);
46+
try {
47+
for (const opptyData of filteredOpportunities) {
48+
let highPageViewsLowFormViewsOptty = opportunities.find(
49+
(oppty) => oppty.getType() === FORM_OPPORTUNITY_TYPES.LOW_VIEWS
50+
&& oppty.getData().form === opptyData.form,
51+
);
52+
53+
const opportunityData = {
54+
siteId: auditData.siteId,
55+
auditId: auditData.auditId,
56+
runbook: 'https://adobe.sharepoint.com/:w:/s/AEM_Forms/EeYKNa4HQkRAleWXjC5YZbMBMhveB08F1yTTUQSrP97Eow?e=cZdsnA',
57+
type: FORM_OPPORTUNITY_TYPES.LOW_VIEWS,
58+
origin: 'AUTOMATION',
59+
title: 'The form has low views',
60+
description: 'The form has low views but the page containing the form has higher traffic',
61+
tags: ['Forms Conversion'],
62+
data: {
63+
...opptyData,
64+
},
65+
guidance: {
66+
recommendations: [
67+
{
68+
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.`,
69+
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.',
70+
type: 'guidance',
71+
rationale: 'Forms that are visible above the fold are more likely to be seen and interacted with by users.',
72+
},
73+
],
74+
},
75+
};
76+
77+
log.info(`Forms Opportunity created high page views low form views ${JSON.stringify(opportunityData, null, 2)}`);
78+
if (!highPageViewsLowFormViewsOptty) {
79+
// eslint-disable-next-line no-await-in-loop
80+
highPageViewsLowFormViewsOptty = await Opportunity.create(opportunityData);
81+
} else {
82+
highPageViewsLowFormViewsOptty.setAuditId(auditData.auditId);
83+
highPageViewsLowFormViewsOptty.setData({
84+
...highPageViewsLowFormViewsOptty.getData(),
85+
...opportunityData.data,
86+
});
87+
highPageViewsLowFormViewsOptty.setGuidance(opportunityData.guidance);
88+
// eslint-disable-next-line no-await-in-loop
89+
await highPageViewsLowFormViewsOptty.save();
90+
}
91+
}
92+
} catch (e) {
93+
log.error(`Creating Forms opportunity for high page views low form views for siteId ${auditData.siteId} failed with error: ${e.message}`, e);
94+
}
95+
log.info(`Successfully synced Opportunity for site: ${auditData.siteId} and high page views low form views audit type.`);
96+
}

src/forms-opportunities/utils.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { GetObjectCommand } from '@aws-sdk/client-s3';
1717
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
1818
import {
1919
getHighPageViewsLowFormCtrMetrics, getHighFormViewsLowConversionMetrics,
20+
getHighPageViewsLowFormViewsMetrics,
2021
} from './formcalc.js';
2122
import { FORM_OPPORTUNITY_TYPES } from './constants.js';
2223

@@ -90,10 +91,10 @@ function getFormMetrics(metricObject) {
9091
});
9192
}
9293

93-
function convertToLowNavOpptyData(metricObject) {
94+
function convertToLowViewOpptyData(metricObject) {
9495
const {
9596
formview: { total: formViews, mobile: formViewsMobile, desktop: formViewsDesktop },
96-
CTA, trafficacquisition,
97+
trafficacquisition,
9798
} = metricObject;
9899
return {
99100
trackedFormKPIName: 'Form Views',
@@ -131,10 +132,18 @@ function convertToLowNavOpptyData(metricObject) {
131132
},
132133
},
133134
],
134-
formNavigation: CTA,
135135
};
136136
}
137137

138+
function convertToLowNavOpptyData(metricObject) {
139+
const {
140+
CTA,
141+
} = metricObject;
142+
const opptyData = convertToLowViewOpptyData(metricObject);
143+
opptyData.formNavigation = CTA;
144+
return opptyData;
145+
}
146+
138147
function convertToLowConversionOpptyData(metricObject) {
139148
const { trafficacquisition } = metricObject;
140149
const deviceWiseMetrics = getFormMetrics(metricObject);
@@ -204,6 +213,8 @@ async function convertToOpportunityData(opportunityType, metricObject, context)
204213
opportunityData = convertToLowConversionOpptyData(metricObject);
205214
} else if (opportunityType === FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION) {
206215
opportunityData = convertToLowNavOpptyData(metricObject);
216+
} else if (opportunityType === FORM_OPPORTUNITY_TYPES.LOW_VIEWS) {
217+
opportunityData = convertToLowViewOpptyData(metricObject);
207218
}
208219

209220
const screenshot = await getPresignedUrl('screenshot-desktop-fullpage.png', context, url, site);
@@ -222,7 +233,8 @@ async function convertToOpportunityData(opportunityType, metricObject, context)
222233
export async function generateOpptyData(
223234
formVitals,
224235
context,
225-
opportunityTypes = [FORM_OPPORTUNITY_TYPES.LOW_CONVERSION, FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION],
236+
opportunityTypes = [FORM_OPPORTUNITY_TYPES.LOW_CONVERSION,
237+
FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION, FORM_OPPORTUNITY_TYPES.LOW_VIEWS],
226238
) {
227239
const formVitalsCollection = formVitals.filter(
228240
(row) => row.formengagement && row.formsubmit && row.formview,
@@ -231,6 +243,7 @@ export async function generateOpptyData(
231243
Object.entries({
232244
[FORM_OPPORTUNITY_TYPES.LOW_CONVERSION]: getHighFormViewsLowConversionMetrics,
233245
[FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION]: getHighPageViewsLowFormCtrMetrics,
246+
[FORM_OPPORTUNITY_TYPES.LOW_VIEWS]: getHighPageViewsLowFormViewsMetrics,
234247
})
235248
.filter(([opportunityType]) => opportunityTypes.includes(opportunityType))
236249
.flatMap(([opportunityType, metricsMethod]) => metricsMethod(formVitalsCollection)
@@ -249,12 +262,13 @@ export function shouldExcludeForm(scrapedFormData) {
249262
* @param formOpportunities
250263
* @param scrapedData
251264
* @param log
265+
* @param excludeUrls urls to exclude from opportunity creation
252266
* @returns {*}
253267
*/
254-
export function filterForms(formOpportunities, scrapedData, log) {
268+
export function filterForms(formOpportunities, scrapedData, log, excludeUrls = new Set()) {
255269
return formOpportunities.filter((opportunity) => {
256270
let urlMatches = false;
257-
if (opportunity.form.includes('search')) {
271+
if (opportunity.form.includes('search') || excludeUrls.has(opportunity.form)) {
258272
return false; // exclude search pages
259273
}
260274
if (isNonEmptyArray(scrapedData?.formData)) {

test/audits/forms/formcalc.test.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,19 @@ describe('Form Calc functions', () => {
4848
expect(result).to.eql([
4949
{
5050
url: 'https://www.surest.com/info/win',
51-
pageViews: 8670,
52-
formViews: 300,
53-
formEngagement: 4300,
51+
formengagement: { total: 4300, desktop: 4000, mobile: 300 },
52+
formsubmit: { total: 0, desktop: 0, mobile: 0 },
53+
formview: { total: 300, desktop: 0, mobile: 300 },
54+
pageview: { total: 8670, desktop: 4670, mobile: 4000 },
55+
trafficacquisition: {},
5456
},
5557
{
5658
url: 'https://www.surest.com/newsletter',
57-
pageViews: 8670,
58-
formViews: 300,
59-
formEngagement: 300,
59+
formengagement: { total: 300, desktop: 0, mobile: 300 },
60+
formsubmit: { total: 0, desktop: 0, mobile: 0 },
61+
formview: { total: 300, desktop: 0, mobile: 300 },
62+
pageview: { total: 8670, desktop: 4670, mobile: 4000 },
63+
trafficacquisition: {},
6064
},
6165
]);
6266
});

test/audits/forms/low-conv-oppoty-handler.test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,15 @@ describe('createLowConversionOpportunities handler method', () => {
185185
expect(dataAccessStub.Opportunity.create).to.be.calledWith(testData.opportunityData5);
186186
expect(logStub.info).to.be.calledWith('Successfully synced Opportunity for site: site-id and high-form-views-low-conversions audit type.');
187187
});
188+
189+
it('should not create low conversion opportunity if another opportunity already exists', async () => {
190+
const excludeUrls = new Set();
191+
excludeUrls.add('https://www.surest.com/newsletter');
192+
excludeUrls.add('https://www.surest.com/info/win-1');
193+
await createLowConversionOpportunities(auditUrl, auditData, undefined, context, excludeUrls);
194+
expect(dataAccessStub.Opportunity.create).to.be.callCount(3);
195+
expect(excludeUrls.has('https://www.surest.com/contact-us')).to.be.true;
196+
expect(excludeUrls.has('https://www.surest.com/info/win-2')).to.be.true;
197+
expect(excludeUrls.has('https://www.surest.com/info/win')).to.be.true;
198+
});
188199
});

test/audits/forms/low-nav-oppoty-handler.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,11 @@ describe('createLowNavigationOpportunities handler method', () => {
194194
expect(dataAccessStub.Opportunity.create).to.not.be.called;
195195
expect(logStub.info).to.be.calledWith('Successfully synced Opportunity for site: site-id and high page views low form nav audit type.');
196196
});
197+
198+
it('should not create low nav opportunity if another opportunity already exists', async () => {
199+
const excludeUrls = new Set();
200+
excludeUrls.add('https://www.surest.com/newsletter');
201+
await createLowNavigationOpportunities(auditUrl, auditData, undefined, context, excludeUrls);
202+
expect(dataAccessStub.Opportunity.create).to.not.be.called;
203+
});
197204
});

0 commit comments

Comments
 (0)