diff --git a/.storybook/main.js b/.storybook/main.js index d1776b26..eb6f4aaf 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -7,7 +7,7 @@ module.exports = { name: '@storybook/addon-coverage', options: { istanbul: { - exclude: ['**/lib-franklin.js', '**/scripts.js'] + exclude: ['**/aem.js', '**/scripts.js'] } } }, '@storybook/addon-mdx-gfm'], diff --git a/404.html b/404.html index c21fc249..291959fe 100644 --- a/404.html +++ b/404.html @@ -11,7 +11,7 @@ + diff --git a/scripts/lib-franklin.js b/scripts/aem.js similarity index 53% rename from scripts/lib-franklin.js rename to scripts/aem.js index 4a568001..af8f44a9 100644 --- a/scripts/lib-franklin.js +++ b/scripts/aem.js @@ -1,5 +1,5 @@ /* - * Copyright 2022 Adobe. All rights reserved. + * Copyright 2023 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -10,16 +10,21 @@ * governing permissions and limitations under the License. */ +/* eslint-env browser */ + /** * log RUM if part of the sample. * @param {string} checkpoint identifies the checkpoint in funnel * @param {Object} data additional data for RUM sample + * @param {string} data.source DOM node that is the source of a checkpoint event, + * identified by #id or .classname + * @param {string} data.target subject of the checkpoint event, + * for instance the href of a link, or a search term */ -export function sampleRUM(checkpoint, data = {}) { +function sampleRUM(checkpoint, data = {}) { sampleRUM.defer = sampleRUM.defer || []; const defer = (fnname) => { - sampleRUM[fnname] = sampleRUM[fnname] - || ((...args) => sampleRUM.defer.push({ fnname, args })); + sampleRUM[fnname] = sampleRUM[fnname] || ((...args) => sampleRUM.defer.push({ fnname, args })); }; sampleRUM.drain = sampleRUM.drain || ((dfnname, fn) => { @@ -28,27 +33,72 @@ export function sampleRUM(checkpoint, data = {}) { .filter(({ fnname }) => dfnname === fnname) .forEach(({ fnname, args }) => sampleRUM[fnname](...args)); }); - sampleRUM.on = (chkpnt, fn) => { sampleRUM.cases[chkpnt] = fn; }; + sampleRUM.always = sampleRUM.always || []; + sampleRUM.always.on = (chkpnt, fn) => { + sampleRUM.always[chkpnt] = fn; + }; + 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 weight = usp.get('rum') === 'on' ? 1 : 100; // with parameter, weight is 1. Defaults to 100. + const id = Array.from({ length: 75 }, (_, i) => String.fromCharCode(48 + i)) + .filter((a) => /\d|[A-Z]/i.test(a)) + .filter(() => Math.random() * 75 > 70) + .join(''); 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 isSelected = random * weight < 1; + const firstReadTime = Date.now(); + const urlSanitizers = { + full: () => window.location.href, + origin: () => window.location.origin, + path: () => window.location.href.replace(/\?.*$/, ''), + }; + // eslint-disable-next-line object-curly-newline, max-len + window.hlx.rum = { + weight, + id, + random, + isSelected, + firstReadTime, + sampleRUM, + sanitizeURL: urlSanitizers[window.hlx.RUM_MASK_URL || 'path'], + }; } - const { weight, id } = window.hlx.rum; + const { weight, id, firstReadTime } = window.hlx.rum; if (window.hlx && window.hlx.rum && window.hlx.rum.isSelected) { + const knownProperties = [ + 'weight', + 'id', + 'referer', + 'checkpoint', + 't', + 'source', + 'target', + 'cwv', + 'CLS', + 'FID', + 'LCP', + 'INP', + ]; 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 body = JSON.stringify( + { + weight, + id, + referer: window.hlx.rum.sanitizeURL(), + checkpoint, + t: Date.now() - firstReadTime, + ...data, + }, + knownProperties, + ); const url = `https://rum.hlx.page/.rum/${weight}`; // eslint-disable-next-line no-unused-expressions navigator.sendBeacon(url, body); @@ -66,7 +116,12 @@ export function sampleRUM(checkpoint, data = {}) { }, }; sendPing(data); - if (sampleRUM.cases[checkpoint]) { sampleRUM.cases[checkpoint](); } + if (sampleRUM.cases[checkpoint]) { + sampleRUM.cases[checkpoint](); + } + } + if (sampleRUM.always[checkpoint]) { + sampleRUM.always[checkpoint](data); } } catch (error) { // something went wrong @@ -74,132 +129,66 @@ export function sampleRUM(checkpoint, data = {}) { } /** - * Loads a CSS file. - * @param {string} href The path to the CSS file + * Setup block utils. */ -export function loadCSS(href, callback) { - if (!document.querySelector(`head > link[href="${href}"]`)) { - const link = document.createElement('link'); - link.setAttribute('rel', 'stylesheet'); - link.setAttribute('href', href); - if (typeof callback === 'function') { - link.onload = (e) => callback(e.type); - link.onerror = (e) => callback(e.type); +function setup() { + window.hlx = window.hlx || {}; + window.hlx.RUM_MASK_URL = 'full'; + window.hlx.codeBasePath = ''; + window.hlx.lighthouse = new URLSearchParams(window.location.search).get('lighthouse') === 'on'; + + const scriptEl = document.querySelector('script[src$="/scripts/scripts.js"]'); + if (scriptEl) { + try { + [window.hlx.codeBasePath] = new URL(scriptEl.src).pathname.split('/scripts/scripts.js'); + } catch (error) { + // eslint-disable-next-line no-console + console.log(error); } - document.head.appendChild(link); - } else if (typeof callback === 'function') { - callback('noop'); } } /** - * Retrieves the content of metadata tags. - * @param {string} name The metadata name (or property) - * @returns {string} The metadata value(s) + * Auto initializiation. */ -export function getMetadata(name) { - const attr = name && name.includes(':') ? 'property' : 'name'; - const meta = [...document.head.querySelectorAll(`meta[${attr}="${name}"]`)].map((m) => m.content).join(', '); - return meta || ''; -} -/** - * Sanitizes a name for use as class name. - * @param {string} name The unsanitized name - * @returns {string} The class name - */ -export function toClassName(name) { - return typeof name === 'string' - ? name.toLowerCase().replace(/[^0-9a-z]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') - : ''; -} +function init() { + setup(); + sampleRUM('top'); -/* - * Sanitizes a name for use as a js property name. - * @param {string} name The unsanitized name - * @returns {string} The camelCased name - */ -export function toCamelCase(name) { - return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase()); -} + window.addEventListener('load', () => sampleRUM('load')); -/** - * 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.__STORYBOOK_PREVIEW__) ? window.hlx.codeBasePath : ''}/icons/${icon}.svg`); // eslint-disable-line no-underscore-dangle - if (resp.ok) { - const iconHTML = await resp.text(); - if (iconHTML.match(/