Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DOP-4831: Save Lighthouse HTML reports to S3 (rather than Atlas) #84

Merged
merged 28 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36,427 changes: 34,326 additions & 2,101 deletions dist/upload-lighthouse/index.js

Large diffs are not rendered by default.

15,119 changes: 14,973 additions & 146 deletions dist/upload-lighthouse/licenses.txt

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions src/upload-lighthouse/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ExtendedSummary, Summary } from './types';

export const DB_NAME = `lighthouse`;
/* Used on PR creation and update (synchronize) */
export const PR_COLL_NAME = `pr_reports`;
/* Used on merge to main in Snooty to keep running scores of production */
export const MAIN_COLL_NAME = `main_reports`;

export const summaryProperties: (keyof Summary)[] = [
'seo',
'performance',
'best-practices',
'pwa',
'accessibility',
];
export const extendedSummaryProperties: (keyof ExtendedSummary)[] = [
'largest-contentful-paint',
'first-contentful-paint',
'total-blocking-time',
'speed-index',
'cumulative-layout-shift',
'interactive',
];
112 changes: 112 additions & 0 deletions src/upload-lighthouse/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as github from '@actions/github';
import { readFileAsync } from '.';
import { extendedSummaryProperties, summaryProperties } from './constants';
import {
ExtendedSummary,
JsonRun,
Manifest,
RunDocument,
SortedRuns,
} from './types';

const getEmptySummary = (): ExtendedSummary => ({
seo: 0,
performance: 0,
'best-practices': 0,
pwa: 0,
accessibility: 0,
'largest-contentful-paint': 0,
'first-contentful-paint': 0,
'speed-index': 0,
interactive: 0,
'total-blocking-time': 0,
'cumulative-layout-shift': 0,
});

const getAverageSummary = (
manifests: Manifest[],
jsonRuns: JsonRun[],
): ExtendedSummary => {
const summary = getEmptySummary();
for (const property of summaryProperties) {
summary[property] =
manifests.reduce((acc, cur) => acc + cur.summary[property], 0) /
manifests.length;
}
for (const property of extendedSummaryProperties) {
summary[property] =
jsonRuns.reduce((acc, cur) => acc + cur.audits[property].score, 0) /
jsonRuns.length;
}
return summary;
};

/* Reads and returns files of runs in arrays */
const getRuns = async (
manifests: Manifest[],
): Promise<{ jsonRuns: JsonRun[]; htmlRuns: string[] }> => {
const jsonRuns = await Promise.all(
manifests.map(async manifest =>
JSON.parse((await readFileAsync(manifest.jsonPath)).toString()),
),
);

const htmlRuns = await Promise.all(
manifests.map(async manifest =>
(await readFileAsync(manifest.htmlPath)).toString(),
),
);

return { jsonRuns, htmlRuns };
};

export const sortAndAverageRuns = async (
manifests: Manifest[],
): Promise<SortedRuns[]> => {
const uniqueUrls = Array.from(
new Set(manifests.map(manifest => manifest.url)),
);

const runs: {
htmlRuns: string[];
summary: ExtendedSummary;
url: string;
}[] = await Promise.all(
uniqueUrls.map(async url => {
const manifestsForUrl = manifests.filter(
manifest => manifest.url === url,
);
const { jsonRuns, htmlRuns } = await getRuns(manifestsForUrl);
const summary = getAverageSummary(manifestsForUrl, jsonRuns);
return { htmlRuns, summary, url };
}),
);

return runs;
};

export const createRunDocument = (
{ url, summary }: SortedRuns,
type: 'mobile' | 'desktop',
): RunDocument => {
const commitHash = github.context.sha;
const author = github.context.actor;
const commitMessage = process.env.COMMIT_MESSAGE || '';
const commitTimestamp = process.env.COMMIT_TIMESTAMP
? new Date(process.env.COMMIT_TIMESTAMP)
: new Date();
const project = process.env.PROJECT_TO_BUILD || '';
const branch = process.env.BRANCH_NAME || '';

return {
commitHash,
commitMessage,
commitTimestamp,
author,
project,
branch,
url,
summary,
type,
};
};
198 changes: 9 additions & 189 deletions src/upload-lighthouse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,201 +5,19 @@
* https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/configuration.md#outputdir

* This action will read the manifest.json file, use this to sort the multiple Lighthouse runs of the same url/environment,
* average the summaries together, read the html and json files of each, combine all of this into one document,
* and finally upload this metadata on each macro-run to the appropriate Atlas collection.
* average the summaries together and finally upload this metadata on each macro-run to the appropriate Atlas collection.
* It also reads the full html reports of each and uploads these larger files to S3.
*/
import fs from 'fs';
import * as github from '@actions/github';
import { promisify } from 'util';
import { MongoClient } from 'mongodb';

const readFileAsync = promisify(fs.readFile);
import { Manifest } from './types';
import { DB_NAME, MAIN_COLL_NAME, PR_COLL_NAME } from './constants';
import { uploadHtmlToS3 } from './upload-to-s3';
import { createRunDocument, sortAndAverageRuns } from './helpers';

/* Summary of important Lighthouse scores of a run already "summarized" by lighthouse library */
interface Summary {
seo: number;
performance: number;
'best-practices': number;
pwa: number;
accessibility: number;
}
const summaryProperties: (keyof Summary)[] = [
'seo',
'performance',
'best-practices',
'pwa',
'accessibility',
];

/* Additional scores to average for DOP purposes */
interface ExtendedSummary extends Summary {
'largest-contentful-paint': number;
'first-contentful-paint': number;
'total-blocking-time': number;
'speed-index': number;
'cumulative-layout-shift': number;
interactive: number;
}
const extendedSummaryProperties: (keyof ExtendedSummary)[] = [
'largest-contentful-paint',
'first-contentful-paint',
'total-blocking-time',
'speed-index',
'cumulative-layout-shift',
'interactive',
];

/* Manifest structure outputted for each Lighthouse run */
interface Manifest {
url: string;
isRepresentativeRun: boolean;
htmlPath: string;
jsonPath: string;
summary: Summary;
}

/*
* General type to help define a very large JSON output
* Documentation of JSON output: https://github.com/GoogleChrome/lighthouse/blob/main/docs/understanding-results.md
*/
interface JsonRun {
[k: string]: unknown;
audits: {
[k in keyof ExtendedSummary]: {
[k: string]: unknown;
score: number;
};
};
}

interface RunDocument {
jsonRuns: JsonRun[];
htmlRuns: string[];
summary: ExtendedSummary;
url: string;
type: 'desktop' | 'mobile';
commitHash: string;
commitMessage: string;
commitTimestamp: Date;
author: string;
project: string;
branch: string;
}

const DB_NAME = `lighthouse`;
/* Used on PR creation and update (synchronize) */
const PR_COLL_NAME = `pr_reports`;
/* Used on merge to main in Snooty to keep running scores of production */
const MAIN_COLL_NAME = `main_reports`;

/* Helpers */
const getEmptySummary = (): ExtendedSummary => ({
seo: 0,
performance: 0,
'best-practices': 0,
pwa: 0,
accessibility: 0,
'largest-contentful-paint': 0,
'first-contentful-paint': 0,
'speed-index': 0,
interactive: 0,
'total-blocking-time': 0,
'cumulative-layout-shift': 0,
});

const getAverageSummary = (
manifests: Manifest[],
jsonRuns: JsonRun[],
): ExtendedSummary => {
const summary = getEmptySummary();
for (const property of summaryProperties) {
summary[property] =
manifests.reduce((acc, cur) => acc + cur.summary[property], 0) /
manifests.length;
}
for (const property of extendedSummaryProperties) {
summary[property] =
jsonRuns.reduce((acc, cur) => acc + cur.audits[property].score, 0) /
jsonRuns.length;
}
return summary;
};

/* Reads and returns files of runs in arrays */
const getRuns = async (
manifests: Manifest[],
): Promise<{ jsonRuns: JsonRun[]; htmlRuns: string[] }> => {
const jsonRuns = [];
const htmlRuns = [];

for (const manifest of manifests) {
jsonRuns.push(
JSON.parse((await readFileAsync(manifest.jsonPath)).toString()),
);

htmlRuns.push((await readFileAsync(manifest.htmlPath)).toString());
}

await Promise.all(jsonRuns);
await Promise.all(htmlRuns);
return { jsonRuns, htmlRuns };
};

interface SortedRuns {
jsonRuns: JsonRun[];
htmlRuns: string[];
summary: ExtendedSummary;
url: string;
}

const sortAndAverageRuns = async (
manifests: Manifest[],
): Promise<SortedRuns[]> => {
const runs: {
jsonRuns: JsonRun[];
htmlRuns: string[];
summary: ExtendedSummary;
url: string;
}[] = [];
const uniqueUrls = new Set(manifests.map(manifest => manifest.url));

for (const url of uniqueUrls) {
const manifestsForUrl = manifests.filter(manifest => manifest.url === url);
const { jsonRuns, htmlRuns } = await getRuns(manifestsForUrl);
const summary = getAverageSummary(manifestsForUrl, jsonRuns);
runs.push({ jsonRuns, htmlRuns, summary, url });
}

return runs;
};

const createRunDocument = (
{ url, summary, htmlRuns, jsonRuns }: SortedRuns,
type: 'mobile' | 'desktop',
): RunDocument => {
const commitHash = github.context.sha;
const author = github.context.actor;
const commitMessage = process.env.COMMIT_MESSAGE || '';
const commitTimestamp = process.env.COMMIT_TIMESTAMP
? new Date(process.env.COMMIT_TIMESTAMP)
: new Date();
const project = process.env.PROJECT_TO_BUILD || '';
const branch = process.env.BRANCH_NAME || '';

return {
commitHash,
commitMessage,
commitTimestamp,
author,
project,
branch,
url,
summary,
htmlRuns,
jsonRuns,
type,
};
};
export const readFileAsync = promisify(fs.readFile);

async function main(): Promise<void> {
const branch = process.env.BRANCH_NAME || '';
Expand Down Expand Up @@ -229,6 +47,7 @@ async function main(): Promise<void> {
/* Construct full document for desktop runs */
for (const desktopRun of desktopRuns) {
desktopRunDocuments.push(createRunDocument(desktopRun, 'desktop'));
await uploadHtmlToS3(desktopRun, 'desktop');
}

/* Average and summarize mobile runs */
Expand All @@ -238,6 +57,7 @@ async function main(): Promise<void> {
/* Construct full document for mobile runs */
for (const mobileRun of mobileRuns) {
mobileRunDocuments.push(createRunDocument(mobileRun, 'mobile'));
await uploadHtmlToS3(mobileRun, 'mobile');
}

/* Merges to main branch are saved to a different collection than PR commits */
Expand Down
Loading
Loading