diff --git a/src/renderer/modal-widgets/SendFeedback.tsx b/src/renderer/modal-widgets/SendFeedback.tsx index de06c9e24..e749db93a 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,39 @@ class ReportIssue extends React.PureComponent { } renderFailed() { - const { errorMessage } = this.state; + const { error, isServiceOffline, copiedToClipboard } = this.state; + const textToDisplay = this.getReportText(); + const errorMessage = error ? getErrorMessage(error) : null; 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 +368,38 @@ class ReportIssue extends React.PureComponent { ); } + getReportText = (): string => { + 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, + 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 +420,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 +430,68 @@ 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, + error: fetchErr, + }); + return; + } + + if ([404, 502, 503, 504].includes(body.status)) { + this.setState({ + stage: ReportStage.Failed, + isServiceOffline: true, + error: `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, + error: `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, + error: 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 +499,18 @@ ${log} return; } - throw new Error( - `Creating report failed: ${JSON.stringify(res, null, 2)}` - ); + this.setState({ + stage: ReportStage.Failed, + isServiceOffline: false, + error: + (res && res.error) || + `Creating report failed: ${JSON.stringify(resData, null, 2)}`, + }); } catch (e) { this.setState({ stage: ReportStage.Failed, - errorMessage: getErrorStack(e), + isServiceOffline: false, + error: e, }); } }); @@ -497,7 +597,9 @@ interface State { includeSystemInfo: boolean; tabIndex: number; reportURL?: string; - errorMessage?: string; + error?: unknown; + 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",