--- phase: 03-api-layer plan: 02 type: execute wave: 1 depends_on: [] files_modified: - backend/src/services/jobProcessorService.ts autonomous: true requirements: - ANLY-02 must_haves: truths: - "Document processing emits upload_started event after job is marked as processing" - "Document processing emits completed event with duration_ms after job succeeds" - "Document processing emits failed event with duration_ms and error_message when job fails" - "Analytics instrumentation does not change existing processing behavior or error handling" artifacts: - path: "backend/src/services/jobProcessorService.ts" provides: "Analytics instrumentation at 3 lifecycle points in processJob()" contains: "recordProcessingEvent" key_links: - from: "backend/src/services/jobProcessorService.ts" to: "backend/src/services/analyticsService.ts" via: "import and call recordProcessingEvent()" pattern: "recordProcessingEvent" --- Instrument the document processing pipeline with fire-and-forget analytics events at key lifecycle points. Purpose: Enables the analytics endpoint (Plan 03-01) to report real processing data. Without instrumentation, `document_processing_events` table stays empty and `GET /admin/analytics` returns zeros. Output: Three `recordProcessingEvent()` calls in `jobProcessorService.processJob()` — one at job start, one at completion, one at failure. @/home/jonathan/.claude/get-shit-done/workflows/execute-plan.md @/home/jonathan/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-api-layer/03-RESEARCH.md @backend/src/services/jobProcessorService.ts @backend/src/services/analyticsService.ts Task 1: Add analytics instrumentation to processJob lifecycle backend/src/services/jobProcessorService.ts **1. Add import at top of file:** ```typescript import { recordProcessingEvent } from './analyticsService'; ``` **2. Emit `upload_started` after `markAsProcessing` (line ~133):** After `await ProcessingJobModel.markAsProcessing(jobId);` and `jobStatusUpdated = true;`, add: ```typescript // Analytics: job processing started (fire-and-forget, void return) recordProcessingEvent({ document_id: job.document_id, user_id: job.user_id, event_type: 'upload_started', }); ``` Place this BEFORE the timeout setup block (before `const processingTimeout = ...`). **3. Emit `completed` after `markAsCompleted` (line ~329):** After `const processingTime = Date.now() - startTime;` and the `logger.info('Job completed successfully', ...)` call, add: ```typescript // Analytics: job completed (fire-and-forget, void return) recordProcessingEvent({ document_id: job.document_id, user_id: job.user_id, event_type: 'completed', duration_ms: processingTime, }); ``` **4. Emit `failed` in catch block (line ~355-368):** After `const processingTime = Date.now() - startTime;` and `logger.error('Job processing failed', ...)`, but BEFORE the `try { await ProcessingJobModel.markAsFailed(...)` block, add: ```typescript // Analytics: job failed (fire-and-forget, void return) // Guard with job check — job is null if findById failed before assignment if (job) { recordProcessingEvent({ document_id: job.document_id, user_id: job.user_id, event_type: 'failed', duration_ms: processingTime, error_message: errorMessage, }); } ``` **Critical constraints:** - `recordProcessingEvent` returns `void` (not `Promise`) — do NOT use `await`. This is the fire-and-forget guarantee (PITFALL-6, STATE.md decision). - Do NOT wrap in try/catch — the function internally catches all errors and logs them. - Do NOT modify any existing code around the instrumentation points — add lines, don't change lines. - Guard `job` in catch block — it can be null if `findById` threw before assignment. - Use `event_type: 'upload_started'` (not `'processing_started'`) — per locked decision, key milestones only: upload started, processing complete, processing failed. cd /home/jonathan/Coding/cim_summary/backend && npx tsc --noEmit 2>&1 | head -30 && npx vitest run --reporter=verbose 2>&1 | tail -20 Verify 3 recordProcessingEvent calls exist in jobProcessorService.ts, none use await processJob() emits upload_started after markAsProcessing, completed with duration after markAsCompleted, and failed with duration+error in catch block. All calls are fire-and-forget (no await). Existing processing logic unchanged — no behavior modification. 1. `npx tsc --noEmit` passes with no errors 2. `npx vitest run` — all existing tests pass (no regressions) 3. `grep -c 'recordProcessingEvent' backend/src/services/jobProcessorService.ts` returns 3 4. `grep 'await recordProcessingEvent' backend/src/services/jobProcessorService.ts` returns nothing (no accidental await) 5. `recordProcessingEvent` import exists at top of file - TypeScript compiles without errors - All existing tests pass (zero regressions) - Three recordProcessingEvent calls at correct lifecycle points - No await on recordProcessingEvent (fire-and-forget preserved) - job null-guard in catch block prevents runtime errors - No changes to existing processing logic After completion, create `.planning/phases/03-api-layer/03-02-SUMMARY.md`