Files
cim_summary/.planning/milestones/v1.0-phases/02-backend-services/02-01-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

8.4 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 01 execute 1
backend/src/models/migrations/013_create_processing_events_table.sql
backend/src/services/analyticsService.ts
backend/src/__tests__/unit/analyticsService.test.ts
true
ANLY-01
ANLY-03
truths artifacts key_links
recordProcessingEvent() writes to document_processing_events table via Supabase
recordProcessingEvent() returns void (not Promise) so callers cannot accidentally await it
A deliberate Supabase write failure logs an error but does not throw or reject
deleteProcessingEventsOlderThan(30) removes rows older than 30 days
path provides contains
backend/src/models/migrations/013_create_processing_events_table.sql document_processing_events table DDL with indexes and RLS CREATE TABLE IF NOT EXISTS document_processing_events
path provides exports
backend/src/services/analyticsService.ts Fire-and-forget analytics event writer and retention delete
recordProcessingEvent
deleteProcessingEventsOlderThan
path provides min_lines
backend/src/__tests__/unit/analyticsService.test.ts Unit tests for analyticsService 50
from to via pattern
backend/src/services/analyticsService.ts backend/src/config/supabase.ts getSupabaseServiceClient() call getSupabaseServiceClient
from to via pattern
backend/src/services/analyticsService.ts document_processing_events table void supabase.from('document_processing_events').insert(...) void.*from('document_processing_events')
Create the analytics migration and fire-and-forget analytics service for persisting document processing events to Supabase.

Purpose: ANLY-01 requires processing events to persist (not in-memory), and ANLY-03 requires instrumentation to be non-blocking. This plan creates the database table and the service that writes to it without blocking the processing pipeline.

Output: Migration 013 SQL file, analyticsService.ts with recordProcessingEvent() and deleteProcessingEventsOlderThan(), 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/01-data-foundation/01-02-SUMMARY.md @backend/src/models/migrations/012_create_monitoring_tables.sql @backend/src/config/supabase.ts @backend/src/utils/logger.ts Task 1: Create analytics migration and analyticsService backend/src/models/migrations/013_create_processing_events_table.sql backend/src/services/analyticsService.ts **Migration 013:** Create `backend/src/models/migrations/013_create_processing_events_table.sql` following the exact pattern from migration 012. The table:
CREATE TABLE IF NOT EXISTS document_processing_events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    document_id UUID NOT NULL,
    user_id UUID NOT NULL,
    event_type TEXT NOT NULL CHECK (event_type IN ('upload_started', 'processing_started', 'completed', 'failed')),
    duration_ms INTEGER,
    error_message TEXT,
    stage TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_document_processing_events_created_at
    ON document_processing_events(created_at);
CREATE INDEX IF NOT EXISTS idx_document_processing_events_document_id
    ON document_processing_events(document_id);

ALTER TABLE document_processing_events ENABLE ROW LEVEL SECURITY;

analyticsService.ts: Create backend/src/services/analyticsService.ts with two exports:

  1. recordProcessingEvent(data: ProcessingEventData): void — Return type MUST be void (not Promise<void>) to prevent accidental await. Inside, call getSupabaseServiceClient() (per-method, not module level), then void supabase.from('document_processing_events').insert({...}).then(({ error }) => { if (error) logger.error(...) }). Never throw, never reject.

  2. deleteProcessingEventsOlderThan(days: number): Promise<number> — Compute cutoff date in JS (new Date(Date.now() - days * 86400000).toISOString()), then delete with .lt('created_at', cutoff). Return the count of deleted rows. This follows the same pattern as HealthCheckModel.deleteOlderThan().

Export the ProcessingEventData interface:

export interface ProcessingEventData {
  document_id: string;
  user_id: string;
  event_type: 'upload_started' | 'processing_started' | 'completed' | 'failed';
  duration_ms?: number;
  error_message?: string;
  stage?: string;
}

Use Winston logger (import { logger } from '../utils/logger'). Use getSupabaseServiceClient from '../config/supabase'. Follow project naming conventions (camelCase file, named exports). cd /home/jonathan/Coding/cim_summary/backend && npx tsc --noEmit --pretty 2>&1 | head -30 Verify 013 migration file exists and analyticsService exports recordProcessingEvent and deleteProcessingEventsOlderThan Migration 013 creates document_processing_events table with indexes and RLS. analyticsService.ts exports recordProcessingEvent (void return) and deleteProcessingEventsOlderThan (Promise<number>). TypeScript compiles.

Task 2: Create analyticsService unit tests backend/src/__tests__/unit/analyticsService.test.ts Create `backend/src/__tests__/unit/analyticsService.test.ts` using the Vitest + Supabase mock pattern established in Phase 1 (01-02-SUMMARY.md).

Mock setup:

  • vi.mock('../../config/supabase') with inline vi.fn() factory
  • vi.mock('../../utils/logger') with inline vi.fn() factory
  • Use vi.mocked() after import for typed access
  • makeSupabaseChain() helper per test (fresh mock state)

Test cases for recordProcessingEvent:

  1. Calls Supabase insert with correct data — verify .from('document_processing_events').insert(...) called with expected fields including created_at
  2. Return type is void (not a Promise) — call recordProcessingEvent(data) and verify the return value is undefined (void), not a thenable
  3. Logs error on Supabase failure but does not throw — mock the .then callback with { error: { message: 'test error' } }, verify logger.error was called
  4. Handles optional fields (duration_ms, error_message, stage) as null — pass data without optional fields, verify insert called with null for those columns

Test cases for deleteProcessingEventsOlderThan: 5. Computes correct cutoff date and deletes — mock Supabase delete chain, verify .lt('created_at', ...) called with ISO date string ~30 days ago 6. Returns count of deleted rows — mock response with data: [{}, {}, {}] (3 rows), verify returns 3

Use beforeEach(() => vi.clearAllMocks()) for test isolation. cd /home/jonathan/Coding/cim_summary/backend && npx vitest run src/tests/unit/analyticsService.test.ts --reporter=verbose 2>&1 All analyticsService tests pass. recordProcessingEvent verified as fire-and-forget (void return, error-swallowing). deleteProcessingEventsOlderThan verified with correct date math and row count return.

1. `npx tsc --noEmit` passes with no errors from new files 2. `npx vitest run src/__tests__/unit/analyticsService.test.ts` — all tests pass 3. Migration 013 SQL is valid and follows 012 pattern 4. `recordProcessingEvent` return type is `void` (not `Promise`)

<success_criteria>

  • Migration 013 creates document_processing_events table with id, document_id, user_id, event_type (CHECK constraint), duration_ms, error_message, stage, created_at
  • Indexes on created_at and document_id exist
  • RLS enabled on the table
  • analyticsService.recordProcessingEvent() is fire-and-forget (void return, no throw)
  • analyticsService.deleteProcessingEventsOlderThan() returns deleted row count
  • All unit tests pass </success_criteria>
After completion, create `.planning/phases/02-backend-services/02-01-SUMMARY.md`