diff --git a/tools/oversight/charts/cwvperf.js b/tools/oversight/charts/cwvperf.js new file mode 100644 index 00000000..4055562f --- /dev/null +++ b/tools/oversight/charts/cwvperf.js @@ -0,0 +1,339 @@ +import { + Chart, TimeScale, LinearScale, registerables, + // eslint-disable-next-line import/no-unresolved, import/extensions +} from 'chartjs'; +// eslint-disable-next-line import/no-unresolved, import/extensions +import 'chartjs-adapter-luxon'; +import { + utils, +} from '@adobe/rum-distiller'; +import AbstractChart from './chart.js'; +import { + truncate, + cssVariable, + cwvInterpolationFn, + INTERPOLATION_THRESHOLD, +} from '../utils.js'; + +const { + scoreBundle, +} = utils; + +Chart.register(TimeScale, LinearScale, ...registerables); + +/** + * The CWVPerfChart is a unique type of multi-series bar chart that + * shows both the overall traffic levels as well as the distribution + * of each of the three core web vitals values within the given date + * range. + */ +export default class CWVPerfChart extends AbstractChart { + /** + * Returns a function that can group the data bundles based on the + * configuration of the chart. As this is a timeline chart, + * the grouping is based on the time slot of the bundle, truncated + * to the granularity of the chart. + * @returns {function} A function that can group the data bundles + */ + get groupBy() { + const groupFn = (bundle) => { + const slotTime = new Date(bundle.timeSlot); + return truncate(slotTime, this.chartConfig.unit); + }; + + groupFn.fillerFn = (existing) => { + const endDate = this.chartConfig.endDate ? new Date(this.chartConfig.endDate) : new Date(); + + let startDate; + if (!this.chartConfig.startDate) { + // set start date depending on the unit + startDate = new Date(endDate); + // roll back to beginning of time + if (this.chartConfig.unit === 'day') startDate.setDate(endDate.getDate() - 30); + if (this.chartConfig.unit === 'hour') startDate.setDate(endDate.getDate() - 7); + if (this.chartConfig.unit === 'week') startDate.setMonth(endDate.getMonth() - 12); + if (this.chartConfig.unit === 'month') startDate.setMonth(endDate.getMonth() - 1); + } else { + startDate = new Date(this.chartConfig.startDate); + } + + const slots = new Set(existing); + const slotTime = new Date(startDate); + // return Array.from(slots); + let maxSlots = 1000; + while (slotTime <= endDate) { + const { unit } = this.chartConfig; + slots.add(truncate(slotTime, unit)); + if (this.chartConfig.unit === 'day') slotTime.setDate(slotTime.getDate() + 1); + if (this.chartConfig.unit === 'hour') slotTime.setHours(slotTime.getHours() + 1); + if (this.chartConfig.unit === 'week') slotTime.setDate(slotTime.getDate() + 7); + if (this.chartConfig.unit === 'month') slotTime.setMonth(slotTime.getMonth() + 1); + maxSlots -= 1; + if (maxSlots < 0) { + // eslint-disable-next-line no-console + console.error('Too many slots'); + break; + } + } + return Array.from(slots); + }; + + return groupFn; + } + + render() { + // eslint-disable-next-line no-undef + this.chart = new Chart(this.elems.canvas, { + type: 'line', + data: { + labels: [], + datasets: [ + { + label: 'Good CWV', + backgroundColor: cssVariable('--spectrum-green-600'), + borderColor: cssVariable('--spectrum-green-600'), + tension: 0.2, + pointRadius: 0, + data: [], + }, + { + label: 'Needs Improvement CWV', + backgroundColor: cssVariable('--spectrum-orange-600'), + borderColor: cssVariable('--spectrum-orange-600'), + tension: 0.2, + pointRadius: 0, + data: [], + }, + { + label: 'Poor CWV', + backgroundColor: cssVariable('--spectrum-red-600'), + borderColor: cssVariable('--spectrum-red-600'), + tension: 0.2, + pointRadius: 0, + data: [], + }, + ], + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + }, + customCanvasBackgroundColor: { + color: 'white', + }, + }, + interaction: { + mode: 'x', + }, + animation: { + duration: 300, + }, + responsive: true, + scales: { + x: { + type: 'time', + display: true, + grid: { + display: false, + }, + border: { + display: false, + }, + offset: true, + time: { + displayFormats: { + day: 'EEE, MMM d', + }, + unit: 'day', + }, + stacked: true, + ticks: { + minRotation: 90, + maxRotation: 90, + autoSkip: false, + }, + }, + y: { + display: false, + stacked: false, + border: { + display: false, + }, + }, + }, + }, + }); + } + + /** + * Defines the series for the chart based on the data chunks + * @param {DataChunks} dataChunks + */ + defineSeries() { + const { dataChunks } = this; + + dataChunks.addSeries('goodCWV', (bundle) => (scoreBundle(bundle) === 'good' ? bundle.weight : undefined)); + dataChunks.addSeries('poorCWV', (bundle) => (scoreBundle(bundle) === 'poor' ? bundle.weight : undefined)); + dataChunks.addSeries('niCWV', (bundle) => (scoreBundle(bundle) === 'ni' ? bundle.weight : undefined)); + dataChunks.addSeries('noCWV', (bundle) => (scoreBundle(bundle) === null ? bundle.weight : undefined)); + + // interpolated series + dataChunks.addInterpolation( + 'iGoodCWV', // name of the series + ['goodCWV', 'niCWV', 'poorCWV', 'noCWV'], // calculate from these series + cwvInterpolationFn('goodCWV'), // interpolation function + ); + + dataChunks.addInterpolation( + 'iNiCWV', + ['goodCWV', 'niCWV', 'poorCWV', 'noCWV'], + cwvInterpolationFn('niCWV'), + ); + + dataChunks.addInterpolation( + 'iPoorCWV', + ['goodCWV', 'niCWV', 'poorCWV', 'noCWV'], + cwvInterpolationFn('poorCWV'), + ); + + dataChunks.addInterpolation( + 'iNoCWV', + ['goodCWV', 'niCWV', 'poorCWV', 'noCWV'], + ({ + goodCWV, niCWV, poorCWV, noCWV, + }) => { + const valueCount = goodCWV.count + niCWV.count + poorCWV.count; + if (valueCount < INTERPOLATION_THRESHOLD) { + // not enough data to interpolate the other values, so + // we report as if there are no CWV at all + const totalWeight = goodCWV.weight + niCWV.weight + poorCWV.weight + noCWV.weight; + return totalWeight; + } + return 0; + }, + ); + } + + async draw() { + const params = new URL(window.location).searchParams; + const view = params.get('view'); + + // eslint-disable-next-line no-unused-vars + const startDate = params.get('startDate'); + const endDate = params.get('endDate'); + + let customView = 'year'; + let unit = 'month'; + let units = 12; + if (view === 'custom') { + const diff = endDate ? new Date(endDate).getTime() - new Date(startDate).getTime() : 0; + if (diff < (1000 * 60 * 60 * 24)) { + // less than a day + customView = 'hour'; + unit = 'hour'; + units = 24; + } else if (diff <= (1000 * 60 * 60 * 24 * 7)) { + // less than a week + customView = 'week'; + unit = 'hour'; + units = Math.round(diff / (1000 * 60 * 60)); + } else if (diff <= (1000 * 60 * 60 * 24 * 31)) { + // less than a month + customView = 'month'; + unit = 'day'; + units = 30; + } else if (diff <= (1000 * 60 * 60 * 24 * 365 * 3)) { + // less than 3 years + customView = 'week'; + unit = 'week'; + units = Math.round(diff / (1000 * 60 * 60 * 24 * 7)); + } + } + + const focus = params.get('focus'); + + if (this.dataChunks.filtered.length < 1000) { + this.elems.lowDataWarning.ariaHidden = 'false'; + } else { + this.elems.lowDataWarning.ariaHidden = 'true'; + } + + const configs = { + month: { + view, + unit: 'day', + units: 30, + focus, + startDate, + endDate, + }, + week: { + view, + unit: 'hour', + units: 24 * 7, + focus, + startDate, + endDate, + }, + year: { + view, + unit: 'week', + units: 52, + focus, + startDate, + endDate, + }, + custom: { + view: customView, + unit, + units, + focus, + startDate, + endDate, + }, + }; + + const config = configs[view]; + + this.config = { ...config, ...this.config }; + this.defineSeries(); + + // group by date, according to the chart config + const group = this.dataChunks.group(this.groupBy); + const chartLabels = Object.keys(group).sort(); + + const { + iGoodCWVs, + iNiCWVs, + iPoorCWVs, + } = Object.entries(this.dataChunks.aggregates) + .sort(([a], [b]) => a.localeCompare(b)) + .reduce((acc, [, totals]) => { + const t = (totals.iGoodCWV.weight + totals.iNiCWV.weight + totals.iPoorCWV.weight) || 1; + acc.iGoodCWVs.push(totals.iGoodCWV.weight / t); + acc.iNiCWVs.push(totals.iNiCWV.weight / t); + acc.iPoorCWVs.push(totals.iPoorCWV.weight / t); + return acc; + }, { + iGoodCWVs: [], + iNiCWVs: [], + iPoorCWVs: [], + }); + + this.chart.data.datasets[0].data = iGoodCWVs; + this.chart.data.datasets[1].data = iNiCWVs; + this.chart.data.datasets[2].data = iPoorCWVs; + + this.chart.data.labels = chartLabels; + this.chart.options.scales.x.time.unit = config.unit; + + this.min = this.chart.options.scales.y.min; + this.stepSize = undefined; + this.clsAlreadyLabeled = false; + this.lcpAlreadyLabeled = false; + + this.chart.update(); + } +} diff --git a/tools/oversight/perf.html b/tools/oversight/perf.html new file mode 100644 index 00000000..c9247142 --- /dev/null +++ b/tools/oversight/perf.html @@ -0,0 +1,159 @@ + + + + Real Use Monitoring (RUM) Explorer | AEM Live + + + + + + + + + + + +
+
+
+
+
+
+ www.aem.live + + + + +
+
+
    +
  • +

    Page views

    +

    0

    +
  • +
  • +

    Visits

    +

    0

    +
  • + +

    Engagement

    +

    0

    +
    +
  • +

    LCP

    +

    0

    +
  • +
  • +

    CLS

    +

    0

    +
  • +
  • +

    INP

    +

    0

    +
  • +
+ +
+ +
+
+ +
+
+
+ + +
+
+
+ + + Device Type and Operating System +
+
desktop
+
All Desktop
+
desktop:windows
+
Windows Desktop
+
desktop:mac
+
Mac Desktop
+
desktop:linux
+
Linux Desktop
+
desktop:chromeos
+
Chrome OS Desktop
+
mobile
+
All Mobile
+
mobile:android
+
Android Mobile
+
mobile:ios
+
iOS Mobile
+
mobile:ipados
+
iPad Mobile
+
bot
+
All Bots
+
bot:seo
+
SEO Bot
+
bot:search
+
Search Engine Crawler
+
bot:ads
+
Ad Bot
+
bot:social
+
Social Media Bot
+
+
+
+
+
+ + + + + + \ No newline at end of file