Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/forms-opportunities/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export const ORIGINS = {
export const OPPTY_OPTIONS_ALL = 'all';
export const OPPORTUNITY_LIMIT = 2;

// Page view threshold constants
export const DAILY_PAGEVIEW_THRESHOLD_DEFAULT = 200;
export const DAILY_PAGEVIEW_THRESHOLD_ALL = 7;

export const successCriteriaLinks = {
111: {
name: 'Non-text Content',
Expand Down
59 changes: 46 additions & 13 deletions src/forms-opportunities/formcalc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
* governing permissions and limitations under the License.
*/

import {
OPPTY_OPTIONS_ALL,
DAILY_PAGEVIEW_THRESHOLD_DEFAULT,
DAILY_PAGEVIEW_THRESHOLD_ALL,
} from './constants.js';

export const FORMS_AUDIT_INTERVAL = 15;
const DAILY_PAGEVIEW_THRESHOLD = 200;
const CR_THRESHOLD_RATIO = 0.3;
const MOBILE = 'mobile';
const DESKTOP = 'desktop';
Expand Down Expand Up @@ -66,16 +71,33 @@ function aggregateFormVitalsByDevice(formVitalsCollection) {
return resultMap;
}

function hasHighViews(views) {
return views > DAILY_PAGEVIEW_THRESHOLD * FORMS_AUDIT_INTERVAL;
/**
* Get the daily pageview threshold based on opportunity options
* @param {string} opptyOptions - opportunity options ('all' for lower threshold)
* @returns {number} - daily pageview threshold
*/
function getPageviewThreshold(opptyOptions) {
return opptyOptions === OPPTY_OPTIONS_ALL
? DAILY_PAGEVIEW_THRESHOLD_ALL
: DAILY_PAGEVIEW_THRESHOLD_DEFAULT;
}

function hasLowerConversionRate(formSubmit, formViews, formEngagement) {
return (formViews > 0 && formSubmit / formViews < CR_THRESHOLD_RATIO) && formEngagement > 0;
function hasHighViews(views, opptyOptions = null) {
const threshold = getPageviewThreshold(opptyOptions);
return views > threshold * FORMS_AUDIT_INTERVAL;
}

function hasLowerConversionRate(formSubmit, formViews, formEngagement, opptyOptions = null) {
if (opptyOptions === OPPTY_OPTIONS_ALL) {
return (formViews > 0 && formSubmit / formViews < CR_THRESHOLD_RATIO);
} else {
return (formViews > 0 && formSubmit / formViews < CR_THRESHOLD_RATIO) && formEngagement > 0;
}
}

function hasLowFormViews(pageViews, formViews, formEngagement) {
if (formViews < DAILY_PAGEVIEW_THRESHOLD * FORMS_AUDIT_INTERVAL) {
function hasLowFormViews(pageViews, formViews, formEngagement, opptyOptions = null) {
const threshold = getPageviewThreshold(opptyOptions);
if (formViews < threshold * FORMS_AUDIT_INTERVAL) {
// If form views are less than this threshold, then we can anyways
// cannot proceed to detecting low form conversion opportunity as
// experimentation wont be possible without increasing views.
Expand All @@ -97,9 +119,10 @@ function hasHighPageViewLowFormCtr(ctaPageViews, ctaClicks, ctaPageTotalClicks,
* Returns the form urls with high form views and low conversion rate
*
* @param {*} formVitalsCollection - form vitals collection
* @param {string} opptyOptions - opportunity options ('all' for lower threshold)
* @returns {Array} - urls with high form views and low conversion rate
*/
export function getHighFormViewsLowConversionMetrics(formVitalsCollection) {
export function getHighFormViewsLowConversionMetrics(formVitalsCollection, opptyOptions = null) {
const resultMap = aggregateFormVitalsByDevice(formVitalsCollection);
const urls = [];
resultMap.forEach((metrics, url) => {
Expand All @@ -108,7 +131,7 @@ export function getHighFormViewsLowConversionMetrics(formVitalsCollection) {
const formEngagement = metrics.formengagement.total;

// eslint-disable-next-line max-len
if (hasHighViews(formViews) && hasLowerConversionRate(formSubmit, formViews, formEngagement)) {
if (hasHighViews(formViews, opptyOptions) && hasLowerConversionRate(formSubmit, formViews, formEngagement, opptyOptions)) {
urls.push({
url,
...metrics,
Expand All @@ -122,17 +145,22 @@ export function getHighFormViewsLowConversionMetrics(formVitalsCollection) {
* Returns the form urls with high page views and low form views
*
* @param resultMap
* @param {string} opptyOptions - opportunity options ('all' for lower threshold)
* @returns {*[]}
*/
export function getHighPageViewsLowFormViewsMetrics(formVitalsCollection) {
export function getHighPageViewsLowFormViewsMetrics(
formVitalsCollection,
opptyOptions = null,
) {
const urls = [];
const resultMap = aggregateFormVitalsByDevice(formVitalsCollection);
resultMap.forEach((metrics, url) => {
const { total: pageViews } = metrics.pageview;
const { total: formViews } = metrics.formview;
const { total: formEngagement } = metrics.formengagement;

if (hasHighViews(pageViews) && hasLowFormViews(pageViews, formViews, formEngagement)) {
if (hasHighViews(pageViews, opptyOptions)
&& hasLowFormViews(pageViews, formViews, formEngagement, opptyOptions)) {
urls.push({
url,
...metrics,
Expand All @@ -146,9 +174,13 @@ export function getHighPageViewsLowFormViewsMetrics(formVitalsCollection) {
* Returns the form urls with high page views containing ctr and low form views
* @param formVitalsCollection
* @param formVitalsByDevice
* @param {string} opptyOptions - opportunity options ('all' for lower threshold)
* @returns {*[]}
*/
export function getHighPageViewsLowFormCtrMetrics(formVitalsCollection) {
export function getHighPageViewsLowFormCtrMetrics(
formVitalsCollection,
opptyOptions = null,
) {
const urls = [];
const formVitalsByDevice = aggregateFormVitalsByDevice(formVitalsCollection);
formVitalsCollection.forEach((entry) => {
Expand Down Expand Up @@ -185,7 +217,8 @@ export function getHighPageViewsLowFormCtrMetrics(formVitalsCollection) {
const f = Object.values(pageview).reduce((sum, val) => sum + val, 0);

// Evaluate conditions and add URL to the result if all are met
if (hasHighViews(x) && hasHighPageViewLowFormCtr(x, y.clicks, z, f)) {
if (hasHighViews(x, opptyOptions)
&& hasHighPageViewLowFormCtr(x, y.clicks, z, f)) {
const deviceData = formVitalsByDevice.get(entry.url);
if (deviceData != null) {
urls.push({
Expand Down
2 changes: 1 addition & 1 deletion src/forms-opportunities/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export async function runAuditAndSendUrlsForScrapingStep(context) {
}

// generating opportunity data from audit to be send to scraper
const formOpportunities = await generateOpptyData(formVitals, context);
const formOpportunities = await generateOpptyData(formVitals, context, undefined, data);
const uniqueUrls = new Set();
for (const opportunity of formOpportunities) {
uniqueUrls.add(opportunity.form);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export default async function createLowConversionOpportunities(auditUrl, auditDa
const { formVitals } = auditData.auditResult;
log.debug(`[Form Opportunity] [Site Id: ${auditData.siteId}] scraped data for form ${JSON.stringify(scrapedData, null, 2)}`);
// eslint-disable-next-line max-len
const formOpportunities = await generateOpptyData(formVitals, context, [FORM_OPPORTUNITY_TYPES.LOW_CONVERSION]);
const formOpportunities = await generateOpptyData(formVitals, context, [FORM_OPPORTUNITY_TYPES.LOW_CONVERSION], opptyOptions);
log.debug(`[Form Opportunity] [Site Id: ${auditData.siteId}] forms opportunities ${JSON.stringify(formOpportunities, null, 2)}`);
let filteredOpportunities = filterForms(formOpportunities, scrapedData, log, excludeForms);
filteredOpportunities.forEach((oppty) => excludeForms.add(oppty.form + oppty.formsource));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default async function createLowNavigationOpportunities(auditUrl, auditDa

const { formVitals } = auditData.auditResult;
// eslint-disable-next-line max-len
const formOpportunities = await generateOpptyData(formVitals, context, [FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION]);
const formOpportunities = await generateOpptyData(formVitals, context, [FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION], opptyOptions);
log.debug(`[Form Opportunity] [Site Id: ${auditData.siteId}] forms opportunities high-page-views-low-form-navigations: ${JSON.stringify(formOpportunities, null, 2)}`);

// for opportunity type high page views low form navigation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default async function createLowViewsOpportunities(auditUrl, auditDataObj

const { formVitals } = auditData.auditResult;
// eslint-disable-next-line max-len
const formOpportunities = await generateOpptyData(formVitals, context, [FORM_OPPORTUNITY_TYPES.LOW_VIEWS]);
const formOpportunities = await generateOpptyData(formVitals, context, [FORM_OPPORTUNITY_TYPES.LOW_VIEWS], opptyOptions);
let filteredOpportunities = filterForms(formOpportunities, scrapedData, log, excludeForms);
filteredOpportunities.forEach((oppty) => excludeForms.add(oppty.form + oppty.formsource));
log.debug(`[Form Opportunity] [Site Id: ${auditData.siteId}] opptyOptions value: ${JSON.stringify(opptyOptions)}`);
Expand Down
12 changes: 10 additions & 2 deletions src/forms-opportunities/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import {
getHighPageViewsLowFormCtrMetrics, getHighFormViewsLowConversionMetrics,
getHighPageViewsLowFormViewsMetrics,
} from './formcalc.js';
import { FORM_OPPORTUNITY_TYPES, successCriteriaLinks } from './constants.js';
import {
FORM_OPPORTUNITY_TYPES,
successCriteriaLinks,
} from './constants.js';
import { calculateCPCValue } from '../support/utils.js';
import { getPresignedUrl as getPresignedUrlUtil } from '../utils/getPresignedUrl.js';
import { isAuditEnabledForSite } from '../common/audit-utils.js';
Expand Down Expand Up @@ -266,18 +269,23 @@ export async function generateOpptyData(
context,
opportunityTypes = [FORM_OPPORTUNITY_TYPES.LOW_CONVERSION,
FORM_OPPORTUNITY_TYPES.LOW_NAVIGATION, FORM_OPPORTUNITY_TYPES.LOW_VIEWS],
opptyOptions = null,
) {
const formVitalsCollection = formVitals.filter(
(row) => row.formengagement && row.formsubmit && row.formview,
);

return Promise.all(
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)
.flatMap(([opportunityType, metricsMethod]) => metricsMethod(
formVitalsCollection,
opptyOptions,
)
.map((metric) => convertToOpportunityData(opportunityType, metric, context))),
);
}
Expand Down
81 changes: 81 additions & 0 deletions test/audits/forms/formcalc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,87 @@ describe('Form Calc functions', () => {
]);
});

it('getHighFormViewsLowConversion with all options', () => {
const result = getHighFormViewsLowConversionMetrics(formVitalsCollection, 'all');
expect(result).to.eql([
{
"url": "https://www.surest.com/info/win",
"formview": {
"total": 300,
"desktop": 0,
"mobile": 300
},
"formengagement": {
"total": 4300,
"desktop": 4000,
"mobile": 300
},
"pageview": {
"total": 8670,
"desktop": 4670,
"mobile": 4000
},
"formsubmit": {
"total": 0,
"desktop": 0,
"mobile": 0
},
"trafficacquisition": {},
"formsource": ".myform"
},
{
"url": "https://www.surest.com/info/win-2",
"formview": {
"total": 3200,
"desktop": 0,
"mobile": 3200
},
"formengagement": {
"total": 4300,
"desktop": 4000,
"mobile": 300
},
"pageview": {
"total": 12670,
"desktop": 4670,
"mobile": 8000
},
"formsubmit": {
"total": 0,
"desktop": 0,
"mobile": 0
},
"trafficacquisition": {},
"formsource": ".myform"
},
{
"url": "https://www.surest.com/newsletter",
"formview": {
"total": 300,
"desktop": 0,
"mobile": 300
},
"formengagement": {
"total": 300,
"desktop": 0,
"mobile": 300
},
"pageview": {
"total": 8670,
"desktop": 4670,
"mobile": 4000
},
"formsubmit": {
"total": 0,
"desktop": 0,
"mobile": 0
},
"trafficacquisition": {},
"formsource": ""
}
]);
});

it('getHighPageViewsLowFormViews', () => {
const result = getHighPageViewsLowFormViewsMetrics(formVitalsCollection);
expect(result).to.eql([
Expand Down