diff --git a/plugins/experimentation/src/index.js b/plugins/experimentation/src/index.js index bf354993..86b96ea2 100644 --- a/plugins/experimentation/src/index.js +++ b/plugins/experimentation/src/index.js @@ -89,12 +89,13 @@ async function replaceInner(path, main) { const html = await resp.text(); // parse with DOMParser to guarantee valid HTML, and no script execution(s) const dom = new DOMParser().parseFromString(html, 'text/html'); - // eslint-disable-next-line no-param-reassign - main.replaceWith(dom.querySelector('main')); + // do not use replaceWith API here since this would replace the main reference + // in scripts.js as well and prevent proper decoration of the sections/blocks + main.innerHTML = dom.querySelector('main').innerHTML; return path; } catch (e) { // eslint-disable-next-line no-console - console.log(`error loading content: ${plainPath}`, e); + console.log(`error loading content: ${path}`, e); } return null; } @@ -210,7 +211,7 @@ function inferEmptyPercentageSplits(variants) { if (variantsWithoutPercentage.length) { const missingPercentage = remainingPercentage / variantsWithoutPercentage.length; variantsWithoutPercentage.forEach((v) => { - v.percentageSplit = missingPercentage.toFixed(2); + v.percentageSplit = missingPercentage.toFixed(4); }); } } @@ -240,12 +241,15 @@ function getConfigForInstantExperiment( variantNames: [], }; - const pages = instantExperiment.split(',').map((p) => new URL(p.trim(), window.location).pathname); + const nbOfVariants = Number(instantExperiment); + const pages = Number.isNaN(nbOfVariants) + ? instantExperiment.split(',').map((p) => new URL(p.trim(), window.location).pathname) + : new Array(nbOfVariants).fill(window.location.pathname); const splitString = context.getMetadata(`${pluginOptions.experimentsMetaTag}-split`); const splits = splitString // custom split - ? splitString.split(',').map((i) => parseInt(i, 10) / 100) + ? splitString.split(',').map((i) => parseFloat(i) / 100) // even split fallback : [...new Array(pages.length)].map(() => 1 / (pages.length + 1)); @@ -261,7 +265,7 @@ function getConfigForInstantExperiment( const vname = `challenger-${i + 1}`; config.variantNames.push(vname); config.variants[vname] = { - percentageSplit: `${splits[i].toFixed(2)}`, + percentageSplit: `${splits[i].toFixed(4)}`, pages: [page], blocks: [], label: `Challenger ${i + 1}`, @@ -435,6 +439,8 @@ export async function runExperiment(document, options, context) { console.debug(`running experiment (${window.hlx.experiment.id}) -> ${window.hlx.experiment.selectedVariant}`); if (experimentConfig.selectedVariant === experimentConfig.variantNames[0]) { + document.body.classList.add(`experiment-${context.toClassName(experimentConfig.id)}`); + document.body.classList.add(`variant-${context.toClassName(experimentConfig.selectedVariant)}`); context.sampleRUM('experiment', { source: experimentConfig.id, target: experimentConfig.selectedVariant, @@ -450,13 +456,18 @@ export async function runExperiment(document, options, context) { const currentPath = window.location.pathname; const control = experimentConfig.variants[experimentConfig.variantNames[0]]; const index = control.pages.indexOf(currentPath); - if (index < 0 || pages[index] === currentPath) { + if (index < 0) { return false; } // Fullpage content experiment document.body.classList.add(`experiment-${context.toClassName(experimentConfig.id)}`); - const result = await replaceInner(pages[index], document.querySelector('main')); + let result; + if (pages[index] !== currentPath) { + result = await replaceInner(pages[index], document.querySelector('main')); + } else { + result = currentPath; + } experimentConfig.servedExperience = result || currentPath; if (!result) { // eslint-disable-next-line no-console @@ -668,14 +679,25 @@ function adjustedRumSamplingRate(checkpoint, options, context) { }; } -export async function loadEager(document, options, context) { - document.addEventListener('rum', (event) => { - const checkpoint = event.detail ? event.detail.checkpoint || '' : ''; - if(['audiences', 'campaign', 'experiment'].includes(checkpoint)) { - adjustedRumSamplingRate(checkpoint, options, context); - } - }); +function adjustRumSampligRate(document, options, context) { + const checkpoints = ['audiences', 'campaign', 'experiment']; + if (context.sampleRUM.always) { // RUM v1.x + checkpoints.forEach((ck) => { + context.sampleRUM.always.on(ck, adjustedRumSamplingRate(ck, options, context)); + }); + } else { // RUM 2.x + document.addEventListener('rum', (event) => { + if (event.detail + && event.detail.checkpoint + && checkpoints.includes(event.detail.checkpoint)) { + adjustedRumSamplingRate(event.detail.checkpoint, options, context); + } + }); + } +} +export async function loadEager(document, options, context) { + adjustRumSampligRate(document, options, context); let res = await runCampaign(document, options, context); if (!res) { res = await runExperiment(document, options, context); @@ -694,9 +716,9 @@ export async function loadLazy(document, options, context) { if (window.location.hostname.endsWith('.live') || (typeof options.isProd === 'function' && options.isProd()) || (options.prodHost - && (options.prodHost === window.location.host - || options.prodHost === window.location.hostname - || options.prodHost === window.location.origin))) { + && (options.prodHost === window.location.host + || options.prodHost === window.location.hostname + || options.prodHost === window.location.origin))) { return; } // eslint-disable-next-line import/no-cycle diff --git a/plugins/experimentation/src/preview.css b/plugins/experimentation/src/preview.css index ea402f4e..49f2b504 100644 --- a/plugins/experimentation/src/preview.css +++ b/plugins/experimentation/src/preview.css @@ -9,25 +9,44 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ + [hidden] { + display: none !important; +} + +.hlx-highlight { + --highlight-size: .5rem; - .hlx-preview-overlay { - z-index: 999; + outline-color: #888; + outline-offset: calc(-1 * var(--highlight-size)); + outline-style: dashed; + outline-width: var(--highlight-size); + background-color: #8882; +} + +.hlx-preview-overlay { + z-index: 99999; position: fixed; - bottom: 32px; - right: 32px; color: #eee; + font-size: 1rem; font-weight: 600; - font-size: 24px; display: flex; flex-direction: column; - gap: 16px; + gap: .5rem; + inset: auto auto 1em; + align-items: center; + justify-content: flex-end; + width: 100%; } .hlx-badge { - border-radius: 32px; - background-color: #888; + --color: #888; + + border-radius: 2em; + background-color: var(--color); + border-style: solid; + border-color: #fff; color: #eee; - padding: 16px 32px; + padding: 1em 1.5em; cursor: pointer; display: flex; align-items: center; @@ -35,14 +54,18 @@ font-size: inherit; overflow: initial; margin: 0; - line-height: 1.5; + justify-content: space-between; text-transform: none; - font-family: system-ui, sans-serif; } .hlx-badge:focus, .hlx-badge:hover { - background-color: #888; + --color: #888; +} + +.hlx-badge:focus-visible { + outline-style: solid; + outline-width: .25em; } .hlx-badge > span { @@ -81,16 +104,22 @@ .hlx-popup { position: absolute; - bottom: 64px; - right: 0; + display: grid; + grid-template: + "header" min-content + "content" 1fr; + bottom: 6.5em; + left: 50%; + transform: translateX(-50%); + max-height: calc(100vh - 100px - var(--nav-height, 100px)); + max-width: calc(100vw - 2em); + min-width: calc(300px - 2em); background-color: #444; - min-width: 300px; border-radius: 16px; box-shadow: 0 0 10px #000; font-size: 12px; text-align: initial; white-space: initial; - line-height: 1.5; } .hlx-popup a:any-link { @@ -100,19 +129,25 @@ display: inline-block; border-radius: 20px; text-decoration: none; - word-break: normal; } .hlx-popup-header { display: grid; + grid-area: header; grid-template: - "label actions" + "label actions" "description actions" / 1fr min-content; background-color: #222; border-radius: 16px 16px 0 0; padding: 24px 16px; - gap: 0 16px; +} + +.hlx-popup-items { + overflow-y: auto; + grid-area: content; + scrollbar-gutter: stable; + scrollbar-width: thin; } .hlx-popup-header-label { @@ -129,11 +164,8 @@ flex-direction: column; } -.hlx-popup h4, -.hlx-popup h5 { - font-family: system-ui, sans-serif; +.hlx-popup h4, .hlx-popup h5 { margin: 0; - color: inherit; } .hlx-popup h4 { @@ -144,27 +176,22 @@ font-size: 14px; } -.hlx-popup p, -.hlx-popup code { + +.hlx-popup p { margin: 0; - padding: 0; - background: inherit; - border: inherit; - color: inherit; - font-size: inherit; - line-height: 1.5; } .hlx-popup::before { - content: ""; + content: ''; width: 0; height: 0; position: absolute; border-left: 15px solid transparent; - border-right: 15px solid transparent; + border-right: 15px solid transparent; border-top: 15px solid #444; bottom: -15px; - right: 30px; + right: 50%; + transform: translateX(50%); } .hlx-hidden { @@ -173,28 +200,29 @@ .hlx-badge.is-active, .hlx-badge[aria-pressed="true"] { - background-color: #280; + --color: #280; } .hlx-badge.is-inactive, .hlx-badge[aria-pressed="false"] { - background-color: #fa0f00; + --color: #fa0f00; } .hlx-popup-item { display: grid; grid-template: - "label actions" + "label actions" "description actions" / 1fr min-content; - margin: 16px; - padding: 16px; - border-radius: 16px; - gap: 0 16px; + margin: 1em; + padding: 1em; + border-radius: 1em; + gap: .5em 1em; } .hlx-popup-item-label { grid-area: label; + white-space: nowrap; } .hlx-popup-item-description { @@ -215,10 +243,47 @@ flex: 0 0 auto; } -/* stylelint-disable-next-line media-feature-range-notation */ -@media (min-width: 900px) { +@media (width >= 600px) { + .hlx-highlight { + --highlight-size: .75rem; + } + + .hlx-preview-overlay { + right: 1em; + align-items: end; + font-size: 1.25rem; + } + + .hlx-popup { + right: 0; + left: auto; + transform: none; + min-width: 300px; + bottom: 8em; + } + + .hlx-popup::before { + right: 26px; + transform: none; + } +} + +@media (width >= 900px) { + .hlx-highlight { + --highlight-size: 1rem; + } + .hlx-preview-overlay { - flex-flow: row-reverse wrap-reverse; - justify-content: flex-start; + flex-flow: row wrap-reverse; + justify-content: flex-end; + font-size: 1.5rem; + } + + .hlx-popup { + bottom: 9em; + } + + .hlx-popup::before { + right: 32px; } } diff --git a/plugins/experimentation/src/preview.js b/plugins/experimentation/src/preview.js index ed0885ce..8cde95b8 100644 --- a/plugins/experimentation/src/preview.js +++ b/plugins/experimentation/src/preview.js @@ -12,9 +12,40 @@ const DOMAIN_KEY_NAME = 'aem-domainkey'; -function createPreviewOverlay(cls) { - const overlay = document.createElement('div'); - overlay.className = cls; +class AemExperimentationBar extends HTMLElement { + connectedCallback() { + // Create a shadow root + const shadow = this.attachShadow({ mode: 'open' }); + + const cssPath = new URL(new Error().stack.split('\n')[2].match(/[a-z]+?:\/\/.*?\/[^:]+/)[0]).pathname.replace('preview.js', 'preview.css'); + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = cssPath; + link.onload = () => { + shadow.querySelector('.hlx-preview-overlay').removeAttribute('hidden'); + }; + shadow.append(link); + + const el = document.createElement('div'); + el.className = 'hlx-preview-overlay'; + el.setAttribute('hidden', true); + shadow.append(el); + } +} +customElements.define('aem-experimentation-bar', AemExperimentationBar); + +function createPreviewOverlay() { + const overlay = document.createElement('aem-experimentation-bar'); + return overlay; +} + +function getOverlay() { + let overlay = document.querySelector('aem-experimentation-bar')?.shadowRoot.children[1]; + if (!overlay) { + const el = createPreviewOverlay(); + document.body.append(el); + [, overlay] = el.shadowRoot.children; + } return overlay; } @@ -40,7 +71,7 @@ function createPopupItem(item) { ${item.description ? `