Triage production log errors: audit crash, Autumn webhook FK, PostHog capture, auth rate-limit IP, log noise (#327)
This commit is contained in:
parent
fffdbc9329
commit
4f17fe4942
@ -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.
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user