diff --git a/.gitignore b/.gitignore index 18c4b2bf..30bd87bb 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ MIGRATION_PLAN.md .playwright-mcp/ .claude/settings.local.json .claude/skills/tool-image-prompt/scripts/.cache/ + +.claude/worktrees diff --git a/tools/optel/oversight/elements/list-facet.js b/tools/optel/oversight/elements/list-facet.js index be1cae51..90d3bbdd 100644 --- a/tools/optel/oversight/elements/list-facet.js +++ b/tools/optel/oversight/elements/list-facet.js @@ -115,6 +115,13 @@ export default class ListFacet extends HTMLElement { if (sortMetric === 'count') return 0; // keep the default order from distiller if (sortMetric === 'weight') return b.weight - a.weight; // order by weight, aka number of page views if (sortMetric === 'value') return a.value.localeCompare(b.value); + if (sortMetric === 'asc') { + // numeric sort for histogram facets (e.g. "00:10 - 00:30") + const aNum = Number.parseFloat(a.value.replace(/[^0-9.]/g, '')); + const bNum = Number.parseFloat(b.value.replace(/[^0-9.]/g, '')); + if (Number.isNaN(aNum) || Number.isNaN(bNum)) return a.value.localeCompare(b.value); + return aNum - bNum; + } // get metric and property (e.g. lcp.mean) const aNum = a.metrics[sortMetric][sortProperty]; const bNum = b.metrics[sortMetric][sortProperty]; diff --git a/tools/optel/oversight/explorer.html b/tools/optel/oversight/explorer.html index 48b56c83..81957f74 100644 --- a/tools/optel/oversight/explorer.html +++ b/tools/optel/oversight/explorer.html @@ -462,20 +462,25 @@

TTFB

- + Form Submit Source (CSS Selector) + - + Form Submit Target + - + + Form Submit Time (mm:ss) + + + Form Field Source (CSS Selector) + diff --git a/tools/optel/oversight/slicer.js b/tools/optel/oversight/slicer.js index 3465eeb4..f4f53bc7 100644 --- a/tools/optel/oversight/slicer.js +++ b/tools/optel/oversight/slicer.js @@ -77,6 +77,28 @@ dataChunks.addSeries('timeOnPage', (bundle) => { return (deltas.reduce((a, b) => Math.max(a, b), -Infinity)) / 1000; }); +function formatTimeToMMSS(seconds) { + if (seconds === undefined || seconds === null || Number.isNaN(seconds)) { + return undefined; + } + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; +} + +function timeToSubmit(bundle) { + const filteredEvents = bundle.events.filter((evt) => evt.checkpoint === 'formsubmit' || evt.checkpoint === 'viewblock'); + const submitEvent = filteredEvents.find((evt) => evt.checkpoint === 'formsubmit'); + if (!submitEvent) { + return undefined; + } + const formViewEvent = filteredEvents.find((evt) => evt.checkpoint === 'viewblock' && evt.source === submitEvent.source); + if (!formViewEvent) { + return undefined; + } + return (submitEvent.timeDelta - formViewEvent.timeDelta) / 1000; +} + dataChunks.addSeries('contentEngagement', (bundle) => { const viewEvents = bundle.events .filter((evt) => evt.checkpoint === 'viewmedia' || evt.checkpoint === 'viewblock'); @@ -213,6 +235,8 @@ function updateDataFacets(filterText, params, checkpoint) { dataChunks.addFacet('checkpoint', facets.checkpoint, 'every', 'none'); + dataChunks.addFacet('formsubmit.time', timeToSubmit); + dataChunks.addFacet( 'conversions', (bundle) => (dataChunks.hasConversion(bundle, conversionSpec) ? 'converted' : 'not-converted'), @@ -285,6 +309,19 @@ function updateDataFacets(filterText, params, checkpoint) { ); } + if (cp === 'formsubmit') { + dataChunks.addHistogramFacet( + 'formsubmit.histogram', + 'formsubmit.time', + { + count: 10, min: 0, max: 600, steps: 'quantiles', + }, + { + format: formatTimeToMMSS, + }, + ); + } + // a bit of special handling here, so we can split the acquisition source if (cp === 'acquisition') { dataChunks.addFacet('acquisition.source', acquisitionSource);