Files
cim_summary/.planning/milestones/v1.0-phases/02-backend-services/02-03-PLAN.md
admin 38a0f0619d chore: complete v1.0 Analytics & Monitoring milestone
Archive milestone artifacts (roadmap, requirements, audit, phase directories)
to .planning/milestones/. Evolve PROJECT.md with validated requirements and
decision outcomes. Create MILESTONES.md and RETROSPECTIVE.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 10:34:18 -05:00

9.2 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-backend-services 03 execute 2
02-02
backend/src/services/alertService.ts
backend/src/__tests__/unit/alertService.test.ts
true
ALRT-01
ALRT-02
ALRT-04
truths artifacts key_links
An alert email is sent when a probe returns 'degraded' or 'down'
A second probe failure within the cooldown period does NOT send a duplicate email
Alert recipient is read from process.env.EMAIL_WEEKLY_RECIPIENT, never hardcoded
Email failure does not throw or break the probe pipeline
Nodemailer transporter is created inside the function call, not at module level (Firebase Secret timing)
An alert_events row is created before sending the email
path provides exports
backend/src/services/alertService.ts Alert deduplication, email sending, and alert event creation
alertService
path provides min_lines
backend/src/__tests__/unit/alertService.test.ts Unit tests for alert deduplication, email sending, recipient config 80
from to via pattern
backend/src/services/alertService.ts backend/src/models/AlertEventModel.ts findRecentByService() for deduplication, create() for alert row AlertEventModel.(findRecentByService|create)
from to via pattern
backend/src/services/alertService.ts nodemailer createTransport + sendMail for email delivery nodemailer.createTransport
from to via pattern
backend/src/services/alertService.ts process.env.EMAIL_WEEKLY_RECIPIENT Config-based recipient (ALRT-04) process.env.EMAIL_WEEKLY_RECIPIENT
Create the alert service with deduplication logic, SMTP email sending via nodemailer, and config-based recipient.

Purpose: ALRT-01 requires email alerts on service degradation/failure. ALRT-02 requires deduplication with cooldown. ALRT-04 requires the recipient to come from configuration, not hardcoded source code.

Output: alertService.ts with evaluateAndAlert() and sendAlertEmail(), and unit tests.

<execution_context> @/home/jonathan/.claude/get-shit-done/workflows/execute-plan.md @/home/jonathan/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-backend-services/02-RESEARCH.md @.planning/phases/01-data-foundation/01-01-SUMMARY.md @.planning/phases/02-backend-services/02-02-PLAN.md @backend/src/models/AlertEventModel.ts @backend/src/index.ts Task 1: Create alertService with deduplication and email backend/src/services/alertService.ts Create `backend/src/services/alertService.ts` with the following structure:

Import the ProbeResult type from './healthProbeService' (created in Plan 02).

Constants:

  • ALERT_COOLDOWN_MINUTES = parseInt(process.env.ALERT_COOLDOWN_MINUTES ?? '60', 10) — configurable cooldown window

Private function createTransporter(): Create nodemailer transporter INSIDE function scope (not module level — PITFALL A: Firebase Secrets not available at module load). Read SMTP config from process.env:

  • host: process.env.EMAIL_HOST ?? 'smtp.gmail.com'
  • port: parseInt(process.env.EMAIL_PORT ?? '587', 10)
  • secure: process.env.EMAIL_SECURE === 'true'
  • auth.user: process.env.EMAIL_USER
  • auth.pass: process.env.EMAIL_PASS

Private function sendAlertEmail(serviceName, alertType, message):

  • Read recipient from process.env.EMAIL_WEEKLY_RECIPIENT (ALRT-04: NEVER hardcode the email address)
  • If no recipient configured, log warning and return (do not throw)
  • Call createTransporter() then transporter.sendMail({ from, to, subject, text, html })
  • Subject format: [CIM Summary] Alert: ${serviceName} — ${alertType}
  • Wrap in try/catch — email failure logs error but does NOT throw (email failure must not break probe pipeline)

Exported function evaluateAndAlert(probeResults: ProbeResult[]): For each ProbeResult where status is 'degraded' or 'down':

  1. Map status to alert_type: 'down' -> 'service_down', 'degraded' -> 'service_degraded'
  2. Call AlertEventModel.findRecentByService(service_name, alert_type, ALERT_COOLDOWN_MINUTES)
  3. If recent alert exists within cooldown, log suppression and skip BOTH row creation AND email (PITFALL 3: prevent alert storms)
  4. If no recent alert, create alert_events row via AlertEventModel.create({ service_name, alert_type, message: error_message or status description })
  5. Then send email via sendAlertEmail()

Export as: export const alertService = { evaluateAndAlert }.

Use Winston logger for all logging. Use import { AlertEventModel } from '../models/AlertEventModel'. cd /home/jonathan/Coding/cim_summary/backend && npx tsc --noEmit --pretty 2>&1 | head -30 Verify alertService.ts exports alertService.evaluateAndAlert. Verify no hardcoded email addresses in source. alertService.ts exports evaluateAndAlert(). Deduplication checks AlertEventModel.findRecentByService() before creating rows or sending email. Recipient read from process.env.EMAIL_WEEKLY_RECIPIENT. Transporter created lazily. Email failures caught and logged. TypeScript compiles.

Task 2: Create alertService unit tests backend/src/__tests__/unit/alertService.test.ts Create `backend/src/__tests__/unit/alertService.test.ts` using the Vitest mock pattern.

Mock dependencies:

  • vi.mock('../../models/AlertEventModel') — mock findRecentByService and create
  • vi.mock('nodemailer') — mock createTransport returning { sendMail: vi.fn().mockResolvedValue({}) }
  • vi.mock('../../utils/logger') — mock logger

Create test ProbeResult fixtures:

  • healthyProbe: { service_name: 'supabase', status: 'healthy', latency_ms: 50 }
  • downProbe: { service_name: 'document_ai', status: 'down', latency_ms: 0, error_message: 'Connection refused' }
  • degradedProbe: { service_name: 'llm_api', status: 'degraded', latency_ms: 6000 }

Test cases:

  1. Healthy probes — no alerts sent — pass array of healthy ProbeResults, verify AlertEventModel.findRecentByService NOT called, sendMail NOT called

  2. Down probe — creates alert_events row and sends email — pass downProbe, mock findRecentByService returning null (no recent alert), verify AlertEventModel.create called with service_name='document_ai' and alert_type='service_down', verify sendMail called

  3. Degraded probe — creates alert with type 'service_degraded' — pass degradedProbe, mock findRecentByService returning null, verify AlertEventModel.create called with alert_type='service_degraded'

  4. Deduplication — suppresses within cooldown — pass downProbe, mock findRecentByService returning an existing alert object (non-null), verify AlertEventModel.create NOT called, sendMail NOT called, logger.info called with 'suppress' in message

  5. Recipient from env — reads process.env.EMAIL_WEEKLY_RECIPIENT — set process.env.EMAIL_WEEKLY_RECIPIENT = 'test@example.com', pass downProbe with no recent alert, verify sendMail called with to: 'test@example.com'

  6. No recipient configured — skips email but still creates alert row — delete process.env.EMAIL_WEEKLY_RECIPIENT, pass downProbe with no recent alert, verify AlertEventModel.create IS called, sendMail NOT called, logger.warn called

  7. Email failure — does not throw — mock sendMail to reject, verify evaluateAndAlert does not throw, verify logger.error called

  8. Multiple probes — processes each independently — pass [downProbe, degradedProbe, healthyProbe], verify findRecentByService called twice (for down and degraded, not for healthy)

Use beforeEach(() => { vi.clearAllMocks(); process.env.EMAIL_WEEKLY_RECIPIENT = 'admin@test.com'; }). cd /home/jonathan/Coding/cim_summary/backend && npx vitest run src/tests/unit/alertService.test.ts --reporter=verbose 2>&1 All alertService tests pass. Deduplication verified (suppresses within cooldown). Email sending verified with config-based recipient. Email failure verified as non-throwing. Multiple probe evaluation verified.

1. `npx tsc --noEmit` passes 2. `npx vitest run src/__tests__/unit/alertService.test.ts` — all tests pass 3. `grep -r 'jpressnell\|bluepoint' backend/src/services/alertService.ts` returns nothing (no hardcoded emails) 4. alertService reads recipient from `process.env.EMAIL_WEEKLY_RECIPIENT`

<success_criteria>

  • alertService exports evaluateAndAlert(probeResults)
  • Deduplication uses AlertEventModel.findRecentByService with configurable cooldown
  • Alert rows created via AlertEventModel.create before email send
  • Suppressed alerts skip BOTH row creation AND email
  • Recipient from process.env, never hardcoded
  • Transporter created inside function, not at module level
  • Email failures caught and logged, never thrown
  • All unit tests pass </success_criteria>
After completion, create `.planning/phases/02-backend-services/02-03-SUMMARY.md`