diff --git a/blocks/footer/footer.js b/blocks/footer/footer.js index fa0093819..462941411 100644 --- a/blocks/footer/footer.js +++ b/blocks/footer/footer.js @@ -1,11 +1,11 @@ -import { readBlockConfig, decorateIcons } from '../../scripts/lib-franklin.js'; +import { readBlockConfig } from '../../scripts/lib-franklin.js'; /** * loads and decorates the footer * @param {Element} block The header block element */ -export default async function decorate(block) { +export default async function decorate(block, plugins) { const cfg = readBlockConfig(block); block.textContent = ''; @@ -14,6 +14,6 @@ export default async function decorate(block) { const html = await resp.text(); const footer = document.createElement('div'); footer.innerHTML = html; - await decorateIcons(footer); + await plugins.decorator.decorateIcons(footer); block.append(footer); } diff --git a/blocks/header/header.js b/blocks/header/header.js index bce97e9c6..738b81ec9 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -1,4 +1,4 @@ -import { readBlockConfig, decorateIcons } from '../../scripts/lib-franklin.js'; +import { readBlockConfig } from '../../scripts/lib-franklin.js'; /** * collapses all open nav sections @@ -16,7 +16,7 @@ function collapseAllNavSections(sections) { * @param {Element} block The header block element */ -export default async function decorate(block) { +export default async function decorate(block, plugins) { const cfg = readBlockConfig(block); block.textContent = ''; @@ -29,7 +29,7 @@ export default async function decorate(block) { // decorate nav DOM const nav = document.createElement('nav'); nav.innerHTML = html; - decorateIcons(nav); + plugins.decorator.decorateIcons(nav); const classes = ['brand', 'sections', 'tools']; classes.forEach((e, j) => { @@ -60,7 +60,7 @@ export default async function decorate(block) { }); nav.prepend(hamburger); nav.setAttribute('aria-expanded', 'false'); - decorateIcons(nav); + plugins.decorator.decorateIcons(nav); block.append(nav); } } diff --git a/head.html b/head.html index e34c617b5..ff8390ffa 100644 --- a/head.html +++ b/head.html @@ -1,4 +1,5 @@ + diff --git a/scripts/delayed.js b/scripts/delayed.js index 920b4ad83..06c1c4122 100644 --- a/scripts/delayed.js +++ b/scripts/delayed.js @@ -1,7 +1 @@ -// eslint-disable-next-line import/no-cycle -import { sampleRUM } from './lib-franklin.js'; - -// Core Web Vitals RUM collection -sampleRUM('cwv'); - -// add more delayed functionality here +// Execute anything that can be postponed to the latest here diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index bd0635779..33de7da16 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -10,68 +10,102 @@ * governing permissions and limitations under the License. */ -/** - * log RUM if part of the sample. - * @param {string} checkpoint identifies the checkpoint in funnel - * @param {Object} data additional data for RUM sample - */ -export function sampleRUM(checkpoint, data = {}) { - sampleRUM.defer = sampleRUM.defer || []; - const defer = (fnname) => { - sampleRUM[fnname] = sampleRUM[fnname] - || ((...args) => sampleRUM.defer.push({ fnname, args })); - }; - sampleRUM.drain = sampleRUM.drain - || ((dfnname, fn) => { - sampleRUM[dfnname] = fn; - sampleRUM.defer - .filter(({ fnname }) => dfnname === fnname) - .forEach(({ fnname, args }) => sampleRUM[fnname](...args)); - }); - sampleRUM.on = (chkpnt, fn) => { sampleRUM.cases[chkpnt] = fn; }; - defer('observe'); - defer('cwv'); - try { - window.hlx = window.hlx || {}; - if (!window.hlx.rum) { - const usp = new URLSearchParams(window.location.search); - const weight = (usp.get('rum') === 'on') ? 1 : 100; // with parameter, weight is 1. Defaults to 100. - // eslint-disable-next-line no-bitwise - const hashCode = (s) => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0); - const id = `${hashCode(window.location.href)}-${new Date().getTime()}-${Math.random().toString(16).substr(2, 14)}`; - const random = Math.random(); - const isSelected = (random * weight < 1); - // eslint-disable-next-line object-curly-newline - window.hlx.rum = { weight, id, random, isSelected, sampleRUM }; - } - const { weight, id } = window.hlx.rum; - if (window.hlx && window.hlx.rum && window.hlx.rum.isSelected) { - const sendPing = (pdata = data) => { - // eslint-disable-next-line object-curly-newline, max-len, no-use-before-define - const body = JSON.stringify({ weight, id, referer: window.location.href, generation: window.hlx.RUM_GENERATION, checkpoint, ...data }); - const url = `https://rum.hlx.page/.rum/${weight}`; - // eslint-disable-next-line no-unused-expressions - navigator.sendBeacon(url, body); - // eslint-disable-next-line no-console - console.debug(`ping:${checkpoint}`, pdata); - }; - sampleRUM.cases = sampleRUM.cases || { - cwv: () => sampleRUM.cwv(data) || true, - lazy: () => { - // use classic script to avoid CORS issues - const script = document.createElement('script'); - script.src = 'https://rum.hlx.page/.rum/@adobe/helix-rum-enhancer@^1/src/index.js'; - document.head.appendChild(script); - return true; - }, - }; - sendPing(data); - if (sampleRUM.cases[checkpoint]) { sampleRUM.cases[checkpoint](); } +export const RumPlugin = () => { + /** + * log RUM if part of the sample. + * @param {string} checkpoint identifies the checkpoint in funnel + * @param {Object} data additional data for RUM sample + */ + function sampleRUM(checkpoint, data = {}) { + sampleRUM.defer = sampleRUM.defer || []; + const defer = (fnname) => { + sampleRUM[fnname] = sampleRUM[fnname] + || ((...args) => sampleRUM.defer.push({ fnname, args })); + }; + sampleRUM.drain = sampleRUM.drain + || ((dfnname, fn) => { + sampleRUM[dfnname] = fn; + sampleRUM.defer + .filter(({ fnname }) => dfnname === fnname) + .forEach(({ fnname, args }) => sampleRUM[fnname](...args)); + }); + sampleRUM.on = (chkpnt, fn) => { sampleRUM.cases[chkpnt] = fn; }; + defer('observe'); + defer('cwv'); + try { + window.hlx = window.hlx || {}; + if (!window.hlx.rum) { + const usp = new URLSearchParams(window.location.search); + const weight = (usp.get('rum') === 'on') ? 1 : 100; // with parameter, weight is 1. Defaults to 100. + // eslint-disable-next-line no-bitwise + const hashCode = (s) => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0); + const id = `${hashCode(window.location.href)}-${new Date().getTime()}-${Math.random().toString(16).substr(2, 14)}`; + const random = Math.random(); + const isSelected = (random * weight < 1); + // eslint-disable-next-line object-curly-newline + window.hlx.rum = { weight, id, random, isSelected, sampleRUM }; + } + const { weight, id } = window.hlx.rum; + if (window.hlx && window.hlx.rum && window.hlx.rum.isSelected) { + const sendPing = (pdata = data) => { + // eslint-disable-next-line object-curly-newline, max-len, no-use-before-define + const body = JSON.stringify({ weight, id, referer: window.location.href, generation: window.hlx.RUM_GENERATION, checkpoint, ...data }); + const url = `https://rum.hlx.page/.rum/${weight}`; + // eslint-disable-next-line no-unused-expressions + navigator.sendBeacon(url, body); + // eslint-disable-next-line no-console + console.debug(`ping:${checkpoint}`, pdata); + }; + sampleRUM.cases = sampleRUM.cases || { + cwv: () => sampleRUM.cwv(data) || true, + lazy: () => { + // use classic script to avoid CORS issues + const script = document.createElement('script'); + script.src = 'https://rum.hlx.page/.rum/@adobe/helix-rum-enhancer@^1/src/index.js'; + document.head.appendChild(script); + return true; + }, + }; + sendPing(data); + if (sampleRUM.cases[checkpoint]) { sampleRUM.cases[checkpoint](); } + } + } catch (error) { + // something went wrong } - } catch (error) { - // something went wrong } -} + + sampleRUM('top'); + + window.addEventListener('load', () => sampleRUM('load')); + + return { + name: 'rum', + + api: { + sampleRUM, + }, + + preEager: async () => { + window.addEventListener('unhandledrejection', (event) => { + sampleRUM('error', { source: event.reason.sourceURL, target: event.reason.line }); + }); + window.addEventListener('error', (event) => { + sampleRUM('error', { source: event.filename, target: event.lineno }); + }); + }, + + postLazy: async () => { + const main = document.querySelector('main'); + sampleRUM('lazy'); + sampleRUM.observe(main.querySelectorAll('div[data-block-name]')); + sampleRUM.observe(main.querySelectorAll('picture > img')); + }, + + preDelayed: async () => { + sampleRUM('cwv'); + }, + }; +}; /** * Loads a CSS file. @@ -123,77 +157,56 @@ export function toCamelCase(name) { return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); } -/** - * Replace icons with inline SVG and prefix with codeBasePath. - * @param {Element} element - */ -export function decorateIcons(element = document) { - element.querySelectorAll('span.icon').forEach(async (span) => { - if (span.classList.length < 2 || !span.classList[1].startsWith('icon-')) { - return; - } - const icon = span.classList[1].substring(5); - // eslint-disable-next-line no-use-before-define - const resp = await fetch(`${window.hlx.codeBasePath}/icons/${icon}.svg`); - if (resp.ok) { - const iconHTML = await resp.text(); - if (iconHTML.match(/