From 700c33096e483b26a3e2ae1fc4d012451b4e014d Mon Sep 17 00:00:00 2001 From: Georgy Butaev <41178744+g-but@users.noreply.github.com> Date: Sat, 29 Aug 2026 10:06:09 +0200 Subject: [PATCH] fix(notifications): stop email dispatch from logging one error per notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isEmailConfigured() exists specifically so callers can short-circuit before attempting a send — this dispatcher wasn't using it. Every dispatched notification for an email-enabled type went straight to getEmailClient(), which throws 'RESEND_API_KEY is not set', caught and logged as a fresh error every time. Found via a fleet-wide log audit: 56 identical errors in 24h, one per queued notification. The condition (no key deployed) is static, not per-notification — it only needs saying once. Now warns once per process via the same "warn once" pattern getEmailClient() already uses for its own placeholder-key case, then returns cleanly. This does not configure email — RESEND_API_KEY is still absent from production, which is a config/product decision (provide a key, or decide notifications don't need email) for someone with the account, not something to fix by adding a credential blind. This fixes the failure MODE: loud once, not flooding forever. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Y9rKLxddothnXEtY6KDziN --- src/services/notifications/dispatcher.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/services/notifications/dispatcher.ts b/src/services/notifications/dispatcher.ts index 57fe43ad9..2ca95407c 100644 --- a/src/services/notifications/dispatcher.ts +++ b/src/services/notifications/dispatcher.ts @@ -13,7 +13,7 @@ import { fromTable } from '@/lib/supabase/untyped'; import { createAdminClient } from '@/lib/supabase/admin'; import { DATABASE_TABLES } from '@/config/database-tables'; -import { getEmailClient } from '@/lib/email/client'; +import { getEmailClient, isEmailConfigured } from '@/lib/email/client'; import { EMAIL_COLORS } from '@/lib/email/templates/layout'; import { SITE_URL } from '@/config/brand'; import { logger } from '@/utils/logger'; @@ -21,6 +21,15 @@ import { logger } from '@/utils/logger'; const LOG_SOURCE = 'NotificationDispatcher'; const FROM_EMAIL = process.env.RESEND_FROM_EMAIL || 'notifications@orangecat.ch'; +// isEmailConfigured() exists specifically so callers can short-circuit before +// attempting a send — this dispatcher wasn't using it, so every dispatched +// notification hit getEmailClient()'s throw and logged a fresh error. Found +// 2026-08-29: 56 identical "RESEND_API_KEY is not set" errors in 24h, one per +// notification, drowning any real email failure in the same log. The +// condition is static (no key deployed), not per-notification, so it only +// needs saying once. +let _unconfiguredWarned = false; + // ===================================================================== // CONFIG: Which notification types trigger emails // ===================================================================== @@ -152,6 +161,19 @@ export class NotificationDispatcher { * Resolves the user's email address and sends via Resend. */ private static async sendEmailNotification(params: DispatchParams): Promise { + if (!isEmailConfigured()) { + if (!_unconfiguredWarned) { + _unconfiguredWarned = true; + logger.warn( + 'RESEND_API_KEY is not set — email notifications are disabled. ' + + 'This warns once per process, not once per notification.', + {}, + LOG_SOURCE + ); + } + return; + } + const admin = createAdminClient(); // Resolve email: profile contact_email -> auth user email