103 lines
2.9 KiB
JavaScript
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");
|