From c182e09f9997e8357f293f2c4c081a212aba0c63 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 18 May 2026 15:33:52 -0700 Subject: [PATCH] Route crash reports separately from feedback (#2280) --- src/main/ipc/crash-reporting.test.ts | 1 + src/main/ipc/crash-reporting.ts | 1 + src/main/ipc/feedback.test.ts | 19 +++++++++++ src/main/ipc/feedback.ts | 5 +++ .../crash-report/CrashReportDialog.tsx | 34 +++++++++++++++++-- 5 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/main/ipc/crash-reporting.test.ts b/src/main/ipc/crash-reporting.test.ts index 7166d5238..049f6e08a 100644 --- a/src/main/ipc/crash-reporting.test.ts +++ b/src/main/ipc/crash-reporting.test.ts @@ -129,6 +129,7 @@ describe('registerCrashReportingHandlers', () => { expect(submitFeedbackMock).toHaveBeenCalledWith( expect.objectContaining({ feedback: expect.stringContaining('[Crash Report]'), + submissionType: 'crash', githubLogin: 'me', githubEmail: 'me@example.com' }) diff --git a/src/main/ipc/crash-reporting.ts b/src/main/ipc/crash-reporting.ts index 16041ead2..4d16e1acb 100644 --- a/src/main/ipc/crash-reporting.ts +++ b/src/main/ipc/crash-reporting.ts @@ -84,6 +84,7 @@ export function registerCrashReportingHandlers(store: CrashReportStore): void { try { const result = await submitFeedback({ feedback: formatCrashReportText(report, args.notes), + submissionType: 'crash', submitAnonymously: args.submitAnonymously, githubLogin: args.githubLogin, githubEmail: args.githubEmail diff --git a/src/main/ipc/feedback.test.ts b/src/main/ipc/feedback.test.ts index f2b06aff2..6f7d8818d 100644 --- a/src/main/ipc/feedback.test.ts +++ b/src/main/ipc/feedback.test.ts @@ -40,6 +40,7 @@ describe('submitFeedback', () => { const body = postedBody() expect(body).toMatchObject({ feedback: 'private bug report', + submissionType: 'feedback', githubLogin: null, githubEmail: null, appVersion: '1.2.3-test' @@ -60,9 +61,27 @@ describe('submitFeedback', () => { const body = postedBody() expect(body).toMatchObject({ feedback: 'public bug report', + submissionType: 'feedback', githubLogin: 'trusted-user', githubEmail: 'trusted@example.com', appVersion: '1.2.3-test' }) }) + + it('marks crash submissions so the backend can route them separately', async () => { + await submitFeedback({ + feedback: '[Crash Report]', + submissionType: 'crash', + submitAnonymously: false, + githubLogin: 'trusted-user', + githubEmail: null + }) + + expect(postedBody()).toMatchObject({ + feedback: '[Crash Report]', + submissionType: 'crash', + githubLogin: 'trusted-user', + githubEmail: null + }) + }) }) diff --git a/src/main/ipc/feedback.ts b/src/main/ipc/feedback.ts index 5b6895c7c..c889ff0de 100644 --- a/src/main/ipc/feedback.ts +++ b/src/main/ipc/feedback.ts @@ -9,15 +9,19 @@ import { app, ipcMain, net } from 'electron' const FEEDBACK_API_URL = 'https://api.onorca.dev/v1/feedback' const FEEDBACK_API_FALLBACK_URL = 'https://www.onorca.dev/v1/feedback' +export type FeedbackSubmissionType = 'feedback' | 'crash' + export type FeedbackSubmitArgs = { feedback: string submitAnonymously?: boolean githubLogin: string | null githubEmail: string | null + submissionType?: FeedbackSubmissionType } type FeedbackSubmitBody = { feedback: string + submissionType: FeedbackSubmissionType githubLogin: string | null githubEmail: string | null appVersion: string @@ -44,6 +48,7 @@ function buildSubmitBody(args: FeedbackSubmitArgs): FeedbackSubmitBody { // stale renderer state or future identity-shaped fields cannot leak upstream. return { feedback: args.feedback, + submissionType: args.submissionType ?? 'feedback', ...identity, appVersion: app.getVersion(), platform: process.platform, diff --git a/src/renderer/src/components/crash-report/CrashReportDialog.tsx b/src/renderer/src/components/crash-report/CrashReportDialog.tsx index dac92d4a0..11482068f 100644 --- a/src/renderer/src/components/crash-report/CrashReportDialog.tsx +++ b/src/renderer/src/components/crash-report/CrashReportDialog.tsx @@ -11,6 +11,7 @@ import { DialogTitle } from '@/components/ui/dialog' import { formatCrashReportText, type CrashReportRecord } from '../../../../shared/crash-reporting' +import type { GitHubViewer } from '../../../../shared/types' function formatSummary(report: CrashReportRecord): string { return `${report.processType} ${report.reason}${ @@ -25,6 +26,7 @@ export function CrashReportDialog(): React.JSX.Element { const [notes, setNotes] = useState('') const [loading, setLoading] = useState(false) const [submitting, setSubmitting] = useState(false) + const [viewer, setViewer] = useState(null) const diagnosticText = useMemo( () => (report ? formatCrashReportText(report, notes) : ''), [notes, report] @@ -70,6 +72,32 @@ export function CrashReportDialog(): React.JSX.Element { }) }, []) + useEffect(() => { + if (!open) { + setViewer(null) + return + } + + let cancelled = false + void window.api.gh + .viewer() + .then((nextViewer) => { + if (!cancelled) { + setViewer(nextViewer) + } + }) + .catch((error) => { + if (!cancelled) { + setViewer(null) + console.error('Failed to load GitHub viewer for crash report:', error) + } + }) + + return () => { + cancelled = true + } + }, [open]) + const handleCopy = async (): Promise => { const result = await window.api.crashReports.copyLatestDiagnostics( report ? { reportId: report.id, notes } : {} @@ -102,8 +130,10 @@ export function CrashReportDialog(): React.JSX.Element { const result = await window.api.crashReports.submit({ reportId: report.id, notes, - submitAnonymously: true, - githubLogin: null, + // Why: crash reporting must degrade to anonymous if gh is unavailable; + // identity lookup is best-effort and never blocks report creation. + submitAnonymously: !viewer, + githubLogin: viewer?.login ?? null, githubEmail: null }) if (!result.ok) {