-
-
Notifications
You must be signed in to change notification settings - Fork 105
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CI Reporter Infrastructure and
expandedJson
reports (#111)
Co-authored-by: Tomas Jansson <[email protected]>
- Loading branch information
Showing
14 changed files
with
36,712 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -87,3 +87,6 @@ dist | |
|
||
|
||
reports | ||
|
||
.vscode/ | ||
.tmp/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { join } from 'node:path' | ||
import fse from 'fs-extra' | ||
import type { ResolvedUserConfig, UnlighthouseRouteReport } from '@unlighthouse/core' | ||
import { reportJsonSimple } from './jsonSimple' | ||
import { reportJsonExpanded } from './jsonExpanded' | ||
import type { ReportJsonExpanded, ReportJsonSimple } from './types' | ||
|
||
export function generateReportPayload(reporter: 'jsonExpanded', reports: UnlighthouseRouteReport[]): ReportJsonExpanded | ||
export function generateReportPayload(reporter: 'jsonSimple', reports: UnlighthouseRouteReport[]): ReportJsonSimple | ||
export function generateReportPayload(reporter: string, reports: UnlighthouseRouteReport[]): any { | ||
if (reporter.startsWith('json')) | ||
return reporter === 'jsonSimple' ? reportJsonSimple(reports) : reportJsonExpanded(reports) | ||
|
||
throw new Error(`Unsupported reporter: ${reporter}.`) | ||
} | ||
|
||
export async function outputReport(reporter: string, config: Partial<ResolvedUserConfig>, payload: any) { | ||
if (reporter.startsWith('json')) { | ||
const path = join(config.root, config.outputPath, 'ci-result.json') | ||
await fse.writeJson(path, payload, { spaces: 2 }) | ||
return path | ||
} | ||
throw new Error(`Unsupported reporter: ${reporter}.`) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import type { CategoryAverageScore, CategoryScore, ExpandedRouteReport, MetricAverageScore, MetricScore, ReportJsonExpanded } from './types' | ||
|
||
const relevantMetrics = [ | ||
'largest-contentful-paint', | ||
'cumulative-layout-shift', | ||
'first-contentful-paint', | ||
'total-blocking-time', | ||
'max-potential-fid', | ||
'interactive', | ||
] | ||
|
||
export function reportJsonExpanded(unlighthouseRouteReports): ReportJsonExpanded { | ||
const routes = unlighthouseRouteReports | ||
.map((report) => { | ||
const categories = Object.values(report.report?.categories ?? {}).reduce( | ||
(prev: { [key: string]: CategoryScore }, category: any): any => ({ | ||
...prev, | ||
[category.key]: { | ||
key: category.key, | ||
id: category.id, | ||
title: category.title, | ||
score: category.score, | ||
}, | ||
}), | ||
{}, | ||
) | ||
const metrics = Object.values(report.report?.audits ?? {}) | ||
.filter((metric: any) => relevantMetrics.includes(metric.id)) | ||
.reduce((prev: { [key: string]: any }, metric: any): any => ({ | ||
...prev, | ||
[metric.id]: { | ||
id: metric.id, | ||
title: metric.title, | ||
description: metric.description, | ||
numericValue: metric.numericValue, | ||
numericUnit: metric.numericUnit, | ||
displayValue: metric.displayValue, | ||
}, | ||
}), {}) | ||
return <ExpandedRouteReport> { | ||
path: report.route.path, | ||
score: report.report?.score, | ||
categories, | ||
metrics, | ||
} | ||
}) | ||
// make the list ordering consistent | ||
.sort((a, b) => a.path.localeCompare(b.path)) | ||
|
||
const averageCategories = extractCategoriesFromRoutes(routes) | ||
const averageMetrics = extractMetricsFromRoutes(routes) | ||
|
||
const summary = { | ||
score: parseFloat( | ||
( | ||
routes.reduce((prev, curr) => prev + curr.score, 0) / routes.length | ||
).toFixed(2), | ||
), | ||
categories: averageCategories, | ||
metrics: averageMetrics, | ||
} | ||
return { | ||
summary, | ||
routes, | ||
} | ||
} | ||
|
||
function extractCategoriesFromRoutes(routes: ExpandedRouteReport[]) { | ||
const categoriesWithAllScores = routes.reduce((prev, curr) => { | ||
return Object.keys(curr.categories).reduce((target, categoryKey) => { | ||
const scores = target[categoryKey] ? target[categoryKey].scores : [] | ||
return { | ||
...target, | ||
[categoryKey]: { | ||
...curr.categories[categoryKey], | ||
scores: [...scores, curr.categories[categoryKey].score], | ||
}, | ||
} | ||
}, prev) | ||
}, {} as { [key: string]: { key: string; id: string; title: string; scores: number[] } }) | ||
|
||
// returns averageCategories | ||
return Object.keys(categoriesWithAllScores).reduce( | ||
( | ||
prev: { | ||
[key: string]: CategoryAverageScore | ||
}, | ||
key: string, | ||
) => { | ||
const averageScore = parseFloat( | ||
( | ||
categoriesWithAllScores[key].scores.reduce( | ||
(prev, curr) => prev + curr, | ||
0, | ||
) / categoriesWithAllScores[key].scores.length | ||
).toFixed(2), | ||
) | ||
const { ...strippedCategory } | ||
= categoriesWithAllScores[key] | ||
return { ...prev, [key]: { ...strippedCategory, averageScore } } | ||
}, | ||
{} as { | ||
[key: string]: CategoryAverageScore | ||
}, | ||
) | ||
} | ||
|
||
function extractMetricsFromRoutes(routes: ExpandedRouteReport[]) { | ||
const metricsWithAllNumericValues = routes.reduce((prev, curr) => { | ||
return Object.keys(curr.metrics).reduce((target, metricKey) => { | ||
const numericValues = target[metricKey] | ||
? target[metricKey].numericValues | ||
: [] | ||
return { | ||
...target, | ||
[metricKey]: { | ||
...curr.metrics[metricKey], | ||
numericValues: [...numericValues, curr.metrics[metricKey].numericValue], | ||
}, | ||
} | ||
}, prev) | ||
}, {} as { [key: string]: Omit<MetricScore, 'numericValue'> & { numericValues: number[] } }) | ||
|
||
// average metrics | ||
return Object.keys(metricsWithAllNumericValues).reduce( | ||
(prev: { [key: string]: MetricAverageScore }, key: string) => { | ||
const averageNumericValue = parseFloat( | ||
( | ||
metricsWithAllNumericValues[key].numericValues.reduce( | ||
(prev, curr) => prev + curr, | ||
0, | ||
) / metricsWithAllNumericValues[key].numericValues.length | ||
).toFixed(2), | ||
) | ||
const { ...strippedMetric } | ||
= metricsWithAllNumericValues[key] | ||
return { ...prev, [key]: { ...strippedMetric, averageNumericValue } } | ||
}, | ||
{} as { [key: string]: MetricAverageScore }, | ||
) | ||
} |
Oops, something went wrong.