Skip to content

Commit ca0d387

Browse files
improve log analysis feedback (#3485)
* feat(feedback): improve log analysis and submission process * refactor(feedback): replace GitHub submission with API-based feedback * feat(feedback): enhance feedback submission with validation and logs toggle
1 parent dccb4d1 commit ca0d387

2 files changed

Lines changed: 125 additions & 116 deletions

File tree

apps/api/src/routes/feedback.ts

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ async function createGitHubIssue(
6666
title: string,
6767
body: string,
6868
labels: string[],
69-
): Promise<{ url: string } | { error: string }> {
69+
): Promise<{ url: string; number: number } | { error: string }> {
7070
if (!env.YUJONGLEE_GITHUB_TOKEN_REPO) {
7171
return { error: "GitHub bot token not configured" };
7272
}
@@ -93,12 +93,37 @@ async function createGitHubIssue(
9393
return { error: `GitHub API error: ${response.status} - ${errorText}` };
9494
}
9595

96-
const data = (await response.json()) as { html_url?: string };
97-
if (!data.html_url) {
96+
const data = (await response.json()) as {
97+
html_url?: string;
98+
number?: number;
99+
};
100+
if (!data.html_url || !data.number) {
98101
return { error: "GitHub API did not return issue URL" };
99102
}
100103

101-
return { url: data.html_url };
104+
return { url: data.html_url, number: data.number };
105+
}
106+
107+
async function addCommentToIssue(
108+
issueNumber: number,
109+
comment: string,
110+
): Promise<void> {
111+
if (!env.YUJONGLEE_GITHUB_TOKEN_REPO) {
112+
return;
113+
}
114+
115+
await fetch(
116+
`https://api.github.com/repos/fastrepl/hyprnote/issues/${issueNumber}/comments`,
117+
{
118+
method: "POST",
119+
headers: {
120+
Authorization: `Bearer ${env.YUJONGLEE_GITHUB_TOKEN_REPO}`,
121+
Accept: "application/vnd.github.v3+json",
122+
"Content-Type": "application/json",
123+
},
124+
body: JSON.stringify({ body: comment }),
125+
},
126+
);
102127
}
103128

104129
export const feedback = new Hono<AppBindings>();
@@ -151,28 +176,14 @@ feedback.post(
151176
`**Git Hash:** ${deviceInfo.gitHash}`,
152177
].join("\n");
153178

154-
let logSection = "";
155-
if (type === "bug" && logs) {
156-
const logSummary = await analyzeLogsWithAI(logs);
157-
if (logSummary?.trim()) {
158-
logSection = `
159-
160-
## Log Summary
161-
\`\`\`
162-
${logSummary}
163-
\`\`\`
164-
`;
165-
}
166-
}
167-
168179
const body =
169180
type === "bug"
170181
? `## Description
171182
${trimmedDescription}
172183
173184
## Device Information
174185
${deviceInfoSection}
175-
${logSection}
186+
176187
---
177188
*This issue was submitted from the Hyprnote desktop app.*
178189
`
@@ -197,6 +208,24 @@ ${deviceInfoSection}
197208
return c.json({ success: false, error: result.error }, 500);
198209
}
199210

211+
if (logs) {
212+
const logSummary = await analyzeLogsWithAI(logs);
213+
const logComment = `## Log Analysis
214+
215+
${logSummary?.trim() ? `### Summary\n\`\`\`\n${logSummary}\n\`\`\`` : "_No errors or warnings found._"}
216+
217+
<details>
218+
<summary>Raw Logs (last 10KB)</summary>
219+
220+
\`\`\`
221+
${logs.slice(-10000)}
222+
\`\`\`
223+
224+
</details>`;
225+
226+
await addCommentToIssue(result.number, logComment);
227+
}
228+
200229
return c.json({ success: true, issueUrl: result.url }, 200);
201230
},
202231
);

apps/desktop/src/components/feedback/feedback-modal.tsx

Lines changed: 77 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
12
import { arch, version as osVersion, platform } from "@tauri-apps/plugin-os";
23
import { Bug, Lightbulb, X } from "lucide-react";
34
import { useCallback, useEffect, useState } from "react";
@@ -8,7 +9,6 @@ import { commands as miscCommands } from "@hypr/plugin-misc";
89
import { commands as openerCommands } from "@hypr/plugin-opener2";
910
import { commands as tracingCommands } from "@hypr/plugin-tracing";
1011
import { Button } from "@hypr/ui/components/ui/button";
11-
import { Checkbox } from "@hypr/ui/components/ui/checkbox";
1212
import { cn } from "@hypr/utils";
1313

1414
import { env } from "../../env";
@@ -29,22 +29,14 @@ export const useFeedbackModal = create<FeedbackModalStore>((set) => ({
2929
close: () => set({ isOpen: false }),
3030
}));
3131

32-
async function openLogsDir(): Promise<boolean> {
33-
const result = await tracingCommands.logsDir();
34-
if (result.status === "ok") {
35-
const revealResult = await openerCommands.revealItemInDir(result.data);
36-
return revealResult.status === "ok";
37-
}
38-
return false;
39-
}
40-
4132
export function FeedbackModal() {
4233
const { isOpen, initialType, close } = useFeedbackModal();
4334
const [type, setType] = useState<FeedbackType>(initialType);
4435
const [description, setDescription] = useState("");
36+
const [attachLogs, setAttachLogs] = useState(true);
4537
const [isSubmitting, setIsSubmitting] = useState(false);
46-
const [gitHash, setGitHash] = useState<string>("");
47-
const [attachLogs, setAttachLogs] = useState(false);
38+
const [submitStatus, setSubmitStatus] = useState<string>("");
39+
const [errorMessage, setErrorMessage] = useState<string>("");
4840

4941
useEffect(() => {
5042
const handleEscape = (e: KeyboardEvent) => {
@@ -65,98 +57,85 @@ export function FeedbackModal() {
6557
useEffect(() => {
6658
if (isOpen) {
6759
setType(initialType);
68-
miscCommands.getGitHash().then((result) => {
69-
setGitHash(result.status === "ok" ? result.data : "unknown");
70-
});
7160
} else {
7261
setDescription("");
73-
setGitHash("");
74-
setAttachLogs(false);
62+
setAttachLogs(true);
63+
setSubmitStatus("");
64+
setErrorMessage("");
7565
}
7666
}, [isOpen, initialType]);
7767

7868
const handleSubmit = useCallback(async () => {
79-
if (!description.trim()) {
69+
const trimmed = description.trim();
70+
if (!trimmed) {
8071
return;
8172
}
8273

74+
if (trimmed.length < 10) {
75+
setErrorMessage("Description must be at least 10 characters");
76+
return;
77+
}
78+
79+
setErrorMessage("");
8380
setIsSubmitting(true);
81+
setSubmitStatus("Submitting...");
8482

8583
try {
8684
const gitHashResult = await miscCommands.getGitHash();
8785
const gitHash =
8886
gitHashResult.status === "ok" ? gitHashResult.data : "unknown";
8987

90-
const deviceInfo = [
91-
`**Platform:** ${platform()}`,
92-
`**Architecture:** ${arch()}`,
93-
`**OS Version:** ${osVersion()}`,
94-
`**App Version:** ${env.VITE_APP_VERSION ?? "unknown"}`,
95-
`**Git Hash:** ${gitHash}`,
96-
].join("\n");
97-
98-
const trimmedDescription = description.trim();
99-
const firstLine = trimmedDescription.split("\n")[0].slice(0, 100).trim();
100-
const title =
101-
firstLine || (type === "bug" ? "Bug Report" : "Feature Request");
102-
103-
let logSection = "";
104-
if (attachLogs) {
105-
const logsOpened = await openLogsDir();
106-
if (logsOpened) {
107-
logSection = `
108-
109-
## Application Logs
110-
Logs will be opened in a separate window. Please attach the log file to this issue.
111-
`;
88+
let logs: string | undefined;
89+
if (type === "bug" && attachLogs) {
90+
setSubmitStatus("Collecting logs...");
91+
const logsResult = await tracingCommands.logContent();
92+
if (logsResult.status === "ok" && logsResult.data) {
93+
logs = logsResult.data.slice(-10000);
11294
}
11395
}
11496

115-
if (type === "bug") {
116-
const body = `## Description
117-
${trimmedDescription}
118-
119-
## Device Information
120-
${deviceInfo}
121-
${logSection}
122-
---
123-
*This issue was submitted from the Hyprnote desktop app.*
124-
`;
97+
setSubmitStatus("Submitting...");
98+
99+
const response = await tauriFetch(`${env.VITE_API_URL}/feedback/submit`, {
100+
method: "POST",
101+
headers: { "Content-Type": "application/json" },
102+
body: JSON.stringify({
103+
type,
104+
description: trimmed,
105+
logs,
106+
deviceInfo: {
107+
platform: platform(),
108+
arch: arch(),
109+
osVersion: osVersion(),
110+
appVersion: env.VITE_APP_VERSION ?? "unknown",
111+
gitHash,
112+
},
113+
}),
114+
});
125115

126-
const url = new URL("https://github.com/fastrepl/hyprnote/issues/new");
127-
url.searchParams.set("title", title);
128-
url.searchParams.set("body", body);
129-
url.searchParams.set("labels", "bug,user-reported");
116+
const data = (await response.json()) as {
117+
success: boolean;
118+
issueUrl?: string;
119+
error?: string;
120+
};
130121

131-
await openerCommands.openUrl(url.toString(), null);
122+
if (data.success && data.issueUrl) {
123+
await openerCommands.openUrl(data.issueUrl, null);
124+
close();
132125
} else {
133-
const body = `## Feature Request
134-
${trimmedDescription}
135-
136-
## Submitted From
137-
${deviceInfo}
138-
${logSection}
139-
---
140-
*This feature request was submitted from the Hyprnote desktop app.*
141-
`;
142-
143-
const url = new URL(
144-
"https://github.com/fastrepl/hyprnote/discussions/new",
145-
);
146-
url.searchParams.set("category", "ideas");
147-
url.searchParams.set("title", title);
148-
url.searchParams.set("body", body);
149-
150-
await openerCommands.openUrl(url.toString(), null);
126+
setErrorMessage(data.error ?? "Failed to submit feedback");
151127
}
152-
153-
close();
154128
} catch (error) {
155-
console.error("Failed to submit feedback:", error);
129+
console.error(
130+
"Failed to submit feedback:",
131+
error instanceof Error ? error.message : String(error),
132+
);
133+
setErrorMessage("Failed to submit feedback. Please try again.");
156134
} finally {
157135
setIsSubmitting(false);
136+
setSubmitStatus("");
158137
}
159-
}, [description, type, close, attachLogs]);
138+
}, [description, type, attachLogs, close]);
160139

161140
if (!isOpen) {
162141
return null;
@@ -234,7 +213,10 @@ ${logSection}
234213
<textarea
235214
id="feedback-description"
236215
value={description}
237-
onChange={(e) => setDescription(e.target.value)}
216+
onChange={(e) => {
217+
setDescription(e.target.value);
218+
if (errorMessage) setErrorMessage("");
219+
}}
238220
placeholder={
239221
isBug
240222
? "What happened? What did you expect to happen? Steps to reproduce..."
@@ -243,32 +225,30 @@ ${logSection}
243225
rows={6}
244226
className={cn([
245227
"w-full px-2.5 py-1.5 rounded-md",
246-
"border border-neutral-200",
228+
"border",
229+
errorMessage ? "border-red-500" : "border-neutral-200",
247230
"text-sm resize-none",
248231
"focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-1",
249232
])}
250233
maxLength={5000}
251234
/>
235+
{errorMessage && (
236+
<p className="text-xs text-red-500 mt-1">{errorMessage}</p>
237+
)}
252238
</div>
253239

254-
<div className="flex items-center gap-2">
255-
<Checkbox
256-
id="attach-logs"
257-
checked={attachLogs}
258-
onCheckedChange={(checked) => setAttachLogs(checked === true)}
259-
/>
260-
<label
261-
htmlFor="attach-logs"
262-
className="text-sm text-neutral-600 cursor-pointer"
263-
>
264-
Open log directory (for manual attachment)
240+
{isBug && (
241+
<label className="flex items-center gap-2 cursor-pointer">
242+
<input
243+
type="checkbox"
244+
checked={attachLogs}
245+
onChange={(e) => setAttachLogs(e.target.checked)}
246+
className="rounded border-neutral-300"
247+
/>
248+
<span className="text-sm text-neutral-600">
249+
Attach app logs to help diagnose the issue
250+
</span>
265251
</label>
266-
</div>
267-
268-
{gitHash && (
269-
<div className="mt-4 text-[10px] text-neutral-100 font-mono">
270-
{gitHash}
271-
</div>
272252
)}
273253
</div>
274254

@@ -279,7 +259,7 @@ ${logSection}
279259
className="h-8 text-sm"
280260
>
281261
{isSubmitting
282-
? "Opening..."
262+
? submitStatus || "Opening..."
283263
: isBug
284264
? "Report Bug"
285265
: "Suggest Feature"}

0 commit comments

Comments
 (0)