feat: add lighthouse endpoint
This commit is contained in:
parent
91ade2ab55
commit
28d982e497
552
pnpm-lock.yaml
552
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
@ -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");
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
13
src/server/prisma/zod/websitelighthousereport.ts
Normal file
13
src/server/prisma/zod/websitelighthousereport.ts
Normal 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),
|
||||||
|
})
|
@ -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 {
|
||||||
|
35
src/server/utils/screenshot/lighthouse.ts
Normal file
35
src/server/utils/screenshot/lighthouse.ts
Normal 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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user