From eb913913725cb127a869aa92b3dc9252494e4e95 Mon Sep 17 00:00:00 2001 From: Xinyi Feng Date: Mon, 11 Nov 2024 12:07:44 -0800 Subject: [PATCH 1/7] for syncing up the integration in aem cs --- src/index.js | 364 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 216 insertions(+), 148 deletions(-) diff --git a/src/index.js b/src/index.js index caf210f..ed92eda 100644 --- a/src/index.js +++ b/src/index.js @@ -10,8 +10,9 @@ * governing permissions and limitations under the License. */ + let isDebugEnabled; -export function setDebugMode(url, pluginOptions) { +function setDebugMode(url, pluginOptions) { const { host, hostname, origin } = url; const { isProd, prodHost } = pluginOptions; isDebugEnabled = (url.hostname === 'localhost' @@ -22,14 +23,14 @@ export function setDebugMode(url, pluginOptions) { return isDebugEnabled; } -export function debug(...args) { +function debug(...args) { if (isDebugEnabled) { // eslint-disable-next-line no-console console.debug.call(this, '[aem-experimentation]', ...args); } } -export const DEFAULT_OPTIONS = { +const DEFAULT_OPTIONS = { // Audiences related properties audiences: {}, @@ -53,7 +54,7 @@ export const DEFAULT_OPTIONS = { * @param {String|String[]} str The string to convert * @returns an array representing the converted string */ -export function stringToArray(str) { +function stringToArray(str) { if (Array.isArray(str)) { return str; } @@ -65,27 +66,12 @@ export function stringToArray(str) { * @param {String} name The unsanitized name * @returns {String} The class name */ -export function toClassName(name) { +function toClassName(name) { return typeof name === 'string' ? name.toLowerCase().replace(/[^0-9a-z]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') : ''; } -/** - * Triggers the callback when the page is actually activated, - * This is to properly handle speculative page prerendering and marketing events. - * @param {Function} cb The callback to run - */ -async function onPageActivation(cb) { - // Speculative prerender-aware execution. - // See: https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API#unsafe_prerendering - if (document.prerendering) { - document.addEventListener('prerenderingchange', cb, { once: true }); - } else { - cb(); - } -} - /** * Fires a Real User Monitoring (RUM) event based on the provided type and configuration. * @param {string} type - the type of event to be fired ("experiment", "campaign", or "audience") @@ -113,9 +99,7 @@ function fireRUM(type, config, pluginOptions, result) { const { source, target } = typeHandlers[type](); const rumType = type === 'experiment' ? 'experiment' : 'audience'; - onPageActivation(() => { - window.hlx?.rum?.sampleRUM(rumType, { source, target }); - }); + window.hlx?.rum?.sampleRUM(rumType, { source, target }); } /** @@ -123,7 +107,7 @@ function fireRUM(type, config, pluginOptions, result) { * @param {String} name The unsanitized name * @returns {String} The camelCased name */ -export function toCamelCase(name) { +function toCamelCase(name) { return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); } @@ -132,7 +116,7 @@ export function toCamelCase(name) { * @param {String} after the string to remove the leading hyphens from, usually is colon * @returns {String} The string without leading hyphens */ -export function removeLeadingHyphens(inputString) { +function removeLeadingHyphens(inputString) { // Remove all leading hyphens which are converted from the space in metadata return inputString.replace(/^(-+)/, ''); } @@ -142,8 +126,8 @@ export function removeLeadingHyphens(inputString) { * @param {String} name The metadata name (or property) * @returns {String} The metadata value(s) */ -export function getMetadata(name) { - const meta = [...document.head.querySelectorAll(`meta[name="${name}"]`)].map((m) => m.content).join(', '); +function getMetadata(name) { + const meta = [...document.body.querySelectorAll(`meta[name="${name}"]`)].map((m) => m.content).join(', '); return meta || ''; } @@ -152,9 +136,9 @@ export function getMetadata(name) { * @param {String} scope The scope/prefix for the metadata * @returns a map of key/value pairs for the given scope */ -export function getAllMetadata(scope) { +function getAllMetadata(scope) { const value = getMetadata(scope); - const metaTags = document.head.querySelectorAll(`meta[name^="${scope}"], meta[property^="${scope}:"]`); + const metaTags = document.body.querySelectorAll(`meta[name^="${scope}"], meta[property^="${scope}:"]`); return [...metaTags].reduce((res, meta) => { const key = removeLeadingHyphens( meta.getAttribute('name') @@ -168,21 +152,110 @@ export function getAllMetadata(scope) { }, value ? { value } : {}); } + /** * Gets all the data attributes that are in the given scope. * @param {String} scope The scope/prefix for the metadata * @returns a map of key/value pairs for the given scope */ // eslint-disable-next-line no-unused-vars -function getAllDataAttributes(el, scope) { - return el.getAttributeNames() - .filter((attr) => attr === `data-${scope}` || attr.startsWith(`data-${scope}-`)) - .reduce((res, attr) => { - const key = attr === `data-${scope}` ? 'value' : attr.replace(`data-${scope}-`, ''); - res[key] = el.getAttribute(attr); - return res; - }, {}); -} +async function getAllEntries(document, scope) { + // Check if the DOM is still loading and attach event listener if needed + if (document.readyState === 'loading') { + return new Promise((resolve) => { + document.addEventListener('DOMContentLoaded', () => { + resolve(getAllEntries(document, scope)); + }); + }); + } + + const entries = []; + document.body.querySelectorAll(`[data-${scope}]`).forEach((element) => { + const experiment = element.getAttribute(`data-${scope}`); + const urls = element.getAttribute(`data-${scope}-variant`).split(',').map((url) => url.trim()); + const length = urls.length; + const vnames = Array.from({ length }, (_, i) => `challenger-${i + 1}`); + + let customLabels = (element.getAttribute(`data-${scope}-name`)||'').split(',').map((url) => url.trim()); + const labels = Array.from({ length }, (_, i) => customLabels[i] || `Challenger ${i + 1}`); + const selector = Array.from(element.classList).map((cls) => `.${cls}`).join(''); + const page = window.location.pathname; + + //split + const split = element.getAttribute(`data-${scope}-split`) + ? element.getAttribute(`data-${scope}-split`).split(',').map((i) => parseFloat(i)) + : Array.from({ length }, () => 1 / length); + + //status + const status = element.getAttribute(`data-${scope}-status`); + + //date + const startDate = element.getAttribute(`data-${scope}-start-date`) ? new Date(element.getAttribute(`data-${scope}-start-date`)) : null; + const endDate = element.getAttribute(`data-${scope}-end-date`) ? new Date(element.getAttribute(`data-${scope}-end-date`)) : null; + + //audience + const audience = element.getAttribute(`data-${scope}-audience`).split(',').map((url) => url.trim()); + const audienceAttributes = Array.from(element.attributes) + .filter(attr => attr.name.startsWith(`data-audience-`)) + .map(attr => ({ audience: attr.name.replace('data-', ''), url: attr.value.trim()})); + + const entry = { + audienceAttributes, + audience, + startDate, + endDate, + status, + split, + experiment, + name: labels, + variant:vnames, + selector, + url: urls, + page + }; + entries.push(entry); + }); + + return entries; + } + + /** + * Gets all the data attributes that are in the given scope. + * @param {String} scope The scope/prefix for the metadata + * @returns a map of key/value pairs for the given scope + */ +// eslint-disable-next-line no-unused-vars +async function getAllDataAttributes(document, scope) { + if (document.readyState === 'loading') { + return new Promise((resolve) => { + document.addEventListener('DOMContentLoaded', () => { + resolve(getAllDataAttributes(document, scope)); + }); + }); + } + + const results = []; + const components = Array.from(document.querySelectorAll('*')).filter(element => + Array.from(element.attributes).some(attr => attr.name.startsWith(`data-${scope}-`)) + ); + + components.forEach(component => { + const obj = {}; + + Array.from(component.attributes).forEach(attribute => { + if (attribute.name.startsWith(`data-${scope}-`)) { + obj[attribute.name.replace(`data-${scope}-`, '')] = attribute.value; + } else if (attribute.name === `data-${scope}`) { + obj['value'] = attribute.value; + } + }); + + obj['selector'] = Array.from(component.classList).map(cls => `.${cls}`).join(''); + + results.push(obj); + }); + return results; +} /** * Gets all the query parameters that are in the given scope. @@ -256,7 +329,6 @@ async function replaceInner(path, el, selector) { const resp = await fetch(path); if (!resp.ok) { // eslint-disable-next-line no-console - console.log('error loading content:', resp); return null; } const html = await resp.text(); @@ -285,7 +357,7 @@ async function replaceInner(path, el, selector) { * @param {Object} options the plugin options * @returns Returns the names of the resolved audiences, or `null` if no audience is configured */ -export async function getResolvedAudiences(pageAudiences, options) { +async function getResolvedAudiences(pageAudiences, options) { if (!pageAudiences.length || !Object.keys(options.audiences).length) { return null; } @@ -405,7 +477,7 @@ function createModificationsHandler( } else { res = url; } - cb(el.tagName === 'MAIN' ? document.body : ns.el, ns.config, res ? url : null); + // cb(el.tagName === 'MAIN' ? document.body : ns.el, ns.config, res ? url : null); if (res) { ns.servedExperience = url; } @@ -476,7 +548,6 @@ function watchMutationsAndApplyFragments( } new MutationObserver(async (_, observer) => { - // eslint-disable-next-line no-restricted-syntax for (const entry of entries) { // eslint-disable-next-line no-await-in-loop const config = await metadataToConfig(pluginOptions, entry, overrides); @@ -530,65 +601,29 @@ async function applyAllModifications( paramNS, pluginOptions, metadataToConfig, - manifestToConfig, + dataAttributeToConfig, getExperienceUrl, cb, ) { - const modificationsHandler = createModificationsHandler( + + const configs = []; + + let dataList = await getAllDataAttributes(document, type); + // dataList = dataList.slice(1,2); + // Fragment-level modifications + if (dataList.length) { + const entries = dataAttributeToConfig(dataList); + watchMutationsAndApplyFragments( type, - getAllQueryParameters(paramNS), - metadataToConfig, + document.body, + entries, + configs, getExperienceUrl, pluginOptions, + metadataToConfig, + getAllQueryParameters(paramNS), cb, - ); - - const configs = []; - - // Full-page modifications - const pageMetadata = getAllMetadata(type); - const pageNS = await modificationsHandler( - document.querySelector('main'), - pageMetadata, - ); - if (pageNS) { - pageNS.type = 'page'; - configs.push(pageNS); - debug('page', type, pageNS); - } - - // Section-level modifications - let sectionMetadata; - await Promise.all([...document.querySelectorAll('.section-metadata')] - .map(async (sm) => { - sectionMetadata = getAllSectionMeta(sm, type); - const sectionNS = await modificationsHandler( - sm.parentElement, - sectionMetadata, - ); - if (sectionNS) { - sectionNS.type = 'section'; - debug('section', type, sectionNS); - configs.push(sectionNS); - } - })); - - if (pageMetadata.manifest) { - let entries = await getManifestEntriesForCurrentPage(pageMetadata.manifest); - if (entries) { - entries = manifestToConfig(entries); - watchMutationsAndApplyFragments( - type, - document.body, - entries, - configs, - getExperienceUrl, - pluginOptions, - metadataToConfig, - getAllQueryParameters(paramNS), - cb, - ); - } + ); } return configs; @@ -612,14 +647,19 @@ function aggregateEntries(type, allowedMultiValuesProperties) { }, {}); } +let experimentCounter = 0; /** * Parses the experiment configuration from the metadata */ async function getExperimentConfig(pluginOptions, metadata, overrides) { - const id = toClassName(metadata.value || metadata.experiment); - if (!id) { + // Generate a unique ID for each experiment instance + const baseId = toClassName(metadata.value || metadata.experiment); + if (!baseId) { return null; } + + // Append a unique suffix to the base ID + const id = `${baseId}-${experimentCounter++}`; let pages = metadata.variants || metadata.url; @@ -647,6 +687,7 @@ async function getExperimentConfig(pluginOptions, metadata, overrides) { // even split : [...new Array(pages.length)].map(() => 1 / (pages.length + 1)); + const variantNames = []; variantNames.push('control'); @@ -723,9 +764,10 @@ async function getExperimentConfig(pluginOptions, metadata, overrides) { config.selectedVariant = toClassName(overrides.variant); } else { // eslint-disable-next-line import/extensions - const { ued } = await import('./ued.js'); - const decision = ued.evaluateDecisionPolicy(toDecisionPolicy(config), {}); - config.selectedVariant = decision.items[0].id; + //const { ued } = await import('http://localhost:4502/apps/wknd/clientlibs/clientlib-experimentation/js/ued.js'); + //const decision = ued.evaluateDecisionPolicy(toDecisionPolicy(config), {}); + //config.selectedVariant = decision.items[0].id; + config.selectedVariant = "control"; } return config; @@ -734,11 +776,51 @@ async function getExperimentConfig(pluginOptions, metadata, overrides) { /** * Parses the campaign manifest. */ -function parseExperimentManifest(entries) { - return Object.values(Object.groupBy( - entries.map((e) => depluralizeProps(e, ['experiment', 'variant', 'split', 'name'])), - ({ experiment }) => experiment, - )).map(aggregateEntries('experiment', ['split', 'url', 'variant', 'name'])); +function parseExperimentManifest(rawEntries) { + const entries = []; + for (const entry of rawEntries) { + const experiment = entry['value']; + const urls = entry['variant'].split(',').map((url) => url.trim()); + const length = urls.length; + const vnames = Array.from({ length }, (_, i) => `challenger-${i + 1}`); + + let customLabels = (entry['name']||'').split(',').map((url) => url.trim()); + const labels = Array.from({ length }, (_, i) => customLabels[i] || `Challenger ${i + 1}`); + const selector = entry['selector']; + const page = window.location.pathname; + + //split + const split = entry['split'] + ? entry['split'].split(',').map((i) => parseFloat(i)) + : Array.from({ length }, () => 1 / length); + + //status + const status = entry['status']; + + //date + const startDate = entry['start-date'] ? new Date(entry['start-date']) : null; + const endDate = entry['end-date'] ? new Date(entry['end-date']) : null; + + //audience + const audience = entry['audience'].split(',').map((url) => url.trim()); + + const experimentEntry = { + audience, + startDate, + endDate, + status, + split, + experiment, + name: labels, + variant:vnames, + selector, + url: urls, + page + }; + + entries.push(experimentEntry); + } + return entries; } function getUrlFromExperimentConfig(config) { @@ -824,22 +906,19 @@ async function getCampaignConfig(pluginOptions, metadata, overrides) { /** * Parses the campaign manifest. */ -function parseCampaignManifest(entries) { - return Object.values(Object.groupBy( - entries.map((e) => depluralizeProps(e, ['campaign'])), - ({ selector }) => selector, - )) - .map(aggregateEntries('campaign', ['campaign', 'url'])) - .map((e) => { - const campaigns = e.campaign; - delete e.campaign; - e.campaigns = {}; - campaigns.forEach((a, i) => { - e.campaigns[toClassName(a)] = e.url[i]; - }); - delete e.url; - return e; - }); +function parseCampaignManifest(rawEntries) { + return rawEntries.map(entry => { + const { selector, audience = '', ...campaigns } = entry; + const audiences = audience.split(',').map(aud => aud.trim()); + const page = window.location.pathname; + + return { + audiences, + campaigns, + page, + selector + }; + }); } function getUrlFromCampaignConfig(config) { @@ -857,7 +936,7 @@ async function runCampaign(document, pluginOptions) { parseCampaignManifest, getUrlFromCampaignConfig, (el, config, result) => { - fireRUM('campaign', config, pluginOptions, result); + //fireRUM('campaign', config, pluginOptions, result); // dispatch event const { selectedCampaign = 'default' } = config; const campaign = result ? toClassName(selectedCampaign) : 'default'; @@ -904,22 +983,17 @@ async function getAudienceConfig(pluginOptions, metadata, overrides) { /** * Parses the audience manifest. */ -function parseAudienceManifest(entries) { - return Object.values(Object.groupBy( - entries.map((e) => depluralizeProps(e, ['audience'])), - ({ selector }) => selector, - )) - .map(aggregateEntries('audience', ['audience', 'url'])) - .map((e) => { - const audiences = e.audience; - delete e.audience; - e.audiences = {}; - audiences.forEach((a, i) => { - e.audiences[toClassName(a)] = e.url[i]; - }); - delete e.url; - return e; - }); +function parseAudienceManifest(rawEntries) { + return rawEntries.map(entry => { + const { selector, ...audiences } = entry; + const page = window.location.pathname; + + return { + audiences, + page, + selector + }; + }); } function getUrlFromAudienceConfig(config) { @@ -929,7 +1003,7 @@ function getUrlFromAudienceConfig(config) { } async function serveAudience(document, pluginOptions) { - document.body.dataset.audiences = Object.keys(pluginOptions.audiences).join(','); + //document.body.dataset.audiences = Object.keys(pluginOptions.audiences).join(','); return applyAllModifications( pluginOptions.audiencesMetaTagPrefix, pluginOptions.audiencesQueryParameter, @@ -938,7 +1012,7 @@ async function serveAudience(document, pluginOptions) { parseAudienceManifest, getUrlFromAudienceConfig, (el, config, result) => { - fireRUM('audience', config, pluginOptions, result); + //fireRUM('audience', config, pluginOptions, result); // dispatch event const { selectedAudience = 'default' } = config; const audience = result ? toClassName(selectedAudience) : 'default'; @@ -955,7 +1029,7 @@ async function serveAudience(document, pluginOptions) { ); } -export async function loadEager(document, options = {}) { +async function loadEager(document, options = {}) { const pluginOptions = { ...DEFAULT_OPTIONS, ...options }; setDebugMode(window.location, pluginOptions); @@ -963,16 +1037,10 @@ export async function loadEager(document, options = {}) { ns.audiences = await serveAudience(document, pluginOptions); ns.experiments = await runExperiment(document, pluginOptions); ns.campaigns = await runCampaign(document, pluginOptions); - - // Backward compatibility - ns.experiment = ns.experiments.find((e) => e.type === 'page'); - ns.audience = ns.audiences.find((e) => e.type === 'page'); - ns.campaign = ns.campaigns.find((e) => e.type === 'page'); } -export async function loadLazy(document, options = {}) { - const pluginOptions = { ...DEFAULT_OPTIONS, ...options }; - // do not show the experimentation pill on prod domains +async function loadLazy(document, options = {}) { + const pluginOptions = { ...DEFAULT_OPTIONS, ...options} ; if (!isDebugEnabled) { return; } From bea071ed4f14b1e4f7a954ab482244f076fc825e Mon Sep 17 00:00:00 2001 From: Xinyi Feng Date: Mon, 11 Nov 2024 13:13:28 -0800 Subject: [PATCH 2/7] delete unused code --- src/index.js | 78 ++++++---------------------------------------------- 1 file changed, 9 insertions(+), 69 deletions(-) diff --git a/src/index.js b/src/index.js index ed92eda..7d27751 100644 --- a/src/index.js +++ b/src/index.js @@ -152,73 +152,6 @@ function getAllMetadata(scope) { }, value ? { value } : {}); } - -/** - * Gets all the data attributes that are in the given scope. - * @param {String} scope The scope/prefix for the metadata - * @returns a map of key/value pairs for the given scope - */ -// eslint-disable-next-line no-unused-vars -async function getAllEntries(document, scope) { - // Check if the DOM is still loading and attach event listener if needed - if (document.readyState === 'loading') { - return new Promise((resolve) => { - document.addEventListener('DOMContentLoaded', () => { - resolve(getAllEntries(document, scope)); - }); - }); - } - - const entries = []; - document.body.querySelectorAll(`[data-${scope}]`).forEach((element) => { - const experiment = element.getAttribute(`data-${scope}`); - const urls = element.getAttribute(`data-${scope}-variant`).split(',').map((url) => url.trim()); - const length = urls.length; - const vnames = Array.from({ length }, (_, i) => `challenger-${i + 1}`); - - let customLabels = (element.getAttribute(`data-${scope}-name`)||'').split(',').map((url) => url.trim()); - const labels = Array.from({ length }, (_, i) => customLabels[i] || `Challenger ${i + 1}`); - const selector = Array.from(element.classList).map((cls) => `.${cls}`).join(''); - const page = window.location.pathname; - - //split - const split = element.getAttribute(`data-${scope}-split`) - ? element.getAttribute(`data-${scope}-split`).split(',').map((i) => parseFloat(i)) - : Array.from({ length }, () => 1 / length); - - //status - const status = element.getAttribute(`data-${scope}-status`); - - //date - const startDate = element.getAttribute(`data-${scope}-start-date`) ? new Date(element.getAttribute(`data-${scope}-start-date`)) : null; - const endDate = element.getAttribute(`data-${scope}-end-date`) ? new Date(element.getAttribute(`data-${scope}-end-date`)) : null; - - //audience - const audience = element.getAttribute(`data-${scope}-audience`).split(',').map((url) => url.trim()); - const audienceAttributes = Array.from(element.attributes) - .filter(attr => attr.name.startsWith(`data-audience-`)) - .map(attr => ({ audience: attr.name.replace('data-', ''), url: attr.value.trim()})); - - const entry = { - audienceAttributes, - audience, - startDate, - endDate, - status, - split, - experiment, - name: labels, - variant:vnames, - selector, - url: urls, - page - }; - entries.push(entry); - }); - - return entries; - } - /** * Gets all the data attributes that are in the given scope. * @param {String} scope The scope/prefix for the metadata @@ -555,6 +488,7 @@ function watchMutationsAndApplyFragments( return; } const el = scope.querySelector(entry.selector); + if (!el) { return; } @@ -609,10 +543,13 @@ async function applyAllModifications( const configs = []; let dataList = await getAllDataAttributes(document, type); + console.log("xinyiyyyyydocument,tyle", document, type); // dataList = dataList.slice(1,2); // Fragment-level modifications if (dataList.length) { + console.log("111111xinyidataList", dataList); const entries = dataAttributeToConfig(dataList); + console.log("111111xinyientries", entries); watchMutationsAndApplyFragments( type, document.body, @@ -804,7 +741,7 @@ function parseExperimentManifest(rawEntries) { //audience const audience = entry['audience'].split(',').map((url) => url.trim()); - const experimentEntry = { + const entryC = { audience, startDate, endDate, @@ -818,8 +755,9 @@ function parseExperimentManifest(rawEntries) { page }; - entries.push(experimentEntry); + entries.push(entryC); } + console.log("entries", entries); return entries; } @@ -921,6 +859,8 @@ function parseCampaignManifest(rawEntries) { }); } + + function getUrlFromCampaignConfig(config) { return config.selectedCampaign ? config.configuredCampaigns[config.selectedCampaign] From c6ce1f651cf96ff437586f4dd6d0a2bc7caae769 Mon Sep 17 00:00:00 2001 From: Xinyi Feng Date: Mon, 11 Nov 2024 13:17:17 -0800 Subject: [PATCH 3/7] remove debug console log --- src/index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/index.js b/src/index.js index 7d27751..5439e4d 100644 --- a/src/index.js +++ b/src/index.js @@ -543,13 +543,9 @@ async function applyAllModifications( const configs = []; let dataList = await getAllDataAttributes(document, type); - console.log("xinyiyyyyydocument,tyle", document, type); - // dataList = dataList.slice(1,2); // Fragment-level modifications if (dataList.length) { - console.log("111111xinyidataList", dataList); const entries = dataAttributeToConfig(dataList); - console.log("111111xinyientries", entries); watchMutationsAndApplyFragments( type, document.body, From a8fcb1199168d1208f2e27908764db9c7c2aeecb Mon Sep 17 00:00:00 2001 From: Xinyi Feng Date: Thu, 14 Nov 2024 09:27:05 -0800 Subject: [PATCH 4/7] address comment --- src/index.js | 348 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 209 insertions(+), 139 deletions(-) diff --git a/src/index.js b/src/index.js index 5439e4d..b72af8e 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,6 @@ * governing permissions and limitations under the License. */ - let isDebugEnabled; function setDebugMode(url, pluginOptions) { const { host, hostname, origin } = url; @@ -72,6 +71,21 @@ function toClassName(name) { : ''; } +/** + * Triggers the callback when the page is actually activated, + * This is to properly handle speculative page prerendering and marketing events. + * @param {Function} cb The callback to run + */ +async function onPageActivation(cb) { + // Speculative prerender-aware execution. + // See: https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API#unsafe_prerendering + if (document.prerendering) { + document.addEventListener('prerenderingchange', cb, { once: true }); + } else { + cb(); + } +} + /** * Fires a Real User Monitoring (RUM) event based on the provided type and configuration. * @param {string} type - the type of event to be fired ("experiment", "campaign", or "audience") @@ -99,7 +113,9 @@ function fireRUM(type, config, pluginOptions, result) { const { source, target } = typeHandlers[type](); const rumType = type === 'experiment' ? 'experiment' : 'audience'; - window.hlx?.rum?.sampleRUM(rumType, { source, target }); + onPageActivation(() => { + window.hlx?.rum?.sampleRUM(rumType, { source, target }); + }); } /** @@ -127,7 +143,7 @@ function removeLeadingHyphens(inputString) { * @returns {String} The metadata value(s) */ function getMetadata(name) { - const meta = [...document.body.querySelectorAll(`meta[name="${name}"]`)].map((m) => m.content).join(', '); + const meta = [...document.head.querySelectorAll(`meta[name="${name}"]`)].map((m) => m.content).join(', '); return meta || ''; } @@ -138,7 +154,7 @@ function getMetadata(name) { */ function getAllMetadata(scope) { const value = getMetadata(scope); - const metaTags = document.body.querySelectorAll(`meta[name^="${scope}"], meta[property^="${scope}:"]`); + const metaTags = document.head.querySelectorAll(`meta[name^="${scope}"], meta[property^="${scope}:"]`); return [...metaTags].reduce((res, meta) => { const key = removeLeadingHyphens( meta.getAttribute('name') @@ -152,45 +168,73 @@ function getAllMetadata(scope) { }, value ? { value } : {}); } - /** +/** * Gets all the data attributes that are in the given scope. * @param {String} scope The scope/prefix for the metadata * @returns a map of key/value pairs for the given scope */ // eslint-disable-next-line no-unused-vars -async function getAllDataAttributes(document, scope) { - if (document.readyState === 'loading') { - return new Promise((resolve) => { - document.addEventListener('DOMContentLoaded', () => { - resolve(getAllDataAttributes(document, scope)); - }); - }); +function getAllDataAttributes(el, scope) { + return el.getAttributeNames() + .filter((attr) => attr === `data-${scope}` || attr.startsWith(`data-${scope}-`)) + .reduce((res, attr) => { + const key = attr === `data-${scope}` ? 'value' : attr.replace(`data-${scope}-`, ''); + res[key] = el.getAttribute(attr); + return res; + }, {}); +} + +function getSelectorForElement(el) { + const parents = []; + let p = el; + while (p && p.tagName !== 'HTML') { + parents.unshift(p); + p = p.parentNode; } + return parents + .map((p) => (p.id && `#${p.id}`) + || (p.className && `.${p.classList[0]}:nth-child(${[...p.parentNode.children].indexOf(p) + 1})`)) + .join(' '); +} - const results = []; - const components = Array.from(document.querySelectorAll('*')).filter(element => - Array.from(element.attributes).some(attr => attr.name.startsWith(`data-${scope}-`)) - ); +// convert the selector to a target selector to find component in variant page (custom function) +// Challenge: experience fragment where CSS class may be different for each variation +function convertToVariantSelector(selector) { + const componentType = selector.match(/\.([\w-]+):/g)?.pop()?.replace(/[:.]/g, '') || ''; + return `.cmp-${componentType}`; +} - components.forEach(component => { - const obj = {}; +function getAllMetadataAttributes(document, scope) { + return [...document.querySelectorAll('*')] + .filter(el => Object.keys(el.dataset).some(key => key.startsWith(scope))) + .map((el) => { + const obj = Object.entries(el.dataset) + .reduce((acc, [key, val]) => { + if (key === scope) { + acc['value'] = val; + } else if (key.startsWith(scope)) { + // remove scope prefix + const unprefixedKey = key.replace(scope, ''); + const camelCaseKey = toCamelCase(unprefixedKey); + acc[camelCaseKey] = val; + } + return acc; + }, {}); - Array.from(component.attributes).forEach(attribute => { - if (attribute.name.startsWith(`data-${scope}-`)) { - obj[attribute.name.replace(`data-${scope}-`, '')] = attribute.value; - } else if (attribute.name === `data-${scope}`) { - obj['value'] = attribute.value; - } - }); - - obj['selector'] = Array.from(component.classList).map(cls => `.${cls}`).join(''); + obj.selector = getSelectorForElement(el); + obj.variantSelector = convertToVariantSelector(obj.selector); - results.push(obj); - }); - return results; -} + // process variants into array + if (obj.variants || obj.variant) { + obj.variants = obj.variants.split(/,\s*\n\s*|\s*,\s*/) + .map(url => url.trim()) + .filter(url => url.length > 0); + } -/** + return obj; + }); +} +/* * Gets all the query parameters that are in the given scope. * @param {String} scope The scope/prefix for the metadata * @returns a map of key/value pairs for the given scope @@ -262,6 +306,7 @@ async function replaceInner(path, el, selector) { const resp = await fetch(path); if (!resp.ok) { // eslint-disable-next-line no-console + console.log('error loading content:', resp); return null; } const html = await resp.text(); @@ -389,7 +434,7 @@ function createModificationsHandler( pluginOptions, cb, ) { - return async (el, metadata) => { + return async (el, metadata, selector) => { const config = await metadataToConfig(pluginOptions, metadata, overrides); if (!config) { return null; @@ -406,11 +451,11 @@ function createModificationsHandler( return; } // eslint-disable-next-line no-await-in-loop - res = await replaceInner(new URL(url, window.location.origin).pathname, el); + res = await replaceInner(new URL(url, window.location.origin).pathname, el, selector); } else { res = url; } - // cb(el.tagName === 'MAIN' ? document.body : ns.el, ns.config, res ? url : null); + cb(el.tagName === 'MAIN' ? document.body : ns.el, ns.config, res ? url : null); if (res) { ns.servedExperience = url; } @@ -481,6 +526,7 @@ function watchMutationsAndApplyFragments( } new MutationObserver(async (_, observer) => { + // eslint-disable-next-line no-restricted-syntax for (const entry of entries) { // eslint-disable-next-line no-await-in-loop const config = await metadataToConfig(pluginOptions, entry, overrides); @@ -488,7 +534,6 @@ function watchMutationsAndApplyFragments( return; } const el = scope.querySelector(entry.selector); - if (!el) { return; } @@ -535,28 +580,88 @@ async function applyAllModifications( paramNS, pluginOptions, metadataToConfig, - dataAttributeToConfig, + manifestToConfig, getExperienceUrl, cb, ) { - - const configs = []; - - let dataList = await getAllDataAttributes(document, type); - // Fragment-level modifications - if (dataList.length) { - const entries = dataAttributeToConfig(dataList); - watchMutationsAndApplyFragments( + const modificationsHandler = createModificationsHandler( type, - document.body, - entries, - configs, + getAllQueryParameters(paramNS), + metadataToConfig, getExperienceUrl, pluginOptions, - metadataToConfig, - getAllQueryParameters(paramNS), cb, - ); + ); + + const configs = []; + + // Full-page modifications + const pageMetadata = getAllMetadata(type); + const pageNS = await modificationsHandler( + document.querySelector('main'), + pageMetadata, + ); + + if (pageNS) { + pageNS.type = 'page'; + configs.push(pageNS); + debug('page', type, pageNS); + } + + // Section-level modifications + let sectionMetadata; + await Promise.all([...document.querySelectorAll('.section-metadata')] + .map(async (sm) => { + sectionMetadata = getAllSectionMeta(sm, type); + const sectionNS = await modificationsHandler( + sm.parentElement, + sectionMetadata, + ); + if (sectionNS) { + sectionNS.type = 'section'; + debug('section', type, sectionNS); + configs.push(sectionNS); + } + })); + + //AEM CS experimentation modifications + const componentDataList = getAllMetadataAttributes(document, type); + await Promise.all(componentDataList.map(async (componentMetadata) => { + const { selector, variantSelector, ...metadata } = componentMetadata; + const component = document.querySelector(selector); + + if (!component) return; + + const componentNS = await modificationsHandler( + component, + metadata, + variantSelector, + ); + + if (componentNS) { + componentNS.type = 'component'; + debug('component', type, componentNS); + configs.push(componentNS); + } + })); + + // fragment modifications + if (pageMetadata.manifest) { + let entries = await getManifestEntriesForCurrentPage(pageMetadata.manifest); + if (entries) { + entries = manifestToConfig(entries); + watchMutationsAndApplyFragments( + type, + document.body, + entries, + configs, + getExperienceUrl, + pluginOptions, + metadataToConfig, + getAllQueryParameters(paramNS), + cb, + ); + } } return configs; @@ -580,19 +685,14 @@ function aggregateEntries(type, allowedMultiValuesProperties) { }, {}); } -let experimentCounter = 0; /** * Parses the experiment configuration from the metadata */ async function getExperimentConfig(pluginOptions, metadata, overrides) { - // Generate a unique ID for each experiment instance - const baseId = toClassName(metadata.value || metadata.experiment); - if (!baseId) { + const id = toClassName(metadata.value || metadata.experiment); + if (!id) { return null; } - - // Append a unique suffix to the base ID - const id = `${baseId}-${experimentCounter++}`; let pages = metadata.variants || metadata.url; @@ -620,7 +720,6 @@ async function getExperimentConfig(pluginOptions, metadata, overrides) { // even split : [...new Array(pages.length)].map(() => 1 / (pages.length + 1)); - const variantNames = []; variantNames.push('control'); @@ -697,10 +796,9 @@ async function getExperimentConfig(pluginOptions, metadata, overrides) { config.selectedVariant = toClassName(overrides.variant); } else { // eslint-disable-next-line import/extensions - //const { ued } = await import('http://localhost:4502/apps/wknd/clientlibs/clientlib-experimentation/js/ued.js'); - //const decision = ued.evaluateDecisionPolicy(toDecisionPolicy(config), {}); - //config.selectedVariant = decision.items[0].id; - config.selectedVariant = "control"; + const { ued } = await import('./ued.js'); + const decision = ued.evaluateDecisionPolicy(toDecisionPolicy(config), {}); + config.selectedVariant = decision.items[0].id; } return config; @@ -709,52 +807,11 @@ async function getExperimentConfig(pluginOptions, metadata, overrides) { /** * Parses the campaign manifest. */ -function parseExperimentManifest(rawEntries) { - const entries = []; - for (const entry of rawEntries) { - const experiment = entry['value']; - const urls = entry['variant'].split(',').map((url) => url.trim()); - const length = urls.length; - const vnames = Array.from({ length }, (_, i) => `challenger-${i + 1}`); - - let customLabels = (entry['name']||'').split(',').map((url) => url.trim()); - const labels = Array.from({ length }, (_, i) => customLabels[i] || `Challenger ${i + 1}`); - const selector = entry['selector']; - const page = window.location.pathname; - - //split - const split = entry['split'] - ? entry['split'].split(',').map((i) => parseFloat(i)) - : Array.from({ length }, () => 1 / length); - - //status - const status = entry['status']; - - //date - const startDate = entry['start-date'] ? new Date(entry['start-date']) : null; - const endDate = entry['end-date'] ? new Date(entry['end-date']) : null; - - //audience - const audience = entry['audience'].split(',').map((url) => url.trim()); - - const entryC = { - audience, - startDate, - endDate, - status, - split, - experiment, - name: labels, - variant:vnames, - selector, - url: urls, - page - }; - - entries.push(entryC); - } - console.log("entries", entries); - return entries; +function parseExperimentManifest(entries) { + return Object.values(Object.groupBy( + entries.map((e) => depluralizeProps(e, ['experiment', 'variant', 'split', 'name'])), + ({ experiment }) => experiment, + )).map(aggregateEntries('experiment', ['split', 'url', 'variant', 'name'])); } function getUrlFromExperimentConfig(config) { @@ -840,23 +897,24 @@ async function getCampaignConfig(pluginOptions, metadata, overrides) { /** * Parses the campaign manifest. */ -function parseCampaignManifest(rawEntries) { - return rawEntries.map(entry => { - const { selector, audience = '', ...campaigns } = entry; - const audiences = audience.split(',').map(aud => aud.trim()); - const page = window.location.pathname; - - return { - audiences, - campaigns, - page, - selector - }; - }); +function parseCampaignManifest(entries) { + return Object.values(Object.groupBy( + entries.map((e) => depluralizeProps(e, ['campaign'])), + ({ selector }) => selector, + )) + .map(aggregateEntries('campaign', ['campaign', 'url'])) + .map((e) => { + const campaigns = e.campaign; + delete e.campaign; + e.campaigns = {}; + campaigns.forEach((a, i) => { + e.campaigns[toClassName(a)] = e.url[i]; + }); + delete e.url; + return e; + }); } - - function getUrlFromCampaignConfig(config) { return config.selectedCampaign ? config.configuredCampaigns[config.selectedCampaign] @@ -872,7 +930,7 @@ async function runCampaign(document, pluginOptions) { parseCampaignManifest, getUrlFromCampaignConfig, (el, config, result) => { - //fireRUM('campaign', config, pluginOptions, result); + fireRUM('campaign', config, pluginOptions, result); // dispatch event const { selectedCampaign = 'default' } = config; const campaign = result ? toClassName(selectedCampaign) : 'default'; @@ -919,17 +977,22 @@ async function getAudienceConfig(pluginOptions, metadata, overrides) { /** * Parses the audience manifest. */ -function parseAudienceManifest(rawEntries) { - return rawEntries.map(entry => { - const { selector, ...audiences } = entry; - const page = window.location.pathname; - - return { - audiences, - page, - selector - }; - }); +function parseAudienceManifest(entries) { + return Object.values(Object.groupBy( + entries.map((e) => depluralizeProps(e, ['audience'])), + ({ selector }) => selector, + )) + .map(aggregateEntries('audience', ['audience', 'url'])) + .map((e) => { + const audiences = e.audience; + delete e.audience; + e.audiences = {}; + audiences.forEach((a, i) => { + e.audiences[toClassName(a)] = e.url[i]; + }); + delete e.url; + return e; + }); } function getUrlFromAudienceConfig(config) { @@ -939,7 +1002,7 @@ function getUrlFromAudienceConfig(config) { } async function serveAudience(document, pluginOptions) { - //document.body.dataset.audiences = Object.keys(pluginOptions.audiences).join(','); + document.body.dataset.audiences = Object.keys(pluginOptions.audiences).join(','); return applyAllModifications( pluginOptions.audiencesMetaTagPrefix, pluginOptions.audiencesQueryParameter, @@ -948,7 +1011,7 @@ async function serveAudience(document, pluginOptions) { parseAudienceManifest, getUrlFromAudienceConfig, (el, config, result) => { - //fireRUM('audience', config, pluginOptions, result); + fireRUM('audience', config, pluginOptions, result); // dispatch event const { selectedAudience = 'default' } = config; const audience = result ? toClassName(selectedAudience) : 'default'; @@ -969,14 +1032,21 @@ async function loadEager(document, options = {}) { const pluginOptions = { ...DEFAULT_OPTIONS, ...options }; setDebugMode(window.location, pluginOptions); + // wait for DOM to be ready + if (document.readyState === 'loading') { + await new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve)); + } + const ns = window.aem || window.hlx || {}; ns.audiences = await serveAudience(document, pluginOptions); ns.experiments = await runExperiment(document, pluginOptions); ns.campaigns = await runCampaign(document, pluginOptions); + return ns; } async function loadLazy(document, options = {}) { - const pluginOptions = { ...DEFAULT_OPTIONS, ...options} ; + const pluginOptions = { ...DEFAULT_OPTIONS, ...options }; + // do not show the experimentation pill on prod domains if (!isDebugEnabled) { return; } From 6b871bbb005f545278d84b58d79b214500e5f435 Mon Sep 17 00:00:00 2001 From: Xinyi Feng Date: Thu, 14 Nov 2024 09:37:43 -0800 Subject: [PATCH 5/7] fix some style --- src/index.js | 55 ++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/src/index.js b/src/index.js index b72af8e..b1b7ac4 100644 --- a/src/index.js +++ b/src/index.js @@ -186,14 +186,14 @@ function getAllDataAttributes(el, scope) { function getSelectorForElement(el) { const parents = []; - let p = el; - while (p && p.tagName !== 'HTML') { - parents.unshift(p); - p = p.parentNode; + let currentElement = el; + while (currentElement && currentElement.tagName !== 'HTML') { + parents.unshift(currentElement); + currentElement = currentElement.parentNode; } return parents - .map((p) => (p.id && `#${p.id}`) - || (p.className && `.${p.classList[0]}:nth-child(${[...p.parentNode.children].indexOf(p) + 1})`)) + .map((element) => (element.id && `#${element.id}`) + || (element.className && `.${element.classList[0]}:nth-child(${[...element.parentNode.children].indexOf(element) + 1})`)) .join(' '); } @@ -206,12 +206,12 @@ function convertToVariantSelector(selector) { function getAllMetadataAttributes(document, scope) { return [...document.querySelectorAll('*')] - .filter(el => Object.keys(el.dataset).some(key => key.startsWith(scope))) + .filter((el) => Object.keys(el.dataset).some((key) => key.startsWith(scope))) .map((el) => { const obj = Object.entries(el.dataset) .reduce((acc, [key, val]) => { if (key === scope) { - acc['value'] = val; + acc.value = val; } else if (key.startsWith(scope)) { // remove scope prefix const unprefixedKey = key.replace(scope, ''); @@ -227,13 +227,14 @@ function getAllMetadataAttributes(document, scope) { // process variants into array if (obj.variants || obj.variant) { obj.variants = obj.variants.split(/,\s*\n\s*|\s*,\s*/) - .map(url => url.trim()) - .filter(url => url.length > 0); + .map((url) => url.trim()) + .filter((url) => url.length > 0); } return obj; }); } + /* * Gets all the query parameters that are in the given scope. * @param {String} scope The scope/prefix for the metadata @@ -624,26 +625,26 @@ async function applyAllModifications( } })); - //AEM CS experimentation modifications + // AEM CS experimentation modifications const componentDataList = getAllMetadataAttributes(document, type); - await Promise.all(componentDataList.map(async (componentMetadata) => { - const { selector, variantSelector, ...metadata } = componentMetadata; - const component = document.querySelector(selector); + await Promise.all(componentDataList.map(async (componentMetadata) => { + const { selector, variantSelector, ...metadata } = componentMetadata; + const component = document.querySelector(selector); - if (!component) return; + if (!component) return; - const componentNS = await modificationsHandler( - component, - metadata, - variantSelector, - ); + const componentNS = await modificationsHandler( + component, + metadata, + variantSelector, + ); - if (componentNS) { - componentNS.type = 'component'; - debug('component', type, componentNS); - configs.push(componentNS); - } - })); + if (componentNS) { + componentNS.type = 'component'; + debug('component', type, componentNS); + configs.push(componentNS); + } + })); // fragment modifications if (pageMetadata.manifest) { @@ -1034,7 +1035,7 @@ async function loadEager(document, options = {}) { // wait for DOM to be ready if (document.readyState === 'loading') { - await new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve)); + await new Promise((resolve) => document.addEventListener('DOMContentLoaded', resolve)); } const ns = window.aem || window.hlx || {}; From c47194bdff8861b0e98b8c76198df9c09f4e21a9 Mon Sep 17 00:00:00 2001 From: Xinyi Feng Date: Fri, 15 Nov 2024 10:00:39 -0800 Subject: [PATCH 6/7] add export/import keywords back, delete the varaintslector logic --- src/index.js | 49 +++++++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/index.js b/src/index.js index b1b7ac4..8431013 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,7 @@ */ let isDebugEnabled; -function setDebugMode(url, pluginOptions) { +export function setDebugMode(url, pluginOptions) { const { host, hostname, origin } = url; const { isProd, prodHost } = pluginOptions; isDebugEnabled = (url.hostname === 'localhost' @@ -22,14 +22,14 @@ function setDebugMode(url, pluginOptions) { return isDebugEnabled; } -function debug(...args) { +export function debug(...args) { if (isDebugEnabled) { // eslint-disable-next-line no-console console.debug.call(this, '[aem-experimentation]', ...args); } } -const DEFAULT_OPTIONS = { +export const DEFAULT_OPTIONS = { // Audiences related properties audiences: {}, @@ -53,7 +53,7 @@ const DEFAULT_OPTIONS = { * @param {String|String[]} str The string to convert * @returns an array representing the converted string */ -function stringToArray(str) { +export function stringToArray(str) { if (Array.isArray(str)) { return str; } @@ -65,7 +65,7 @@ function stringToArray(str) { * @param {String} name The unsanitized name * @returns {String} The class name */ -function toClassName(name) { +export function toClassName(name) { return typeof name === 'string' ? name.toLowerCase().replace(/[^0-9a-z]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') : ''; @@ -123,7 +123,7 @@ function fireRUM(type, config, pluginOptions, result) { * @param {String} name The unsanitized name * @returns {String} The camelCased name */ -function toCamelCase(name) { +export function toCamelCase(name) { return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); } @@ -132,7 +132,7 @@ function toCamelCase(name) { * @param {String} after the string to remove the leading hyphens from, usually is colon * @returns {String} The string without leading hyphens */ -function removeLeadingHyphens(inputString) { +export function removeLeadingHyphens(inputString) { // Remove all leading hyphens which are converted from the space in metadata return inputString.replace(/^(-+)/, ''); } @@ -142,7 +142,7 @@ function removeLeadingHyphens(inputString) { * @param {String} name The metadata name (or property) * @returns {String} The metadata value(s) */ -function getMetadata(name) { +export function getMetadata(name) { const meta = [...document.head.querySelectorAll(`meta[name="${name}"]`)].map((m) => m.content).join(', '); return meta || ''; } @@ -152,7 +152,7 @@ function getMetadata(name) { * @param {String} scope The scope/prefix for the metadata * @returns a map of key/value pairs for the given scope */ -function getAllMetadata(scope) { +export function getAllMetadata(scope) { const value = getMetadata(scope); const metaTags = document.head.querySelectorAll(`meta[name^="${scope}"], meta[property^="${scope}:"]`); return [...metaTags].reduce((res, meta) => { @@ -197,13 +197,6 @@ function getSelectorForElement(el) { .join(' '); } -// convert the selector to a target selector to find component in variant page (custom function) -// Challenge: experience fragment where CSS class may be different for each variation -function convertToVariantSelector(selector) { - const componentType = selector.match(/\.([\w-]+):/g)?.pop()?.replace(/[:.]/g, '') || ''; - return `.cmp-${componentType}`; -} - function getAllMetadataAttributes(document, scope) { return [...document.querySelectorAll('*')] .filter((el) => Object.keys(el.dataset).some((key) => key.startsWith(scope))) @@ -222,7 +215,6 @@ function getAllMetadataAttributes(document, scope) { }, {}); obj.selector = getSelectorForElement(el); - obj.variantSelector = convertToVariantSelector(obj.selector); // process variants into array if (obj.variants || obj.variant) { @@ -319,7 +311,11 @@ async function replaceInner(path, el, selector) { newEl = dom.querySelector(selector); } if (!newEl) { - newEl = dom.querySelector(el.tagName === 'MAIN' ? 'main' : 'main > div'); + if (el.tagName === 'MAIN') { + newEl = dom.querySelector('main'); + } else { + newEl = dom.querySelector('main > div') || dom.querySelector('body > div'); + } } el.innerHTML = newEl.innerHTML; return path; @@ -336,7 +332,7 @@ async function replaceInner(path, el, selector) { * @param {Object} options the plugin options * @returns Returns the names of the resolved audiences, or `null` if no audience is configured */ -async function getResolvedAudiences(pageAudiences, options) { +export async function getResolvedAudiences(pageAudiences, options) { if (!pageAudiences.length || !Object.keys(options.audiences).length) { return null; } @@ -435,7 +431,7 @@ function createModificationsHandler( pluginOptions, cb, ) { - return async (el, metadata, selector) => { + return async (el, metadata) => { const config = await metadataToConfig(pluginOptions, metadata, overrides); if (!config) { return null; @@ -452,7 +448,7 @@ function createModificationsHandler( return; } // eslint-disable-next-line no-await-in-loop - res = await replaceInner(new URL(url, window.location.origin).pathname, el, selector); + res = await replaceInner(new URL(url, window.location.origin).pathname, el); } else { res = url; } @@ -628,7 +624,7 @@ async function applyAllModifications( // AEM CS experimentation modifications const componentDataList = getAllMetadataAttributes(document, type); await Promise.all(componentDataList.map(async (componentMetadata) => { - const { selector, variantSelector, ...metadata } = componentMetadata; + const { selector, ...metadata } = componentMetadata; const component = document.querySelector(selector); if (!component) return; @@ -636,7 +632,6 @@ async function applyAllModifications( const componentNS = await modificationsHandler( component, metadata, - variantSelector, ); if (componentNS) { @@ -1029,13 +1024,15 @@ async function serveAudience(document, pluginOptions) { ); } -async function loadEager(document, options = {}) { +export async function loadEager(document, options = {}) { const pluginOptions = { ...DEFAULT_OPTIONS, ...options }; setDebugMode(window.location, pluginOptions); // wait for DOM to be ready if (document.readyState === 'loading') { - await new Promise((resolve) => document.addEventListener('DOMContentLoaded', resolve)); + await new Promise((resolve) => { + document.addEventListener('DOMContentLoaded', resolve); + }); } const ns = window.aem || window.hlx || {}; @@ -1045,7 +1042,7 @@ async function loadEager(document, options = {}) { return ns; } -async function loadLazy(document, options = {}) { +export async function loadLazy(document, options = {}) { const pluginOptions = { ...DEFAULT_OPTIONS, ...options }; // do not show the experimentation pill on prod domains if (!isDebugEnabled) { From 2083fd10f6006c81844df015d9c5f68844c87b55 Mon Sep 17 00:00:00 2001 From: Xinyi Feng Date: Mon, 18 Nov 2024 08:07:58 -0800 Subject: [PATCH 7/7] add check on empty selctor in getAllMetadataAttributes(), to exclude the attributes: data-{scope}-* added in callback from runExperiment(),runCampaign(), serveAudience() --- src/index.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/index.js b/src/index.js index 8431013..9d0ec28 100644 --- a/src/index.js +++ b/src/index.js @@ -200,30 +200,32 @@ function getSelectorForElement(el) { function getAllMetadataAttributes(document, scope) { return [...document.querySelectorAll('*')] .filter((el) => Object.keys(el.dataset).some((key) => key.startsWith(scope))) - .map((el) => { - const obj = Object.entries(el.dataset) + .flatMap((el) => { + const attributes = Object.entries(el.dataset) .reduce((acc, [key, val]) => { if (key === scope) { acc.value = val; } else if (key.startsWith(scope)) { - // remove scope prefix - const unprefixedKey = key.replace(scope, ''); + const unprefixedKey = key.slice(scope.length); const camelCaseKey = toCamelCase(unprefixedKey); acc[camelCaseKey] = val; } return acc; }, {}); - obj.selector = getSelectorForElement(el); + const selector = getSelectorForElement(el); + if (!selector) return []; - // process variants into array - if (obj.variants || obj.variant) { - obj.variants = obj.variants.split(/,\s*\n\s*|\s*,\s*/) + attributes.selector = selector; + + if (attributes.variants || attributes.variant) { + attributes.variants = (attributes.variants || attributes.variant) + .split(/,\s*\n\s*|\s*,\s*/) .map((url) => url.trim()) .filter((url) => url.length > 0); } - return obj; + return [attributes]; }); }