Unverified 提交 beeaef57 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Replace ChatLogsData with comprehensive SessionDebugBundle schema (#2488)

## Summary - Replaces the old `ChatLogsDataSchema` with a new `SessionDebugBundleSchema` (schema version 1) that captures all non-sensitive data needed for debugging chat sessions - Each message now includes AI SDK JSON (with base64 images stripped), model name, token usage, timestamps, commit hashes, request ID, and approval state - Adds non-sensitive user settings snapshot, app metadata, custom provider/model definitions, and MCP server configurations to the debug bundle - Updates HelpDialog to use the new schema and adds `Session Schema: v2.0` marker to GitHub issue templates ## Test plan - [ ] Verify `npm run ts` passes (type-check) - [ ] Verify `npm run lint` passes - [ ] Verify `npm test` passes (all 661 unit tests) - [ ] Manual: Open Help dialog → Upload Chat Session → verify the review modal shows messages, codebase, logs, and system info correctly - [ ] Manual: Complete upload and verify the GitHub issue template includes `Session Schema: v2.0` - [ ] Manual: Download uploaded JSON and verify it contains: `schemaVersion`, `system`, `settings`, `app`, `chat` (with `aiMessagesJson` per message), `providers`, `mcpServers`, `codebase`, `logs` - [ ] Manual: Verify no API keys, OAuth tokens, or MCP env vars appear in uploaded data 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2488"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Expands what gets captured and uploaded for support sessions (settings/app metadata/providers/MCP and richer message data), so any sanitization or schema mismatch could leak data or break the support upload flow. > > **Overview** > Switches chat-session uploads from `ChatLogsData` to a new versioned `SessionDebugBundle` (`SESSION_DEBUG_SCHEMA_VERSION = 2`) and updates the IPC contract/exports accordingly. > > The main-process handler now assembles a comprehensive bundle (system/runtime info, sanitized settings snapshot, app metadata, full chat messages with stripped image/file blobs, custom provider/model summaries, MCP server configs without env/header secrets, codebase snapshot, and last ~1000 log lines), and `HelpDialog` is updated to fetch/review/upload this bundle and annotate created GitHub issues with `Session Schema: v2.0` (plus adds a settings section to the bug report template). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7b94199939aa6963787e8fe69716e20cd6570b7d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 fd917781
......@@ -21,7 +21,7 @@ import { ipc } from "@/ipc/types";
import { useState, useEffect } from "react";
import { useAtomValue } from "jotai";
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { ChatLogsData } from "@/ipc/types";
import { SessionDebugBundle } from "@/ipc/types";
import { showError } from "@/lib/toast";
import { HelpBotDialog } from "./HelpBotDialog";
import { useSettings } from "@/hooks/useSettings";
......@@ -37,7 +37,9 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
const [isLoading, setIsLoading] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [reviewMode, setReviewMode] = useState(false);
const [chatLogsData, setChatLogsData] = useState<ChatLogsData | null>(null);
const [debugBundle, setDebugBundle] = useState<SessionDebugBundle | null>(
null,
);
const [uploadComplete, setUploadComplete] = useState(false);
const [sessionId, setSessionId] = useState("");
const [isHelpBotOpen, setIsHelpBotOpen] = useState(false);
......@@ -52,7 +54,7 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
setIsLoading(false);
setIsUploading(false);
setReviewMode(false);
setChatLogsData(null);
setDebugBundle(null);
setUploadComplete(false);
setSessionId("");
};
......@@ -76,6 +78,20 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
const debugInfo = await ipc.system.getSystemDebugInfo();
// Create a formatted issue body with the debug info
const settingsLines = settings
? [
`- Selected Model: ${settings.selectedModel?.provider}:${settings.selectedModel?.name}`,
`- Chat Mode: ${settings.selectedChatMode ?? "default"}`,
`- Auto Approve Changes: ${settings.autoApproveChanges ?? "n/a"}`,
`- Dyad Pro Enabled: ${settings.enableDyadPro ?? "n/a"}`,
`- Thinking Budget: ${settings.thinkingBudget ?? "n/a"}`,
`- Runtime Mode: ${settings.runtimeMode2 ?? "n/a"}`,
`- Release Channel: ${settings.releaseChannel ?? "n/a"}`,
`- Auto Fix Problems: ${settings.enableAutoFixProblems ?? "n/a"}`,
`- Native Git: ${settings.enableNativeGit ?? "n/a"}`,
].join("\n")
: "Settings not available";
const issueBody = `
<!-- Please fill in all fields in English -->
......@@ -96,6 +112,9 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
- Telemetry ID: ${debugInfo.telemetryId || "n/a"}
- Model: ${debugInfo.selectedLanguageModel || "n/a"}
## Settings
${settingsLines}
## Logs
\`\`\`
${debugInfo.logs.slice(-3_500) || "No logs available"}
......@@ -131,10 +150,10 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
setIsUploading(true);
try {
// Get chat logs (includes debug info, chat data, and codebase)
const chatLogs = await ipc.misc.getChatLogs(selectedChatId);
const debugBundle = await ipc.misc.getSessionDebugBundle(selectedChatId);
// Store data for review and switch to review mode
setChatLogsData(chatLogs);
setDebugBundle(debugBundle);
setReviewMode(true);
} catch (error) {
console.error("Failed to upload chat session:", error);
......@@ -147,17 +166,10 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
};
const handleSubmitChatLogs = async () => {
if (!chatLogsData) return;
if (!debugBundle) return;
setIsUploading(true);
try {
// Prepare data for upload
const chatLogsJson = {
systemInfo: chatLogsData.debugInfo,
chat: chatLogsData.chat,
codebaseSnippet: chatLogsData.codebase,
};
// Get signed URL
const response = await fetch(
"https://upload-logs.dyad.sh/generate-upload-url",
......@@ -180,10 +192,11 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
const { uploadUrl, filename } = await response.json();
// Upload the full debug bundle directly
await ipc.system.uploadToSignedUrl({
url: uploadUrl,
contentType: "application/json",
data: chatLogsJson,
data: debugBundle,
});
// Extract session ID (filename without extension)
......@@ -201,7 +214,7 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
const handleCancelReview = () => {
setReviewMode(false);
setChatLogsData(null);
setDebugBundle(null);
};
const handleOpenGitHubIssue = () => {
......@@ -210,6 +223,7 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
<!-- Please fill in all fields in English -->
Session ID: ${sessionId}
Session Schema: v2.0
Pro User ID: ${userBudget?.redactedUserId || "n/a"}
## Issue Description (required)
......@@ -276,7 +290,7 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"}
);
}
if (reviewMode && chatLogsData) {
if (reviewMode && debugBundle) {
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
......@@ -302,7 +316,7 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"}
<div className="border rounded-md p-3">
<h3 className="font-medium mb-2">Chat Messages</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto">
{chatLogsData.chat.messages.map((msg) => (
{debugBundle.chat.messages.map((msg) => (
<div key={msg.id} className="mb-2">
<span className="font-semibold">
{msg.role === "user" ? "You" : "Assistant"}:{" "}
......@@ -316,29 +330,63 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"}
<div className="border rounded-md p-3">
<h3 className="font-medium mb-2">Codebase Snapshot</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto font-mono">
{chatLogsData.codebase}
{debugBundle.codebase}
</div>
</div>
<div className="border rounded-md p-3">
<h3 className="font-medium mb-2">Logs</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto font-mono">
{chatLogsData.debugInfo.logs}
{debugBundle.logs}
</div>
</div>
<div className="border rounded-md p-3">
<h3 className="font-medium mb-2">System Information</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-32 overflow-y-auto">
<p>Dyad Version: {chatLogsData.debugInfo.dyadVersion}</p>
<p>Platform: {chatLogsData.debugInfo.platform}</p>
<p>Architecture: {chatLogsData.debugInfo.architecture}</p>
<p>Dyad Version: {debugBundle.system.dyadVersion}</p>
<p>Platform: {debugBundle.system.platform}</p>
<p>Architecture: {debugBundle.system.architecture}</p>
<p>
Node Version:{" "}
{chatLogsData.debugInfo.nodeVersion || "Not available"}
{debugBundle.system.nodeVersion || "Not available"}
</p>
</div>
</div>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">Settings</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.settings, null, 2)}
</div>
</details>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">
App Metadata
</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.app, null, 2)}
</div>
</details>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">
Custom Providers & Models
</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.providers, null, 2)}
</div>
</details>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">
MCP Servers
</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.mcpServers, null, 2)}
</div>
</details>
</div>
<div className="flex justify-between mt-4 pt-2 sticky bottom-0 bg-background">
......
......@@ -280,7 +280,12 @@ export type {
export type { SecurityReviewResult } from "./security";
// Misc types
export type { ChatLogsData, DeepLinkData, AppOutput, EnvVar } from "./misc";
export type {
SessionDebugBundle,
DeepLinkData,
AppOutput,
EnvVar,
} from "./misc";
// Free agent quota types
export type { FreeAgentQuotaStatus } from "./free_agent_quota";
......
......@@ -41,39 +41,241 @@ export const SetAppEnvVarsParamsSchema = z.object({
});
// =============================================================================
// Chat Logs Schemas
// Session Debug Bundle Schemas
// =============================================================================
export const ChatLogsDataSchema = z.object({
debugInfo: z.object({
nodeVersion: z.string().nullable(),
pnpmVersion: z.string().nullable(),
nodePath: z.string().nullable(),
telemetryId: z.string(),
telemetryConsent: z.string(),
telemetryUrl: z.string(),
/**
* Schema version for the session debug bundle format.
* Bump this when making breaking changes to the schema.
*/
export const SESSION_DEBUG_SCHEMA_VERSION = 2;
// -- System info --
const DebugSystemInfoSchema = z.object({
/** Dyad application version (from package.json) */
dyadVersion: z.string(),
/** OS platform: "darwin", "win32", "linux" */
platform: z.string(),
/** CPU architecture: "x64", "arm64" */
architecture: z.string(),
logs: z.string(),
selectedLanguageModel: z.string(),
/** Node.js version, or null if not found */
nodeVersion: z.string().nullable(),
/** pnpm version, or null if not found */
pnpmVersion: z.string().nullable(),
/** Resolved path to the node binary, or null */
nodePath: z.string().nullable(),
/** Electron version */
electronVersion: z.string(),
/** Telemetry ID for cross-referencing server-side logs. Null if user opted out. */
telemetryId: z.string().nullable(),
});
// -- Non-sensitive settings snapshot --
const DebugSettingsSchema = z.object({
/** Currently selected language model */
selectedModel: z.object({
name: z.string(),
provider: z.string(),
customModelId: z.number().optional(),
}),
chat: z.object({
/** Active chat mode for the session */
selectedChatMode: z.string().nullable(),
/** Default chat mode preference */
defaultChatMode: z.string().nullable(),
/** Whether changes are auto-approved without review */
autoApproveChanges: z.boolean().nullable(),
/** Whether Dyad Pro is enabled */
enableDyadPro: z.boolean().nullable(),
/** Thinking budget level: "low" | "medium" | "high" */
thinkingBudget: z.string().nullable(),
/** Max chat turns kept in context window */
maxChatTurnsInContext: z.number().nullable(),
/** Whether auto-fix problems is enabled */
enableAutoFixProblems: z.boolean().nullable(),
/** Whether native git is enabled */
enableNativeGit: z.boolean().nullable(),
/** Whether auto-update is enabled */
enableAutoUpdate: z.boolean(),
/** Release channel: "stable" | "beta" */
releaseChannel: z.string(),
/** Runtime mode: "host" | "docker" */
runtimeMode2: z.string().nullable(),
/** UI zoom level */
zoomLevel: z.string().nullable(),
/** Preview device mode: "desktop" | "tablet" | "mobile" */
previewDeviceMode: z.string().nullable(),
/** Whether turbo edits mode is enabled */
enableProLazyEditsMode: z.boolean().nullable(),
/** Turbo edits mode variant: "off" | "v1" | "v2" */
proLazyEditsMode: z.string().nullable(),
/** Whether smart files context mode is enabled (Pro) */
enableProSmartFilesContextMode: z.boolean().nullable(),
/** Whether web search is enabled (Pro) */
enableProWebSearch: z.boolean().nullable(),
/** Smart context option: "balanced" | "conservative" | "deep" */
proSmartContextOption: z.string().nullable(),
/** Whether Supabase write SQL migration is enabled */
enableSupabaseWriteSqlMigration: z.boolean().nullable(),
/** Agent tool consent settings per tool */
agentToolConsents: z.record(z.string(), z.string()).nullable(),
/** Experiment flags */
experiments: z.record(z.string(), z.boolean()).nullable(),
/** Custom node path override */
customNodePath: z.string().nullable(),
/** Map of provider ID -> whether configured (has API key). No secrets. */
providerSetupStatus: z.record(z.string(), z.boolean()),
});
// -- App metadata --
const DebugAppInfoSchema = z.object({
id: z.number(),
title: z.string(),
messages: z.array(
z.object({
name: z.string(),
/** Relative app path (not full filesystem path) */
path: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
// Integration identifiers (non-secret)
githubOrg: z.string().nullable(),
githubRepo: z.string().nullable(),
githubBranch: z.string().nullable(),
supabaseProjectId: z.string().nullable(),
supabaseOrganizationSlug: z.string().nullable(),
neonProjectId: z.string().nullable(),
vercelProjectId: z.string().nullable(),
vercelProjectName: z.string().nullable(),
vercelDeploymentUrl: z.string().nullable(),
// Dev commands
installCommand: z.string().nullable(),
startCommand: z.string().nullable(),
// Chat context configuration
chatContext: z.any().nullable(),
// Theme
themeId: z.string().nullable(),
});
// -- Message with full debug detail --
const DebugMessageSchema = z.object({
id: z.number(),
role: z.string(),
role: z.enum(["user", "assistant"]),
/** Human-readable message text */
content: z.string(),
approvalState: z.string().nullable().optional(),
/** ISO 8601 timestamp */
createdAt: z.string(),
/**
* Full AI SDK structured message data (tool calls, image refs, multi-turn state).
* Base64 image data is stripped and replaced with:
* { type: "image", image: "[stripped]", mediaType: "...", _strippedByteLength: N }
*/
aiMessagesJson: z.any().nullable(),
/** Model name used to generate this response (assistant messages only) */
model: z.string().nullable(),
/** Total tokens used for this response (assistant messages only) */
totalTokens: z.number().nullable(),
/** Approval state: "approved" | "rejected" | null */
approvalState: z.enum(["approved", "rejected"]).nullable(),
/** Git commit hash of codebase when this message was created */
sourceCommitHash: z.string().nullable(),
/** Git commit hash of codebase after changes from this message were applied */
commitHash: z.string().nullable(),
/** Pro request UUID for billing/tracking */
requestId: z.string().nullable(),
/** Whether this message used the free agent mode quota */
usingFreeAgentModeQuota: z.boolean().nullable(),
});
// -- Chat with messages --
const DebugChatSchema = z.object({
id: z.number(),
appId: z.number(),
title: z.string().nullable(),
/** Git commit hash at start of this chat */
initialCommitHash: z.string().nullable(),
/** ISO 8601 timestamp */
createdAt: z.string(),
messages: z.array(DebugMessageSchema),
});
// -- Provider / model configuration (no secrets) --
const DebugProvidersSchema = z.object({
/** Custom provider definitions from language_model_providers table */
customProviders: z.array(
z.object({
id: z.string(),
name: z.string(),
hasApiBaseUrl: z.boolean(),
envVarName: z.string().nullable(),
}),
),
/** Custom model definitions from language_models table */
customModels: z.array(
z.object({
id: z.number(),
displayName: z.string(),
apiName: z.string(),
builtinProviderId: z.string().nullable(),
customProviderId: z.string().nullable(),
maxOutputTokens: z.number().nullable(),
contextWindow: z.number().nullable(),
}),
),
});
// -- MCP server configuration (no env/header secrets) --
const DebugMcpServerSchema = z.object({
id: z.number(),
name: z.string(),
transport: z.string(),
command: z.string().nullable(),
args: z.array(z.string()).nullable(),
url: z.string().nullable(),
enabled: z.boolean(),
// NOTE: envJson and headersJson are intentionally EXCLUDED (may contain secrets)
});
// -- Top-level bundle --
/**
* Complete session debug bundle for upload.
*
* Contains all non-sensitive data needed to debug a chat session:
* system info, user settings, app config, full chat messages with
* AI SDK JSON, provider/model setup, MCP servers, codebase snapshot,
* and application logs.
*
* Sensitive data (API keys, OAuth tokens, MCP env vars) is stripped.
* Base64 image data in AI SDK messages is replaced with placeholders.
*/
export const SessionDebugBundleSchema = z.object({
/** Schema version number. Bump on breaking changes. */
schemaVersion: z.number(),
/** ISO 8601 timestamp of when this bundle was exported */
exportedAt: z.string(),
/** Runtime environment info */
system: DebugSystemInfoSchema,
/** Non-sensitive user settings snapshot */
settings: DebugSettingsSchema,
/** App configuration and integration metadata */
app: DebugAppInfoSchema,
/** Chat with full message history including AI SDK JSON */
chat: DebugChatSchema,
/** Custom provider and model definitions (no secrets) */
providers: DebugProvidersSchema,
/** MCP server configurations (no env/header secrets) */
mcpServers: z.array(DebugMcpServerSchema),
/** Formatted codebase snapshot */
codebase: z.string(),
/** Application logs (last 1000 lines) */
logs: z.string(),
});
export type ChatLogsData = z.infer<typeof ChatLogsDataSchema>;
export type SessionDebugBundle = z.infer<typeof SessionDebugBundleSchema>;
// =============================================================================
// Deep Link Schemas
......@@ -133,11 +335,11 @@ export const miscContracts = {
output: z.void(),
}),
// Chat logs
getChatLogs: defineContract({
channel: "get-chat-logs",
// Session debug bundle
getSessionDebugBundle: defineContract({
channel: "get-session-debug-bundle",
input: z.number(), // chatId
output: ChatLogsDataSchema,
output: SessionDebugBundleSchema,
}),
// Console logs
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论