Integrate PageSpeed Insights audits
This commit is contained in:
314
convex/pageSpeed.ts
Normal file
314
convex/pageSpeed.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
import { internal } from "./_generated/api";
|
||||
import type { Doc, Id } from "./_generated/dataModel";
|
||||
import { internalMutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
const PAGE_SPEED_COUNTER_TEMPLATE = {
|
||||
leadsFound: 1,
|
||||
leadsCreated: 0,
|
||||
auditsCreated: 1,
|
||||
outreachPrepared: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
type PageSpeedLead = Pick<
|
||||
Doc<"leads">,
|
||||
"_id" | "contactStatus"
|
||||
> & {
|
||||
websiteUrl: string;
|
||||
};
|
||||
|
||||
const runStatus = v.union(
|
||||
v.literal("pending"),
|
||||
v.literal("running"),
|
||||
v.literal("succeeded"),
|
||||
v.literal("failed"),
|
||||
v.literal("canceled"),
|
||||
);
|
||||
const pageSpeedStrategy = v.union(v.literal("mobile"), v.literal("desktop"));
|
||||
const pageSpeedResultStatus = v.union(
|
||||
v.literal("succeeded"),
|
||||
v.literal("failed"),
|
||||
);
|
||||
const pageSpeedErrorType = v.union(
|
||||
v.literal("quota"),
|
||||
v.literal("timeout"),
|
||||
v.literal("unavailable"),
|
||||
v.literal("invalid_url"),
|
||||
v.literal("api_error"),
|
||||
v.literal("unknown"),
|
||||
);
|
||||
|
||||
export const queueLeadPageSpeedAudit = internalMutation({
|
||||
args: {
|
||||
leadId: v.id("leads"),
|
||||
parentRunId: v.optional(v.id("agentRuns")),
|
||||
},
|
||||
returns: v.union(v.id("agentRuns"), v.null()),
|
||||
handler: async (ctx, args): Promise<Id<"agentRuns"> | null> => {
|
||||
const now = Date.now();
|
||||
const lead = await ctx.db.get(args.leadId);
|
||||
|
||||
if (!lead || lead.priority === "blocked" || lead.priority === "defer") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!lead.websiteUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingPending = await ctx.db
|
||||
.query("agentRuns")
|
||||
.withIndex("by_type_and_status_and_leadId", (q) =>
|
||||
q.eq("type", "audit").eq("status", "pending").eq("leadId", args.leadId),
|
||||
)
|
||||
.take(1);
|
||||
|
||||
const existingRunning = await ctx.db
|
||||
.query("agentRuns")
|
||||
.withIndex("by_type_and_status_and_leadId", (q) =>
|
||||
q.eq("type", "audit").eq("status", "running").eq("leadId", args.leadId),
|
||||
)
|
||||
.take(1);
|
||||
|
||||
if (existingPending.length > 0) {
|
||||
return existingPending[0]._id;
|
||||
}
|
||||
if (existingRunning.length > 0) {
|
||||
return existingRunning[0]._id;
|
||||
}
|
||||
|
||||
const runId = await ctx.db.insert("agentRuns", {
|
||||
type: "audit",
|
||||
leadId: args.leadId,
|
||||
status: "pending",
|
||||
currentStep: "pagespeed_insights",
|
||||
counters: PAGE_SPEED_COUNTER_TEMPLATE,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
await ctx.db.insert("agentRunEvents", {
|
||||
runId,
|
||||
level: "info",
|
||||
message: "PageSpeed-Analyse wurde in die Warteschlange gesetzt.",
|
||||
details: [
|
||||
{ label: "Lead", value: args.leadId },
|
||||
...(args.parentRunId ? [{ label: "Parent-Run", value: args.parentRunId }] : []),
|
||||
],
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
await ctx.scheduler.runAfter(
|
||||
0,
|
||||
internal.pageSpeedAction.processPageSpeedAudit,
|
||||
{
|
||||
runId,
|
||||
},
|
||||
);
|
||||
|
||||
return runId;
|
||||
},
|
||||
});
|
||||
|
||||
export const startPageSpeedAuditRun = internalMutation({
|
||||
args: {
|
||||
runId: v.id("agentRuns"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
lead: v.object({
|
||||
_id: v.id("leads"),
|
||||
websiteUrl: v.string(),
|
||||
contactStatus: v.union(
|
||||
v.literal("new"),
|
||||
v.literal("missing_contact"),
|
||||
v.literal("audit_ready"),
|
||||
v.literal("outreach_ready"),
|
||||
v.literal("contacted"),
|
||||
v.literal("replied"),
|
||||
v.literal("do_not_contact"),
|
||||
),
|
||||
}),
|
||||
auditId: v.optional(v.id("audits")),
|
||||
}),
|
||||
v.null(),
|
||||
),
|
||||
handler: async (ctx, args): Promise<
|
||||
{ lead: PageSpeedLead; auditId?: Id<"audits"> } | null
|
||||
> => {
|
||||
const now = Date.now();
|
||||
const run = await ctx.db.get(args.runId);
|
||||
|
||||
if (!run) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (run.type !== "audit") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (run.status !== "pending") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!run.leadId) {
|
||||
await ctx.db.patch(args.runId, {
|
||||
status: "failed",
|
||||
currentStep: "pagespeed_insights",
|
||||
errorSummary: "Run hat keine Lead-ID.",
|
||||
updatedAt: now,
|
||||
finishedAt: now,
|
||||
});
|
||||
|
||||
await ctx.db.insert("agentRunEvents", {
|
||||
runId: args.runId,
|
||||
level: "error",
|
||||
message:
|
||||
"PageSpeed-Analyse konnte nicht gestartet werden: Kein Lead verknüpft.",
|
||||
details: [{ label: "Lead-ID", value: "unbekannt" }],
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const lead = await ctx.db.get(run.leadId);
|
||||
if (!lead) {
|
||||
await ctx.db.patch(args.runId, {
|
||||
status: "failed",
|
||||
currentStep: "pagespeed_insights",
|
||||
errorSummary: "Lead wurde nicht gefunden.",
|
||||
updatedAt: now,
|
||||
finishedAt: now,
|
||||
});
|
||||
|
||||
await ctx.db.insert("agentRunEvents", {
|
||||
runId: args.runId,
|
||||
level: "error",
|
||||
message:
|
||||
"PageSpeed-Analyse konnte nicht gestartet werden: Kein Lead mit Website-URL.",
|
||||
details: [{ label: "Lead-ID", value: run.leadId }],
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!lead.websiteUrl) {
|
||||
await ctx.db.patch(args.runId, {
|
||||
status: "failed",
|
||||
currentStep: "pagespeed_insights",
|
||||
errorSummary: "Lead hat keine Website-URL.",
|
||||
updatedAt: now,
|
||||
finishedAt: now,
|
||||
});
|
||||
|
||||
await ctx.db.insert("agentRunEvents", {
|
||||
runId: args.runId,
|
||||
level: "error",
|
||||
message:
|
||||
"PageSpeed-Analyse konnte nicht gestartet werden: Kein Lead mit Website-URL.",
|
||||
details: [{ label: "Lead-ID", value: lead._id }],
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.runId, {
|
||||
status: "running",
|
||||
currentStep: "pagespeed_insights",
|
||||
startedAt: now,
|
||||
updatedAt: now,
|
||||
errorSummary: undefined,
|
||||
});
|
||||
|
||||
await ctx.db.insert("agentRunEvents", {
|
||||
runId: args.runId,
|
||||
level: "info",
|
||||
message: "PageSpeed-Analyse gestartet.",
|
||||
details: [{ label: "Lead-ID", value: lead._id }],
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
return {
|
||||
lead: {
|
||||
_id: lead._id,
|
||||
websiteUrl: lead.websiteUrl,
|
||||
contactStatus: lead.contactStatus,
|
||||
},
|
||||
...(run.auditId ? { auditId: run.auditId } : {}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const persistPageSpeedResult = internalMutation({
|
||||
args: {
|
||||
leadId: v.id("leads"),
|
||||
auditId: v.optional(v.id("audits")),
|
||||
runId: v.id("agentRuns"),
|
||||
strategy: pageSpeedStrategy,
|
||||
status: pageSpeedResultStatus,
|
||||
sourceUrl: v.string(),
|
||||
finalUrl: v.optional(v.string()),
|
||||
rawStorageId: v.optional(v.id("_storage")),
|
||||
errorType: v.optional(pageSpeedErrorType),
|
||||
errorSummary: v.optional(v.string()),
|
||||
fetchedAt: v.number(),
|
||||
normalized: v.optional(
|
||||
v.object({
|
||||
scores: v.optional(
|
||||
v.object({
|
||||
performance: v.optional(v.number()),
|
||||
accessibility: v.optional(v.number()),
|
||||
bestPractices: v.optional(v.number()),
|
||||
seo: v.optional(v.number()),
|
||||
}),
|
||||
),
|
||||
metrics: v.optional(
|
||||
v.object({
|
||||
firstContentfulPaintMs: v.optional(v.number()),
|
||||
largestContentfulPaintMs: v.optional(v.number()),
|
||||
cumulativeLayoutShift: v.optional(v.number()),
|
||||
totalBlockingTimeMs: v.optional(v.number()),
|
||||
speedIndexMs: v.optional(v.number()),
|
||||
}),
|
||||
),
|
||||
opportunities: v.optional(v.array(v.string())),
|
||||
implications: v.optional(v.array(v.string())),
|
||||
}),
|
||||
),
|
||||
},
|
||||
returns: v.id("pageSpeedResults"),
|
||||
handler: async (ctx, args): Promise<Id<"pageSpeedResults">> => {
|
||||
return await ctx.db.insert("pageSpeedResults", {
|
||||
...args,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const finishPageSpeedAuditRun = internalMutation({
|
||||
args: {
|
||||
runId: v.id("agentRuns"),
|
||||
status: runStatus,
|
||||
errorSummary: v.optional(v.string()),
|
||||
errors: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const now = Date.now();
|
||||
|
||||
await ctx.db.patch(args.runId, {
|
||||
status: args.status,
|
||||
updatedAt: now,
|
||||
finishedAt: now,
|
||||
currentStep: "pagespeed_insights",
|
||||
errorSummary: args.errorSummary,
|
||||
counters: {
|
||||
...PAGE_SPEED_COUNTER_TEMPLATE,
|
||||
errors: args.errors ?? 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user