UNPKG

studiocms

Version:

Astro Native CMS for AstroDB. Built from the ground up by the Astro community.

114 lines (102 loc) 3.9 kB
import type { WebVitalsRating } from './schemas.js'; import type { IntermediateWebVitalsRouteSummary, WebVitalsResponseItem, WebVitalsRouteSummary, } from './types.js'; /** * Processes an array of Web Vitals response items and returns a summary of web vitals for each route. * * @param data - An array of WebVitalsResponseItem objects representing the web vitals data. * @returns An array of WebVitalsRouteSummary objects summarizing the web vitals for each route. * * The function performs the following steps: * 1. Groups the data by route and name. * 2. Processes each group separately, ensuring the sample size is at least 4. * 3. Sorts the metrics within each group by rating and value. * 4. Assigns quartiles to the metrics using NTILE(4) logic. * 5. Identifies the end of each quartile. * 6. Selects only the metrics in quartile 3 where quartile_end is true. * 7. Converts the selected metrics into WebVitalsRouteSummary structures. * * The resulting summaries include the route, whether the route passes core web vitals, the metrics for LCP, CLS, and INP, and a score. */ export function processWebVitalsRouteSummary( data: WebVitalsResponseItem[] ): WebVitalsRouteSummary[] { // Step 1: Group data by route and name const grouped: Record<string, WebVitalsResponseItem[]> = data.reduce( (acc, item) => { const key = `${item.route}-${item.name}`; if (!acc[key]) acc[key] = []; acc[key].push(item); return acc; }, {} as Record<string, WebVitalsResponseItem[]> ); const results: WebVitalsResponseItem[] = []; // Step 2: Process each group separately for (const metrics of Object.values(grouped)) { if (metrics.length < 4) continue; // Ensure sample_size >= 4 // Step 3: Sort within the group metrics.sort((a, b) => a.rating.localeCompare(b.rating) || a.value - b.value); // Step 4: Assign quartiles (NTILE(4) logic) const quartileSize = Math.ceil(metrics.length / 4); metrics.forEach((metric, index) => { metric.quartile = Math.floor(index / quartileSize) + 1; // 1-based quartile index }); // Step 5: Identify `quartile_end` metrics.forEach((metric, index, arr) => { const nextItem = arr[index + 1]; metric.quartile_end = !nextItem || nextItem.quartile !== metric.quartile; }); // Step 6: Select only quartile 3 where quartile_end is true results.push(...metrics.filter((metric) => metric.quartile === 3 && metric.quartile_end)); } // Step 7: Convert into WebVitalsRouteSummary structure const summaries: Record<string, IntermediateWebVitalsRouteSummary> = {}; for (const item of results) { const { route, name, rating, value } = item; // biome-ignore lint/suspicious/noAssignInExpressions: This is a valid use case for assignment in expressions. const routeSummary = (summaries[route] ||= { route, passingCoreWebVitals: true, metrics: {}, score: 0, }); routeSummary.metrics[name as 'LCP' | 'CLS' | 'INP'] = { rating, value, sampleSize: results.length, }; if (['LCP', 'CLS', 'INP'].includes(name) && rating !== 'good') { routeSummary.passingCoreWebVitals = false; } } return Object.values(summaries) .filter((route): route is WebVitalsRouteSummary => Boolean(route.metrics.CLS && route.metrics.LCP && route.metrics.INP) ) .map((route) => ({ ...route, score: simpleScore( route.metrics.LCP.rating, route.metrics.CLS.rating, route.metrics.INP.rating ), })) .sort((a, b) => a.score - b.score); } /** * Scoring Function: Computes a weighted score for core web vitals */ export const weighting = { LCP: 0.4, CLS: 0.3, INP: 0.3 }; export const scoring = { good: 1, 'needs-improvement': 0.5, poor: 0 }; export const simpleScore = ( lcpRating: WebVitalsRating, clsRating: WebVitalsRating, inpRating: WebVitalsRating ) => scoring[lcpRating] * weighting.LCP + scoring[clsRating] * weighting.CLS + scoring[inpRating] * weighting.INP;