feat: implement telemetry system for application usage tracking
- Added telemetry utility to capture application events and metrics. - Integrated PostHog for event tracking with distinct user identification. - Implemented telemetry initialization, event capturing, and shutdown procedures. feat: add UV environment setup for Python management - Created utilities to manage Python installation and configuration. - Implemented network optimization checks for Python installation mirrors. - Added functions to set up managed Python environments with error handling. feat: enhance host API communication with token management - Introduced host API token retrieval and management for secure requests. - Updated host API fetch functions to include token in headers. - Added support for creating event sources with authentication. test: add comprehensive tests for gateway protocol and startup helpers - Implemented unit tests for gateway protocol helpers, event dispatching, and state management. - Added tests for startup recovery strategies and process policies. - Ensured coverage for connection monitoring and restart governance logic.
This commit is contained in:
@@ -4,11 +4,16 @@ import { logout, readPersistedAuthToken } from '../router/auth-session';
|
||||
|
||||
type RequestInitLike = Pick<RequestInit, 'method' | 'headers' | 'body'>;
|
||||
|
||||
const HOST_API_PORT = 13210;
|
||||
const HOST_API_BASE = `http://127.0.0.1:${HOST_API_PORT}`;
|
||||
|
||||
type LooseIpcBridge = {
|
||||
invoke<T = unknown>(channel: string, ...args: any[]): Promise<T>;
|
||||
on?(channel: string, callback: (...args: any[]) => void): () => void;
|
||||
};
|
||||
|
||||
let cachedHostApiToken: string | null = null;
|
||||
|
||||
function normalizeHeaders(headers?: HeadersInit): Headers {
|
||||
return new Headers(headers ?? {});
|
||||
}
|
||||
@@ -67,6 +72,58 @@ function handleUnauthorized(): void {
|
||||
logout({ reason: 'unauthorized', from });
|
||||
}
|
||||
|
||||
async function getHostApiToken(): Promise<string> {
|
||||
if (cachedHostApiToken) {
|
||||
return cachedHostApiToken;
|
||||
}
|
||||
|
||||
cachedHostApiToken = await invokeIpc<string>(IPC_EVENTS.HOST_API_TOKEN);
|
||||
return cachedHostApiToken;
|
||||
}
|
||||
|
||||
async function fetchViaLocalHostApi<T>(
|
||||
path: string,
|
||||
method: string,
|
||||
headers: Headers,
|
||||
body: BodyInit | null | undefined,
|
||||
): Promise<T> {
|
||||
const hostApiToken = await getHostApiToken();
|
||||
const localHeaders = new Headers(headers);
|
||||
if (hostApiToken && !localHeaders.has('X-Host-Api-Token')) {
|
||||
localHeaders.set('X-Host-Api-Token', hostApiToken);
|
||||
}
|
||||
if (body != null && !localHeaders.has('Content-Type')) {
|
||||
localHeaders.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
const response = await fetch(`${HOST_API_BASE}${path}`, {
|
||||
method,
|
||||
headers: localHeaders,
|
||||
body,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (isUnauthorizedStatus(response.status)) {
|
||||
handleUnauthorized();
|
||||
}
|
||||
const text = await response.text();
|
||||
if (!isUnauthorizedStatus(response.status) && isUnauthorizedMessage(text)) {
|
||||
handleUnauthorized();
|
||||
}
|
||||
throw new Error(text || response.statusText || `Request failed with ${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') ?? '';
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
if (contentType.includes('application/json')) {
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
return (await response.text()) as unknown as T;
|
||||
}
|
||||
|
||||
export function hasHostApiBridge(): boolean {
|
||||
return typeof window !== 'undefined' && Boolean(window.api?.invoke);
|
||||
}
|
||||
@@ -89,6 +146,15 @@ export function onIpc(channel: string, callback: (...args: any[]) => void): () =
|
||||
return bridge.on ? bridge.on(channel, callback) : () => {};
|
||||
}
|
||||
|
||||
function shouldFallbackToBrowser(message: string): boolean {
|
||||
const normalized = message.toLowerCase();
|
||||
return normalized.includes('invalid ipc channel: hostapi:fetch')
|
||||
|| normalized.includes("no handler registered for 'hostapi:fetch'")
|
||||
|| normalized.includes('no handler registered for "hostapi:fetch"')
|
||||
|| normalized.includes('no handler registered for hostapi:fetch')
|
||||
|| normalized.includes('window is not defined');
|
||||
}
|
||||
|
||||
export async function hostApiFetch<T>(path: string, init?: RequestInitLike): Promise<T> {
|
||||
const method = init?.method ?? 'GET';
|
||||
const headers = normalizeHeaders(init?.headers);
|
||||
@@ -112,8 +178,17 @@ export async function hostApiFetch<T>(path: string, init?: RequestInitLike): Pro
|
||||
};
|
||||
|
||||
if (hasHostApiBridge()) {
|
||||
const response = await invokeIpc(IPC_EVENTS.HOST_API_FETCH, request);
|
||||
return extractResult<T>(response);
|
||||
try {
|
||||
const response = await invokeIpc(IPC_EVENTS.HOST_API_FETCH, request);
|
||||
return extractResult<T>(response);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!shouldFallbackToBrowser(message)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fetchViaLocalHostApi<T>(path, method, headers, normalizeBody(init?.body));
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof fetch === 'function') {
|
||||
@@ -135,6 +210,9 @@ export async function hostApiFetch<T>(path: string, init?: RequestInitLike): Pro
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') ?? '';
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
if (contentType.includes('application/json')) {
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
@@ -144,3 +222,13 @@ export async function hostApiFetch<T>(path: string, init?: RequestInitLike): Pro
|
||||
|
||||
throw new Error(`No HTTP bridge available for ${path}`);
|
||||
}
|
||||
|
||||
export async function createHostEventSource(path = '/api/events'): Promise<EventSource> {
|
||||
const token = await getHostApiToken();
|
||||
const separator = path.includes('?') ? '&' : '?';
|
||||
return new EventSource(`${HOST_API_BASE}${path}${separator}token=${encodeURIComponent(token)}`);
|
||||
}
|
||||
|
||||
export function getHostApiBase(): string {
|
||||
return HOST_API_BASE;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user