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

140 lines
6.9 KiB
Markdown

---
phase: 02-backend-services
plan: 01
subsystem: analytics
tags: [supabase, vitest, fire-and-forget, analytics, postgresql, migrations]
# Dependency graph
requires:
- phase: 01-data-foundation/01-01
provides: getSupabaseServiceClient per-method call pattern, migration file format
- phase: 01-data-foundation/01-02
provides: makeSupabaseChain() Vitest mock pattern, vi.mock hoisting rules
provides:
- Migration 013: document_processing_events table DDL with indexes and RLS
- analyticsService.recordProcessingEvent(): fire-and-forget void write to Supabase
- analyticsService.deleteProcessingEventsOlderThan(): retention delete returning row count
- Unit tests for both exports (6 tests)
affects:
- 02-backend-services/02-02 (monitoring services)
- 02-backend-services/02-03 (health probe scheduler)
- 03-api (callers that instrument processing pipeline events)
# Tech tracking
tech-stack:
added: []
patterns:
- "Fire-and-forget analytics: void return type (not Promise<void>) prevents accidental await on critical path"
- "void supabase.from(...).insert(...).then(callback) pattern for non-blocking writes with error logging"
- "getSupabaseServiceClient() called per-method inside each exported function, never cached at module level"
key-files:
created:
- backend/src/models/migrations/013_create_processing_events_table.sql
- backend/src/services/analyticsService.ts
- backend/src/__tests__/unit/analyticsService.test.ts
modified: []
key-decisions:
- "recordProcessingEvent return type is void (not Promise<void>) — prevents callers from accidentally awaiting analytics writes on the critical processing path"
- "Optional fields (duration_ms, error_message, stage) coalesce to null in insert payload — consistent nullability in DB"
- "created_at set explicitly in insert payload (not relying on DB DEFAULT) so it matches the event occurrence time"
patterns-established:
- "Analytics void function test: expect(result).toBeUndefined() + expect(typeof result).toBe('undefined') — toHaveProperty throws on undefined, use typeof check instead"
- "Fire-and-forget error path test: mock .insert().then() directly to control the resolved value, flush microtask queue with await Promise.resolve() before asserting logger call"
requirements-completed: [ANLY-01, ANLY-03]
# Metrics
duration: 3min
completed: 2026-02-24
---
# Phase 02 Plan 01: Analytics Service Summary
**Fire-and-forget analyticsService with document_processing_events migration — void recordProcessingEvent that logs errors without throwing, and deleteProcessingEventsOlderThan returning row count**
## Performance
- **Duration:** 3 min
- **Started:** 2026-02-24T19:21:16Z
- **Completed:** 2026-02-24T19:24:17Z
- **Tasks:** 2
- **Files modified:** 3
## Accomplishments
- Migration 013 creates `document_processing_events` table with `event_type` CHECK constraint, indexes on `created_at` and `document_id`, and RLS enabled — follows migration 012 pattern exactly
- `recordProcessingEvent()` has `void` return type (not `Promise<void>`) and uses `void supabase.from(...).insert(...).then(callback)` to ensure errors are logged but never thrown, never blocking the processing pipeline
- `deleteProcessingEventsOlderThan()` computes cutoff via `Date.now() - days * 86400000`, deletes with `.lt('created_at', cutoff)`, returns `data.length` as row count
- 6 unit tests covering all exports: insert payload, void return type, error swallowing + logging, null coalescing for optional fields, cutoff date math, and row count return
## Task Commits
Each task was committed atomically:
1. **Task 1: Create analytics migration and analyticsService** - `ef88541` (feat)
2. **Task 2: Create analyticsService unit tests** - `cf30811` (test)
**Plan metadata:** (docs commit to follow)
## Files Created/Modified
- `backend/src/models/migrations/013_create_processing_events_table.sql` - document_processing_events DDL with UUID PK, CHECK constraint on event_type, indexes on created_at + document_id, RLS enabled
- `backend/src/services/analyticsService.ts` - recordProcessingEvent (void, fire-and-forget) and deleteProcessingEventsOlderThan (Promise<number>)
- `backend/src/__tests__/unit/analyticsService.test.ts` - 6 unit tests using established makeSupabaseChain() pattern
## Decisions Made
- `recordProcessingEvent` return type is `void` (not `Promise<void>`) — the type system itself prevents accidental `await`, matching the architecture decision in STATE.md ("Analytics writes are always fire-and-forget")
- Optional fields coalesce to `null` in the insert payload rather than omitting them — keeps the DB row shape consistent and predictable
- `created_at` is set explicitly in the insert payload (not via DB DEFAULT) to accurately reflect event occurrence time rather than DB write time
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Fixed toHaveProperty assertion on undefined return value**
- **Found during:** Task 2 (first test run)
- **Issue:** `expect(result).not.toHaveProperty('then')` throws `TypeError: Cannot convert undefined or null to object` when `result` is `undefined` — Vitest's `toHaveProperty` cannot introspect `undefined`
- **Fix:** Replaced with `expect(typeof result).toBe('undefined')` which correctly verifies the return is not a thenable without requiring the value to be an object
- **Files modified:** `backend/src/__tests__/unit/analyticsService.test.ts`
- **Verification:** All 6 tests pass after fix
- **Committed in:** cf30811 (Task 2 commit)
---
**Total deviations:** 1 auto-fixed (Rule 1 — runtime error in test assertion)
**Impact on plan:** Fix required for tests to run. The replacement assertion is semantically equivalent and more idiomatic for checking void returns.
## Issues Encountered
- Vitest `toHaveProperty` throws on `undefined`/`null` values rather than returning false — use `typeof result` checks when verifying void returns instead.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- `analyticsService` is ready for callers — import `recordProcessingEvent` from `'../services/analyticsService'` and call it without `await` at instrumentation points
- Migration 013 SQL ready to run against Supabase (requires manual `psql` or Dashboard execution)
- `makeSupabaseChain()` pattern from Phase 1 confirmed working for service-layer tests (not just model-layer tests)
- Ready for Phase 2 plan 02: monitoring services that will call `recordProcessingEvent` during health probe lifecycle
---
*Phase: 02-backend-services*
*Completed: 2026-02-24*
## Self-Check: PASSED
- FOUND: backend/src/models/migrations/013_create_processing_events_table.sql
- FOUND: backend/src/services/analyticsService.ts
- FOUND: backend/src/__tests__/unit/analyticsService.test.ts
- FOUND: .planning/phases/02-backend-services/02-01-SUMMARY.md
- FOUND commit ef88541 (Task 1: analytics migration + service)
- FOUND commit cf30811 (Task 2: unit tests)