studiocms
Version:
Astro Native CMS for AstroDB. Built from the ground up by the Astro community.
90 lines (76 loc) • 3.21 kB
text/typescript
import type { WebVitalsRating } from './schemas.js';
import type { WebVitalsResponseItem, WebVitalsSummary } from './types.js';
/**
* Processes an array of web vitals response items and generates a summary.
*
* @param data - An array of `WebVitalsResponseItem` objects to be processed.
* @returns A `WebVitalsSummary` object containing the processed summary.
*
* The function performs the following steps:
* 1. Groups the input data by the `name` property.
* 2. Processes each metric group separately.
* 3. Sorts the metrics within each group by `rating` and `value`.
* 4. Assigns quartiles to each metric.
* 5. Identifies the end of each rating group.
* 6. Computes the sample size for each metric group.
* 7. Computes histogram densities for each rating.
* 8. Computes the 75th percentile (P75) for each metric group.
* 9. Filters the final results based on specific conditions.
*/
export function processWebVitalsSummary(data: WebVitalsResponseItem[]): WebVitalsSummary {
// Step 1: Group by `name`
const grouped: Record<string, WebVitalsResponseItem[]> = data.reduce(
(acc, item) => {
if (!acc[item.name]) acc[item.name] = [];
acc[item.name].push(item);
return acc;
},
{} as Record<string, WebVitalsResponseItem[]>
);
const summary: WebVitalsSummary = {};
// Step 2: Process each metric separately
for (const [metricName, metrics] of Object.entries(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);
for (let i = 0; i < metrics.length; i++) {
metrics[i].quartile = Math.floor(i / quartileSize) + 1; // 1-based quartile index
}
// Step 5: Identify `rating_end`
for (let i = 0; i < metrics.length; i++) {
const nextItem = metrics[i + 1];
metrics[i].rating_end = !nextItem || nextItem.rating !== metrics[i].rating;
}
// Step 6: Compute sample size
const sampleSize = metrics.length;
// Step 7: Compute histogram densities
const histogram: Record<WebVitalsRating, number> = {
good: 0,
'needs-improvement': 0,
poor: 0,
};
for (let i = 0; i < metrics.length; i++) {
if (metrics[i].rating_end) {
const ratingCount = metrics.filter((m) => m.rating === metrics[i].rating).length;
histogram[metrics[i].rating] = ratingCount / sampleSize;
}
}
// Step 8: Compute percentiles (P75)
const p75Index = Math.floor(metrics.length * 0.75);
const p75Metric = metrics[p75Index] || null;
const percentiles: Partial<
Record<'p75', { value: number; rating: WebVitalsRating }> | undefined
> = p75Metric ? { p75: { value: p75Metric.value, rating: p75Metric.rating } } : {};
// Step 9: Filter the final results based on SQL conditions
const finalMetrics = metrics.filter(
// biome-ignore lint/style/noNonNullAssertion: This is a valid use case for non-null assertion
(metric) => metric.rating_end || metric.quartile! * 25 === 75
);
if (finalMetrics.length > 0) {
summary[metricName] = { histogram, percentiles, sampleSize };
}
}
return summary;
}