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>
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 |
|
true |
|
|
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:
-
recordProcessingEvent(data: ProcessingEventData): void— Return type MUST bevoid(notPromise<void>) to prevent accidentalawait. Inside, callgetSupabaseServiceClient()(per-method, not module level), thenvoid supabase.from('document_processing_events').insert({...}).then(({ error }) => { if (error) logger.error(...) }). Never throw, never reject. -
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 asHealthCheckModel.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.
Mock setup:
vi.mock('../../config/supabase')with inlinevi.fn()factoryvi.mock('../../utils/logger')with inlinevi.fn()factory- Use
vi.mocked()after import for typed access makeSupabaseChain()helper per test (fresh mock state)
Test cases for recordProcessingEvent:
- Calls Supabase insert with correct data — verify
.from('document_processing_events').insert(...)called with expected fields includingcreated_at - Return type is void (not a Promise) — call
recordProcessingEvent(data)and verify the return value isundefined(void), not a thenable - Logs error on Supabase failure but does not throw — mock the
.thencallback with{ error: { message: 'test error' } }, verifylogger.errorwas called - Handles optional fields (duration_ms, error_message, stage) as null — pass data without optional fields, verify insert called with
nullfor 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.
<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>