diff --git a/src/main/ipc/feedback.test.ts b/src/main/ipc/feedback.test.ts index ebc86399fdd..9753a1a8cf2 100644 --- a/src/main/ipc/feedback.test.ts +++ b/src/main/ipc/feedback.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { fetchMock, handlers } = vi.hoisted(() => ({ fetchMock: vi.fn(), @@ -29,11 +29,16 @@ function postedBody(): Record { describe('submitFeedback', () => { beforeEach(() => { + vi.useRealTimers() handlers.clear() fetchMock.mockReset() fetchMock.mockResolvedValue(okResponse()) }) + afterEach(() => { + vi.useRealTimers() + }) + it('strips GitHub identity and anonymous contact fields when submitted anonymously', async () => { const anonymousArgs = { feedback: 'private bug report', @@ -94,6 +99,29 @@ describe('submitFeedback', () => { }) }) + it('falls back when the primary feedback request stalls', async () => { + vi.useFakeTimers() + fetchMock.mockImplementation((url: string, init?: RequestInit) => { + if (url.includes('api.onorca.dev')) { + return new Promise((_resolve, reject) => { + init?.signal?.addEventListener('abort', () => reject(new Error('request aborted'))) + }) + } + return Promise.resolve(okResponse()) + }) + + const result = submitFeedback({ + feedback: 'stalled primary', + submitAnonymously: false, + githubLogin: 'trusted-user', + githubEmail: 'trusted@example.com' + }) + await vi.advanceTimersByTimeAsync(10_000) + + await expect(Promise.race([result, Promise.resolve('pending')])).resolves.toEqual({ ok: true }) + expect(fetchMock).toHaveBeenCalledTimes(2) + }) + it('forces renderer IPC submissions onto the feedback lane', async () => { registerFeedbackHandlers() await handlers.get('feedback:submit')?.(null, { diff --git a/src/main/ipc/feedback.ts b/src/main/ipc/feedback.ts index f062b5b650f..b86ca20586b 100644 --- a/src/main/ipc/feedback.ts +++ b/src/main/ipc/feedback.ts @@ -8,6 +8,7 @@ import { app, ipcMain, net } from 'electron' // same pattern used by updater-changelog.ts and updater-nudge.ts. const FEEDBACK_API_URL = 'https://api.onorca.dev/v1/feedback' const FEEDBACK_API_FALLBACK_URL = 'https://www.onorca.dev/v1/feedback' +const FEEDBACK_REQUEST_TIMEOUT_MS = 10_000 export type FeedbackSubmissionType = 'feedback' | 'crash' @@ -61,11 +62,20 @@ function buildSubmitBody(args: InternalFeedbackSubmitArgs): FeedbackSubmitBody { } async function postFeedback(url: string, body: FeedbackSubmitBody): Promise { - return net.fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body) - }) + const controller = new AbortController() + // Why: a silent feedback endpoint should not leave IPC or crash-report + // submission flows pending forever. + const timeout = setTimeout(() => controller.abort(), FEEDBACK_REQUEST_TIMEOUT_MS) + try { + return await net.fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + signal: controller.signal + }) + } finally { + clearTimeout(timeout) + } } export async function submitFeedback(