diff --git a/src/lib/auth-config.ts b/src/lib/auth-config.ts index f5838eb..a9553ad 100644 --- a/src/lib/auth-config.ts +++ b/src/lib/auth-config.ts @@ -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. diff --git a/src/server/billing/autumn-webhook.ts b/src/server/billing/autumn-webhook.ts index e565d08..15687fb 100644 --- a/src/server/billing/autumn-webhook.ts +++ b/src/server/billing/autumn-webhook.ts @@ -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); } } diff --git a/src/server/features/audit/repositories/AuditRepository.ts b/src/server/features/audit/repositories/AuditRepository.ts index 4760352..61573b2 100644 --- a/src/server/features/audit/repositories/AuditRepository.ts +++ b/src/server/features/audit/repositories/AuditRepository.ts @@ -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, diff --git a/src/server/features/rank-tracking/services/scheduledRankChecks.ts b/src/server/features/rank-tracking/services/scheduledRankChecks.ts index 54a0238..8c64b9c 100644 --- a/src/server/features/rank-tracking/services/scheduledRankChecks.ts +++ b/src/server/features/rank-tracking/services/scheduledRankChecks.ts @@ -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; } diff --git a/src/server/mcp/oauth-provider.ts b/src/server/mcp/oauth-provider.ts index 0ed76ec..ad534d1 100644 --- a/src/server/mcp/oauth-provider.ts +++ b/src/server/mcp/oauth-provider.ts @@ -109,7 +109,20 @@ function oauthErrorResponse(error: { status: number; headers: Record; }) { - 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"); diff --git a/src/server/workflows/SiteAuditWorkflow.ts b/src/server/workflows/SiteAuditWorkflow.ts index 55169ec..821b716 100644 --- a/src/server/workflows/SiteAuditWorkflow.ts +++ b/src/server/workflows/SiteAuditWorkflow.ts @@ -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 { }); } 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);