From dabd4a5ecfe2d34c2e5d5daaf2fbe59f2f9e50f9 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 24 Feb 2026 15:44:12 -0500 Subject: [PATCH] feat(03-02): instrument processJob with fire-and-forget analytics events - Add import for recordProcessingEvent from analyticsService - Emit upload_started after markAsProcessing (job processing start) - Emit completed with duration_ms after markAsCompleted (job success) - Emit failed with duration_ms and error_message in catch block (job failure) - All calls are void/fire-and-forget (no await) per PITFALL-6 constraint - Null-guard on job in catch block prevents runtime errors --- backend/src/services/jobProcessorService.ts | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/backend/src/services/jobProcessorService.ts b/backend/src/services/jobProcessorService.ts index 1ddbd74..ec03bd8 100644 --- a/backend/src/services/jobProcessorService.ts +++ b/backend/src/services/jobProcessorService.ts @@ -3,6 +3,7 @@ import { ProcessingJobModel, ProcessingJob } from '../models/ProcessingJobModel' import { DocumentModel } from '../models/DocumentModel'; import { fileStorageService } from './fileStorageService'; import { unifiedDocumentProcessor } from './unifiedDocumentProcessor'; +import { recordProcessingEvent } from './analyticsService'; export class JobProcessorService { private isProcessing = false; @@ -133,6 +134,13 @@ export class JobProcessorService { await ProcessingJobModel.markAsProcessing(jobId); jobStatusUpdated = true; // Track that we've updated status + // Analytics: job processing started (fire-and-forget, void return) + recordProcessingEvent({ + document_id: job.document_id, + user_id: job.user_id, + event_type: 'upload_started', + }); + // Add timeout protection (28 minutes for API immediate processing, 14 for scheduled function) // API endpoint has 30-min Cloud Run timeout; scheduled function has 15 min const processingTimeout = 28 * 60 * 1000; // 28 minutes in milliseconds @@ -333,6 +341,14 @@ export class JobProcessorService { processingTime, attempts: job.attempts + 1, }); + + // 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, + }); })(), timeoutPromise ]); @@ -363,6 +379,18 @@ export class JobProcessorService { attempts: job ? job.attempts + 1 : 'unknown', }); + // 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, + }); + } + // Mark job as failed (will auto-retry if attempts < max_attempts) try { await ProcessingJobModel.markAsFailed(jobId, errorMessage);