feat(diagnostics): add repository.ts with session, trace, log, screenshot CRUD
This commit is contained in:
parent
dea1521dd5
commit
f272a44bbe
307
services/platform-service/src/modules/diagnostics/repository.ts
Normal file
307
services/platform-service/src/modules/diagnostics/repository.ts
Normal 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);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user