/** * Server-side logging utilities * * Logs project events to Firestore for monitoring, debugging, and analytics. */ import { getAdminDb } from '@/lib/firebase/admin'; import { FieldValue } from 'firebase-admin/firestore'; import type { CreateProjectLogInput, ProjectLogEntry, ProjectLogFilters, ProjectLogStats } from '@/lib/types/logs'; /** * Log a project-related event * * This is a fire-and-forget operation - errors are logged but not thrown * to avoid impacting the main request flow. * * @param input - Log entry data * * @example * ```typescript * await logProjectEvent({ * projectId: 'proj123', * userId: 'user456', * eventType: 'chat_interaction', * mode: 'vision_mode', * phase: 'vision_ready', * artifactsUsed: ['Product Model', '5 Vector Chunks'], * usedVectorSearch: true, * vectorChunkCount: 5, * promptVersion: '1.0', * modelUsed: 'gemini-2.0-flash-exp', * success: true, * errorMessage: null, * }); * ``` */ export async function logProjectEvent(input: CreateProjectLogInput): Promise { try { const adminDb = getAdminDb(); const docRef = adminDb.collection('project_logs').doc(); await docRef.set({ ...input, id: docRef.id, createdAt: FieldValue.serverTimestamp(), }); // Silent success } catch (error) { // Log to console but don't throw - logging should never break the main flow console.error('[Logs] Failed to log project event:', error); } } /** * Query project logs with filters * * @param filters - Query filters * @returns Array of log entries */ export async function queryProjectLogs( filters: ProjectLogFilters ): Promise { try { const adminDb = getAdminDb(); let query = adminDb.collection('project_logs').orderBy('createdAt', 'desc'); // Apply filters if (filters.projectId) { query = query.where('projectId', '==', filters.projectId) as any; } if (filters.userId) { query = query.where('userId', '==', filters.userId) as any; } if (filters.eventType) { query = query.where('eventType', '==', filters.eventType) as any; } if (filters.mode) { query = query.where('mode', '==', filters.mode) as any; } if (filters.phase) { query = query.where('phase', '==', filters.phase) as any; } if (filters.success !== undefined) { query = query.where('success', '==', filters.success) as any; } if (filters.startDate) { query = query.where('createdAt', '>=', filters.startDate) as any; } if (filters.endDate) { query = query.where('createdAt', '<=', filters.endDate) as any; } if (filters.limit) { query = query.limit(filters.limit) as any; } const snapshot = await query.get(); return snapshot.docs.map((doc) => { const data = doc.data(); return { ...data, createdAt: data.createdAt?.toDate?.() ?? data.createdAt, } as ProjectLogEntry; }); } catch (error) { console.error('[Logs] Failed to query project logs:', error); return []; } } /** * Get aggregated stats for a project * * @param projectId - Project ID to analyze * @param since - Optional date to filter from * @returns Aggregated statistics */ export async function getProjectLogStats( projectId: string, since?: Date ): Promise { try { const filters: ProjectLogFilters = { projectId, limit: 1000 }; if (since) { filters.startDate = since; } const logs = await queryProjectLogs(filters); const stats: ProjectLogStats = { totalLogs: logs.length, successCount: 0, errorCount: 0, byEventType: {}, byMode: {}, avgVectorChunks: 0, vectorSearchUsageRate: 0, }; let totalVectorChunks = 0; let vectorSearchCount = 0; logs.forEach((log) => { // Success/error counts if (log.success) { stats.successCount++; } else { stats.errorCount++; } // By event type stats.byEventType[log.eventType] = (stats.byEventType[log.eventType] ?? 0) + 1; // By mode if (log.mode) { stats.byMode[log.mode] = (stats.byMode[log.mode] ?? 0) + 1; } // Vector search stats if (log.usedVectorSearch) { vectorSearchCount++; if (log.vectorChunkCount) { totalVectorChunks += log.vectorChunkCount; } } }); // Calculate averages if (vectorSearchCount > 0) { stats.avgVectorChunks = totalVectorChunks / vectorSearchCount; stats.vectorSearchUsageRate = vectorSearchCount / logs.length; } return stats; } catch (error) { console.error('[Logs] Failed to get project log stats:', error); return { totalLogs: 0, successCount: 0, errorCount: 0, byEventType: {}, byMode: {}, avgVectorChunks: 0, vectorSearchUsageRate: 0, }; } } /** * Delete old logs (for maintenance/cleanup) * * @param before - Delete logs older than this date * @returns Number of logs deleted */ export async function deleteOldLogs(before: Date): Promise { try { const adminDb = getAdminDb(); const snapshot = await adminDb .collection('project_logs') .where('createdAt', '<', before) .limit(500) // Process in batches to avoid overwhelming Firestore .get(); if (snapshot.empty) { return 0; } const batch = adminDb.batch(); snapshot.docs.forEach((doc) => { batch.delete(doc.ref); }); await batch.commit(); console.log(`[Logs] Deleted ${snapshot.size} old logs`); return snapshot.size; } catch (error) { console.error('[Logs] Failed to delete old logs:', error); return 0; } }