Integrate PageSpeed Insights audits
This commit is contained in:
6
convex/_generated/api.d.ts
vendored
6
convex/_generated/api.d.ts
vendored
@@ -8,6 +8,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type * as auditInputs from "../auditInputs.js";
|
||||
import type * as audits from "../audits.js";
|
||||
import type * as blacklist from "../blacklist.js";
|
||||
import type * as campaigns from "../campaigns.js";
|
||||
@@ -16,6 +17,8 @@ import type * as http from "../http.js";
|
||||
import type * as leadDiscovery from "../leadDiscovery.js";
|
||||
import type * as leads from "../leads.js";
|
||||
import type * as outreach from "../outreach.js";
|
||||
import type * as pageSpeed from "../pageSpeed.js";
|
||||
import type * as pageSpeedAction from "../pageSpeedAction.js";
|
||||
import type * as runs from "../runs.js";
|
||||
import type * as settings from "../settings.js";
|
||||
import type * as storage from "../storage.js";
|
||||
@@ -29,6 +32,7 @@ import type {
|
||||
} from "convex/server";
|
||||
|
||||
declare const fullApi: ApiFromModules<{
|
||||
auditInputs: typeof auditInputs;
|
||||
audits: typeof audits;
|
||||
blacklist: typeof blacklist;
|
||||
campaigns: typeof campaigns;
|
||||
@@ -37,6 +41,8 @@ declare const fullApi: ApiFromModules<{
|
||||
leadDiscovery: typeof leadDiscovery;
|
||||
leads: typeof leads;
|
||||
outreach: typeof outreach;
|
||||
pageSpeed: typeof pageSpeed;
|
||||
pageSpeedAction: typeof pageSpeedAction;
|
||||
runs: typeof runs;
|
||||
settings: typeof settings;
|
||||
storage: typeof storage;
|
||||
|
||||
60
convex/auditInputs.ts
Normal file
60
convex/auditInputs.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { v } from "convex/values";
|
||||
|
||||
import type { Doc, Id } from "./_generated/dataModel";
|
||||
import { internalQuery } from "./_generated/server";
|
||||
import { buildPageSpeedAuditInputs, type PageSpeedMinimalAuditResult } from "../lib/pagespeed-audit-input";
|
||||
|
||||
function normalizePageSpeedResultRow(
|
||||
row: Doc<"pageSpeedResults">,
|
||||
): PageSpeedMinimalAuditResult {
|
||||
return {
|
||||
strategy: row.strategy,
|
||||
status: row.status,
|
||||
sourceUrl: row.sourceUrl,
|
||||
...(row.finalUrl ? { finalUrl: row.finalUrl } : {}),
|
||||
...(row.normalized ? { normalized: row.normalized } : {}),
|
||||
...(row.errorType ? { errorType: row.errorType } : {}),
|
||||
...(row.errorSummary ? { errorSummary: row.errorSummary } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export const getPageSpeedAuditInputs = internalQuery({
|
||||
args: {
|
||||
leadId: v.optional(v.id("leads")),
|
||||
auditId: v.optional(v.id("audits")),
|
||||
},
|
||||
handler: async (
|
||||
ctx,
|
||||
args,
|
||||
): Promise<{
|
||||
technicalSignals: string[];
|
||||
customerImplications: string[];
|
||||
internalNotes: string[];
|
||||
}> => {
|
||||
let results: Doc<"pageSpeedResults">[];
|
||||
|
||||
if (args.auditId) {
|
||||
results = await ctx.db
|
||||
.query("pageSpeedResults")
|
||||
.withIndex("by_auditId", (q) => q.eq("auditId", args.auditId as Id<"audits">))
|
||||
.order("desc")
|
||||
.take(50);
|
||||
return buildPageSpeedAuditInputs(results.map(normalizePageSpeedResultRow));
|
||||
}
|
||||
|
||||
if (args.leadId) {
|
||||
results = await ctx.db
|
||||
.query("pageSpeedResults")
|
||||
.withIndex("by_leadId", (q) => q.eq("leadId", args.leadId as Id<"leads">))
|
||||
.order("desc")
|
||||
.take(50);
|
||||
return buildPageSpeedAuditInputs(results.map(normalizePageSpeedResultRow));
|
||||
}
|
||||
|
||||
return {
|
||||
technicalSignals: [],
|
||||
customerImplications: [],
|
||||
internalNotes: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -95,6 +95,16 @@ export const RUN_STATUSES = [
|
||||
] as const;
|
||||
export const RUN_EVENT_LEVELS = ["info", "warning", "error"] as const;
|
||||
export const SCREENSHOT_VIEWPORTS = ["desktop", "mobile"] as const;
|
||||
export const PAGE_SPEED_STRATEGIES = ["mobile", "desktop"] as const;
|
||||
export const PAGE_SPEED_RESULT_STATUSES = ["succeeded", "failed"] as const;
|
||||
export const PAGE_SPEED_ERROR_TYPES = [
|
||||
"quota",
|
||||
"timeout",
|
||||
"unavailable",
|
||||
"invalid_url",
|
||||
"api_error",
|
||||
"unknown",
|
||||
] as const;
|
||||
|
||||
export type CampaignStatus = (typeof CAMPAIGN_STATUSES)[number];
|
||||
export type LeadPriority = (typeof LEAD_PRIORITIES)[number];
|
||||
@@ -114,6 +124,9 @@ export type RunType = (typeof RUN_TYPES)[number];
|
||||
export type RunStatus = (typeof RUN_STATUSES)[number];
|
||||
export type RunEventLevel = (typeof RUN_EVENT_LEVELS)[number];
|
||||
export type ScreenshotViewport = (typeof SCREENSHOT_VIEWPORTS)[number];
|
||||
export type PageSpeedStrategy = (typeof PAGE_SPEED_STRATEGIES)[number];
|
||||
export type PageSpeedResultStatus = (typeof PAGE_SPEED_RESULT_STATUSES)[number];
|
||||
export type PageSpeedErrorType = (typeof PAGE_SPEED_ERROR_TYPES)[number];
|
||||
|
||||
export type SettingsRow = {
|
||||
key: string;
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
289
convex/pageSpeedAction.ts
Normal file
289
convex/pageSpeedAction.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
"use node";
|
||||
|
||||
import { api, internal } from "./_generated/api";
|
||||
import { internalAction } from "./_generated/server";
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
import { v } from "convex/values";
|
||||
import {
|
||||
classifyPageSpeedError,
|
||||
fetchPageSpeedResult,
|
||||
normalizePageSpeedResult,
|
||||
type PageSpeedErrorType,
|
||||
} from "../lib/pagespeed-insights";
|
||||
|
||||
const STRATEGIES = ["mobile", "desktop"] as const;
|
||||
export const MAX_RAW_PAGESPEED_BYTES = 1_000_000;
|
||||
const RAW_PAGESPEED_BYTES_SUMMARY =
|
||||
"PageSpeed-Rohdaten sind groesser als das interne Speicherlimit.";
|
||||
const DEFAULT_PAGESPEED_TIMEOUT_MS = 60_000;
|
||||
const MIN_PAGESPEED_TIMEOUT_MS = 10_000;
|
||||
const MAX_PAGESPEED_TIMEOUT_MS = 120_000;
|
||||
|
||||
function toPersistedPageSpeedNormalizedResult(
|
||||
normalized: ReturnType<typeof normalizePageSpeedResult>,
|
||||
) {
|
||||
return {
|
||||
...(normalized.scores ? { scores: normalized.scores } : {}),
|
||||
metrics: normalized.metrics,
|
||||
opportunities: normalized.opportunities,
|
||||
implications: normalized.implications,
|
||||
};
|
||||
}
|
||||
|
||||
function parsePageSpeedTimeoutMs(raw: string | undefined): number {
|
||||
if (!raw) {
|
||||
return DEFAULT_PAGESPEED_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return DEFAULT_PAGESPEED_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
return Math.min(
|
||||
Math.max(parsed, MIN_PAGESPEED_TIMEOUT_MS),
|
||||
MAX_PAGESPEED_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePageSpeedTimeoutMs() {
|
||||
return parsePageSpeedTimeoutMs(process.env.PAGESPEED_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
function isPageSpeedErrorType(value: unknown): value is PageSpeedErrorType {
|
||||
return (
|
||||
value === "quota" ||
|
||||
value === "timeout" ||
|
||||
value === "unavailable" ||
|
||||
value === "invalid_url" ||
|
||||
value === "api_error" ||
|
||||
value === "unknown"
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeValue(value: string, secret?: string | null) {
|
||||
if (!secret || !value) {
|
||||
return value;
|
||||
}
|
||||
const escapedSecret = secret.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return value.replace(new RegExp(escapedSecret, "g"), "[REDACTED]");
|
||||
}
|
||||
|
||||
function classifyPageSpeedFailure(input: unknown, apiKey?: string | null) {
|
||||
const directType =
|
||||
typeof input === "object" &&
|
||||
input !== null &&
|
||||
"errorType" in input &&
|
||||
(input as { errorType?: unknown }).errorType;
|
||||
|
||||
const normalizedType = isPageSpeedErrorType(directType) ? directType : null;
|
||||
if (normalizedType) {
|
||||
const message =
|
||||
input instanceof Error && input.message
|
||||
? input.message
|
||||
: typeof input === "string"
|
||||
? input
|
||||
: "PageSpeed-Analyse fehlgeschlagen.";
|
||||
return {
|
||||
errorType: normalizedType,
|
||||
errorSummary: sanitizeValue(message, apiKey),
|
||||
};
|
||||
}
|
||||
|
||||
const classified = classifyPageSpeedError({
|
||||
error: input,
|
||||
});
|
||||
const errorSummary = sanitizeValue(classified.message, apiKey);
|
||||
|
||||
return {
|
||||
errorType: classified.errorType,
|
||||
errorSummary,
|
||||
};
|
||||
}
|
||||
|
||||
export const processPageSpeedAudit = internalAction({
|
||||
args: {
|
||||
runId: v.id("agentRuns"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const apiKeyRaw = process.env.PAGESPEED_API_KEY?.trim();
|
||||
const apiKey = apiKeyRaw ? apiKeyRaw : undefined;
|
||||
|
||||
let started:
|
||||
| {
|
||||
lead: {
|
||||
_id: Id<"leads">;
|
||||
websiteUrl: string;
|
||||
};
|
||||
auditId?: Id<"audits">;
|
||||
}
|
||||
| null = null;
|
||||
|
||||
try {
|
||||
started = await ctx.runMutation(internal.pageSpeed.startPageSpeedAuditRun, {
|
||||
runId: args.runId,
|
||||
});
|
||||
} catch (error) {
|
||||
const { errorSummary } = classifyPageSpeedFailure(error, apiKeyRaw);
|
||||
|
||||
await ctx.runMutation(internal.pageSpeed.finishPageSpeedAuditRun, {
|
||||
runId: args.runId,
|
||||
status: "failed",
|
||||
errors: 1,
|
||||
errorSummary,
|
||||
});
|
||||
|
||||
await ctx.runMutation(api.runs.appendEvent, {
|
||||
runId: args.runId,
|
||||
level: "error",
|
||||
message: "PageSpeed-Analyse fehlgeschlagen.",
|
||||
details: [{ label: "Fehler", value: errorSummary }],
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!started) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceUrl = started.lead.websiteUrl;
|
||||
const timeoutMs = resolvePageSpeedTimeoutMs();
|
||||
|
||||
let failedStrategies = 0;
|
||||
let succeededStrategies = 0;
|
||||
|
||||
try {
|
||||
for (const strategy of STRATEGIES) {
|
||||
const fetchedAt = Date.now();
|
||||
try {
|
||||
const raw = await fetchPageSpeedResult({
|
||||
url: sourceUrl,
|
||||
strategy,
|
||||
apiKey,
|
||||
timeoutMs,
|
||||
});
|
||||
const rawJson = JSON.stringify(raw) ?? "null";
|
||||
const rawJsonBytes = new TextEncoder().encode(rawJson).byteLength;
|
||||
if (rawJsonBytes > MAX_RAW_PAGESPEED_BYTES) {
|
||||
failedStrategies += 1;
|
||||
await ctx.runMutation(internal.pageSpeed.persistPageSpeedResult, {
|
||||
leadId: started.lead._id,
|
||||
...(started.auditId ? { auditId: started.auditId } : {}),
|
||||
runId: args.runId,
|
||||
strategy,
|
||||
status: "failed",
|
||||
sourceUrl,
|
||||
errorType: "api_error",
|
||||
errorSummary: RAW_PAGESPEED_BYTES_SUMMARY,
|
||||
fetchedAt,
|
||||
});
|
||||
|
||||
await ctx.runMutation(api.runs.appendEvent, {
|
||||
runId: args.runId,
|
||||
level: "warning",
|
||||
message: `PageSpeed-Analyse für ${strategy} fehlgeschlagen.`,
|
||||
details: [
|
||||
{ label: "Strategie", value: strategy },
|
||||
{
|
||||
label: "Fehler",
|
||||
value: RAW_PAGESPEED_BYTES_SUMMARY,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawStorageId = await ctx.storage.store(
|
||||
new Blob([rawJson], { type: "application/json" }),
|
||||
);
|
||||
const normalized = normalizePageSpeedResult({
|
||||
strategy,
|
||||
sourceUrl,
|
||||
raw,
|
||||
});
|
||||
|
||||
await ctx.runMutation(internal.pageSpeed.persistPageSpeedResult, {
|
||||
leadId: started.lead._id,
|
||||
...(started.auditId ? { auditId: started.auditId } : {}),
|
||||
runId: args.runId,
|
||||
strategy,
|
||||
status: "succeeded",
|
||||
sourceUrl,
|
||||
finalUrl: normalized.finalUrl,
|
||||
rawStorageId,
|
||||
fetchedAt,
|
||||
normalized: toPersistedPageSpeedNormalizedResult(normalized),
|
||||
});
|
||||
|
||||
await ctx.runMutation(api.runs.appendEvent, {
|
||||
runId: args.runId,
|
||||
level: "info",
|
||||
message: `PageSpeed-Analyse für ${strategy} abgeschlossen.`,
|
||||
details: [{ label: "Strategie", value: strategy }],
|
||||
});
|
||||
succeededStrategies += 1;
|
||||
} catch (error) {
|
||||
const { errorType, errorSummary } = classifyPageSpeedFailure(
|
||||
error,
|
||||
apiKeyRaw,
|
||||
);
|
||||
failedStrategies += 1;
|
||||
|
||||
await ctx.runMutation(internal.pageSpeed.persistPageSpeedResult, {
|
||||
leadId: started.lead._id,
|
||||
...(started.auditId ? { auditId: started.auditId } : {}),
|
||||
runId: args.runId,
|
||||
strategy,
|
||||
status: "failed",
|
||||
sourceUrl,
|
||||
errorType,
|
||||
errorSummary,
|
||||
fetchedAt,
|
||||
});
|
||||
|
||||
await ctx.runMutation(api.runs.appendEvent, {
|
||||
runId: args.runId,
|
||||
level: "warning",
|
||||
message: `PageSpeed-Analyse für ${strategy} fehlgeschlagen.`,
|
||||
details: [
|
||||
{ label: "Strategie", value: strategy },
|
||||
{ label: "Fehler", value: errorSummary },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const status = succeededStrategies > 0 ? "succeeded" : "failed";
|
||||
const errors = failedStrategies;
|
||||
await ctx.runMutation(internal.pageSpeed.finishPageSpeedAuditRun, {
|
||||
runId: args.runId,
|
||||
status,
|
||||
errors,
|
||||
errorSummary:
|
||||
status === "failed" && errors > 0
|
||||
? "Ein oder mehrere PageSpeed-Strategien konnten nicht ausgeführt werden."
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return args.runId;
|
||||
} catch (error) {
|
||||
const { errorSummary } = classifyPageSpeedFailure(error, apiKeyRaw);
|
||||
await ctx.runMutation(internal.pageSpeed.finishPageSpeedAuditRun, {
|
||||
runId: args.runId,
|
||||
status: "failed",
|
||||
errors: Math.max(1, failedStrategies),
|
||||
errorSummary,
|
||||
});
|
||||
|
||||
await ctx.runMutation(api.runs.appendEvent, {
|
||||
runId: args.runId,
|
||||
level: "error",
|
||||
message: "PageSpeed-Analyse fehlgeschlagen.",
|
||||
details: [{ label: "Fehler", value: errorSummary, source: "pagespeed_action" }],
|
||||
});
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -95,6 +95,22 @@ const runEventLevel = v.union(
|
||||
...RUN_EVENT_LEVELS.map((level) => v.literal(level)),
|
||||
);
|
||||
const screenshotViewport = v.union(v.literal("desktop"), v.literal("mobile"));
|
||||
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"),
|
||||
);
|
||||
const settingsValue = v.union(v.string(), v.number(), v.boolean(), v.null());
|
||||
const auditMetricSummary = v.object({
|
||||
performanceScore: v.optional(v.number()),
|
||||
@@ -255,6 +271,48 @@ export default defineSchema({
|
||||
.index("by_auditId_and_viewport", ["auditId", "viewport"])
|
||||
.index("by_storageId", ["storageId"]),
|
||||
|
||||
pageSpeedResults: defineTable({
|
||||
leadId: v.id("leads"),
|
||||
auditId: v.optional(v.id("audits")),
|
||||
runId: v.optional(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(),
|
||||
createdAt: 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())),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.index("by_leadId", ["leadId"])
|
||||
.index("by_runId", ["runId"])
|
||||
.index("by_auditId", ["auditId"])
|
||||
.index("by_leadId_and_strategy", ["leadId", "strategy"]),
|
||||
|
||||
websiteCrawlPages: defineTable({
|
||||
leadId: v.id("leads"),
|
||||
runId: v.optional(v.id("agentRuns")),
|
||||
|
||||
@@ -433,6 +433,27 @@ export const processLeadEnrichment = internalAction({
|
||||
|
||||
const rootUrl = normalizeCrawlUrl(started.lead.websiteUrl);
|
||||
if (!rootUrl) {
|
||||
try {
|
||||
await ctx.runMutation(internal.pageSpeed.queueLeadPageSpeedAudit, {
|
||||
leadId: started.lead._id,
|
||||
parentRunId: runId,
|
||||
});
|
||||
} catch (pageSpeedQueueError) {
|
||||
await ctx.runMutation(api.runs.appendEvent, {
|
||||
runId,
|
||||
level: "warning",
|
||||
message: "PageSpeed-Analyse konnte nicht in die Warteschlange gesetzt werden.",
|
||||
details: [
|
||||
{ label: "Lead", value: started.lead._id },
|
||||
{
|
||||
label: "Fehler",
|
||||
value: messageFromError(pageSpeedQueueError),
|
||||
source: "pagespeed_queue",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.runMutation(internal.websiteEnrichment.finishLeadEnrichmentRun, {
|
||||
runId,
|
||||
status: "failed",
|
||||
@@ -665,6 +686,27 @@ export const processLeadEnrichment = internalAction({
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await ctx.runMutation(internal.pageSpeed.queueLeadPageSpeedAudit, {
|
||||
leadId: started.lead._id,
|
||||
parentRunId: runId,
|
||||
});
|
||||
} catch (pageSpeedQueueError) {
|
||||
await ctx.runMutation(api.runs.appendEvent, {
|
||||
runId,
|
||||
level: "warning",
|
||||
message: "PageSpeed-Analyse konnte nicht in die Warteschlange gesetzt werden.",
|
||||
details: [
|
||||
{ label: "Lead", value: started.lead._id },
|
||||
{
|
||||
label: "Fehler",
|
||||
value: messageFromError(pageSpeedQueueError),
|
||||
source: "pagespeed_queue",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.runMutation(internal.websiteEnrichment.finishLeadEnrichmentRun, {
|
||||
runId,
|
||||
status: "succeeded",
|
||||
@@ -681,6 +723,7 @@ export const processLeadEnrichment = internalAction({
|
||||
});
|
||||
|
||||
return runId;
|
||||
|
||||
} catch (error) {
|
||||
const errorSummary = messageFromError(error);
|
||||
|
||||
@@ -702,6 +745,26 @@ export const processLeadEnrichment = internalAction({
|
||||
});
|
||||
|
||||
if (started) {
|
||||
try {
|
||||
await ctx.runMutation(internal.pageSpeed.queueLeadPageSpeedAudit, {
|
||||
leadId: started.lead._id,
|
||||
parentRunId: runId,
|
||||
});
|
||||
} catch (pageSpeedQueueError) {
|
||||
await ctx.runMutation(api.runs.appendEvent, {
|
||||
runId,
|
||||
level: "warning",
|
||||
message: "PageSpeed-Analyse konnte nicht in die Warteschlange gesetzt werden.",
|
||||
details: [
|
||||
{ label: "Lead", value: started.lead._id },
|
||||
{
|
||||
label: "Fehler",
|
||||
value: messageFromError(pageSpeedQueueError),
|
||||
source: "pagespeed_queue",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
await ctx.runMutation(internal.websiteEnrichment.patchLeadFromWebsiteEnrichment, {
|
||||
leadId: started.lead._id,
|
||||
currentContactStatus: started.lead.contactStatus,
|
||||
|
||||
Reference in New Issue
Block a user