Skip to content

Commit

Permalink
DOP-4831: Save Lighthouse HTML reports to S3 (rather than Atlas) (#84)
Browse files Browse the repository at this point in the history
* comment out most of upload lighthouse, attempt getting upload ready

* packaged

* eliminate secret

* packaged with s3

* use correct client

* body

* package

* print out names

* ready

* return and no leading slash

* leading slash gone

* use entire file

* full file

* no stream

* no stream

* use aws upload

* buffered

* buffered

* use new upload to s3 within normal action

* combine

* public-read

* package

* organize into files

* package

* lint

* pr feedback

* package

* remove trailing slash
  • Loading branch information
mmeigs authored Jul 22, 2024
1 parent ea16021 commit 0a63f37
Show file tree
Hide file tree
Showing 7 changed files with 49,550 additions and 2,436 deletions.
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

0 comments on commit 0a63f37

Please sign in to comment.