diff --git a/services/platform-service/src/modules/diagnostics/types.ts b/services/platform-service/src/modules/diagnostics/types.ts new file mode 100644 index 00000000..c00c431b --- /dev/null +++ b/services/platform-service/src/modules/diagnostics/types.ts @@ -0,0 +1,381 @@ +/** + * Diagnostics types — remote debug session management. + * + * Cosmos containers: + * - debug_sessions (pk: /id, TTL: 7 days) + * - debug_traces (pk: /pk composite ${productId}:${sessionId}, TTL: 7 days) + * - debug_logs (pk: /pk composite ${productId}:${sessionId}, TTL: 3 days) + * - debug_screenshots (pk: /sessionId) — metadata only, images in Azure Blob + * + * @module diagnostics + */ + +import { z } from 'zod'; + +// ───────────────────────────────────────────────────────────────────────────── +// Enums +// ───────────────────────────────────────────────────────────────────────────── + +export const SessionStatusEnum = z.enum(['pending', 'active', 'paused', 'completed', 'cancelled']); +export type SessionStatus = z.infer; + +export const CollectionLevelEnum = z.enum(['standard', 'debug', 'trace']); +export type CollectionLevel = z.infer; + +export const LogLevelEnum = z.enum(['debug', 'info', 'warn', 'error', 'fatal']); +export type LogLevel = z.infer; + +export const ScreenshotTriggerEnum = z.enum(['manual', 'error', 'interval', 'user_request']); +export type ScreenshotTrigger = z.infer; + +export const SpanStatusEnum = z.enum(['ok', 'error', 'unset']); +export type SpanStatus = z.infer; + +export const SpanKindEnum = z.enum(['internal', 'server', 'client', 'producer', 'consumer']); +export type SpanKind = z.infer; + +// ───────────────────────────────────────────────────────────────────────────── +// DebugSessionDoc +// ───────────────────────────────────────────────────────────────────────────── + +export interface DebugSessionDoc { + id: string; + productId: string; + + // Target (at least one required) + targetUserId?: string; + targetAnonymousId?: string; + targetDeviceId?: string; + targetSessionId?: string; + + // Status + status: SessionStatus; + + // Collection config + collectionLevel: CollectionLevel; + captureLogs: boolean; + captureNetwork: boolean; + captureScreenshots: boolean; + screenshotOnError: boolean; + maxDurationMinutes: number; + + // Timestamps + createdAt: string; + updatedAt: string; + startedAt?: string; + endedAt?: string; + expiresAt: string; + + // Stats (denormalized) + logCount: number; + traceCount: number; + screenshotCount: number; + + // Audit + createdBy: string; + updatedBy?: string; + + // Consent tracking + userConsent?: { + consentedAt: string; + consentMethod: 'prompt' | 'pre_consent' | 'auto'; + }; +} + +// ───────────────────────────────────────────────────────────────────────────── +// DebugTraceDoc (OpenTelemetry-compatible) +// ───────────────────────────────────────────────────────────────────────────── + +export interface DebugTraceDoc { + id: string; + pk: string; // Composite: ${productId}:${sessionId} + sessionId: string; + productId: string; + + // OTel context + traceId: string; + parentId?: string; + spanId: string; + name: string; + kind?: SpanKind; + + // Timing + startTime: string; + endTime?: string; + durationMs?: number; + + // Context + attributes: Record; + status: SpanStatus; + statusMessage?: string; + + // Events within span + events?: Array<{ + name: string; + timestamp: string; + attributes?: Record; + }>; + + // Links to other traces + links?: Array<{ + traceId: string; + spanId: string; + attributes?: Record; + }>; +} + +// ───────────────────────────────────────────────────────────────────────────── +// DebugLogEntryDoc +// ───────────────────────────────────────────────────────────────────────────── + +export interface DebugLogEntryDoc { + id: string; + pk: string; // Composite: ${productId}:${sessionId} + sessionId: string; + productId: string; + + level: LogLevel; + message: string; + messageHash?: string; + + // Timestamps + timestamp: string; + receivedAt?: string; + + // Source context + module: string; + file?: string; + line?: number; + function?: string; + + // Thread/task context + threadId?: string; + correlationId?: string; + + // Context (PII-scanned) + context: Record; + + // PII redaction metadata + redaction?: { + fieldsRedacted: string[]; + patternsMatched: string[]; + }; +} + +// ───────────────────────────────────────────────────────────────────────────── +// DebugScreenshotDoc (metadata only) +// ───────────────────────────────────────────────────────────────────────────── + +export interface DebugScreenshotDoc { + id: string; + sessionId: string; + productId: string; + + // Blob storage reference + blobUrl: string; + blobPath: string; + containerName: string; + + // Metadata + capturedAt: string; + trigger: ScreenshotTrigger; + + // Dimensions + width: number; + height: number; + format: 'png' | 'jpeg' | 'webp'; + sizeBytes: number; + + // Privacy + sensitiveViewsBlurred: boolean; + blurRegions?: Array<{ x: number; y: number; w: number; h: number }>; + + // Context + screenName?: string; + breadcrumbAtCapture?: string; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Input Schemas +// ───────────────────────────────────────────────────────────────────────────── + +export const CreateDebugSessionSchema = z.object({ + productId: z.string().min(1), + targetUserId: z.string().optional(), + targetAnonymousId: z.string().optional(), + targetDeviceId: z.string().optional(), + targetSessionId: z.string().optional(), + collectionLevel: CollectionLevelEnum.default('debug'), + captureLogs: z.boolean().default(true), + captureNetwork: z.boolean().default(true), + captureScreenshots: z.boolean().default(false), + screenshotOnError: z.boolean().default(true), + maxDurationMinutes: z.number().min(5).max(1440).default(60), +}); + +export const UpdateDebugSessionSchema = z.object({ + status: SessionStatusEnum.optional(), + collectionLevel: CollectionLevelEnum.optional(), + captureLogs: z.boolean().optional(), + captureNetwork: z.boolean().optional(), + captureScreenshots: z.boolean().optional(), + screenshotOnError: z.boolean().optional(), + maxDurationMinutes: z.number().min(5).max(1440).optional(), +}); + +export const ListDebugSessionsQuerySchema = z.object({ + productId: z.string().optional(), + status: SessionStatusEnum.optional(), + targetUserId: z.string().optional(), + from: z.string().datetime().optional(), + to: z.string().datetime().optional(), + limit: z.coerce.number().min(1).max(100).default(20), + offset: z.coerce.number().min(0).default(0), +}); + +export const IngestTracesSchema = z.object({ + sessionId: z.string().min(1), + traces: z + .array( + z.object({ + traceId: z.string().min(1), + spanId: z.string().min(1), + parentId: z.string().optional(), + name: z.string().min(1), + kind: SpanKindEnum.optional(), + startTime: z.string().datetime(), + endTime: z.string().datetime().optional(), + durationMs: z.number().optional(), + attributes: z.record(z.unknown()).default({}), + status: SpanStatusEnum, + statusMessage: z.string().optional(), + events: z + .array( + z.object({ + name: z.string(), + timestamp: z.string().datetime(), + attributes: z.record(z.unknown()).optional(), + }) + ) + .optional(), + links: z + .array( + z.object({ + traceId: z.string(), + spanId: z.string(), + attributes: z.record(z.unknown()).optional(), + }) + ) + .optional(), + }) + ) + .min(1) + .max(50), +}); + +export const IngestLogsSchema = z.object({ + sessionId: z.string().min(1), + logs: z + .array( + z.object({ + level: LogLevelEnum, + message: z.string().max(4096), + timestamp: z.string().datetime(), + module: z.string().min(1), + file: z.string().optional(), + line: z.number().optional(), + function: z.string().optional(), + threadId: z.string().optional(), + correlationId: z.string().optional(), + context: z.record(z.unknown()).default({}), + }) + ) + .min(1) + .max(50), +}); + +export const CreateScreenshotMetadataSchema = z.object({ + sessionId: z.string().min(1), + capturedAt: z.string().datetime(), + trigger: ScreenshotTriggerEnum, + width: z.number().positive(), + height: z.number().positive(), + format: z.enum(['png', 'jpeg', 'webp']), + sizeBytes: z.number().positive(), + sensitiveViewsBlurred: z.boolean(), + blurRegions: z + .array(z.object({ x: z.number(), y: z.number(), w: z.number(), h: z.number() })) + .optional(), + screenName: z.string().optional(), + breadcrumbAtCapture: z.string().optional(), +}); + +export const QueryTracesSchema = z.object({ + limit: z.coerce.number().min(1).max(200).default(50), + continuationToken: z.string().optional(), +}); + +export const QueryLogsSchema = z.object({ + level: LogLevelEnum.optional(), + from: z.string().datetime().optional(), + to: z.string().datetime().optional(), + search: z.string().max(256).optional(), + limit: z.coerce.number().min(1).max(200).default(50), + continuationToken: z.string().optional(), +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Inferred Types +// ───────────────────────────────────────────────────────────────────────────── + +export type CreateDebugSessionInput = z.infer; +export type UpdateDebugSessionInput = z.infer; +export type ListDebugSessionsQuery = z.infer; +export type IngestTracesInput = z.infer; +export type IngestLogsInput = z.infer; +export type CreateScreenshotMetadataInput = z.infer; +export type QueryTracesInput = z.infer; +export type QueryLogsInput = z.infer; + +// ───────────────────────────────────────────────────────────────────────────── +// Event Bus Event Types +// ───────────────────────────────────────────────────────────────────────────── + +export interface DiagnosticsSessionCreatedEvent { + sessionId: string; + productId: string; + targetUserId?: string; + createdBy: string; +} + +export interface DiagnosticsSessionUpdatedEvent { + sessionId: string; + productId: string; + changes: Partial; + updatedBy: string; +} + +export interface DiagnosticsSessionCancelledEvent { + sessionId: string; + productId: string; + reason?: string; + cancelledBy: string; +} + +export interface DiagnosticsSessionCompletedEvent { + sessionId: string; + productId: string; + stats: { + logCount: number; + traceCount: number; + screenshotCount: number; + }; + endedAt: string; +} + +export interface DiagnosticsIngestFatalEvent { + sessionId: string; + productId: string; + logEntry: DebugLogEntryDoc; + timestamp: string; +}