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-131211 Personalization Entitlement Support #1104

Merged
merged 12 commits into from
Aug 17, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const getQueryParameters = (params) => {

const emptyEntitlements = () => ({
clouds: {},
arrangment_codes: {},
arrangement_codes: {},
fulfilled_codes: {},
offer_families: {},
offers: {},
Expand All @@ -47,7 +47,7 @@ const mapSubscriptionCodes = (allOffers) => {

const {
clouds,
arrangment_codes,
arrangement_codes,
fulfilled_codes,
offer_families,
offers,
Expand All @@ -66,7 +66,7 @@ const mapSubscriptionCodes = (allOffers) => {
}

if (offer.product_arrangement_code) {
arrangment_codes[offer.product_arrangement_code] = true;
arrangement_codes[offer.product_arrangement_code] = true;
}

if (Array.isArray(fulfilled_items)) {
Expand All @@ -86,7 +86,7 @@ const mapSubscriptionCodes = (allOffers) => {

return {
clouds,
arrangment_codes,
arrangement_codes,
fulfilled_codes,
offer_families,
offers,
Expand Down
174 changes: 140 additions & 34 deletions libs/features/personalization/personalization.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
/* eslint-disable no-console */
import { createTag, getConfig, loadLink, loadScript, updateConfig } from '../../utils/utils.js';
import {
createTag, getConfig, loadIms, loadLink, loadScript, updateConfig,
} from '../../utils/utils.js';

const CLASS_EL_DELETE = 'p13n-deleted';
const CLASS_EL_REPLACE = 'p13n-replaced';
const LS_ENT_KEY = 'milo:entitlements';
const LS_ENT_EXPIRE_KEY = 'milo:entitlements:expire';
const ENT_CACHE_EXPIRE = 1000 * 60 * 60 * 3; // 3 hours
const ENT_CACHE_REFRESH = 1000 * 60 * 3; // 3 minutes
const PAGE_URL = new URL(window.location.href);

/* c8 ignore start */
export const PERSONALIZATION_TAGS = {
all: () => true,
chrome: () => navigator.userAgent.includes('Chrome') && !navigator.userAgent.includes('Mobile'),
firefox: () => navigator.userAgent.includes('Firefox') && !navigator.userAgent.includes('Mobile'),
android: () => navigator.userAgent.includes('Android'),
Expand All @@ -16,10 +23,18 @@ export const PERSONALIZATION_TAGS = {
darkmode: () => window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches,
lightmode: () => !PERSONALIZATION_TAGS.darkmode(),
};

export const ENTITLEMENT_TAGS = {
photoshop: (ents) => ents.photoshop_cc,
lightroom: (ents) => ents.lightroom_cc,
};
/* c8 ignore stop */

const personalizationKeys = Object.keys(PERSONALIZATION_TAGS);
const entitlementKeys = Object.keys(ENTITLEMENT_TAGS);

// Replace any non-alpha chars except comma, space and hyphen
const RE_KEY_REPLACE = /[^a-z0-9\- ,=]/g;
const RE_KEY_REPLACE = /[^a-z0-9\- _,=]/g;

const MANIFEST_KEYS = [
'action',
Expand All @@ -45,7 +60,8 @@ const createFrag = (url, manifestId) => {
const COMMANDS = {
insertcontentafter: (el, target, manifestId) => el
.insertAdjacentElement('afterend', createFrag(target, manifestId)),
insertcontentbefore: (el, target, manifestId) => el.insertAdjacentElement('beforebegin', createFrag(target, manifestId)),
insertcontentbefore: (el, target, manifestId) => el
.insertAdjacentElement('beforebegin', createFrag(target, manifestId)),
removecontent: (el, target, manifestId) => {
if (target === 'false') return;
if (manifestId) {
Expand Down Expand Up @@ -83,7 +99,7 @@ const fetchData = async (url, type = DATA_TYPE.JSON) => {
}
return await resp[type]();
} catch (e) {
/* c8 ignore next 2 */
/* c8 ignore next 3 */
console.log(`Error loading content: ${url}`, e.message || e);
}
return null;
Expand Down Expand Up @@ -117,7 +133,6 @@ function normalizePath(p) {
}
return path;
}
/* c8 ignore stop */

const matchGlob = (searchStr, inputStr) => {
const pattern = searchStr.replace(/\*\*/g, '.*');
Expand All @@ -135,17 +150,7 @@ export async function replaceInner(path, element) {
element.innerHTML = html;
return true;
}

const checkForParamMatch = (paramStr) => {
const [name, val] = paramStr.split('param-')[1].split('=');
if (!name) return false;
const searchParamVal = PAGE_URL.searchParams.get(name);
if (searchParamVal !== null) {
if (val) return val === searchParamVal;
return true; // if no val is set, just check for existence of param
}
return false;
};
/* c8 ignore stop */

const setMetadata = (metadata) => {
const { selector, val } = metadata;
Expand Down Expand Up @@ -185,7 +190,7 @@ function handleCommands(commands, manifestId, rootEl = document) {

COMMANDS[cmd.action](selectorEl, cmd.target, manifestId);
} else {
/* c8 ignore next */
/* c8 ignore next 2 */
console.log('Invalid command found: ', cmd);
}
});
Expand Down Expand Up @@ -251,6 +256,7 @@ export function parseConfig(data) {
return null;
}

/* c8 ignore start */
function parsePlaceholders(placeholders, config, selectedVariantName = '') {
if (!placeholders?.length || selectedVariantName === 'no changes') return config;
const valueNames = [
Expand All @@ -271,30 +277,128 @@ function parsePlaceholders(placeholders, config, selectedVariantName = '') {
return config;
}

function getPersonalizationVariant(manifestPath, variantNames = [], variantLabel = null) {
const fetchEntitlements = async () => {
const [{ default: getUserEntitlements }] = await Promise.all([
import('../../blocks/global-navigation/utilities/getUserEntitlements.js'),
Copy link
Contributor

Choose a reason for hiding this comment

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

There's one caveat with entitlements: the IMS client ID needs to be subscribed to JIL API. Maybe we should add some documentation to ensure all teams are aware of this step.

loadIms(),
]);
return getUserEntitlements();
};

const setEntLocalStorage = (ents) => {
localStorage.setItem(LS_ENT_KEY, JSON.stringify(ents));
localStorage.setItem(LS_ENT_EXPIRE_KEY, Date.now());
};

const loadEntsFromLocalStorage = () => {
const ents = localStorage.getItem(LS_ENT_KEY);
const expireDate = localStorage.getItem(LS_ENT_EXPIRE_KEY);
const now = Date.now();
if (!ents || !expireDate || (now - expireDate) > ENT_CACHE_EXPIRE) return null;
if ((now - expireDate) > ENT_CACHE_REFRESH) {
// refresh entitlements in background
setTimeout(() => {
fetchEntitlements().then((newEnts) => {
setEntLocalStorage(newEnts);
});
}, 5000);
}
return JSON.parse(ents);
};

const clearEntLocalStorage = () => {
localStorage.removeItem(LS_ENT_KEY);
localStorage.removeItem(LS_ENT_EXPIRE_KEY);
};

export const getEntitlements = (() => {
let ents;
return (async () => {
if (window.adobeIMS && !window.adobeIMS.isSignedInUser()) {
clearEntLocalStorage();
return {};
}
if (!ents) {
ents = loadEntsFromLocalStorage();
}
if (!ents) {
ents = await fetchEntitlements();
setEntLocalStorage(ents);
}
return ents;
});
})();

const getFlatEntitlements = async () => {
const ents = await getEntitlements();
return {
...ents.arrangement_codes,
...ents.clouds,
...ents.fulfilled_codes,
};
};

const checkForEntitlementMatch = (name, entitlements) => {
const entName = name.split('ent-')[1];
if (!entName) return false;
return entitlements[entName];
};
/* c8 ignore stop */

const checkForParamMatch = (paramStr) => {
const [name, val] = paramStr.split('param-')[1].split('=');
if (!name) return false;
const searchParamVal = PAGE_URL.searchParams.get(name);
if (searchParamVal !== null) {
if (val) return val === searchParamVal;
return true; // if no val is set, just check for existence of param
}
return false;
};

async function getPersonalizationVariant(manifestPath, variantNames = [], variantLabel = null) {
const config = getConfig();
let manifestFound = false;
if (config.mep?.override !== '') {
config.mep?.override.split(',').forEach((item) => {
let manifest;
/* c8 ignore start */
config.mep?.override.split(',').some((item) => {
const pair = item.trim().split('--');
if (pair[0] === manifestPath && pair.length > 1) {
// eslint-disable-next-line prefer-destructuring
manifestFound = pair[1];
[, manifest] = pair;
return true;
}
return false;
});
if (manifestFound) return manifestFound;
/* c8 ignore stop */
if (manifest) return manifest;
}

const tagNames = Object.keys(PERSONALIZATION_TAGS);
const matchingVariant = variantNames.find((variant) => {
// handle multiple variants that are space / comma delimited
const names = variant.split(',').map((v) => v.trim()).filter(Boolean);
return names.some((name) => {
if (name === variantLabel) return true;
if (name.startsWith('param-')) return checkForParamMatch(name);
return tagNames.includes(name) && PERSONALIZATION_TAGS[name]();
});
});
const variantInfo = variantNames.reduce((acc, name) => {
const vNames = name.split(',').map((v) => v.trim()).filter(Boolean);
acc[name] = vNames;
acc.allNames = [...acc.allNames, ...vNames];
return acc;
}, { allNames: [] });

const hasEntitlementPrefix = variantInfo.allNames.some((name) => name.startsWith('ent-'));
const hasEntitlementTag = entitlementKeys.some((tag) => variantInfo.allNames.includes(tag));

let entitlements = {};
if (hasEntitlementPrefix || hasEntitlementTag) {
entitlements = await getFlatEntitlements();
}

const matchVariant = (name) => {
if (name === variantLabel) return true;
if (name.startsWith('param-')) return checkForParamMatch(name);
if (name.startsWith('ent-')) return checkForEntitlementMatch(name, entitlements);
if (entitlementKeys.includes(name)) {
return ENTITLEMENT_TAGS[name](entitlements);
}
return personalizationKeys.includes(name) && PERSONALIZATION_TAGS[name]();
};

const matchingVariant = variantNames.find((variant) => variantInfo[variant].some(matchVariant));
return matchingVariant;
}

Expand All @@ -319,7 +423,7 @@ export async function getPersConfig(name, variantLabel, manifestData, manifestPa
return null;
}

const selectedVariantName = getPersonalizationVariant(
const selectedVariantName = await getPersonalizationVariant(
manifestPath,
config.variantNames,
variantLabel,
Expand Down Expand Up @@ -434,6 +538,8 @@ export async function applyPers(manifests) {
decoratePreviewCheck(config, []);
return;
}

getEntitlements();
const cleanedManifests = cleanManifestList(manifests);

let results = [];
Expand Down
10 changes: 6 additions & 4 deletions libs/martech/martech.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@ const getDtmLib = (env) => ({
url:
env.name === 'prod'
? env.consumer?.marTechUrl || 'https://assets.adobedtm.com/d4d114c60e50/a0e989131fd5/launch-5dd5dd2177e6.min.js'
// TODO: This is a custom launch script for milo-target - update before merging to main
: env.consumer?.marTechUrl || 'https://assets.adobedtm.com/d4d114c60e50/a0e989131fd5/launch-2c94beadc94f-development.min.js',
: env.consumer?.marTechUrl || 'https://assets.adobedtm.com/d4d114c60e50/a0e989131fd5/launch-a27b33fc2dc0-development.min.js',
});

export default async function init({ persEnabled = false, persManifests }) {
Expand Down Expand Up @@ -135,9 +134,12 @@ export default async function init({ persEnabled = false, persManifests }) {
if (persEnabled) {
const targetManifests = await getTargetPersonalization();
if (targetManifests || persManifests?.length) {
const { preloadManifests } = await import('../features/personalization/manifest-utils.js');
const [{ preloadManifests }, { applyPers, getEntitlements }] = await Promise.all([
import('../features/personalization/manifest-utils.js'),
import('../features/personalization/personalization.js'),
]);
getEntitlements();
const manifests = preloadManifests({ targetManifests, persManifests });
const { applyPers } = await import('../features/personalization/personalization.js');
await applyPers(manifests);
}
}
Expand Down
12 changes: 6 additions & 6 deletions libs/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,6 @@ const AUTO_BLOCKS = [
{ 'offer-preview': '/tools/commerce' },
];
const ENVS = {
local: {
name: 'local',
edgeConfigId: '8d2805dd-85bf-4748-82eb-f99fdad117a6',
pdfViewerClientId: '600a4521c23d4c7eb9c7b039bee534a0',
},
stage: {
name: 'stage',
ims: 'stg1',
Expand All @@ -113,6 +108,11 @@ const ENVS = {
pdfViewerClientId: '3c0a5ddf2cc04d3198d9e48efc390fa9',
},
};
ENVS.local = {
...ENVS.stage,
name: 'local',
};

const LANGSTORE = 'langstore';

const PAGE_URL = new URL(window.location.href);
Expand Down Expand Up @@ -752,7 +752,7 @@ async function checkForPageMods() {
if (targetEnabled) {
await loadMartech({ persEnabled: true, persManifests, targetMd });
} else if (persManifests.length) {
// load the personalization only
loadIms().catch(() => {});
const { preloadManifests } = await import('../features/personalization/manifest-utils.js');
const manifests = preloadManifests({ persManifests }, { getConfig, loadLink });

Expand Down
2 changes: 1 addition & 1 deletion test/blocks/global-navigation/mocks/subscriptionsAll.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const formatted = {
document_cloud: true,
experience_cloud: true,
},
arrangment_codes: {
arrangement_codes: {
dm_data_launch_ml: true,
dm_analytics_enterprise_ml: true,
dm_audience_manager_enterprise_ml: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { res, formatted } from '../mocks/subscriptionsAll.js';

const emptyEntitlements = () => ({
clouds: {},
arrangment_codes: {},
arrangement_codes: {},
fulfilled_codes: {},
offer_families: {},
offers: {},
Expand Down