diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 67c0507..30f5bc5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -33,4 +33,9 @@ body: id: logs attributes: label: Logs/screenshots - description: Paste relevant logs or attach screenshots + description: Paste relevant logs or attach screenshots. You can generate a sanitized bundle in-app from Settings → General → Copy Debug Info. + - type: textarea + id: debug_info + attributes: + label: Debug info bundle + description: Paste the copied debug info JSON block from Settings → General → Copy Debug Info. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3eb60e0..f391319 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,6 +54,7 @@ Use the bug report template and include: - Repro steps - Expected vs actual behavior - Logs and screenshots where possible +- Debug bundle from `Settings → General → Copy Debug Info` (paste into issue and optionally attach the generated `debug-info-*.json` file path shown in-app) ## Recognition diff --git a/electron/src/handlers/debug.handler.ts b/electron/src/handlers/debug.handler.ts new file mode 100644 index 0000000..8041f6a --- /dev/null +++ b/electron/src/handlers/debug.handler.ts @@ -0,0 +1,217 @@ +import { app, ipcMain } from "electron"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import crypto from "crypto"; +import { getDataDir } from "../lib/data-dir"; +import { loadMcpServers } from "../lib/mcp-store"; +import { getCurrentLogFile, getLogsDir, log } from "../lib/logger"; +import { sessions } from "./oagent-sessions.handler"; +import { oapSessions } from "./oap-sessions.handler"; +import { terminals } from "./terminal.handler"; + +interface ProjectRecord { + id: string; + name: string; + path: string; +} + +interface DebugIssuePayload { + generatedAt: string; + app: Record; + runtime: Record; + projects: unknown[]; + mcp: unknown; + logs: unknown; +} + +function shortHash(value: string): string { + return crypto.createHash("sha256").update(value).digest("hex").slice(0, 12); +} + +function sanitizePath(value: string): { name: string; hash: string } { + return { + name: path.basename(value), + hash: shortHash(value), + }; +} + +function readProjects(): ProjectRecord[] { + const filePath = path.join(getDataDir(), "projects.json"); + if (!fs.existsSync(filePath)) return []; + try { + return JSON.parse(fs.readFileSync(filePath, "utf-8")) as ProjectRecord[]; + } catch { + return []; + } +} + +function sanitizeUrl(raw?: string): string | undefined { + if (!raw) return undefined; + try { + const parsed = new URL(raw); + return `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`; + } catch { + return ""; + } +} + +function redactSensitive(text: string): string { + const home = os.homedir(); + const escapedHome = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return text + .replace(new RegExp(escapedHome, "g"), "") + .replace(/[A-Za-z]:\\[^ \n\r\t"']+/g, "") + .replace(/(token|key|secret|password)=([^&\s]+)/gi, "$1=[REDACTED]") + .replace(/\b(gh[opus]_[A-Za-z0-9]{20,})\b/g, "[REDACTED]") + .replace(/\b(sk-or-v1-[A-Za-z0-9_-]{8,}|sk-[A-Za-z0-9_-]{8,})\b/g, "[REDACTED]") + .replace(/\bBearer\s+[A-Za-z0-9\-._~+/]+=*\b/gi, "Bearer [REDACTED]"); +} + +function parseRecentLogDetails() { + const logDir = getLogsDir(); + const fileEntries = fs.existsSync(logDir) + ? fs.readdirSync(logDir) + .filter((name) => name.endsWith(".log")) + .map((name) => { + const fullPath = path.join(logDir, name); + const stat = fs.statSync(fullPath); + return { name, fullPath, stat }; + }) + .sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs) + : []; + + const recentFiles = fileEntries.slice(0, 5).map((entry) => ({ + file: entry.name, + bytes: entry.stat.size, + modifiedAt: new Date(entry.stat.mtimeMs).toISOString(), + })); + + const newest = fileEntries[0]; + const labelCounts: Record = {}; + const recentErrors: Array<{ ts: string; label: string; message: string }> = []; + + if (newest) { + const lines = fs.readFileSync(newest.fullPath, "utf-8").split("\n").filter(Boolean); + const tail = lines.slice(-600); + for (const line of tail) { + const match = line.match(/^\[(?[^\]]+)\]\s\[(?