feat: add lighthouse endpoint

This commit is contained in:
moonrailgun 2024-09-14 01:14:22 +08:00
parent 91ade2ab55
commit 28d982e497
8 changed files with 765 additions and 7 deletions

File diff suppressed because it is too large Load Diff

View File

@ -50,6 +50,7 @@
"is-localhost-ip": "^2.0.0", "is-localhost-ip": "^2.0.0",
"isolated-vm": "^4.7.2", "isolated-vm": "^4.7.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lighthouse": "^12.2.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"maxmind": "^4.3.18", "maxmind": "^4.3.18",
"md5": "^2.3.0", "md5": "^2.3.0",

View File

@ -0,0 +1,21 @@
-- CreateEnum
CREATE TYPE "WebsiteLighthouseReportStatus" AS ENUM ('Pending', 'Success', 'Failed');
-- CreateTable
CREATE TABLE "WebsiteLighthouseReport" (
"id" VARCHAR(30) NOT NULL,
"websiteId" VARCHAR(30),
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
"url" TEXT NOT NULL,
"result" TEXT NOT NULL,
"status" "WebsiteLighthouseReportStatus" NOT NULL DEFAULT 'Pending',
CONSTRAINT "WebsiteLighthouseReport_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "WebsiteLighthouseReport_createdAt_idx" ON "WebsiteLighthouseReport"("createdAt");
-- CreateIndex
CREATE INDEX "WebsiteLighthouseReport_websiteId_idx" ON "WebsiteLighthouseReport"("websiteId");

View File

@ -249,6 +249,25 @@ model WebsiteSessionData {
@@index([sessionId]) @@index([sessionId])
} }
enum WebsiteLighthouseReportStatus {
Pending
Success
Failed
}
model WebsiteLighthouseReport {
id String @id() @default(cuid()) @db.VarChar(30)
websiteId String? @db.VarChar(30)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
url String
result String // json string
status WebsiteLighthouseReportStatus @default(Pending)
@@index([createdAt])
@@index([websiteId])
}
model Telemetry { model Telemetry {
id String @id @unique @default(cuid()) @db.VarChar(30) id String @id @unique @default(cuid()) @db.VarChar(30)
workspaceId String @db.VarChar(30) workspaceId String @db.VarChar(30)

View File

@ -9,6 +9,7 @@ export * from "./websitesession.js"
export * from "./websiteevent.js" export * from "./websiteevent.js"
export * from "./websiteeventdata.js" export * from "./websiteeventdata.js"
export * from "./websitesessiondata.js" export * from "./websitesessiondata.js"
export * from "./websitelighthousereport.js"
export * from "./telemetry.js" export * from "./telemetry.js"
export * from "./telemetrysession.js" export * from "./telemetrysession.js"
export * from "./telemetryevent.js" export * from "./telemetryevent.js"

View File

@ -0,0 +1,13 @@
import * as z from "zod"
import * as imports from "./schemas/index.js"
import { WebsiteLighthouseReportStatus } from "@prisma/client"
export const WebsiteLighthouseReportModelSchema = z.object({
id: z.string(),
websiteId: z.string().nullish(),
createdAt: z.date(),
updatedAt: z.date(),
url: z.string(),
result: z.string(),
status: z.nativeEnum(WebsiteLighthouseReportStatus),
})

View File

@ -1,5 +1,6 @@
import { import {
OpenApiMetaInfo, OpenApiMetaInfo,
publicProcedure,
router, router,
workspaceOwnerProcedure, workspaceOwnerProcedure,
workspaceProcedure, workspaceProcedure,
@ -32,6 +33,10 @@ import {
} from '../../model/_schema/filter.js'; } from '../../model/_schema/filter.js';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { WebsiteQueryFilters } from '../../utils/prisma.js'; import { WebsiteQueryFilters } from '../../utils/prisma.js';
import { WebsiteLighthouseReportStatus } from '@prisma/client';
import { generateLighthouse } from '../../utils/screenshot/lighthouse.js';
import { WebsiteLighthouseReportModelSchema } from '../../prisma/zod/websitelighthousereport.js';
import { method } from 'lodash-es';
const websiteNameSchema = z.string().max(100); const websiteNameSchema = z.string().max(100);
const websiteDomainSchema = z.union([ const websiteDomainSchema = z.union([
@ -550,6 +555,131 @@ export const websiteRouter = router({
return websiteInfo; return websiteInfo;
}), }),
generateLighthouseReport: workspaceProcedure
.meta(
buildWebsiteOpenapi({
method: 'POST',
path: '/generateLighthouseReport',
})
)
.input(
z.object({
websiteId: z.string().cuid2(),
url: z.string().url(),
})
)
.output(z.string())
.mutation(async ({ input }) => {
const { websiteId, url } = input;
const websiteInfo = await prisma.websiteLighthouseReport.create({
data: {
url,
websiteId,
status: WebsiteLighthouseReportStatus.Pending,
result: '',
},
});
generateLighthouse(url)
.then(async (result) => {
await prisma.websiteLighthouseReport.update({
where: {
id: websiteInfo.id,
},
data: {
status: WebsiteLighthouseReportStatus.Success,
result: JSON.stringify(result),
},
});
})
.catch(async () => {
await prisma.websiteLighthouseReport.update({
where: {
id: websiteInfo.id,
},
data: {
status: WebsiteLighthouseReportStatus.Failed,
},
});
});
return 'success';
}),
getLighthouseReport: workspaceProcedure
.meta(
buildWebsiteOpenapi({
method: 'GET',
path: '/getLighthouseReport',
})
)
.input(
z.object({
websiteId: z.string().cuid2(),
})
)
.output(
z.array(
WebsiteLighthouseReportModelSchema.pick({
id: true,
status: true,
createdAt: true,
})
)
)
.query(async ({ input }) => {
const { websiteId } = input;
const list = await prisma.websiteLighthouseReport.findMany({
where: {
websiteId,
},
take: 10,
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
status: true,
createdAt: true,
},
});
return list;
}),
getLighthouseJSON: publicProcedure
.meta({
openapi: {
tags: [OPENAPI_TAG.WEBSITE],
protect: true,
method: 'GET',
path: '/lighthouse/{lighthouseId}',
},
})
.input(
z.object({
lighthouseId: z.string().cuid2(),
})
)
.output(z.record(z.string(), z.any()))
.query(async ({ input }) => {
const { lighthouseId } = input;
const res = await prisma.websiteLighthouseReport.findFirst({
where: {
id: lighthouseId,
},
select: {
result: true,
},
});
try {
return JSON.parse(res?.result ?? '{}');
} catch (err) {
return {};
}
}),
}); });
function buildWebsiteOpenapi(meta: OpenApiMetaInfo): OpenApiMeta { function buildWebsiteOpenapi(meta: OpenApiMetaInfo): OpenApiMeta {

View File

@ -0,0 +1,35 @@
import puppeteer from 'puppeteer';
import lighthouse, { Result } from 'lighthouse';
export async function generateLighthouse(url: string): Promise<Result> {
// Use Puppeteer to launch headless Chrome
// - Omit `--enable-automation` (See https://github.com/GoogleChrome/lighthouse/issues/12988)
// - Don't use 800x600 default viewport
const browser = await puppeteer.launch({
// Set to false if you want to see the script in action.
headless: 'new',
defaultViewport: null,
ignoreDefaultArgs: ['--enable-automation'],
});
const page = await browser.newPage();
// Wait for Lighthouse to open url, then inject our stylesheet.
browser.on('targetchanged', async (target) => {
if (page && page.url() === url) {
await page.addStyleTag({ content: '* {color: red}' });
}
});
// Lighthouse will open the URL.
// Puppeteer will observe `targetchanged` and inject our stylesheet.
const res = await lighthouse(url, undefined, undefined, page);
if (!res) {
throw new Error('Lighthouse failed to generate report');
}
const { lhr } = res;
await browser.close();
return lhr;
}