Triage production log errors: audit crash, Autumn webhook FK, PostHog capture, auth rate-limit IP, log noise (#327)

This commit is contained in:
Ben Senescu 2026-07-01 09:57:30 -04:00 committed by Ben Senescu
parent fffdbc9329
commit 4f17fe4942
6 changed files with 55 additions and 6 deletions

View File

@ -6,6 +6,15 @@ import { GSC_OAUTH_PROVIDER_ID, GSC_OAUTH_SCOPES } from "@/shared/gsc";
export function createBaseAuthConfig() {
return {
...baseAuthOptions,
advanced: {
ipAddress: {
// On Cloudflare Workers the client IP arrives in CF-Connecting-IP;
// x-forwarded-for (better-auth's default) is absent, so without this
// getIp() returns null and rate limiting is silently skipped on every
// /api/auth endpoint. Header lookup is case-insensitive.
ipAddressHeaders: ["cf-connecting-ip"],
},
},
account: {
// Encrypt OAuth access/refresh tokens at rest in D1. Also covers the
// google social-login tokens; the key derives from BETTER_AUTH_SECRET.

View File

@ -1,5 +1,6 @@
import { z } from "zod";
import { getRequiredEnvValue } from "@/server/lib/runtime-env";
import { captureServerError } from "@/server/lib/posthog";
import { syncAutumnCustomerStatus } from "./customer-status-sync";
import { verifySvixSignature } from "./svix";
@ -53,7 +54,17 @@ export async function handleAutumnWebhookRequest(request: Request) {
try {
await syncAutumnCustomerStatus(customerId);
} catch (error) {
console.error("Autumn billing.updated sync failed", error);
// Drizzle truncates its own message to "Failed query:"; the real driver
// detail (postgres.js code/constraint) lives on error.cause, so log that
// explicitly. Also forward to PostHog — this raw handler runs outside the
// server-function middleware, so nothing else captures it as a $exception.
console.error("Autumn billing.updated sync failed", customerId, error, {
cause: error instanceof Error ? error.cause : undefined,
});
await captureServerError(error, {
source: "autumn_webhook",
customer_id: customerId,
});
return json({ error: "Webhook processing failed" }, 500);
}
}

View File

@ -116,6 +116,16 @@ async function batchWriteResults(
pages: StepPageResult[],
lighthouseResults: LighthouseResult[],
) {
// The `finalize` workflow step can retry after a partial write (multi-chunk
// inserts aren't atomic, and steps after the insert can throw). Clear any
// rows from a prior attempt first so the re-run is idempotent — otherwise
// stable page ids collide on the PK and lighthouse rows silently duplicate.
// audit_lighthouse_results.page_id FKs audit_pages, so delete it first.
await db
.delete(auditLighthouseResults)
.where(eq(auditLighthouseResults.auditId, auditId));
await db.delete(auditPages).where(eq(auditPages.auditId, auditId));
await executeInBatches(pages, (tx, page) =>
tx.insert(auditPages).values({
id: page.id,

View File

@ -20,9 +20,6 @@ export async function runScheduledRankChecks(env: Env) {
try {
// Skip configs whose org doesn't have a paid plan
if (isHosted && !(await customerHasPaidPlan(config.organizationId))) {
console.log(
`[cron] Skipping config ${config.id} (${config.domain}) — org ${config.organizationId} no longer has access`,
);
continue;
}

View File

@ -109,7 +109,20 @@ function oauthErrorResponse(error: {
status: number;
headers: Record<string, string>;
}) {
console.warn(`[oauth] ${error.status} ${error.code}: ${error.description}`);
// 401s here are the standard OAuth discovery handshake, not failures: an
// unauthenticated /mcp hit returns `invalid_token` (which triggers the
// client's .well-known discovery), and the DCR client_secret_post shim makes
// a client's first token attempt return `invalid_client` before it retries
// with the secret. Log those at debug so they stop masquerading as errors;
// keep 5xx at error and everything else (bad client metadata, etc.) at warn.
const line = `[oauth] ${error.status} ${error.code}: ${error.description}`;
if (error.status === 401) {
console.debug(line);
} else if (error.status >= 500) {
console.error(line);
} else {
console.warn(line);
}
const headers = new Headers(error.headers);
headers.set("Content-Type", "application/json");

View File

@ -13,7 +13,7 @@ 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 { captureServerError, captureServerEvent } from "@/server/lib/posthog";
import { runAuditPhases } from "@/server/workflows/siteAuditWorkflowPhases";
import { pgStep } from "@/server/workflows/pgStep";
@ -64,6 +64,15 @@ export class SiteAuditWorkflow extends WorkflowEntrypoint<Env, AuditParams> {
});
} catch (error) {
console.error(`Audit ${auditId} failed:`, error);
// Workflow entrypoints run outside the server-function middleware, so
// nothing else forwards this throw to PostHog as a $exception. Capture it
// here (awaited — Workflows have no ctx.waitUntil) before re-throwing.
await captureServerError(error, {
source: "site_audit_workflow",
audit_id: auditId,
organization_id: billingCustomer.organizationId,
project_id: projectId,
});
await pgStep(step, "mark-failed", undefined, async () => {
await AuditRepository.failAudit(auditId, event.instanceId);