Files
clean-architecture-frontend…/scripts/collect-web-vitals-evidence.mjs
T

103 lines
2.9 KiB
JavaScript

import { mkdir, readFile, writeFile } from "node:fs/promises";
import {
evaluateFieldBudget,
percentile75,
} from "../src/application/policies/performance-budgets.js";
const inputPath =
process.env.FIELD_WEB_VITALS_INPUT ??
"config/performance/field-input.example.json";
const input =
/** @type {{
* releaseId: string,
* samples: Array<{
* timestamp: string,
* consent: boolean,
* releaseId: string,
* routeId: string,
* lcpMs: number,
* cls: number,
* inpMs: number
* }>
* }} */ (JSON.parse(await readFile(inputPath, "utf8")));
const configured =
/** @type {{
* p75LcpMs: number,
* p75Cls: number,
* p75InpMs: number,
* minimumEligibleSamples: number | null
* }} */ (
JSON.parse(await readFile("config/performance/budgets.json", "utf8")).field
);
const minimumEligibleSamples = process.env.MIN_ELIGIBLE_SAMPLES
? Number(process.env.MIN_ELIGIBLE_SAMPLES)
: configured.minimumEligibleSamples;
const end = new Date();
const start = new Date(end);
start.setUTCDate(start.getUTCDate() - 28);
const eligible = input.samples.filter((sample) => {
const timestamp = new Date(sample.timestamp);
return (
sample.consent === true &&
sample.releaseId === input.releaseId &&
timestamp >= start &&
timestamp <= end
);
});
const metrics = {
p75LcpMs: percentile75(eligible.map((sample) => sample.lcpMs)),
p75Cls: percentile75(eligible.map((sample) => sample.cls)),
p75InpMs: percentile75(eligible.map((sample) => sample.inpMs)),
};
const thresholds = { ...configured, minimumEligibleSamples };
const result = evaluateFieldBudget(
{ metrics, eligibleSamples: eligible.length },
thresholds,
);
const routeSamples = Object.fromEntries(
Object.entries(
eligible.reduce(
(counts, sample) => {
counts[sample.routeId] = (counts[sample.routeId] ?? 0) + 1;
return counts;
},
/** @type {Record<string, number>} */ ({}),
),
).sort(([left], [right]) => left.localeCompare(right)),
);
const report = {
schemaVersion: 1,
generatedAt: end.toISOString(),
window: { days: 28, start: start.toISOString(), end: end.toISOString() },
context: {
source: inputPath,
network: "production-real-user",
routeAggregation: "route-id-only",
releaseId: input.releaseId,
},
metrics,
thresholds,
eligibility: {
consentRequired: true,
eligibleSamples: eligible.length,
minimumEligibleSamples,
routeSamples,
},
status: result.status,
passed: result.passed,
};
await mkdir("artifacts/performance", { recursive: true });
await writeFile(
"artifacts/performance/field-web-vitals.json",
`${JSON.stringify(report, null, 2)}\n`,
);
if (!result.passed) {
process.stderr.write(
`Field Web Vitals: ${result.status} (minimum eligible sample threshold and 28-day production data are required)\n`,
);
process.exit(1);
}
process.stdout.write("Field Web Vitals: PASS\n");