Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MWPW-159113 [Personalization] Parallelize personalization network requests to be as efficient as possible #2970

Open
wants to merge 15 commits into
base: stage
Choose a base branch
from
Open
74 changes: 53 additions & 21 deletions libs/features/personalization/personalization.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
/* eslint-disable no-console */

import { createTag, getConfig, loadLink, loadScript, localizeLink } from '../../utils/utils.js';
import { getEntitlementMap } from './entitlements.js';

const entitlementsPromise = import('./entitlements.js');

/* c8 ignore start */
const PHONE_SIZE = window.screen.width < 550 || window.screen.height < 550;
Expand Down Expand Up @@ -691,6 +692,7 @@ async function getPersonalizationVariant(manifestPath, variantNames = [], varian

const variantInfo = buildVariantInfo(variantNames);

const { getEntitlementMap } = await entitlementsPromise;
const entitlementKeys = Object.values(await getEntitlementMap());
const hasEntitlementTag = entitlementKeys.some((tag) => variantInfo.allNames.includes(tag));

Expand Down Expand Up @@ -1003,32 +1005,40 @@ export async function applyPers(manifests, postLCP = false) {
config.mep.martech = `|${pznVariants.join('--')}|${pznManifests.join('--')}`;
}

export const combineMepSources = async (persEnabled, promoEnabled, mepParam) => {
export const combineMepSources = async (persEnabled, promoEnabled, promoUtilsPromise, mepParam) => {
let persManifests = [];
let initialPersManifestsPromises = [];
let initialPersManifests = [];

if (persEnabled) {
persManifests = persEnabled.toLowerCase()
initialPersManifests = persEnabled.toLowerCase()
.split(/,|(\s+)|(\\n)/g)
.filter((path) => path?.trim())
.map((manifestPath) => ({ manifestPath }));
initialPersManifestsPromises = initialPersManifests.map(({ manifestPath }) => {
const normalizedURL = normalizePath(manifestPath);
return fetch(normalizedURL, { mode: 'same-origin' });
Comment on lines +1085 to +1086
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: inline normalizePath

});
}

if (promoEnabled) {
const { default: getPromoManifests } = await import('./promo-utils.js');
const { default: getPromoManifests } = await promoUtilsPromise;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could also move code from promo-utils.js directly here, i think it was not a good decision in the first place to have a separate file.
promo-utils.js contents seem small enough

persManifests = persManifests.concat(getPromoManifests(promoEnabled, PAGE_URL.searchParams));
}

if (mepParam && mepParam !== 'off') {
const persManifestPaths = persManifests.map((manifest) => {
const { manifestPath } = manifest;
if (manifestPath.startsWith('/')) return manifestPath;
try {
const url = new URL(manifestPath);
return url.pathname;
} catch (e) {
return manifestPath;
}
});
const persManifestPaths = persManifests
.concat(initialPersManifests)
.map((manifest) => {
const { manifestPath } = manifest;
if (manifestPath.startsWith('/')) return manifestPath;
try {
const url = new URL(manifestPath);
return url.pathname;
} catch (e) {
return manifestPath;
}
});

mepParam.split('---').forEach((manifestPair) => {
const manifestPath = manifestPair.trim().toLowerCase().split('--')[0];
Expand All @@ -1037,15 +1047,22 @@ export const combineMepSources = async (persEnabled, promoEnabled, mepParam) =>
}
});
}
return persManifests;
return initialPersManifestsPromises.concat(persManifests
.filter((m) => !('disabled' in m) || !m.disabled)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if i remember correct, we shouldn't filter out disabled manifests.
those are 'promotions' manifests that are schedule is in the future/past, but Content Authors need to see them in the Page mep preview interface. However we don't load the .json itself (at least this is how it used to work) unless the manifest is active.

if you are still able to see a disabled manifest in the page then we are good

.map((manifest) => {
const normalizedURL = normalizePath(manifest.manifestPath);
return fetch(normalizedURL, { mode: 'same-origin' });
}));
};

export async function init(enablements = {}) {
let manifests = [];
const {
mepParam, mepHighlight, mepButton, pzn, promo, target, postLCP,
} = enablements;
const promoUtilsPromise = promo ? import('./promo-utils.js') : null;
const config = getConfig();
let manifestPromises = [];
if (!postLCP) {
config.mep = {
handleFragmentCommand,
Expand All @@ -1058,14 +1075,29 @@ export async function init(enablements = {}) {
experiments: [],
geoPrefix: config.locale?.prefix.split('/')[1]?.toLowerCase() || 'en-us',
};
manifests = manifests.concat(await combineMepSources(pzn, promo, mepParam));
manifests?.forEach((manifest) => {
if (manifest.disabled) return;
const normalizedURL = normalizePath(manifest.manifestPath);
loadLink(normalizedURL, { as: 'fetch', crossorigin: 'anonymous', rel: 'preload' });
});

manifestPromises = manifestPromises
.concat(await combineMepSources(pzn, promo, promoUtilsPromise, mepParam));
}

manifestPromises = manifestPromises.map((mPromise) => mPromise.then(async (resp) => {
try {
if (!resp.ok) {
/* c8 ignore next 5 */
if (resp.status === 404) {
throw new Error('File not found');
}
throw new Error(`Invalid response: ${resp.status} ${resp.statusText}`);
}
const manifestData = await resp.json();
return { manifestData, manifestPath: resp.url };
} catch (e) {
/* c8 ignore next 3 */
console.log(`Error loading content: ${resp.url}`, e.message || e);
}
return null;
}));
manifests = (await Promise.all(manifestPromises)).filter(Boolean);
if (target === true || (target === 'gnav' && postLCP)) {
const { getTargetPersonalization } = await import('../../martech/martech.js');
const { targetManifests, targetPropositions } = await getTargetPersonalization();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const getFetchPromise = (data, type = 'json') => new Promise((resolve) => {
resolve({
ok: true,
[type]: () => data,
url: 'path/to/manifest.json',
});
});

Expand Down
24 changes: 15 additions & 9 deletions test/features/personalization/personalization.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ document.body.innerHTML = await readFile({ path: './mocks/personalization.html'
const getFetchPromise = (data, type = 'json') => new Promise((resolve) => {
resolve({
ok: true,
url: '/path/to/manifest',
[type]: () => data,
});
});
Expand All @@ -39,6 +40,7 @@ describe('Functional Test', () => {
'11111111-aaaa-bbbb-6666-cccccccccccc': 'my-special-app',
'22222222-xxxx-bbbb-7777-cccccccccccc': 'fireflies',
};
stub(URLSearchParams.prototype, 'get').returns([{ instant: '2023-02-11' }]);
});

it('Invalid selector should not fail page render and rest of items', async () => {
Expand Down Expand Up @@ -386,27 +388,31 @@ describe('MEP Utils', () => {
});
it('combines promos and personalization', async () => {
document.head.innerHTML = await readFile({ path: '../../utils/mocks/mep/head-promo.html' });
// const searchParams = new URLSearchParams(window.location.search);
// searchParams.set('foo', 'bar');
// window.location.search = searchParams.toString();
const promos = { manifestnames: 'pre-black-friday-global,black-friday-global' };
const manifests = await combineMepSources('/pers/manifest.json', promos, undefined);
const promoUtilsPromise = import('../../../libs/features/personalization/promo-utils.js');
const manifestPromises = await combineMepSources(
'/pers/manifest.json',
promos,
promoUtilsPromise,
undefined,
);
const manifests = await Promise.all(manifestPromises);
expect(manifests.length).to.equal(3);
expect(manifests[0].manifestPath).to.equal('/pers/manifest.json');
expect(manifests[1].manifestPath).to.equal('/pre-black-friday.json');
expect(manifests[2].manifestPath).to.equal('/black-friday.json');
});
it('combines promos and personalization and mep param', async () => {
document.head.innerHTML = await readFile({ path: '../../utils/mocks/mep/head-promo.html' });
const promos = { manifestnames: 'pre-black-friday-global,black-friday-global' };
const promoUtilsPromise = import('../../../libs/features/personalization/promo-utils.js');
const manifests = await combineMepSources(
'/pers/manifest.json',
promos,
promoUtilsPromise,
'/pers/manifest.json--var1---/mep-param/manifest1.json--all---/mep-param/manifest2.json--all',
);
expect(manifests.length).to.equal(5);
expect(manifests[0].manifestPath).to.equal('/pers/manifest.json');
expect(manifests[1].manifestPath).to.equal('/pre-black-friday.json');
expect(manifests[2].manifestPath).to.equal('/black-friday.json');
expect(manifests[3].manifestPath).to.equal('/mep-param/manifest1.json');
expect(manifests[4].manifestPath).to.equal('/mep-param/manifest2.json');
});
});
});
25 changes: 16 additions & 9 deletions test/utils/utils-mep.test.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,42 @@
import { readFile } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
import { stub, restore } from 'sinon';
import { getMepEnablement } from '../../libs/utils/utils.js';
import { combineMepSources } from '../../libs/features/personalization/personalization.js';

describe('MEP Utils', () => {
describe('combineMepSources', async () => {
before(() => {
stub(URLSearchParams.prototype, 'get').returns([{ instant: '2023-02-11' }]);
});
after(() => {
restore();
});
it('yields an empty list when everything is undefined', async () => {
const manifests = await combineMepSources(undefined, undefined, undefined);
expect(manifests.length).to.equal(0);
});
it('combines promos and personalization', async () => {
document.head.innerHTML = await readFile({ path: './mocks/mep/head-promo.html' });
const manifests = await combineMepSources('/pers/manifest.json', { manifestnames: 'pre-black-friday-global,black-friday-global' }, undefined);
const promoUtilsPromise = import('../../libs/features/personalization/promo-utils.js');
const manifests = await combineMepSources(
'/pers/manifest.json',
{ manifestnames: 'pre-black-friday-global,black-friday-global' },
promoUtilsPromise,
undefined,
);
expect(manifests.length).to.equal(3);
expect(manifests[0].manifestPath).to.equal('/pers/manifest.json');
expect(manifests[1].manifestPath).to.equal('/pre-black-friday.json');
expect(manifests[2].manifestPath).to.equal('/black-friday.json');
});
it('combines promos and personalization and mep param', async () => {
document.head.innerHTML = await readFile({ path: './mocks/mep/head-promo.html' });
const promoUtilsPromise = import('../../libs/features/personalization/promo-utils.js');
const manifests = await combineMepSources(
'/pers/manifest.json',
{ manifestnames: 'pre-black-friday-global,black-friday-global' },
promoUtilsPromise,
'/pers/manifest.json--var1---/mep-param/manifest1.json--all---/mep-param/manifest2.json--all',
);
expect(manifests.length).to.equal(5);
expect(manifests[0].manifestPath).to.equal('/pers/manifest.json');
expect(manifests[1].manifestPath).to.equal('/pre-black-friday.json');
expect(manifests[2].manifestPath).to.equal('/black-friday.json');
expect(manifests[3].manifestPath).to.equal('/mep-param/manifest1.json');
expect(manifests[4].manifestPath).to.equal('/mep-param/manifest2.json');
});
});
describe('getMepEnablement', async () => {
Expand Down
6 changes: 5 additions & 1 deletion test/utils/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -766,9 +766,13 @@ describe('Utils', () => {
resolve({
ok: true,
json: () => MANIFEST_JSON,
url: 'https://main--milo--adobecom.hlx.page/products/special-offers-manifest.json',
});
});
}
before(() => {
sinon.stub(URLSearchParams.prototype, 'get').withArgs('instant').returns('2023-12-02');
});

it('should process personalization manifest and save in config', async () => {
window.fetch = sinon.stub().returns(htmlResponse());
Expand All @@ -777,7 +781,7 @@ describe('Utils', () => {
const resultConfig = utils.getConfig();
const resultExperiment = resultConfig.mep.experiments[0];
expect(resultConfig.mep.preview).to.be.true;
expect(resultConfig.mep.experiments.length).to.equal(3);
expect(resultConfig.mep.experiments.length).to.equal(1);
expect(resultExperiment.manifest).to.equal('https://main--milo--adobecom.hlx.page/products/special-offers-manifest.json');
});
});
Expand Down
Loading