Integrate PageSpeed Insights audits
This commit is contained in:
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;
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user