From c744452fb857e2c25497ca26d59e58c221003e7e Mon Sep 17 00:00:00 2001 From: Vivian A Goodrich <101133187+vgoodric@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:30:30 -0700 Subject: [PATCH] Mwpw-138750 analytics update 2.1 (#1531) * mwpw-138750 analytics update 2.0 * pass placeholders from Target * override and full url * remove unneeded if * add manifestUrl to Target * add if back in to prevent duplicates * add analytics btn and fix btn if Trgt on & no rtrn * shorten analytics off title * add indent to page button info * make variantLable lower case for compare * add ? if variantLabel is null * merge manifest-utils into personalization * remove 2 manifest-utils references * remove 1 import of manifest-utils * remove redundant function * rework promise syntax * update to "analytics manifest name" * consolidate call of preview button * update unit tests * change controlPageLoad to true * move functions to attributes.js * streamline return line * update regex for non latin alphabet * remove empty line * requested updates from Peyer * restore constants in attributes * mwpw-139041-ut-fix-v2 * rebase * update normalizePath to other version * ut coverage --------- Co-authored-by: vivgoodrich Co-authored-by: Blaine Gunn --- libs/blocks/accordion/accordion.js | 4 +- .../global-navigation/utilities/utilities.js | 3 +- .../personalization/add-preview-to-config.js | 19 ++- .../personalization/manifest-utils.js | 39 ----- .../personalization/personalization.js | 135 +++++++++++------- libs/features/personalization/preview.css | 4 + libs/features/personalization/preview.js | 55 ++++--- libs/martech/analytics.js | 25 ++-- libs/martech/attributes.js | 61 +++++++- libs/martech/attributes.md | 82 ++++++++++- libs/martech/martech.js | 18 ++- libs/templates/404/404.js | 2 +- libs/utils/utils.js | 21 +-- test/blocks/accordion/accordion.test.js | 6 +- .../utilities/utilities.test.js | 4 +- .../mocks/manifestTestOrPromo.json | 44 ++++++ .../mocks/personalization.html | 3 +- .../personalization/personalization.test.js | 9 ++ .../{analytics.test.js => attributes.test.js} | 4 +- 19 files changed, 380 insertions(+), 158 deletions(-) delete mode 100644 libs/features/personalization/manifest-utils.js create mode 100644 test/features/personalization/mocks/manifestTestOrPromo.json rename test/martech/{analytics.test.js => attributes.test.js} (87%) diff --git a/libs/blocks/accordion/accordion.js b/libs/blocks/accordion/accordion.js index e8d4972d2d..eaa55615ca 100644 --- a/libs/blocks/accordion/accordion.js +++ b/libs/blocks/accordion/accordion.js @@ -1,6 +1,6 @@ import { createTag } from '../../utils/utils.js'; import { decorateButtons } from '../../utils/decorate.js'; -import { processTrackingLabels } from '../../martech/analytics.js'; +import { processTrackingLabels } from '../../martech/attributes.js'; const faq = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: [] }; const mediaCollection = {}; @@ -74,7 +74,7 @@ function createItem(accordion, id, heading, num, edit) { const panelId = `accordion-${id}-content-${num}`; const icon = createTag('span', { class: 'accordion-icon' }); const hTag = heading.querySelector('h1, h2, h3, h4, h5, h6'); - const analyticsString = `open-${num}|${processTrackingLabels(heading.textContent)}`; + const analyticsString = `open-${num}--${processTrackingLabels(heading.textContent)}`; const button = createTag('button', { type: 'button', id: triggerId, diff --git a/libs/blocks/global-navigation/utilities/utilities.js b/libs/blocks/global-navigation/utilities/utilities.js index cfb0cc7f18..f6555dd828 100644 --- a/libs/blocks/global-navigation/utilities/utilities.js +++ b/libs/blocks/global-navigation/utilities/utilities.js @@ -1,4 +1,5 @@ import { getConfig, getMetadata, loadStyle, loadLana } from '../../../utils/utils.js'; +import { processTrackingLabels } from '../../../martech/attributes.js'; loadLana(); @@ -51,7 +52,7 @@ export const getFedsPlaceholderConfig = () => { export function getAnalyticsValue(str, index) { if (typeof str !== 'string' || !str.length) return str; - let analyticsValue = str.trim().replace(/[^\w]+/g, '_').replace(/^_+|_+$/g, ''); + let analyticsValue = processTrackingLabels(str, false, 30); analyticsValue = typeof index === 'number' ? `${analyticsValue}-${index}` : analyticsValue; return analyticsValue; diff --git a/libs/features/personalization/add-preview-to-config.js b/libs/features/personalization/add-preview-to-config.js index f19a7d41bc..df308340dd 100644 --- a/libs/features/personalization/add-preview-to-config.js +++ b/libs/features/personalization/add-preview-to-config.js @@ -17,14 +17,23 @@ export default async function addPreviewToConfig({ }); if (config.mep.override !== '') { + const persManifestPaths = persManifests.map((manifest) => { + if (manifest.startsWith('/')) return manifest; + try { + const url = new URL(manifest); + return url.pathname; + } catch (e) { + return manifest; + } + }); + config.mep.override.split(',').forEach((manifestPair) => { - persManifests.push(manifestPair.trim().toLowerCase().split('--')[0]); + const manifestTitle = manifestPair.trim().toLowerCase().split('--')[0]; + if (!persManifestPaths.includes(manifestTitle)) { + persManifests.push(manifestTitle); + } }); } - if (config.mep.preview && !targetEnabled && !persManifests.length) { - import('./preview.js') - .then(({ default: decoratePreviewMode }) => decoratePreviewMode([])); - } return persManifests; } diff --git a/libs/features/personalization/manifest-utils.js b/libs/features/personalization/manifest-utils.js deleted file mode 100644 index 0b33e52caa..0000000000 --- a/libs/features/personalization/manifest-utils.js +++ /dev/null @@ -1,39 +0,0 @@ -import { loadLink } from '../../utils/utils.js'; - -export const appendJsonExt = (path) => (path.endsWith('.json') ? path : `${path}.json`); - -export const normalizePath = (p) => { - let path = p; - - if (!path.includes('/')) { - return path; - } - - if (path.startsWith('http')) { - try { - path = new URL(path).pathname; - } catch (e) { /* return path below */ } - } else if (!path.startsWith('/')) { - path = `/${path}`; - } - return path; -}; - -export const preloadManifests = ({ targetManifests = [], persManifests = [] }) => { - let manifests = targetManifests; - - manifests = manifests.concat( - persManifests.map((manifestPath) => ({ manifestPath: appendJsonExt(manifestPath) })), - ); - - for (const manifest of manifests) { - if (!manifest.manifestData && manifest.manifestPath) { - manifest.manifestPath = normalizePath(manifest.manifestPath); - loadLink( - manifest.manifestPath, - { as: 'fetch', crossorigin: 'anonymous', rel: 'preload' }, - ); - } - } - return manifests; -}; diff --git a/libs/features/personalization/personalization.js b/libs/features/personalization/personalization.js index 7199c29e1c..0ac3974859 100644 --- a/libs/features/personalization/personalization.js +++ b/libs/features/personalization/personalization.js @@ -1,4 +1,5 @@ /* eslint-disable no-console */ + import { createTag, getConfig, loadIms, loadLink, loadScript, updateConfig, } from '../../utils/utils.js'; @@ -11,6 +12,53 @@ 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); +export const NON_TRACKED_MANIFEST_TYPE = 'test or promo'; + +export const appendJsonExt = (path) => (path.endsWith('.json') ? path : `${path}.json`); + +export const normalizePath = (p) => { + let path = p; + + if (!path.includes('/')) { + return path; + } + + const config = getConfig(); + + if (path.startsWith(config.codeRoot) + || path.includes('.hlx.') + || path.startsWith(`https://${config.productionDomain}`)) { + try { + path = new URL(path).pathname; + } catch (e) { /* return path below */ } + } else if (!path.startsWith('http') && !path.startsWith('/')) { + path = `/${path}`; + } + return path; +}; + +export const preloadManifests = ({ targetManifests = [], persManifests = [] }) => { + let manifests = targetManifests; + + manifests = manifests.concat( + persManifests.map((manifestPath) => ({ + manifestPath: appendJsonExt(manifestPath), + manifestUrl: manifestPath, + })), + ); + + for (const manifest of manifests) { + if (!manifest.manifestData && manifest.manifestPath) { + manifest.manifestPath = normalizePath(manifest.manifestPath); + loadLink( + manifest.manifestPath, + { as: 'fetch', crossorigin: 'anonymous', rel: 'preload' }, + ); + } + } + return manifests; +}; + /* c8 ignore start */ export const PERSONALIZATION_TAGS = { all: () => true, @@ -123,28 +171,6 @@ const consolidateObjects = (arr, prop) => arr.reduce((propMap, item) => { return propMap; }, {}); -/* c8 ignore start */ -function normalizePath(p) { - let path = p; - - if (!path.includes('/')) { - return path; - } - - const config = getConfig(); - - if (path.startsWith(config.codeRoot) - || path.includes('.hlx.') - || path.startsWith(`https://${config.productionDomain}`)) { - try { - path = new URL(path).pathname; - } catch (e) { /* return path below */ } - } else if (!path.startsWith('http') && !path.startsWith('/')) { - path = `/${path}`; - } - return path; -} - const matchGlob = (searchStr, inputStr) => { const pattern = searchStr.replace(/\*\*/g, '.*'); const reg = new RegExp(`^${pattern}$`, 'i'); // devtool bug needs this backtick: ` @@ -403,7 +429,7 @@ async function getPersonalizationVariant(manifestPath, variantNames = [], varian } const matchVariant = (name) => { - if (name === variantLabel) return true; + if (name === variantLabel?.toLowerCase()) return true; if (name.startsWith('param-')) return checkForParamMatch(name); if (name.startsWith('ent-')) return checkForEntitlementMatch(name, entitlements); if (entitlementKeys.includes(name)) { @@ -416,21 +442,30 @@ async function getPersonalizationVariant(manifestPath, variantNames = [], varian return matchingVariant; } -export async function getPersConfig(name, variantLabel, manifestData, manifestPath) { +export async function getPersConfig(info) { + const { + name, + manifestData, + manifestPath, + manifestUrl, + manifestPlaceholders, + manifestInfo, + variantLabel, + } = info; let data = manifestData; if (!data) { const fetchedData = await fetchData(manifestPath, DATA_TYPE.JSON); if (fetchData) data = fetchedData; } - let placeholders = false; - if (data?.placeholders?.data) { - placeholders = data.placeholders.data; - } const persData = data?.experiences?.data || data?.data || data; if (!persData) return null; const config = parseConfig(persData); + const infoTab = manifestInfo || data?.info?.data; + config.manifestType = infoTab?.find((element) => element.key?.toLowerCase() === 'manifest-type')?.value?.toLowerCase() || 'personalization'; + config.manifestOverrideName = infoTab?.find((element) => element.key?.toLowerCase() === 'manifest-override-name')?.value?.toLowerCase(); + if (!config) { /* c8 ignore next 3 */ console.log('Error loading personalization config: ', name || manifestPath); @@ -453,6 +488,7 @@ export async function getPersConfig(name, variantLabel, manifestData, manifestPa config.selectedVariant = 'default'; } + const placeholders = manifestPlaceholders || data?.placeholders?.data; if (placeholders) { updateConfig( parsePlaceholders(placeholders, getConfig(), config.selectedVariantName), @@ -461,6 +497,7 @@ export async function getPersConfig(name, variantLabel, manifestData, manifestPa config.name = name; config.manifest = manifestPath; + config.manifestUrl = manifestUrl; return config; } @@ -475,14 +512,9 @@ const normalizeFragPaths = ({ selector, val }) => ({ }); export async function runPersonalization(info, config) { - const { - name, - manifestData, - manifestPath, - variantLabel, - } = info; + const { manifestPath } = info; - const experiment = await getPersConfig(name, variantLabel, manifestData, manifestPath); + const experiment = await getPersConfig(info); if (!experiment) return null; @@ -539,21 +571,10 @@ function cleanManifestList(manifests) { return cleanedList; } -const decoratePreviewCheck = async (config, experiments) => { - if (config.mep?.preview) { - const { default: decoratePreviewMode } = await import('./preview.js'); - decoratePreviewMode(experiments); - } -}; - export async function applyPers(manifests) { const config = getConfig(); - if (!manifests?.length) { - /* c8 ignore next */ - decoratePreviewCheck(config, []); - return; - } + if (!manifests?.length) return; getEntitlements(); const cleanedManifests = cleanManifestList(manifests); @@ -572,9 +593,19 @@ export async function applyPers(manifests) { expBlocks: consolidateObjects(results, 'blocks'), expFragments: consolidateObjects(results, 'fragments'), }); - const trackingManifests = results.map((r) => r.experiment.manifest.split('/').pop().replace('.json', '')); - const trackingVariants = results.map((r) => r.experiment.selectedVariantName); - document.body.dataset.mep = `${trackingVariants.join('--')}|${trackingManifests.join('--')}`; - - decoratePreviewCheck(config, experiments); + const pznList = results.filter((r) => (r.experiment.manifestType !== NON_TRACKED_MANIFEST_TYPE)); + if (!pznList.length) { + document.body.dataset.mep = 'nopzn|nopzn'; + return; + } + const pznVariants = pznList.map((r) => { + const val = r.experiment.selectedVariantName.replace('target-', '').trim().slice(0, 15); + return val === 'default' ? 'nopzn' : val; + }); + const pznManifests = pznList.map((r) => { + const val = r.experiment?.manifestOverrideName || r.experiment?.manifest; + return val.split('/').pop().replace('.json', '').trim() + .slice(0, 15); + }); + document.body.dataset.mep = `${pznVariants.join('--')}|${pznManifests.join('--')}`; } diff --git a/libs/features/personalization/preview.css b/libs/features/personalization/preview.css index 845e0c32c6..fbc039e739 100644 --- a/libs/features/personalization/preview.css +++ b/libs/features/personalization/preview.css @@ -35,6 +35,10 @@ margin-left: 16px; } +.mep-popup-header > div > div:not(.mep-manifest-page-info-title) { + padding-left: 15px; +} + @media screen and (max-width: 599px) { .mep-hidden .mep-badge .mep-open { margin-left: 0; diff --git a/libs/features/personalization/preview.js b/libs/features/personalization/preview.js index 7d3c775653..f1b79669e0 100644 --- a/libs/features/personalization/preview.js +++ b/libs/features/personalization/preview.js @@ -1,4 +1,5 @@ import { createTag, getConfig, getMetadata, loadStyle, MILO_EVENTS } from '../../utils/utils.js'; +import { NON_TRACKED_MANIFEST_TYPE } from './personalization.js'; function updatePreviewButton() { const selectedInputs = document.querySelectorAll( @@ -83,7 +84,7 @@ async function getEditManifestUrl(a, w) { a.href = editUrl; return true; } - w.location = a.dataset.manifestPath; + w.location = a.dataset.manifestUrl; return false; } @@ -129,41 +130,60 @@ function createPreviewPill(manifests) { let manifestList = ''; const manifestParameter = []; manifests.forEach((manifest) => { - const { variantNames } = manifest; + const { + variantNames, + manifestPath = manifest.manifest, + selectedVariantName, + name, + manifestType, + manifestUrl, + manifestOverrideName, + } = manifest; let radio = ''; variantNames.forEach((variant) => { const checked = { attribute: '', class: '', }; - if (variant === manifest.selectedVariantName) { + if (variant === selectedVariantName) { checked.attribute = 'checked="checked"'; checked.class = 'class="mep-manifest-selected-variant"'; - manifestParameter.push(`${manifest.manifest}--${variant}`); + manifestParameter.push(`${manifestPath}--${variant}`); } radio += `
- - + +
`; }); const checked = { attribute: '', class: '', }; - if (!manifest.variantNames.includes(manifest.selectedVariantName)) { + if (!variantNames.includes(selectedVariantName)) { checked.attribute = 'checked="checked"'; checked.class = 'class="mep-manifest-selected-variant"'; - manifestParameter.push(`${manifest.manifest}--default`); + manifestParameter.push(`${manifestPath}--default`); } radio += `
- - + +
`; - const targetTitle = manifest.name ? `${manifest.name}
${manifest.manifest}` : manifest.manifest; - manifestList += `
+ + const manifestFileName = manifestPath.split('/').pop(); + const targetTitle = name ? `${name}
${manifestFileName}` : manifestFileName; + let analyticsTitle = ''; + if (manifestType === NON_TRACKED_MANIFEST_TYPE) { + analyticsTitle = 'N/A for this manifest type'; + } else if (manifestOverrideName) { + analyticsTitle = manifestOverrideName; + } else { + analyticsTitle = manifestFileName.replace('.json', '').slice(0, 20); + } + manifestList += `
${targetTitle} - + +
@@ -193,6 +213,7 @@ function createPreviewPill(manifests) {

${manifests.length} Manifest(s) served

+
Page Info:
Target integration feature is ${targetOnText}
Personalization feature is ${personalizationOnText}
Page's Locale is ${config.locale.ietf}
@@ -250,11 +271,11 @@ function addMarkerData(manifests) { }); } -export default async function decoratePreviewMode(manifests) { - const { miloLibs, codeRoot } = getConfig(); +export default async function decoratePreviewMode() { + const { miloLibs, codeRoot, experiments } = getConfig(); loadStyle(`${miloLibs || codeRoot}/features/personalization/preview.css`); - addMarkerData(manifests); + addMarkerData(experiments); document.addEventListener(MILO_EVENTS.DEFERRED, () => { - createPreviewPill(manifests); + createPreviewPill(experiments); }, { once: true }); } diff --git a/libs/martech/analytics.js b/libs/martech/analytics.js index f4d53b2f65..7434690478 100644 --- a/libs/martech/analytics.js +++ b/libs/martech/analytics.js @@ -1,9 +1,16 @@ -export function processTrackingLabels(text, charLimit = 20) { - return text?.trim().replace(/\s+/g, ' ').split('|').join(' ') +export function processTrackingLabels(text, config, charLimit = 20) { + if (!config) { + import('../utils/utils.js').then((utils) => { + // eslint-disable-next-line no-param-reassign + config = utils.getConfig(); + }); + } + const analyticsValue = text?.replace(/[^\u00C0-\u1FFF\u2C00-\uD7FF\w]+/g, ' ').replace(/^_+|_+$/g, '').trim() .slice(0, charLimit); + return analyticsValue; } -export function decorateDefaultLinkAnalytics(block) { +export function decorateDefaultLinkAnalytics(block, config) { if (block.classList.length && !block.className.includes('metadata') && !block.classList.contains('link-block') @@ -22,18 +29,18 @@ export function decorateDefaultLinkAnalytics(block) { || item.querySelector('img')?.getAttribute('alt') || 'no label'; } - label = processTrackingLabels(label); - item.setAttribute('daa-ll', `${label}-${linkCount}|${header}`); + label = processTrackingLabels(label, config); + item.setAttribute('daa-ll', `${label}-${linkCount}--${header}`); } linkCount += 1; } else { - header = processTrackingLabels(item.textContent); + header = processTrackingLabels(item.textContent, config); } }); } } -export async function decorateSectionAnalytics(section, idx) { +export async function decorateSectionAnalytics(section, idx, config) { document.querySelector('main')?.setAttribute('daa-im', 'true'); section.setAttribute('daa-lh', `s${idx + 1}`); section.querySelectorAll('[data-block] [data-block]').forEach((block) => { @@ -41,8 +48,8 @@ export async function decorateSectionAnalytics(section, idx) { }); section.querySelectorAll('[data-block]').forEach((block, blockIdx) => { const blockName = block.classList[0] || ''; - block.setAttribute('daa-lh', `b${blockIdx + 1}|${blockName}|${document.body.dataset.mep}`); - decorateDefaultLinkAnalytics(block); + block.setAttribute('daa-lh', `b${blockIdx + 1}|${blockName.slice(0, 15)}|${document.body.dataset.mep}`); + decorateDefaultLinkAnalytics(block, config); block.removeAttribute('data-block'); }); } diff --git a/libs/martech/attributes.js b/libs/martech/attributes.js index 9b450df998..7f7e850c8b 100644 --- a/libs/martech/attributes.js +++ b/libs/martech/attributes.js @@ -1,10 +1,65 @@ -const RE_ALPHANUM = /[^0-9a-z]/gi; -const RE_TRIM_UNDERSCORE = /^_+|_+$/g; +export function processTrackingLabels(text, config, charLimit = 20) { + if (!config) { + import('../utils/utils.js').then((utils) => { + // eslint-disable-next-line no-param-reassign + config = utils.getConfig(); + }); + } + const analyticsValue = text?.replace(/[^\u00C0-\u1FFF\u2C00-\uD7FF\w]+/g, ' ').replace(/^_+|_+$/g, '').trim() + .slice(0, charLimit); + return analyticsValue; +} + +export function decorateDefaultLinkAnalytics(block, config) { + if (block.classList.length + && !block.className.includes('metadata') + && !block.classList.contains('link-block') + && !block.classList.contains('section') + && block.nodeName === 'DIV') { + let header = ''; + let linkCount = 1; + block.querySelectorAll('h1, h2, h3, h4, h5, h6, a:not(.video.link-block), button, .tracking-header') + .forEach((item) => { + if (item.nodeName === 'A' || item.nodeName === 'BUTTON') { + if (!item.hasAttribute('daa-ll')) { + let label = item.textContent?.trim(); + if (label === '') { + label = item.getAttribute('title') + || item.getAttribute('aria-label') + || item.querySelector('img')?.getAttribute('alt') + || 'no label'; + } + label = processTrackingLabels(label, config); + item.setAttribute('daa-ll', `${label}-${linkCount}--${header}`); + } + linkCount += 1; + } else { + header = processTrackingLabels(item.textContent, config); + } + }); + } +} -// decorateBlockAnalytics & decorateLinkAnalytics are being sunset +export async function decorateSectionAnalytics(section, idx, config) { + document.querySelector('main')?.setAttribute('daa-im', 'true'); + section.setAttribute('daa-lh', `s${idx + 1}`); + section.querySelectorAll('[data-block] [data-block]').forEach((block) => { + block.removeAttribute('data-block'); + }); + section.querySelectorAll('[data-block]').forEach((block, blockIdx) => { + const blockName = block.classList[0] || ''; + block.setAttribute('daa-lh', `b${blockIdx + 1}|${blockName.slice(0, 15)}|${document.body.dataset.mep}`); + decorateDefaultLinkAnalytics(block, config); + block.removeAttribute('data-block'); + }); +} + +// below functions are being sunset export function decorateBlockAnalytics() { return false; } export function decorateLinkAnalytics() { return false; } +const RE_ALPHANUM = /[^0-9a-z]/gi; +const RE_TRIM_UNDERSCORE = /^_+|_+$/g; export const analyticsGetLabel = (txt) => txt.replaceAll('&', 'and') .replace(RE_ALPHANUM, '_') .replace(RE_TRIM_UNDERSCORE, ''); diff --git a/libs/martech/attributes.md b/libs/martech/attributes.md index e2f782023a..ff8818d711 100644 --- a/libs/martech/attributes.md +++ b/libs/martech/attributes.md @@ -1,6 +1,86 @@ +# Adobe analytics +https://wiki.corp.adobe.com/display/marketingtech/Analytics +> Add tracking attributes to the DOM. +NOTE: most blocks do not need to add custom analytics. If you do need to add custom analytics to a block, you must follow 2 rules: +1. do not add daa-lh to any element on or inside the blocks +2. if you add daa-ll to an element, you must have exactly 2 "levels". The levels are separated by the pipe character. + a. Level 1 is usually the link text and number + b. Level 2 is usually the header text + Example: Buy now-2|Everyone can Photosh + Even if you have no header text, you must still have 2 levels. + Example: Buy now-2| + + + + +## function list + +### processTrackingLabels + +> Used in decorateDefaultLinkAnalytics. Can be used inside a block to process text for use in daa-ll. +Input a string and outputs the clean string. Optional 2nd parameter of character length. + +### decorateDefaultLinkAnalytics + +> Used in decorateSectionAnalytics, so every block will be passed through this function. +Can be used inside a block to add daa-ll attributes. Does not overwrite existing daa-ll attributes. +You only need to call this function in the block if you need the daa-ll attributes added during block decoration. +Input the block element no return value. + +### decorateSectionAnalytics + +> Used in utils post block decoration. +1. Adds daa-lh to body with information about personalization and testing +2. Adds daa-im="true" to main +3. Adds a daa-lh to sections with the number. Example: daa-lh="s2" +4. Adds a daa-lh to blocks with the in and number. Example: daa-lh="b3|homepage-brick|smb--var1-Golf marquee|homepage--ACE0759" +5. Runs each block through decorateDefaultLinkAnalytics + + + + +## types of tracking +> tracking combines the daa-ll value with all daa-lh values on container elements. But the combined order may surprise you. Let's say you had the following DOM. +section: daa-lh="s2" +block: daa-lh="b3|homepage-brick|smb--var1-Golf marquee|homepage--ACE0759" +link: daa-ll="Online PDF tools-5|Acrobat" + +The combined analytic would be `Online PDF tools-5|Acrobat|s2|b3|homepage-brick|smb--var1-Golf marquee|homepage--ACE0759` +This order is the reason the personalization and testing information is repeated on each block. If an analytic is truncated due to length, it is mostly likely to be cut off. + +### click tracking +> Any link with daa-ll will send tracking when a user clicks an element. Same value as impression tracking but limited to 100 characters. + +### impression tracking +> Only links with daa-ll and under a daa-im="true" value sent. Sent after page load (sometimes on unload). Same value as click tracking but limited to 250 characters per link. + + + + +## attribute list + +### `daa-im` + +Simple attribute flag if set to true will capture impressions for each link in that container. Added to header and main but NOT footer. + +### `daa-lh` + +Hierarchy values that are used to create a complete view of where each interaction is located. + +Our code will combine hierarchy values to create where (unique identifier) for every interaction. + +### `daa-ll` + +Interaction identifier. See above for strict rules on what format should be used if setting custom daa-ll values. + + + + + +###### # Sunset functions -> All functions in this file are being deprecated. Use functions in analytics.js instead +> Previous functions in this file are being deprecated. ## decorateBlockAnalytics diff --git a/libs/martech/martech.js b/libs/martech/martech.js index 30c79d31e5..94ae871c5b 100644 --- a/libs/martech/martech.js +++ b/libs/martech/martech.js @@ -53,7 +53,10 @@ const handleAlloyResponse = (response) => { return { manifestPath: content.manifestLocation || content.manifestPath, + manifestUrl: content.manifestLocation, manifestData: content.manifestContent?.experiences?.data || content.manifestContent?.data, + manifestPlaceholders: content.manifestContent?.placeholders?.data, + manifestInfo: content.manifestContent?.info.data, name: item.meta['activity.name'], variantLabel: item.meta['experience.name'] && `target-${item.meta['experience.name']}`, meta: item.meta, @@ -105,10 +108,6 @@ export default async function init({ persEnabled = false, persManifests }) { `${config.miloLibs || config.codeRoot}/features/personalization/personalization.js`, { as: 'script', rel: 'modulepreload' }, ); - loadLink( - `${config.miloLibs || config.codeRoot}/features/personalization/manifest-utils.js`, - { as: 'script', rel: 'modulepreload' }, - ); } setDeep( @@ -120,7 +119,7 @@ export default async function init({ persEnabled = false, persManifests }) { window.marketingtech = { adobe: { - launch: { url, controlPageLoad: false }, + launch: { url, controlPageLoad: true }, alloy: { edgeConfigId }, target: false, }, @@ -134,14 +133,13 @@ export default async function init({ persEnabled = false, persManifests }) { if (persEnabled) { const targetManifests = await getTargetPersonalization(); - if (targetManifests || persManifests?.length) { - const [{ preloadManifests }, { applyPers, getEntitlements }] = await Promise.all([ - import('../features/personalization/manifest-utils.js'), - import('../features/personalization/personalization.js'), - ]); + if (targetManifests?.length || persManifests?.length) { + const { preloadManifests, applyPers, getEntitlements } = await import('../features/personalization/personalization.js'); getEntitlements(); const manifests = preloadManifests({ targetManifests, persManifests }); await applyPers(manifests); + } else { + document.body.dataset.mep = 'nopzn|nopzn'; } } } diff --git a/libs/templates/404/404.js b/libs/templates/404/404.js index 84b50cd3ac..bcbcef6698 100644 --- a/libs/templates/404/404.js +++ b/libs/templates/404/404.js @@ -12,7 +12,7 @@ async function get404(path) { const main = document.body.querySelector('main'); main.append(section); await loadArea(main); - import('../../martech/analytics.js').then((analytics) => { + import('../../martech/attributes.js').then((analytics) => { document.querySelectorAll('main > div').forEach((area, idx) => analytics.decorateSectionAnalytics(area, idx)); }); } diff --git a/libs/utils/utils.js b/libs/utils/utils.js index 40fb59e128..f6832402ee 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -799,10 +799,6 @@ async function checkForPageMods() { `${base}/features/personalization/personalization.js`, { as: 'script', rel: 'modulepreload' }, ); - loadLink( - `${base}/features/personalization/manifest-utils.js`, - { as: 'script', rel: 'modulepreload' }, - ); } if (persEnabled) { @@ -812,8 +808,10 @@ async function checkForPageMods() { } const { env } = getConfig(); + let previewOn = false; const mep = PAGE_URL.searchParams.get('mep'); if (mep !== null || (env?.name !== 'prod' && (persEnabled || targetEnabled))) { + previewOn = true; const { default: addPreviewToConfig } = await import('../features/personalization/add-preview-to-config.js'); persManifests = await addPreviewToConfig({ pageUrl: PAGE_URL, @@ -827,14 +825,17 @@ async function checkForPageMods() { await loadMartech({ persEnabled: true, persManifests, targetMd }); } else if (persManifests.length) { loadIms().catch(() => {}); - const { preloadManifests } = await import('../features/personalization/manifest-utils.js'); + const { preloadManifests, applyPers } = await import('../features/personalization/personalization.js'); const manifests = preloadManifests({ persManifests }, { getConfig, loadLink }); - const { applyPers } = await import('../features/personalization/personalization.js'); - await applyPers(manifests); } else { - document.body.dataset.mep = 'default|default'; + document.body.dataset.mep = 'nopzn|nopzn'; + } + + if (previewOn) { + import('../features/personalization/preview.js') + .then(({ default: decoratePreviewMode }) => decoratePreviewMode()); } } @@ -990,8 +991,8 @@ async function documentPostSectionLoading(config) { const { default: delayed } = await import('../scripts/delayed.js'); delayed([getConfig, getMetadata, loadScript, loadStyle, loadIms]); - import('../martech/analytics.js').then((analytics) => { - document.querySelectorAll('main > div').forEach((section, idx) => analytics.decorateSectionAnalytics(section, idx)); + import('../martech/attributes.js').then((analytics) => { + document.querySelectorAll('main > div').forEach((section, idx) => analytics.decorateSectionAnalytics(section, idx, config)); }); } diff --git a/test/blocks/accordion/accordion.test.js b/test/blocks/accordion/accordion.test.js index 0d1126815d..f72c34d6d1 100644 --- a/test/blocks/accordion/accordion.test.js +++ b/test/blocks/accordion/accordion.test.js @@ -33,15 +33,15 @@ describe('Accordion', () => { // handleClick() const firstAccordionButton = document.body.querySelector('dt button'); expect(firstAccordionButton.getAttribute('aria-expanded')).to.equal('false'); - expect(firstAccordionButton.getAttribute('daa-ll')).to.equal('open-1|What if my dough did'); + expect(firstAccordionButton.getAttribute('daa-ll')).to.equal('open-1--What if my dough did'); firstAccordionButton.click(); expect(firstAccordionButton.getAttribute('aria-expanded')).to.equal('true'); - expect(firstAccordionButton.getAttribute('daa-ll')).to.equal('close-1|What if my dough did'); + expect(firstAccordionButton.getAttribute('daa-ll')).to.equal('close-1--What if my dough did'); // handleClick() => expanded = true. firstAccordionButton.click(); expect(firstAccordionButton.getAttribute('aria-expanded')).to.equal('false'); - expect(firstAccordionButton.getAttribute('daa-ll')).to.equal('open-1|What if my dough did'); + expect(firstAccordionButton.getAttribute('daa-ll')).to.equal('open-1--What if my dough did'); // ensure

is kept expect(firstAccordionButton.parentElement.tagName).to.equal('H1'); diff --git a/test/blocks/global-navigation/utilities/utilities.test.js b/test/blocks/global-navigation/utilities/utilities.test.js index 2d6c8bc74a..33dcf703e6 100644 --- a/test/blocks/global-navigation/utilities/utilities.test.js +++ b/test/blocks/global-navigation/utilities/utilities.test.js @@ -57,8 +57,8 @@ describe('global navigation utilities', () => { it('getAnalyticsValue should return a string', () => { expect(getAnalyticsValue('test')).to.equal('test'); - expect(getAnalyticsValue('test test')).to.equal('test_test'); - expect(getAnalyticsValue('test test 1', 2)).to.equal('test_test_1-2'); + expect(getAnalyticsValue('test test?')).to.equal('test test'); + expect(getAnalyticsValue('test test 1?', 2)).to.equal('test test 1-2'); }); describe('decorateCta', () => { diff --git a/test/features/personalization/mocks/manifestTestOrPromo.json b/test/features/personalization/mocks/manifestTestOrPromo.json new file mode 100644 index 0000000000..24ec52fae0 --- /dev/null +++ b/test/features/personalization/mocks/manifestTestOrPromo.json @@ -0,0 +1,44 @@ +{ + "info":{ + "total":2, + "offset":0, + "limit":2, + "data":[ + { + "key":"manifest-type", + "value":"Test or Promo" + }, + { + "key":"manifest-override-name", + "value":"" + } + ] + }, + "experiences":{ + "total":1, + "offset":0, + "limit":1, + "data":[ + { + "action":"replaceContent", + "selector":".marquee", + "page filter (optional)":"", + "android":"https://main--milo--adobecom.hlx.page/drafts/vgoodrich/fragments/139173-mep-and/android" + } + ] + }, + "placeholders":{ + "total":0, + "offset":0, + "limit":0, + "data":[] + }, + ":version":3, + ":names":[ + "info", + "experiences", + "placeholders" + ], + ":type":"multi-sheet" +} + diff --git a/test/features/personalization/mocks/personalization.html b/test/features/personalization/mocks/personalization.html index ef2bcf65a9..c99690ed34 100644 --- a/test/features/personalization/mocks/personalization.html +++ b/test/features/personalization/mocks/personalization.html @@ -1,4 +1,4 @@ - +
@@ -111,3 +111,4 @@

How to leverage Milo Expe

+ diff --git a/test/features/personalization/personalization.test.js b/test/features/personalization/personalization.test.js index 7b91b3145e..6e5edb3958 100644 --- a/test/features/personalization/personalization.test.js +++ b/test/features/personalization/personalization.test.js @@ -21,6 +21,15 @@ const setFetchResponse = (data, type = 'json') => { // Note that the manifestPath doesn't matter as we stub the fetch describe('Functional Test', () => { + it('test or promo manifest', async () => { + let manifestJson = await readFile({ path: './mocks/manifestTestOrPromo.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + expect(document.body.dataset.mep).to.equal('nopzn|nopzn'); + }); + it('replaceContent should replace an element with a fragment', async () => { let manifestJson = await readFile({ path: './mocks/manifestReplace.json' }); manifestJson = JSON.parse(manifestJson); diff --git a/test/martech/analytics.test.js b/test/martech/attributes.test.js similarity index 87% rename from test/martech/analytics.test.js rename to test/martech/attributes.test.js index 088191d711..45e9c8b1ac 100644 --- a/test/martech/analytics.test.js +++ b/test/martech/attributes.test.js @@ -4,7 +4,7 @@ import { expect } from '@esm-bundle/chai'; describe('Analytics', async () => { beforeEach(async () => { await readFile({ path: './mocks/body.html' }); - const analytics = await import('../../libs/martech/analytics.js'); + const analytics = await import('../../libs/martech/attributes.js'); document.body.outerHTML = await readFile({ path: './mocks/body.html' }); document.querySelectorAll('main > div').forEach((section, idx) => analytics.decorateSectionAnalytics(section, idx)); }); @@ -16,6 +16,6 @@ describe('Analytics', async () => { const block = section.querySelector(':scope > div')?.getAttribute('daa-lh'); expect(block).to.equal('b1|icon-block|smb|hp'); const link = section.querySelector('#unit-test')?.getAttribute('daa-ll'); - expect(link).to.equal('Learn more-3|Do more with Adobe P'); + expect(link).to.equal('Learn more-3--Do more with Adobe P'); }); });