metatron-open-seo/src/server/workflows/SiteAuditWorkflow.ts

91 lines
2.7 KiB
TypeScript

/**
* Cloudflare Workflow for site audit crawling.
*
* Each step is durable - if a step fails, it retries without redoing
* completed steps.
*/
import {
WorkflowEntrypoint,
type WorkflowEvent,
type WorkflowStep,
} from "cloudflare:workers";
import { withPgClient } from "@/db";
import type { BillingCustomerContext } from "@/server/billing/subscription";
import { AuditRepository } from "@/server/features/audit/repositories/AuditRepository";
import type { AuditConfig } from "@/server/lib/audit/types";
import { captureServerEvent } from "@/server/lib/posthog";
import { runAuditPhases } from "@/server/workflows/siteAuditWorkflowPhases";
interface AuditParams {
auditId: string;
billingCustomer: BillingCustomerContext;
projectId: string;
startUrl: string;
config: AuditConfig;
}
export class SiteAuditWorkflow extends WorkflowEntrypoint<Env, AuditParams> {
async run(event: WorkflowEvent<AuditParams>, step: WorkflowStep) {
// Scope a per-request Postgres client for this workflow invocation (no-op in
// D1 mode). The socket is reclaimed when the invocation ends, so there is
// nothing to tear down here.
return withPgClient(() => this.runScoped(event, step));
}
private async runScoped(
event: WorkflowEvent<AuditParams>,
step: WorkflowStep,
) {
const { auditId, billingCustomer, projectId, startUrl, config } =
event.payload;
const audit = await AuditRepository.getAuditForWorkflow(
auditId,
event.instanceId,
);
if (!audit) {
throw new Error("Audit workflow context mismatch");
}
if (audit.projectId !== projectId) {
throw new Error("Audit workflow project mismatch");
}
try {
await runAuditPhases(step, {
auditId,
workflowInstanceId: event.instanceId,
billingCustomer,
projectId,
startUrl,
config,
});
} catch (error) {
console.error(`Audit ${auditId} failed:`, error);
await step.do("mark-failed", async () => {
await AuditRepository.failAudit(auditId, event.instanceId);
const latestAudit = await AuditRepository.getAuditForWorkflow(
auditId,
event.instanceId,
);
await captureServerEvent({
distinctId: billingCustomer.userId,
event: "site_audit:complete",
organizationId: billingCustomer.organizationId,
properties: {
project_id: projectId,
status: "failed",
pages_crawled: latestAudit?.pagesCrawled,
pages_total: latestAudit?.pagesTotal,
run_lighthouse: config.lighthouseStrategy !== "none",
},
});
});
throw error;
}
}
}