@@ -23625,6 +23625,22 @@ var METRIC_KEYS = [
2362523625 "speed-index",
2362623626 "interactive"
2362723627];
23628+ var METRIC_DISPLAY_NAMES = {
23629+ "first-contentful-paint": "First Contentful Paint",
23630+ "largest-contentful-paint": "Largest Contentful Paint",
23631+ "cumulative-layout-shift": "Cumulative Layout Shift",
23632+ "total-blocking-time": "Total Blocking Time",
23633+ "speed-index": "Speed Index",
23634+ "interactive": "Time to Interactive"
23635+ };
23636+ var METRIC_SHORT_NAMES = {
23637+ "first-contentful-paint": "FCP",
23638+ "largest-contentful-paint": "LCP",
23639+ "cumulative-layout-shift": "CLS",
23640+ "total-blocking-time": "TBT",
23641+ "speed-index": "SI",
23642+ "interactive": "TTI"
23643+ };
2362823644
2362923645// src/utils.ts
2363023646function isPathSafe(inputPath) {
@@ -23669,6 +23685,15 @@ function buildRegressionsList(regressions) {
2366923685 md += "\n";
2367023686 return md;
2367123687}
23688+ function fmtMetricValue(key, value) {
23689+ if (key === "cumulative-layout-shift") {
23690+ return value.toFixed(3);
23691+ }
23692+ if (key === "total-blocking-time") {
23693+ return `${Math.round(value)}ms`;
23694+ }
23695+ return `${(value / 1e3).toFixed(2)}s`;
23696+ }
2367223697
2367323698// src/lhr.ts
2367423699function warn(message) {
@@ -23901,14 +23926,35 @@ function buildIssueBody(analysis, consecutiveFailLimit) {
2390123926 const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2390223927 const branch = process.env.GITHUB_REF?.replace("refs/heads/", "") ?? "unknown";
2390323928 const commit = process.env.GITHUB_SHA?.substring(0, 7) ?? "unknown";
23904- let body = `## Lighthouse Performance Alert
23929+ const failedUrls = analysis.urls.filter((u) => !u.passed);
23930+ const failingProfiles = failedUrls.flatMap((u) => u.profiles.filter((p) => !p.passed));
23931+ const totalProfiles = analysis.urls.reduce((s, u) => s + u.profiles.length, 0);
23932+ const totalErrors = failingProfiles.reduce(
23933+ (s, p) => s + countAssertionLevels(filterFailedAssertions(p.assertions)).errors,
23934+ 0
23935+ );
23936+ const totalWarnings = failingProfiles.reduce(
23937+ (s, p) => s + countAssertionLevels(filterFailedAssertions(p.assertions)).warnings,
23938+ 0
23939+ );
23940+ const totalRegressions = failingProfiles.reduce((s, p) => s + p.regressions.length, 0);
23941+ let body = `## \u{1F534} Lighthouse Performance Alert
2390523942
2390623943`;
23907- body += `**Timestamp:** ${timestamp}
23944+ const parts = [];
23945+ if (totalErrors > 0) parts.push(`${totalErrors} error${pl(totalErrors)}`);
23946+ if (totalWarnings > 0) parts.push(`${totalWarnings} warning${pl(totalWarnings)}`);
23947+ if (totalRegressions > 0) parts.push(`${totalRegressions} regression${pl(totalRegressions)}`);
23948+ body += `> **${analysis.urls.length} URL${pl(analysis.urls.length)}** across `;
23949+ body += `**${totalProfiles} profile${pl(totalProfiles)}** \u2014 `;
23950+ body += `**${failingProfiles.length} failing** \xB7 ${parts.join(" \xB7 ")}
23951+
2390823952`;
23909- body += `**Branch:** ${branch}
23953+ body += buildStatusMatrix(analysis);
23954+ body += `\`${branch}\` \xB7 \`${commit}\` \xB7 ${fmtDate(timestamp)}
23955+
2391023956`;
23911- body += `**Commit:** ${commit}
23957+ body += `---
2391223958
2391323959`;
2391423960 for (const url of analysis.urls) {
@@ -23918,35 +23964,166 @@ function buildIssueBody(analysis, consecutiveFailLimit) {
2391823964`;
2391923965 for (const pr of url.profiles) {
2392023966 if (pr.passed) continue;
23921- body += `#### ${pr.profile}
23967+ body += buildProfileSection(pr, consecutiveFailLimit);
23968+ }
23969+ }
23970+ body += `---
2392223971
2392323972`;
23924- if (pr.reportLink) {
23925- body += `[View report](${pr.reportLink})
23926-
23973+ const runCount = failingProfiles[0]?.runMetrics?.length;
23974+ body += `<sub>\u{1F916} Auto-managed by AutoLighthouse`;
23975+ if (runCount && runCount > 1) body += ` \xB7 ${runCount} runs per profile`;
23976+ body += `</sub>`;
23977+ return body;
23978+ }
23979+ function pl(n) {
23980+ return n !== 1 ? "s" : "";
23981+ }
23982+ function fmtDate(iso) {
23983+ const d = new Date(iso);
23984+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
23985+ const hh = String(d.getUTCHours()).padStart(2, "0");
23986+ const mm = String(d.getUTCMinutes()).padStart(2, "0");
23987+ return `${months[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()} ${hh}:${mm} UTC`;
23988+ }
23989+ function profileStatusIcon(pr) {
23990+ const failures = filterFailedAssertions(pr.assertions);
23991+ const { errors } = countAssertionLevels(failures);
23992+ if (errors > 0) return "\u{1F534}";
23993+ if (failures.length > 0) return "\u{1F7E1}";
23994+ if (pr.regressions.length > 0) return "\u{1F4C9}";
23995+ return "\u{1F7E2}";
23996+ }
23997+ function buildStatusMatrix(analysis) {
23998+ const profileSet = /* @__PURE__ */ new Set();
23999+ for (const url of analysis.urls) {
24000+ for (const p of url.profiles) profileSet.add(p.profile);
24001+ }
24002+ const profiles = Array.from(profileSet);
24003+ if (profiles.length === 0) return "";
24004+ const icons = { desktop: "\u{1F5A5}\uFE0F", mobile: "\u{1F4F1}", tablet: "\u{1F4F1}" };
24005+ let md = `| URL |`;
24006+ for (const p of profiles) md += ` ${icons[p] ?? ""} ${p} |`;
24007+ md += `
24008+ |-----|`;
24009+ for (const _ of profiles) md += `:---:|`;
24010+ md += `
2392724011`;
24012+ for (const url of analysis.urls) {
24013+ md += `| \`${url.pathname}\` |`;
24014+ for (const name of profiles) {
24015+ const pr = url.profiles.find((p) => p.profile === name);
24016+ if (!pr) {
24017+ md += ` \u2014 |`;
24018+ continue;
2392824019 }
23929- const failures = filterFailedAssertions(pr.assertions);
23930- if (failures.length > 0) {
23931- const { errors, warnings } = countAssertionLevels(failures);
23932- body += `**Assertion Failures:** ${errors} error(s), ${warnings} warning(s)
24020+ md += ` ${profileStatusIcon(pr)} |`;
24021+ }
24022+ md += `
24023+ `;
24024+ }
24025+ md += `
24026+ `;
24027+ return md;
24028+ }
24029+ function buildProfileSection(pr, consecutiveFailLimit) {
24030+ const icon = profileStatusIcon(pr);
24031+ let md = `<details open>
24032+ <summary><b>${icon} ${pr.profile}</b>`;
24033+ const failures = filterFailedAssertions(pr.assertions);
24034+ if (failures.length > 0) {
24035+ const { errors, warnings } = countAssertionLevels(failures);
24036+ const counts = [];
24037+ if (errors > 0) counts.push(`${errors} error${pl(errors)}`);
24038+ if (warnings > 0) counts.push(`${warnings} warning${pl(warnings)}`);
24039+ md += ` \xB7 ${counts.join(", ")}`;
24040+ }
24041+ if (pr.regressions.length > 0) {
24042+ md += ` \xB7 ${pr.regressions.length} regression${pl(pr.regressions.length)}`;
24043+ }
24044+ if (pr.reportLink) {
24045+ md += ` \xB7 <a href="${pr.reportLink}">View Report \u2197</a>`;
24046+ }
24047+ md += `</summary>
2393324048
2393424049`;
23935- body += buildAssertionTable(failures);
23936- }
23937- if (pr.regressions.length > 0) {
23938- body += buildRegressionsList(pr.regressions);
23939- }
23940- if (pr.consecutiveFailures >= consecutiveFailLimit) {
23941- body += `\u26A0\uFE0F **Persistent failure** \u2014 ${pr.consecutiveFailures} consecutive runs
24050+ if (failures.length > 0) {
24051+ md += `**Assertion Failures**
2394224052
2394324053`;
24054+ md += `| Audit | Level | Actual | Threshold |
24055+ `;
24056+ md += `|-------|-------|--------|----------|
24057+ `;
24058+ for (const a of failures) {
24059+ const lvl = a.level === "error" ? "\u{1F534} error" : "\u{1F7E1} warn";
24060+ md += `| ${a.auditId} | ${lvl} | ${a.actual ?? "\u2014"} | ${a.operator ?? ""} ${a.expected ?? "\u2014"} |
24061+ `;
24062+ }
24063+ md += "\n";
24064+ }
24065+ if (pr.regressions.length > 0) {
24066+ md += buildRegressionsList(pr.regressions);
24067+ }
24068+ const runs = pr.runMetrics;
24069+ if (runs && runs.length > 0) {
24070+ md += buildMetricsTable(pr.metrics, runs);
24071+ }
24072+ if (pr.consecutiveFailures >= consecutiveFailLimit) {
24073+ md += `> \u26A0\uFE0F **Persistent failure** \u2014 ${pr.consecutiveFailures} consecutive runs
24074+
24075+ `;
24076+ }
24077+ md += `</details>
24078+
24079+ `;
24080+ return md;
24081+ }
24082+ function buildMetricsTable(median, runs) {
24083+ let md = `**Core Web Vitals** _(median of ${runs.length} run${pl(runs.length)})_
24084+
24085+ `;
24086+ md += `| Metric | Median | Range |
24087+ `;
24088+ md += `|--------|-------:|------:|
24089+ `;
24090+ for (const key of METRIC_KEYS) {
24091+ const medVal = median[key];
24092+ if (medVal === void 0) continue;
24093+ const sorted = runs.map((r) => r[key]).filter((v) => v !== void 0).sort((a, b) => a - b);
24094+ if (sorted.length === 0) continue;
24095+ const range = `${fmtMetricValue(key, sorted[0])} \u2013 ${fmtMetricValue(key, sorted[sorted.length - 1])}`;
24096+ md += `| ${METRIC_DISPLAY_NAMES[key]} | ${fmtMetricValue(key, medVal)} | ${range} |
24097+ `;
24098+ }
24099+ md += "\n";
24100+ if (runs.length > 1) {
24101+ md += `<details>
24102+ <summary>\u{1F4CA} Individual runs (${runs.length})</summary>
24103+
24104+ `;
24105+ md += `| # |`;
24106+ for (const key of METRIC_KEYS) md += ` ${METRIC_SHORT_NAMES[key]} |`;
24107+ md += `
24108+ |---|`;
24109+ for (const _ of METRIC_KEYS) md += `---:|`;
24110+ md += `
24111+ `;
24112+ for (let i = 0; i < runs.length; i++) {
24113+ md += `| ${i + 1} |`;
24114+ for (const key of METRIC_KEYS) {
24115+ const v = runs[i][key];
24116+ md += ` ${v !== void 0 ? fmtMetricValue(key, v) : "\u2014"} |`;
2394424117 }
24118+ md += `
24119+ `;
2394524120 }
24121+ md += `
24122+ </details>
24123+
24124+ `;
2394624125 }
23947- body += `---
23948- _This issue is auto-managed by AutoLighthouse._`;
23949- return body;
24126+ return md;
2395024127}
2395124128async function manageIssue(octokit, analysis, consecutiveFailLimit) {
2395224129 const existingIssue = await findOpenIssue(octokit);
@@ -24081,16 +24258,25 @@ async function run() {
2408124258 const raw = [];
2408224259 for (const artifact of artifacts) {
2408324260 const failedAssertions = artifact.assertions.filter((a) => !a.passed);
24261+ const lhrsByUrl = /* @__PURE__ */ new Map();
2408424262 for (const lhrPath of artifact.lhrPaths) {
2408524263 const lhr = parseLhr(lhrPath);
2408624264 if (!lhr) continue;
2408724265 const url = extractUrl(lhr);
2408824266 if (!url) continue;
24089- const pathname = extractPathname(url);
2409024267 const metrics = extractMetrics(lhr);
24268+ let entry = lhrsByUrl.get(url);
24269+ if (!entry) {
24270+ entry = { pathname: extractPathname(url), allMetrics: [] };
24271+ lhrsByUrl.set(url, entry);
24272+ }
24273+ entry.allMetrics.push(metrics);
24274+ }
24275+ for (const [url, { pathname, allMetrics }] of lhrsByUrl) {
24276+ const metrics = medianMetrics(allMetrics);
2409124277 const urlAssertions = failedAssertions.filter((a) => !a.url || a.url === url);
2409224278 const reportLink = artifact.links[url] ?? void 0;
24093- raw.push({ profile: artifact.profile, url, pathname, metrics, assertions: urlAssertions, reportLink });
24279+ raw.push({ profile: artifact.profile, url, pathname, metrics, runMetrics: allMetrics, assertions: urlAssertions, reportLink });
2409424280 }
2409524281 }
2409624282 const urlMap = /* @__PURE__ */ new Map();
@@ -24119,6 +24305,7 @@ async function run() {
2411924305 const profileResult = {
2412024306 profile: r.profile,
2412124307 metrics: r.metrics,
24308+ runMetrics: r.runMetrics,
2412224309 regressions,
2412324310 assertions: r.assertions,
2412424311 consecutiveFailures: newConsecutive,
@@ -24186,6 +24373,15 @@ async function run() {
2418624373 }
2418724374 }
2418824375}
24376+ function medianMetrics(allMetrics) {
24377+ if (allMetrics.length === 1) return allMetrics[0];
24378+ const result = {};
24379+ for (const key of METRIC_KEYS) {
24380+ const values = allMetrics.map((m) => m[key]).filter((v) => v !== void 0).sort((a, b) => a - b);
24381+ result[key] = values.length > 0 ? values[Math.floor(values.length / 2)] : void 0;
24382+ }
24383+ return result;
24384+ }
2418924385function extractPathname(url) {
2419024386 try {
2419124387 return new URL(url).pathname || "/";
0 commit comments