import { config } from './config.js'; export interface PlatformClientOptions { /** Bearer token for the upstream request */ token?: string; /** x-request-id to propagate */ requestId?: string; /** x-product-id to forward */ productId?: string; } async function platformFetch( path: string, init: RequestInit, opts: PlatformClientOptions ): Promise { const url = `${config.PLATFORM_SERVICE_URL}${path}`; const headers: Record = { 'Content-Type': 'application/json', ...(opts.token ? { Authorization: `Bearer ${opts.token}` } : {}), ...(opts.requestId ? { 'x-request-id': opts.requestId } : {}), ...(opts.productId ? { 'x-product-id': opts.productId } : {}), }; const res = await fetch(url, { ...init, headers: { ...headers, ...(init.headers ?? {}) }, signal: AbortSignal.timeout(10_000), }); if (!res.ok) { const body = await res.text().catch(() => ''); throw new Error(`platform-service ${init.method ?? 'GET'} ${path} → ${res.status}: ${body}`); } return res.json() as Promise; } // ── Telemetry ───────────────────────────────────────────────────────────────── export interface TelemetryQueryResult { events: unknown[]; total: number; continuationToken?: string; } export async function telemetryQuery( params: { productId: string; eventType?: string; from?: string; to?: string; limit?: number; continuationToken?: string; }, opts: PlatformClientOptions ): Promise { const qs = new URLSearchParams(); qs.set('productId', params.productId); if (params.eventType) qs.set('eventType', params.eventType); if (params.from) qs.set('from', params.from); if (params.to) qs.set('to', params.to); qs.set( 'limit', String(Math.min(params.limit ?? config.QUERY_DEFAULT_LIMIT, config.QUERY_MAX_LIMIT)) ); if (params.continuationToken) qs.set('continuationToken', params.continuationToken); return platformFetch(`/api/telemetry/query?${qs}`, { method: 'GET' }, opts); } export interface TelemetryCluster { /** Cluster doc ID: ${fingerprint}:${yyyyMM} */ id: string; /** Partition key: ${productId}:${platform}:${module} */ pk: string; fingerprint: string; severity: 'warn' | 'error' | 'fatal'; totalCount: number; lastSeenAt: string; firstSeenAt: string; platform: string; module: string; eventName: string; sampleMessage?: string; status: 'open' | 'resolved' | 'ignored'; resolvedBy?: string; resolvedAt?: string; } export async function telemetryClusters( params: { from?: string; to?: string; platform?: string; module?: string }, opts: PlatformClientOptions ): Promise<{ clusters: TelemetryCluster[]; total: number }> { const qs = new URLSearchParams(); if (params.from) qs.set('from', params.from); if (params.to) qs.set('to', params.to); if (params.platform) qs.set('platform', params.platform); if (params.module) qs.set('module', params.module); return platformFetch<{ clusters: TelemetryCluster[]; total: number }>( `/api/telemetry/clusters?${qs}`, { method: 'GET' }, opts ); } export async function telemetryMetrics(opts: PlatformClientOptions): Promise { return platformFetch('/api/telemetry/metrics', { method: 'GET' }, opts); } export async function telemetryListPolicies(opts: PlatformClientOptions): Promise { return platformFetch('/api/telemetry/policies', { method: 'GET' }, opts); } export async function telemetryPreviewPolicy( body: { targeting?: Record }, opts: PlatformClientOptions ): Promise { return platformFetch( '/api/telemetry/policies/preview', { method: 'POST', body: JSON.stringify(body) }, opts ); } export async function telemetryCreatePolicy( body: Record, opts: PlatformClientOptions ): Promise { return platformFetch( '/api/telemetry/policies', { method: 'POST', body: JSON.stringify(body) }, opts ); } export async function telemetryUpdatePolicy( policyId: string, body: Record, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/telemetry/policies/${encodeURIComponent(policyId)}`, { method: 'PUT', body: JSON.stringify(body) }, opts ); } export async function telemetryDeletePolicy( policyId: string, opts: PlatformClientOptions ): Promise<{ success: boolean }> { return platformFetch<{ success: boolean }>( `/api/telemetry/policies/${encodeURIComponent(policyId)}`, { method: 'DELETE' }, opts ); } export async function telemetryUpdateCluster( clusterId: string, pk: string, status: 'open' | 'resolved' | 'ignored', opts: PlatformClientOptions ): Promise { const qs = new URLSearchParams({ pk }); return platformFetch( `/api/telemetry/clusters/${encodeURIComponent(clusterId)}?${qs}`, { method: 'PATCH', body: JSON.stringify({ status }) }, opts ); } // ── Diagnostics ─────────────────────────────────────────────────────────────── export interface DebugSession { id: string; productId: string; status: string; collectionLevel: string; captureLogs: boolean; captureNetwork: boolean; captureScreenshots: boolean; maxDurationMinutes: number; logCount: number; traceCount: number; createdAt: string; expiresAt: string; targetUserId?: string; targetAnonymousId?: string; } export async function diagnosticsListSessions( params: { productId?: string; status?: string; limit?: number; offset?: number; }, opts: PlatformClientOptions ): Promise<{ sessions: DebugSession[]; total: number }> { const qs = new URLSearchParams(); if (params.productId) qs.set('productId', params.productId); if (params.status) qs.set('status', params.status); qs.set( 'limit', String(Math.min(params.limit ?? config.QUERY_DEFAULT_LIMIT, config.QUERY_MAX_LIMIT)) ); if (params.offset) qs.set('offset', String(params.offset)); return platformFetch<{ sessions: DebugSession[]; total: number }>( `/api/diagnostics/sessions?${qs}`, { method: 'GET' }, opts ); } export async function diagnosticsCreateSession( body: { productId: string; targetUserId?: string; targetAnonymousId?: string; collectionLevel?: 'standard' | 'debug' | 'trace'; captureLogs?: boolean; captureNetwork?: boolean; maxDurationMinutes?: number; }, opts: PlatformClientOptions ): Promise { return platformFetch( '/api/diagnostics/sessions', { method: 'POST', body: JSON.stringify(body) }, opts ); } export async function diagnosticsGetSession( sessionId: string, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/diagnostics/sessions/${encodeURIComponent(sessionId)}`, { method: 'GET' }, opts ); } export async function diagnosticsUpdateSession( sessionId: string, body: { status?: string; collectionLevel?: string; maxDurationMinutes?: number }, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/diagnostics/sessions/${encodeURIComponent(sessionId)}`, { method: 'PATCH', body: JSON.stringify(body) }, opts ); } export async function diagnosticsGetLogs( sessionId: string, params: { level?: string; from?: string; to?: string; limit?: number }, opts: PlatformClientOptions ): Promise<{ logs: unknown[]; continuationToken?: string }> { const qs = new URLSearchParams(); if (params.level) qs.set('level', params.level); if (params.from) qs.set('from', params.from); if (params.to) qs.set('to', params.to); qs.set( 'limit', String(Math.min(params.limit ?? config.QUERY_DEFAULT_LIMIT, config.QUERY_MAX_LIMIT)) ); return platformFetch<{ logs: unknown[]; continuationToken?: string }>( `/api/diagnostics/sessions/${encodeURIComponent(sessionId)}/logs?${qs}`, { method: 'GET' }, opts ); } export async function diagnosticsGetTraces( sessionId: string, params: { limit?: number; continuationToken?: string }, opts: PlatformClientOptions ): Promise<{ traces: unknown[]; continuationToken?: string }> { const qs = new URLSearchParams(); qs.set( 'limit', String(Math.min(params.limit ?? config.QUERY_DEFAULT_LIMIT, config.QUERY_MAX_LIMIT)) ); if (params.continuationToken) qs.set('continuationToken', params.continuationToken); return platformFetch<{ traces: unknown[]; continuationToken?: string }>( `/api/diagnostics/sessions/${encodeURIComponent(sessionId)}/traces?${qs}`, { method: 'GET' }, opts ); }