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() {
|
export function createBaseAuthConfig() {
|
||||||
return {
|
return {
|
||||||
...baseAuthOptions,
|
...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: {
|
account: {
|
||||||
// Encrypt OAuth access/refresh tokens at rest in D1. Also covers the
|
// Encrypt OAuth access/refresh tokens at rest in D1. Also covers the
|
||||||
// google social-login tokens; the key derives from BETTER_AUTH_SECRET.
|
// google social-login tokens; the key derives from BETTER_AUTH_SECRET.
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getRequiredEnvValue } from "@/server/lib/runtime-env";
|
import { getRequiredEnvValue } from "@/server/lib/runtime-env";
|
||||||
|
import { captureServerError } from "@/server/lib/posthog";
|
||||||
import { syncAutumnCustomerStatus } from "./customer-status-sync";
|
import { syncAutumnCustomerStatus } from "./customer-status-sync";
|
||||||
import { verifySvixSignature } from "./svix";
|
import { verifySvixSignature } from "./svix";
|
||||||
|
|
||||||
@ -53,7 +54,17 @@ export async function handleAutumnWebhookRequest(request: Request) {
|
|||||||
try {
|
try {
|
||||||
await syncAutumnCustomerStatus(customerId);
|
await syncAutumnCustomerStatus(customerId);
|
||||||
} catch (error) {
|
} 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);
|
return json({ error: "Webhook processing failed" }, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,6 +116,16 @@ async function batchWriteResults(
|
|||||||
pages: StepPageResult[],
|
pages: StepPageResult[],
|
||||||
lighthouseResults: LighthouseResult[],
|
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) =>
|
await executeInBatches(pages, (tx, page) =>
|
||||||
tx.insert(auditPages).values({
|
tx.insert(auditPages).values({
|
||||||
id: page.id,
|
id: page.id,
|
||||||
|
|||||||
@ -20,9 +20,6 @@ export async function runScheduledRankChecks(env: Env) {
|
|||||||
try {
|
try {
|
||||||
// Skip configs whose org doesn't have a paid plan
|
// Skip configs whose org doesn't have a paid plan
|
||||||
if (isHosted && !(await customerHasPaidPlan(config.organizationId))) {
|
if (isHosted && !(await customerHasPaidPlan(config.organizationId))) {
|
||||||
console.log(
|
|
||||||
`[cron] Skipping config ${config.id} (${config.domain}) — org ${config.organizationId} no longer has access`,
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -109,7 +109,20 @@ function oauthErrorResponse(error: {
|
|||||||
status: number;
|
status: number;
|
||||||
headers: Record<string, string>;
|
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);
|
const headers = new Headers(error.headers);
|
||||||
headers.set("Content-Type", "application/json");
|
headers.set("Content-Type", "application/json");
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { withPgClient } from "@/db";
|
|||||||
import type { BillingCustomerContext } from "@/server/billing/subscription";
|
import type { BillingCustomerContext } from "@/server/billing/subscription";
|
||||||
import { AuditRepository } from "@/server/features/audit/repositories/AuditRepository";
|
import { AuditRepository } from "@/server/features/audit/repositories/AuditRepository";
|
||||||
import type { AuditConfig } from "@/server/lib/audit/types";
|
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 { runAuditPhases } from "@/server/workflows/siteAuditWorkflowPhases";
|
||||||
import { pgStep } from "@/server/workflows/pgStep";
|
import { pgStep } from "@/server/workflows/pgStep";
|
||||||
|
|
||||||
@ -64,6 +64,15 @@ export class SiteAuditWorkflow extends WorkflowEntrypoint<Env, AuditParams> {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Audit ${auditId} failed:`, 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 pgStep(step, "mark-failed", undefined, async () => {
|
||||||
await AuditRepository.failAudit(auditId, event.instanceId);
|
await AuditRepository.failAudit(auditId, event.instanceId);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user