blob: 5f0fdde23b5dc553782e7d7f68313723bcae6b00 [file]
import { DiagnosticLogBuffer, truncateUtf8 } from '@maka/core';
import { installConsoleDiagnosticLogCapture } from '@maka/core/node';
import { redactSecrets } from '@maka/core';
import type { HostDiagnosticsResult } from '@maka/runtime-host/protocol';
import type { DesktopErrorDiagnosticInput } from '../preload/diagnostics-contract.js';
const INPUT_LIMITS = {
title: 512,
description: 24 * 1024,
details: 24 * 1024,
rendererUserAgent: 2 * 1024,
rendererLocale: 64,
} as const;
const INPUT_TRUNCATION_MARKER = '\n<diagnostic input truncated>';
export interface DesktopDiagnosticEnvironment {
readonly appVersion: string;
readonly buildMode: 'dev' | 'packaged';
readonly buildCommit: string | null;
readonly electronVersion: string;
readonly nodeVersion: string;
readonly chromeVersion: string;
readonly platform: NodeJS.Platform;
readonly arch: string;
readonly osRelease: string;
readonly locale: string;
readonly workspacePath: string;
readonly homePath: string;
readonly processUptimeSeconds: number;
}
export type RuntimeHostDiagnosticRead =
| { readonly ok: true; readonly value: HostDiagnosticsResult }
| { readonly ok: false; readonly error: string };
export const mainProcessLogBuffer = new DiagnosticLogBuffer();
let logCaptureInstalled = false;
export function installMainProcessLogCapture(buffer: DiagnosticLogBuffer = mainProcessLogBuffer): void {
if (logCaptureInstalled) return;
logCaptureInstalled = true;
installConsoleDiagnosticLogCapture(buffer);
}
export function parseDesktopErrorDiagnosticInput(input: unknown): DesktopErrorDiagnosticInput {
if (!input || typeof input !== 'object' || Array.isArray(input)) {
throw new TypeError('Invalid Desktop diagnostic input');
}
const record = input as Record<string, unknown>;
const allowedKeys = new Set([
'surface',
'title',
'description',
'details',
'rendererUserAgent',
'rendererLocale',
]);
if (Object.keys(record).some((key) => !allowedKeys.has(key))) {
throw new TypeError('Invalid Desktop diagnostic input');
}
if (record.surface !== 'toast' && record.surface !== 'renderer_crash') {
throw new TypeError('Invalid Desktop diagnostic surface');
}
const title = requireDiagnosticString(record.title, 'title', INPUT_LIMITS.title);
return {
surface: record.surface,
title,
...optionalBoundedString(record, 'description', INPUT_LIMITS.description),
...optionalBoundedString(record, 'details', INPUT_LIMITS.details),
...optionalBoundedString(record, 'rendererUserAgent', INPUT_LIMITS.rendererUserAgent),
...optionalBoundedString(record, 'rendererLocale', INPUT_LIMITS.rendererLocale),
};
}
export function formatDesktopErrorDiagnosticReport(
input: DesktopErrorDiagnosticInput,
environment: DesktopDiagnosticEnvironment,
mainLogs: readonly string[],
runtimeHost: RuntimeHostDiagnosticRead,
capturedAt = new Date(),
): string {
const lines = [
'Maka Desktop diagnostic report',
`Captured at: ${capturedAt.toISOString()}`,
'',
'Error',
`Surface: ${input.surface}`,
`Title: ${input.title}`,
];
if (input.description) lines.push(`Description: ${input.description}`);
if (input.details) lines.push('', 'Details:', input.details);
lines.push(
'',
'Environment',
`Maka: ${environment.appVersion}`,
`Build: ${environment.buildMode}${environment.buildCommit ? ` @ ${environment.buildCommit.slice(0, 12)}` : ''}`,
`Electron: ${environment.electronVersion}`,
`Chrome: ${environment.chromeVersion}`,
`Node: ${environment.nodeVersion}`,
`OS: ${environment.platform} ${environment.osRelease} (${environment.arch})`,
`Locale: ${environment.locale}`,
`Renderer locale: ${input.rendererLocale ?? '<unknown>'}`,
`Renderer user agent: ${input.rendererUserAgent ?? '<unknown>'}`,
`Workspace: ${environment.workspacePath}`,
`Main process uptime: ${Math.max(0, Math.floor(environment.processUptimeSeconds))}s`,
'',
`Recent main-process logs (${mainLogs.length})`,
...(mainLogs.length > 0 ? mainLogs : ['<none captured>']),
);
lines.push('', 'Runtime Host');
if (runtimeHost.ok) {
const host = runtimeHost.value;
lines.push(
`Epoch: ${host.hostEpoch}`,
`Protocol: v${host.protocolVersion} · compatibility ${host.compatibilityEpoch}`,
`State: ${host.state}`,
`Process: ${host.pid} · uptime ${host.processUptimeSeconds}s`,
`Runtime: Node ${host.nodeVersion} · ${host.platform} ${host.osRelease} (${host.arch})`,
`Activity: ${host.connections} connections · ${host.activeOperations} operations · ${host.activeResidencies} residencies`,
`Recent Runtime Host logs (${host.logs.length})`,
...(host.logs.length > 0 ? host.logs : ['<none captured>']),
);
} else {
lines.push(`Diagnostics unavailable: ${runtimeHost.error}`);
}
const redacted = redactSecrets(lines.join('\n'));
return collapseHomePath(redacted, environment.homePath, environment.platform);
}
function optionalBoundedString(
record: Record<string, unknown>,
key: keyof typeof INPUT_LIMITS,
maximum: number,
): Partial<Record<typeof key, string>> {
const value = record[key];
if (value === undefined) return {};
return { [key]: requireDiagnosticString(value, key, maximum) };
}
function requireDiagnosticString(value: unknown, label: string, maximumBytes: number): string {
if (typeof value !== 'string' || value.length === 0) {
throw new TypeError(`Invalid Desktop diagnostic ${label}`);
}
return truncateUtf8(value, maximumBytes, INPUT_TRUNCATION_MARKER);
}
function collapseHomePath(value: string, homePath: string, platform: NodeJS.Platform): string {
if (!homePath) return value;
const escaped = homePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return value.replace(new RegExp(escaped, platform === 'win32' ? 'gi' : 'g'), '~');
}