233 lines
5.7 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
|