From 86d64a62a4176bd68cc09c8cd123f1001ef1a3d0 Mon Sep 17 00:00:00 2001 From: Ricardo Romero Date: Fri, 9 Oct 2026 20:15:09 -0300 Subject: [PATCH 1/2] fallback to manual github issue reporting when crash server is unavailable --- src/renderer/modal-widgets/SendFeedback.tsx | 186 ++++++++++++++---- .../modal-widgets/send-feedback-helpers.ts | 54 +++++ src/static/locales/en.json | 5 +- src/static/locales/es.json | 3 + src/static/locales/pt_BR.json | 3 + 5 files changed, 210 insertions(+), 41 deletions(-) create mode 100644 src/renderer/modal-widgets/send-feedback-helpers.ts diff --git a/src/renderer/modal-widgets/SendFeedback.tsx b/src/renderer/modal-widgets/SendFeedback.tsx index de06c9e24..d46599d5d 100644 --- a/src/renderer/modal-widgets/SendFeedback.tsx +++ b/src/renderer/modal-widgets/SendFeedback.tsx @@ -1,5 +1,6 @@ -import { getErrorStack } from "common/butlerd/errors"; +import { getErrorMessage, getErrorStack } from "common/butlerd/errors"; import { actions } from "common/actions"; +import urls from "common/constants/urls"; import { fillShape } from "common/format/shape"; import { ModalWidgetProps } from "common/modals"; import { SendFeedbackParams, SendFeedbackResponse } from "common/modals/types"; @@ -21,6 +22,10 @@ import { injectIntl } from "renderer/hocs/injectIntl"; import { T } from "renderer/t"; import { ModalButtons, ModalButtonSpacer } from "renderer/basics/modal-styles"; import { sysinfo } from "renderer/bridge"; +import { + buildReportText, + isDaleResponse, +} from "renderer/modal-widgets/send-feedback-helpers"; enum ReportStage { Filling, @@ -148,7 +153,7 @@ class ReportIssue extends React.PureComponent { } catch (e) { this.setState({ system: { - state: `Could not gather system information: ${getErrorStack(e)}`, + state: `Could not gather system information: ${getErrorMessage(e)}`, }, }); } @@ -322,14 +327,38 @@ class ReportIssue extends React.PureComponent { } renderFailed() { - const { errorMessage } = this.state; + const { errorMessage, isServiceOffline, copiedToClipboard } = this.state; + const textToDisplay = this.getReportText(); return ( -

{T(["send_feedback.error.intro"])}

- {errorMessage} +

+ {isServiceOffline + ? T(["send_feedback.error.service_offline"]) + : T(["send_feedback.error.intro"])} +

+ {errorMessage ? ( +

+ {errorMessage} +

+ ) : null} + {textToDisplay} + + + + @@ -338,6 +367,36 @@ class ReportIssue extends React.PureComponent { ); } + getReportText = (): string => { + const { system, includeSystemInfo, message, errorStack } = this.state; + const { log } = this.props.modal.widgetParams; + return buildReportText({ + system, + includeSystemInfo, + message, + log, + errorStack, + }); + }; + + onCopyToClipboard = () => { + const { dispatch } = this.props; + dispatch(actions.copyToClipboard({ text: this.getReportText() })); + this.setState({ copiedToClipboard: true }); + setTimeout(() => { + this.setState({ copiedToClipboard: false }); + }, 3000); + }; + + onOpenGitHubIssue = () => { + const { dispatch } = this.props; + dispatch( + actions.openInExternalBrowser({ + url: `${urls.itchRepo}/issues/new`, + }) + ); + }; + onViewReport = () => { const { dispatch } = this.props; const { reportURL } = this.state; @@ -358,8 +417,9 @@ class ReportIssue extends React.PureComponent { doAsync(async () => { const daleURL = "https://dale.itch.zone"; - const { system, includeSystemInfo, message } = this.state; - const { log } = this.props.modal.widgetParams; + const { system, includeSystemInfo } = this.state; + const reportText = this.getReportText(); + try { const params = new URLSearchParams(); if (includeSystemInfo) { @@ -367,36 +427,73 @@ class ReportIssue extends React.PureComponent { } else { params.set("system", "(redacted)"); } - params.set( - "log", - ` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -> Message -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -${message} - -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -> Log -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -${log} -` - ); - const body = await fetch(daleURL, { - method: "post", - // fetch *can* take a URLSearchParams, thank you very much - body: params as any, - }); - if (body.status != 200) { - throw new Error( - `Post ${daleURL}: got HTTP ${ - body.status - }. Body: ${await body.text()}` - ); + params.set("log", reportText); + + let body: Response; + try { + body = await fetch(daleURL, { + method: "post", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params.toString(), + }); + } catch (fetchErr) { + this.setState({ + stage: ReportStage.Failed, + isServiceOffline: true, + errorMessage: + "Could not reach crash report server (dale.itch.zone). The server is offline or unreachable.", + errorStack: getErrorStack(fetchErr), + }); + return; + } + + if ([404, 502, 503, 504].includes(body.status)) { + this.setState({ + stage: ReportStage.Failed, + isServiceOffline: true, + errorMessage: `The crash report server (dale.itch.zone) is currently offline (HTTP ${body.status}).`, + }); + return; + } + + if (body.status !== 200) { + let bodyText: string; + try { + bodyText = await body.text(); + } catch (readErr) { + bodyText = `(failed to read response body: ${getErrorMessage( + readErr + )})`; + } + this.setState({ + stage: ReportStage.Failed, + isServiceOffline: false, + errorMessage: `Server returned HTTP ${body.status}${ + bodyText ? `: ${bodyText}` : "" + }`, + }); + return; } - const res = await body.json(); - if (res.success) { + + let resData: unknown; + try { + resData = await body.json(); + } catch (jsonErr) { + this.setState({ + stage: ReportStage.Failed, + isServiceOffline: false, + errorMessage: `Failed to parse response from server: ${getErrorMessage( + jsonErr + )}`, + errorStack: getErrorStack(jsonErr), + }); + return; + } + + const res = isDaleResponse(resData) ? resData : null; + if (res && res.success && res.id) { this.setState({ stage: ReportStage.Done, reportURL: `${daleURL}/${res.id}`, @@ -404,13 +501,19 @@ ${log} return; } - throw new Error( - `Creating report failed: ${JSON.stringify(res, null, 2)}` - ); + this.setState({ + stage: ReportStage.Failed, + isServiceOffline: false, + errorMessage: + (res && res.error) || + `Creating report failed: ${JSON.stringify(resData, null, 2)}`, + }); } catch (e) { this.setState({ stage: ReportStage.Failed, - errorMessage: getErrorStack(e), + isServiceOffline: false, + errorMessage: getErrorMessage(e), + errorStack: getErrorStack(e), }); } }); @@ -498,6 +601,9 @@ interface State { tabIndex: number; reportURL?: string; errorMessage?: string; + errorStack?: string; + isServiceOffline?: boolean; + copiedToClipboard?: boolean; } export default injectIntl( diff --git a/src/renderer/modal-widgets/send-feedback-helpers.ts b/src/renderer/modal-widgets/send-feedback-helpers.ts new file mode 100644 index 000000000..6eb419c7e --- /dev/null +++ b/src/renderer/modal-widgets/send-feedback-helpers.ts @@ -0,0 +1,54 @@ +export interface BuildReportTextParams { + message: string; + system: unknown; + includeSystemInfo: boolean; + log?: string; + errorStack?: string; +} + +export function buildReportText(params: BuildReportTextParams): string { + const { system, includeSystemInfo, message, log, errorStack } = params; + + const sections = [ + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", + "> Message", + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", + "", + message, + "", + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", + "> System Information", + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", + "", + includeSystemInfo ? JSON.stringify(system, null, 2) : "(redacted)", + "", + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", + "> Log", + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", + "", + log || "(no log output available)", + ]; + + if (errorStack) { + sections.push( + "", + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", + "> Error Stack Trace", + "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", + "", + errorStack + ); + } + + return sections.join("\n"); +} + +export interface DaleResponse { + success?: boolean; + id?: string | number; + error?: string; +} + +export function isDaleResponse(data: unknown): data is DaleResponse { + return typeof data === "object" && data !== null; +} diff --git a/src/static/locales/en.json b/src/static/locales/en.json index f41d1c4c5..66d957b6c 100644 --- a/src/static/locales/en.json +++ b/src/static/locales/en.json @@ -674,5 +674,8 @@ "browser.stop": "Stop loading", "browser.reload": "Reload page", "browser.more_menu": "More options", - "game.manage": "Manage game" + "game.manage": "Manage game", + "send_feedback.error.service_offline": "The crash reporting service is currently offline. You can copy your report and file an issue on GitHub instead:", + "send_feedback.error.copy_report": "Copy report to clipboard", + "send_feedback.error.open_github": "File issue on GitHub" } diff --git a/src/static/locales/es.json b/src/static/locales/es.json index 9bb9cc352..f11811e20 100644 --- a/src/static/locales/es.json +++ b/src/static/locales/es.json @@ -360,6 +360,9 @@ "send_feedback.success.thanks": "Gracias por tu comentario", "send_feedback.success.promise": "Será revisado lo más pronto posible, convertido en un reporte ejecutable y esperamos arreglarlo en un próximo lanzamiento.", "send_feedback.error.intro": "Lo sentimos, no pudimos enviar tu reporte.", + "send_feedback.error.service_offline": "El servicio de reporte de errores se encuentra fuera de línea. Puedes copiar tu reporte y reportar el problema en GitHub en su lugar:", + "send_feedback.error.copy_report": "Copiar reporte al portapapeles", + "send_feedback.error.open_github": "Reportar problema en GitHub", "send_feedback.nav.previous": "Previo", "send_feedback.nav.next": "Siguiente", "send_feedback.nav.close": "Cerrar", diff --git a/src/static/locales/pt_BR.json b/src/static/locales/pt_BR.json index 463435dc0..fae8b6fd6 100644 --- a/src/static/locales/pt_BR.json +++ b/src/static/locales/pt_BR.json @@ -367,6 +367,9 @@ "send_feedback.success.thanks": "Obrigado pelo seu comentário", "send_feedback.success.promise": "Ele será revisado assim que nós pudermos, convertendo-o para uma questão acionável e, espera-se, corrigida em uma versão futura.", "send_feedback.error.intro": "Desculpe, não foi possível enviar seu relatório.", + "send_feedback.error.service_offline": "O serviço de envio de relatórios está offline no momento. Você pode copiar seu relatório e reportar o problema no GitHub:", + "send_feedback.error.copy_report": "Copiar relatório para a área de transferência", + "send_feedback.error.open_github": "Reportar problema no GitHub", "send_feedback.nav.previous": "Anterior", "send_feedback.nav.next": "Próximo", "send_feedback.nav.close": "Fechar", From a579ce3e2c7f9a88a3ac9f9ad760daf9fc0b82b4 Mon Sep 17 00:00:00 2001 From: Ricardo Romero Date: Wed, 14 Oct 2026 11:16:34 -0300 Subject: [PATCH 2/2] refactor(SendFeedback): standardize on error in state and drop errorMessage --- src/renderer/modal-widgets/SendFeedback.tsx | 28 +++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/renderer/modal-widgets/SendFeedback.tsx b/src/renderer/modal-widgets/SendFeedback.tsx index d46599d5d..e749db93a 100644 --- a/src/renderer/modal-widgets/SendFeedback.tsx +++ b/src/renderer/modal-widgets/SendFeedback.tsx @@ -327,8 +327,9 @@ class ReportIssue extends React.PureComponent { } renderFailed() { - const { errorMessage, isServiceOffline, copiedToClipboard } = this.state; + const { error, isServiceOffline, copiedToClipboard } = this.state; const textToDisplay = this.getReportText(); + const errorMessage = error ? getErrorMessage(error) : null; return ( @@ -368,8 +369,10 @@ class ReportIssue extends React.PureComponent { } getReportText = (): string => { - const { system, includeSystemInfo, message, errorStack } = this.state; + const { system, includeSystemInfo, message, error } = this.state; const { log } = this.props.modal.widgetParams; + const errorStack = + error instanceof Error && error.stack ? getErrorStack(error) : undefined; return buildReportText({ system, includeSystemInfo, @@ -442,9 +445,7 @@ class ReportIssue extends React.PureComponent { this.setState({ stage: ReportStage.Failed, isServiceOffline: true, - errorMessage: - "Could not reach crash report server (dale.itch.zone). The server is offline or unreachable.", - errorStack: getErrorStack(fetchErr), + error: fetchErr, }); return; } @@ -453,7 +454,7 @@ class ReportIssue extends React.PureComponent { this.setState({ stage: ReportStage.Failed, isServiceOffline: true, - errorMessage: `The crash report server (dale.itch.zone) is currently offline (HTTP ${body.status}).`, + error: `The crash report server (dale.itch.zone) is currently offline (HTTP ${body.status}).`, }); return; } @@ -470,7 +471,7 @@ class ReportIssue extends React.PureComponent { this.setState({ stage: ReportStage.Failed, isServiceOffline: false, - errorMessage: `Server returned HTTP ${body.status}${ + error: `Server returned HTTP ${body.status}${ bodyText ? `: ${bodyText}` : "" }`, }); @@ -484,10 +485,7 @@ class ReportIssue extends React.PureComponent { this.setState({ stage: ReportStage.Failed, isServiceOffline: false, - errorMessage: `Failed to parse response from server: ${getErrorMessage( - jsonErr - )}`, - errorStack: getErrorStack(jsonErr), + error: jsonErr, }); return; } @@ -504,7 +502,7 @@ class ReportIssue extends React.PureComponent { this.setState({ stage: ReportStage.Failed, isServiceOffline: false, - errorMessage: + error: (res && res.error) || `Creating report failed: ${JSON.stringify(resData, null, 2)}`, }); @@ -512,8 +510,7 @@ class ReportIssue extends React.PureComponent { this.setState({ stage: ReportStage.Failed, isServiceOffline: false, - errorMessage: getErrorMessage(e), - errorStack: getErrorStack(e), + error: e, }); } }); @@ -600,8 +597,7 @@ interface State { includeSystemInfo: boolean; tabIndex: number; reportURL?: string; - errorMessage?: string; - errorStack?: string; + error?: unknown; isServiceOffline?: boolean; copiedToClipboard?: boolean; }