feat(diagnostics): add repository.ts with session, trace, log, screenshot CRUD

This commit is contained in:
saravanakumardb1 2026-03-02 23:34:35 -08:00
parent dea1521dd5
commit f272a44bbe

View File

@ -0,0 +1,307 @@
/**
* Diagnostics repository debug session management and data ingestion.
*
* Uses @bytelyst/datastore for cloud-agnostic storage.
*
* @module diagnostics
*/
import type { FilterMap } from '@bytelyst/datastore';
import { getCollection } from '../../lib/datastore.js';
import type {
DebugSessionDoc,
DebugTraceDoc,
DebugLogEntryDoc,
DebugScreenshotDoc,
ListDebugSessionsQuery,
QueryTracesInput,
QueryLogsInput,
} from './types.js';
// ─────────────────────────────────────────────────────────────────────────────
// Collection accessors
// ─────────────────────────────────────────────────────────────────────────────
function sessionsCollection() {
return getCollection<DebugSessionDoc>('debug_sessions', '/id');
}
function tracesCollection() {
return getCollection<DebugTraceDoc>('debug_traces', '/pk');
}
function logsCollection() {
return getCollection<DebugLogEntryDoc>('debug_logs', '/pk');
}
function screenshotsCollection() {
return getCollection<DebugScreenshotDoc>('debug_screenshots', '/sessionId');
}
// ─────────────────────────────────────────────────────────────────────────────
// Helper: Build composite partition key
// ─────────────────────────────────────────────────────────────────────────────
function buildPk(productId: string, sessionId: string): string {
return `${productId}:${sessionId}`;
}
// ─────────────────────────────────────────────────────────────────────────────
// Sessions
// ─────────────────────────────────────────────────────────────────────────────
export async function createSession(doc: DebugSessionDoc): Promise<DebugSessionDoc> {
await sessionsCollection().upsert(doc);
return doc;
}
export async function getSession(sessionId: string): Promise<DebugSessionDoc | null> {
const result = await sessionsCollection().findMany({
filter: { id: sessionId },
limit: 1,
});
return result[0] ?? null;
}
export async function updateSession(
sessionId: string,
updates: Partial<DebugSessionDoc>
): Promise<DebugSessionDoc | null> {
const existing = await getSession(sessionId);
if (!existing) return null;
const updated = { ...existing, ...updates, updatedAt: new Date().toISOString() };
await sessionsCollection().upsert(updated);
return updated;
}
export async function deleteSession(sessionId: string): Promise<boolean> {
// Datastore doesn't have delete - use soft delete
const existing = await getSession(sessionId);
if (!existing) return false;
await sessionsCollection().upsert({
...existing,
status: 'cancelled',
updatedAt: new Date().toISOString(),
});
return true;
}
export async function listSessions(
query: ListDebugSessionsQuery
): Promise<{ sessions: DebugSessionDoc[]; total: number }> {
const filter: FilterMap = {};
if (query.productId) filter.productId = query.productId;
if (query.status) filter.status = query.status;
if (query.targetUserId) filter.targetUserId = query.targetUserId;
// Date range filtering on createdAt
if (query.from || query.to) {
filter.createdAt = {};
if (query.from) (filter.createdAt as Record<string, string>).gte = query.from;
if (query.to) (filter.createdAt as Record<string, string>).lte = query.to;
}
const result = await sessionsCollection().findMany({
filter,
sort: { createdAt: -1 },
limit: query.limit,
offset: query.offset,
});
// Get total count (simplified - in production use COUNT query)
const allResult = await sessionsCollection().findMany({ filter });
const total = allResult.length;
return { sessions: result, total };
}
export async function getActiveSessionForTarget(
productId: string,
target: { userId?: string; anonymousId?: string; deviceId?: string }
): Promise<DebugSessionDoc | null> {
const filter: FilterMap = { productId, status: 'active' };
if (target.userId) filter.targetUserId = target.userId;
if (target.anonymousId) filter.targetAnonymousId = target.anonymousId;
if (target.deviceId) filter.targetDeviceId = target.deviceId;
const result = await sessionsCollection().findMany({
filter,
sort: { createdAt: -1 },
limit: 1,
});
// Check if session hasn't expired
const session = result[0];
if (!session) return null;
const now = new Date().toISOString();
if (session.expiresAt < now) return null;
return session;
}
// ─────────────────────────────────────────────────────────────────────────────
// Traces
// ─────────────────────────────────────────────────────────────────────────────
export async function ingestTraces(
productId: string,
sessionId: string,
traces: DebugTraceDoc[]
): Promise<void> {
const pk = buildPk(productId, sessionId);
// Prepare docs with composite pk
const docs = traces.map(trace => ({
...trace,
pk,
sessionId,
productId,
}));
// Batch upsert for idempotency
await Promise.all(docs.map(doc => tracesCollection().upsert(doc)));
}
export async function getTraces(
productId: string,
sessionId: string,
query: QueryTracesInput
): Promise<{ traces: DebugTraceDoc[]; continuationToken?: string }> {
const pk = buildPk(productId, sessionId);
const result = await tracesCollection().findMany({
filter: { pk },
sort: { startTime: 1 },
limit: query.limit,
});
return {
traces: result,
continuationToken: undefined,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// Logs
// ─────────────────────────────────────────────────────────────────────────────
export async function ingestLogs(
productId: string,
sessionId: string,
logs: DebugLogEntryDoc[]
): Promise<void> {
const pk = buildPk(productId, sessionId);
// Prepare docs with composite pk and receivedAt
const now = new Date().toISOString();
const docs = logs.map(log => ({
...log,
pk,
sessionId,
productId,
receivedAt: now,
}));
// Batch upsert
await Promise.all(docs.map(doc => logsCollection().upsert(doc)));
}
export async function getLogs(
productId: string,
sessionId: string,
query: QueryLogsInput
): Promise<{ logs: DebugLogEntryDoc[]; continuationToken?: string }> {
const pk = buildPk(productId, sessionId);
const filter: FilterMap = { pk };
if (query.level) filter.level = query.level;
// Date range
if (query.from || query.to) {
filter.timestamp = {};
if (query.from) (filter.timestamp as Record<string, string>).gte = query.from;
if (query.to) (filter.timestamp as Record<string, string>).lte = query.to;
}
// Search in message (if supported by datastore, otherwise filter in memory)
// Note: Cosmos supports CONTAINS in queries
const result = await logsCollection().findMany({
filter,
sort: { timestamp: -1 },
limit: query.limit,
});
// In-memory search filtering if needed
let logs = result;
if (query.search) {
const searchLower = query.search.toLowerCase();
logs = logs.filter(
(log: DebugLogEntryDoc) =>
log.message.toLowerCase().includes(searchLower) ||
log.module.toLowerCase().includes(searchLower)
);
}
return {
logs,
continuationToken: undefined,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// Screenshots
// ─────────────────────────────────────────────────────────────────────────────
export async function createScreenshotMetadata(
doc: DebugScreenshotDoc
): Promise<DebugScreenshotDoc> {
await screenshotsCollection().upsert(doc);
return doc;
}
export async function getScreenshots(sessionId: string): Promise<DebugScreenshotDoc[]> {
const result = await screenshotsCollection().findMany({
filter: { sessionId },
sort: { capturedAt: -1 },
});
return result;
}
export async function getScreenshot(screenshotId: string): Promise<DebugScreenshotDoc | null> {
const result = await screenshotsCollection().findMany({
filter: { id: screenshotId },
limit: 1,
});
return result[0] ?? null;
}
// ─────────────────────────────────────────────────────────────────────────────
// Stats update (denormalized counters)
// ─────────────────────────────────────────────────────────────────────────────
export async function updateSessionStats(
sessionId: string,
stats: { logCount?: number; traceCount?: number; screenshotCount?: number }
): Promise<void> {
const existing = await getSession(sessionId);
if (!existing) return;
const updated: DebugSessionDoc = {
...existing,
...(stats.logCount !== undefined && { logCount: existing.logCount + stats.logCount }),
...(stats.traceCount !== undefined && { traceCount: existing.traceCount + stats.traceCount }),
...(stats.screenshotCount !== undefined && {
screenshotCount: existing.screenshotCount + stats.screenshotCount,
}),
updatedAt: new Date().toISOString(),
};
await sessionsCollection().upsert(updated);
}