Files
vibn-frontend/lib/server/logs.ts

233 lines
5.7 KiB
TypeScript

/**
* 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<void> {
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<ProjectLogEntry[]> {
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<ProjectLogStats> {
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<number> {
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;
}
}