diff --git a/libs/utils/logWebVitals.js b/libs/utils/logWebVitals.js
new file mode 100644
index 0000000000..8c092fb5e4
--- /dev/null
+++ b/libs/utils/logWebVitals.js
@@ -0,0 +1,95 @@
+const LANA_CLIENT_ID = 'pageperf';
+
+let lanaSent;
+
+function sendToLana(lanaData) {
+ if (lanaSent) return;
+ const ua = window.navigator.userAgent;
+
+ Object.assign(lanaData, {
+ chromeVer: ua.match(/Chrome\/(\d+\.\d+\.\d+\.\d+)/)?.[1] || '',
+ country: sessionStorage.getItem('akamai') || '',
+ // eslint-disable-next-line compat/compat
+ downlink: window.navigator?.connection?.downlink || '',
+ loggedIn: window.adobeIMS?.isSignedInUser() || false,
+ os: (ua.match(/Windows/) && 'win')
+ || (ua.match(/Mac/) && 'mac')
+ || (ua.match(/Android/) && 'android')
+ || (ua.match(/Linux/) && 'linux')
+ || '',
+ windowHeight: window.innerHeight,
+ windowWidth: window.innerWidth,
+ url: `${window.location.host}${window.location.pathname}`,
+ });
+
+ lanaData.cls ||= 0;
+ const lanaDataStr = Object.entries(lanaData)
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([key, value]) => `${key}=${value}`)
+ .join(',');
+
+ window.lana.log(lanaDataStr, {
+ clientId: LANA_CLIENT_ID,
+ sampleRate: 100,
+ });
+
+ lanaSent = true;
+}
+
+function observeCLS(lanaData) {
+ let cls = 0;
+ /* c8 ignore start */
+ new PerformanceObserver((entryList) => {
+ for (const entry of entryList.getEntries()) {
+ if (!entry.hadRecentInput) cls += entry.value;
+ }
+ lanaData.cls = cls.toPrecision(4);
+ }).observe({ type: 'layout-shift', buffered: true });
+}
+
+function getElementInfo(el) {
+ const elSrc = el.src || el.currentSrc || el.href || el.poster;
+ if (elSrc) {
+ try {
+ const srcUrl = new URL(elSrc);
+ return srcUrl.origin === window.location.origin ? srcUrl.pathname : srcUrl.href;
+ } catch {
+ // fall through
+ }
+ }
+ const elHtml = el.outerHTML.replaceAll(',', '');
+ if (elHtml.length <= 100) return elHtml;
+ return `${el.outerHTML.substring(0, 100)}...`;
+}
+
+function observeLCP(lanaData, delay) {
+ new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ const lastEntry = entries[entries.length - 1]; // Use the latest LCP candidate
+ lanaData.lcp = parseInt(lastEntry.startTime, 10);
+ const lcpEl = lastEntry.element;
+ lanaData.lcpElType = lcpEl.nodeName.toLowerCase();
+ lanaData.lcpEl = getElementInfo(lcpEl);
+
+ setTimeout(() => {
+ sendToLana(lanaData);
+ }, parseInt(delay, 10));
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
+ /* c8 ignore stop */
+}
+
+function logMepExperiments(lanaData, mep) {
+ mep?.experiments?.forEach((exp, idx) => {
+ // only log manifests that affect the page
+ if (exp.selectedVariantName === 'default') return;
+ lanaData[`manifest${idx + 1}path`] = exp.manifestPath;
+ lanaData[`manifest${idx + 1}selected`] = exp.selectedVariantName;
+ });
+}
+
+export default function webVitals(mep, { delay = 1000 } = {}) {
+ const lanaData = {};
+ logMepExperiments(lanaData, mep);
+ observeCLS(lanaData);
+ observeLCP(lanaData, delay);
+}
diff --git a/libs/utils/utils.js b/libs/utils/utils.js
index 5feb6d18c7..e897310fec 100644
--- a/libs/utils/utils.js
+++ b/libs/utils/utils.js
@@ -998,6 +998,18 @@ export function scrollToHashedElement(hash) {
});
}
+function logPagePerf() {
+ if (getMetadata('pageperf') !== 'on') return;
+ const isChrome = () => {
+ const nav = window.navigator;
+ return nav.userAgent.includes('Chrome') && nav.vendor.includes('Google');
+ };
+ const sampleRate = parseInt(getMetadata('pageperf-rate'), 10) || 50;
+ if (!isChrome() || Math.random() * 100 > sampleRate) return;
+ import('./logWebVitals.js')
+ .then((mod) => mod.default(getConfig().mep, getMetadata('pageperf-delay') || 1000));
+}
+
export async function loadDeferred(area, blocks, config) {
const event = new Event(MILO_EVENTS.DEFERRED);
area.dispatchEvent(event);
@@ -1025,6 +1037,8 @@ export async function loadDeferred(area, blocks, config) {
sampleRUM.observe(blocks);
sampleRUM.observe(area.querySelectorAll('picture > img'));
});
+
+ logPagePerf();
}
function initSidekick() {
diff --git a/test/utils/logWebVitals.test.js b/test/utils/logWebVitals.test.js
new file mode 100644
index 0000000000..01414945b7
--- /dev/null
+++ b/test/utils/logWebVitals.test.js
@@ -0,0 +1,53 @@
+/* eslint-disable no-use-before-define */
+import { expect } from '@esm-bundle/chai';
+import { readFile } from '@web/test-runner-commands';
+import logWebVitals from '../../libs/utils/logWebVitals.js';
+
+document.body.innerHTML = await readFile({ path: './mocks/body.html' });
+
+describe('Log Web Vitals', () => {
+ it('Logs data to lana', (done) => {
+ window.lana = {
+ log: (logStr, logOpts) => {
+ const vitals = logStr.split(',').reduce((acc, pair) => {
+ const [key, value] = pair.split('=');
+ acc[key] = value;
+ return acc;
+ }, {});
+
+ expect(vitals).to.have.property('chromeVer');
+ const cls = parseFloat(vitals.cls);
+ expect(cls).to.be.within(0, 1);
+ expect(vitals).to.have.property('country');
+ const downlink = parseFloat(vitals.downlink);
+ expect(downlink).to.be.within(0, 10);
+ expect(parseInt(vitals.lcp, 10)).to.be.greaterThan(1);
+ expect(vitals.lcpEl).to.be.equal('/test/utils/mocks/media_.png');
+ expect(vitals.lcpElType).to.be.equal('img');
+ expect(vitals.loggedIn).to.equal('false');
+ expect(vitals.manifest3path).to.equal('/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/cci-all-apps-q3.json');
+ expect(vitals.manifest3selected).to.equal('all');
+ expect(vitals.manifest4path).to.equal('/cc-shared/fragments/tests/2024/q2/ace0875/ace0875.json');
+ expect(vitals.manifest4selected).to.equal('target-var-marqueelink');
+ expect(vitals.os).to.be.oneOf(['mac', 'win', 'android', 'linux', '']);
+ expect(vitals.url).to.equal('localhost:2000/');
+ expect(parseInt(vitals.windowHeight, 10)).to.be.greaterThan(200);
+ expect(parseInt(vitals.windowWidth, 10)).to.be.greaterThan(200);
+
+ expect(logOpts.clientId).to.equal('pageperf');
+ expect(logOpts.sampleRate).to.equal(100);
+
+ done();
+ },
+ };
+ logWebVitals(mepObject, { delay: 0 });
+ });
+});
+
+// Sample log string:
+// eslint-disable-next-line max-len
+// chromeVer=127.0.6533.17,cls=0.1842,country=,downlink=10,lcp=82,loggedIn=false,manifest3path=/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/cci-all-apps-q3.json,manifest3selected=all,manifest4path=/cc-shared/fragments/tests/2024/q2/ace0875/ace0875.json,manifest4selected=target-var-marqueelink,os=mac,url=localhost:2000/,windowHeight=600,windowWidth=800');
+
+const mepObject = JSON.parse(`
+{"preview":true,"variantOverride":{"/products/illustrator.json":"default","/cc-shared/fragments/promos/2024/americas/ste-back-to-school-q3/ste-back-to-school-q3.json":"default","/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/cci-all-apps-q3.json":"all","/cc-shared/fragments/tests/2024/q2/ace0875/ace0875.json":"target-var-marqueelink"},"highlight":false,"targetEnabled":true,"experiments":[{"variantNames":["target-edu_pzn","target-b2b_pzn","target-cpro_pzn","phone & cc-all-apps-any","cc-all-apps-any","illustrator-any"],"manifestOverrideName":"","manifestType":"personalization","executionOrder":"1-0","manifestPath":"/products/illustrator.json","selectedVariantName":"default","name":"PZN | US | Illustrator","manifest":"https://main--cc--adobecom.hlx.live/products/illustrator.json"},{"variantNames":["all"],"manifestOverrideName":"","manifestType":"promo","executionOrder":"1-1","manifestPath":"/cc-shared/fragments/promos/2024/americas/ste-back-to-school-q3/ste-back-to-school-q3.json","selectedVariantName":"default","selectedVariant":"default","manifest":"https://main--cc--adobecom.hlx.page/cc-shared/fragments/promos/2024/americas/ste-back-to-school-q3/ste-back-to-school-q3.json","disabled":true,"event":{"name":"ste-bts-americas","start":"2024-08-19T14:00:00.000Z","end":"2024-09-03T14:00:00.000Z"}},{"variantNames":["all"],"manifestOverrideName":"","manifestType":"promo","executionOrder":"1-1","manifestPath":"/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/cci-all-apps-q3.json","run":true,"selectedVariantName":"all","selectedVariant":{"commands":[{"action":"replace","selector":".marquee","pageFilter":"**/products/illustrator","target":"https://main--cc--adobecom.hlx.page/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/products/illustrator/marquee-gen-ai","selectorType":"css","manifestId":"cci-all-apps-q3.json","targetManifestId":false},{"action":"insertbefore","selector":"main > div","pageFilter":"**/products/illustrator","target":"https://main--cc--adobecom.hlx.page/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/shared/creativecloud/individual/modal#modal-hash:delay=1","selectorType":"css","manifestId":"cci-all-apps-q3.json","targetManifestId":false}],"fragments":[{"selector":"/cc-shared/fragments/merch/products/illustrator/mini-compare/creativecloud/individual/default","val":"/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/products/illustrator/creativecloud/individual/mini-compare","action":"replace","manifestId":"cci-all-apps-q3.json","targetManifestId":false}]},"manifest":"https://main--cc--adobecom.hlx.page/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/cci-all-apps-q3.json","disabled":false,"event":{"name":"cci-all-apps-q3","start":"2024-07-22T14:00:00.000Z","end":"2024-08-04T14:00:00.000Z"}},{"variantNames":["target-var-marqueelink"],"manifestOverrideName":"","manifestType":"test","executionOrder":"1-2","manifestPath":"/cc-shared/fragments/tests/2024/q2/ace0875/ace0875.json","run":true,"selectedVariantName":"target-var-marqueelink","selectedVariant":{"commands":[{"action":"replace","selector":".marquee","pageFilter":"","target":"https://www.adobe.com/cc-shared/fragments/tests/2024/q2/ace0875/ace0875","selectorType":"css","manifestId":"ace0875.json","targetManifestId":"ace0875"}],"fragments":[{"selector":"/cc-shared/fragments/tests/2024/q2/ace0758/illustrator/marquee-default","val":"/cc-shared/fragments/tests/2024/q2/ace0875/ace0875","action":"replace","manifestId":"ace0875.json","targetManifestId":"ace0875"}]},"manifest":"https://www.adobe.com/cc-shared/fragments/tests/2024/q2/ace0875/ace0875.json"}],"blocks":{},"fragments":{"/cc-shared/fragments/merch/products/illustrator/mini-compare/creativecloud/individual/default":{"action":"replace","fragment":"/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/products/illustrator/creativecloud/individual/mini-compare","selector":"/cc-shared/fragments/merch/products/illustrator/mini-compare/creativecloud/individual/default","manifestPath":"/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/cci-all-apps-q3.json","manifestId":"cci-all-apps-q3.json","targetManifestId":false},"/cc-shared/fragments/tests/2024/q2/ace0758/illustrator/marquee-default":{"action":"replace","fragment":"/cc-shared/fragments/tests/2024/q2/ace0875/ace0875","selector":"/cc-shared/fragments/tests/2024/q2/ace0758/illustrator/marquee-default","manifestPath":"/cc-shared/fragments/tests/2024/q2/ace0875/ace0875.json","manifestId":"ace0875.json","targetManifestId":"ace0875"}},"commands":[{"action":"replace","selector":".marquee","pageFilter":"**/products/illustrator","target":"https://main--cc--adobecom.hlx.page/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/products/illustrator/marquee-gen-ai","selectorType":"css","manifestId":"cci-all-apps-q3.json","targetManifestId":false},{"action":"insertbefore","selector":"main > div","pageFilter":"**/products/illustrator","target":"https://main--cc--adobecom.hlx.page/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/shared/creativecloud/individual/modal#modal-hash:delay=1","selectorType":"css","manifestId":"cci-all-apps-q3.json","targetManifestId":false},{"action":"replace","selector":".marquee","pageFilter":"","target":"https://www.adobe.com/cc-shared/fragments/tests/2024/q2/ace0875/ace0875","selectorType":"css","manifestId":"ace0875.json","targetManifestId":"ace0875"}],"martech":"|nopzn|illustrator"}
+`);
diff --git a/test/utils/logWebVitalsUtils.test.js b/test/utils/logWebVitalsUtils.test.js
new file mode 100644
index 0000000000..82afe1eb73
--- /dev/null
+++ b/test/utils/logWebVitalsUtils.test.js
@@ -0,0 +1,51 @@
+/* eslint-disable no-use-before-define */
+import { expect } from '@esm-bundle/chai';
+import { readFile } from '@web/test-runner-commands';
+import { getConfig, loadDeferred } from '../../libs/utils/utils.js';
+
+document.head.innerHTML = `
+ ';
+ ';
+ ';
+`;
+
+document.body.innerHTML = await readFile({ path: './mocks/body.html' });
+
+describe('Log Web Vitals', () => {
+ it('Logs data to lana', (done) => {
+ window.lana = {
+ log: (logStr, logOpts) => {
+ const vitals = logStr.split(',').reduce((acc, pair) => {
+ const [key, value] = pair.split('=');
+ acc[key] = value;
+ return acc;
+ }, {});
+
+ expect(vitals).to.have.property('chromeVer');
+ const cls = parseFloat(vitals.cls);
+ expect(cls).to.be.within(0, 1);
+ expect(vitals).to.have.property('country');
+ const downlink = parseFloat(vitals.downlink);
+ expect(downlink).to.be.within(0, 10);
+ expect(parseInt(vitals.lcp, 10)).to.be.greaterThan(1);
+ expect(vitals.lcpEl).to.be.equal('/test/utils/mocks/media_.png');
+ expect(vitals.lcpElType).to.be.equal('img');
+ expect(vitals.loggedIn).to.equal('false');
+ expect(vitals.os).to.be.oneOf(['mac', 'win', 'android', 'linux', '']);
+ expect(vitals.url).to.equal('localhost:2000/');
+ expect(parseInt(vitals.windowHeight, 10)).to.be.greaterThan(200);
+ expect(parseInt(vitals.windowWidth, 10)).to.be.greaterThan(200);
+
+ expect(logOpts.clientId).to.equal('pageperf');
+ expect(logOpts.sampleRate).to.equal(100);
+
+ done();
+ },
+ };
+ loadDeferred(document, undefined, getConfig());
+ }).timeout(5000);
+});
+
+// Sample log string:
+// eslint-disable-next-line max-len
+// chromeVer=127.0.6533.17,cls=0.1842,country=,downlink=10,lcp=82,loggedIn=false,manifest3path=/cc-shared/fragments/promos/2024/americas/cci-all-apps-q3/cci-all-apps-q3.json,manifest3selected=all,manifest4path=/cc-shared/fragments/tests/2024/q2/ace0875/ace0875.json,manifest4selected=target-var-marqueelink,os=mac,url=localhost:2000/,windowHeight=600,windowWidth=800');