#include "dlg_crash_report.h" #include "../../../client/crash/crash_reporter.h" #include "../../../client/settings/cache_settings.h" #include "../../logger.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include DlgCrashReport::DlgCrashReport(QWidget *parent, bool _realSending, CrashReporter *_reporter) : QDialog(parent), realSending(_realSending), reporter(_reporter) { infoLabel = new QLabel(this); infoLabel->setWordWrap(true); accountLabel = new QLabel(this); accountLabel->setWordWrap(true); previewArea = new QPlainTextEdit(this); previewArea->setReadOnly(true); previewArea->setPlainText(buildRedactedPreview()); rememberBox = new QCheckBox(this); sendButton = new QPushButton(this); sendButton->setDefault(true); connect(sendButton, &QPushButton::clicked, this, &DlgCrashReport::actSend); dontSendButton = new QPushButton(this); dontSendButton->setAutoDefault(false); connect(dontSendButton, &QPushButton::clicked, this, &DlgCrashReport::actDontSend); auto *buttonLayout = new QHBoxLayout; buttonLayout->addWidget(rememberBox); buttonLayout->addStretch(); buttonLayout->addWidget(dontSendButton); buttonLayout->addWidget(sendButton); auto *mainLayout = new QVBoxLayout; mainLayout->addWidget(infoLabel); mainLayout->addWidget(accountLabel); mainLayout->addWidget(previewArea); mainLayout->addLayout(buttonLayout); setLayout(mainLayout); resize(700, 500); retranslateUi(); } void DlgCrashReport::changeEvent(QEvent *event) { if (event->type() == QEvent::LanguageChange) { retranslateUi(); } QDialog::changeEvent(event); } void DlgCrashReport::retranslateUi() { setWindowTitle(tr("Send crash report")); if (realSending) { // The preview below prefers the crashed session's persisted log tail // (Logger::getPreviousSessionTail), falling back to this restart's in-memory buffer only // when no previous session was saved. The native crash data (minidump + version/OS // metadata cached in the Sentry DB dir by breakpad) is flushed separately on // giveConsent(); the preview is attached as crash-log.txt. infoLabel->setText( tr("Cockatrice closed unexpectedly. Sending uploads the stored crash data from the previous " "run (crash dump plus app version and operating system info) together with the redacted " "log excerpt shown below (last lines from the crashed session, attached as crash-log.txt). " "If no log from the crashed session was saved, this restart's early logs are shown instead.")); } else { infoLabel->setText( tr("Cockatrice closed unexpectedly (mock preview, nothing will be uploaded). A real report " "would contain the stored crash data from the previous run plus the redacted log excerpt " "shown below (last lines from the crashed session). If no log from the crashed session was " "saved, this restart's early logs are shown instead. A mock file is saved locally instead.")); } accountLabel->setText(tr("No account needed, and no contact info is collected. Login links " "(cockatrice://...) are scrubbed before sending.")); rememberBox->setText(tr("Remember my choice")); sendButton->setText(tr("Send")); dontSendButton->setText(tr("Don't send")); } QString DlgCrashReport::buildRedactedPreview() { Logger &logger = Logger::getInstance(); QStringList header; header.append(logger.getClientVersion()); header.append(logger.getSystemArchitecture()); header.append(logger.getSystemLocale()); header.append(logger.getClientInstallInfo()); header.append(QStringLiteral("---")); QList logLines = logger.getPreviousSessionTail(); if (logLines.isEmpty()) { logLines = logger.getLogBuffer(); } const int previewLines = 60; int start = logLines.size() > previewLines ? logLines.size() - previewLines : 0; QStringList lines = header; for (int i = start; i < logLines.size(); ++i) { lines.append(CrashReporter::redactCockatriceUrls(CrashReporter::sanitizeLogLine(logLines.at(i)))); } return lines.join(QStringLiteral("\n")); } QString DlgCrashReport::writeMockReport(const QString &previewText) { Logger &logger = Logger::getInstance(); QJsonObject report; report[QStringLiteral("mock")] = true; report[QStringLiteral("timestamp")] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); report[QStringLiteral("clientVersion")] = logger.getClientVersion(); report[QStringLiteral("systemArchitecture")] = logger.getSystemArchitecture(); report[QStringLiteral("systemLocale")] = logger.getSystemLocale(); report[QStringLiteral("installInfo")] = logger.getClientInstallInfo(); report[QStringLiteral("logPreview")] = previewText; QString fileName = QStringLiteral("crash-report-mock-%1.json") .arg(QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd-HHmmss"))); QString filePath = QDir::tempPath() + QStringLiteral("/") + fileName; QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return QString(); } file.write(QJsonDocument(report).toJson()); file.close(); return filePath; } void DlgCrashReport::actSend() { if (rememberBox->isChecked()) { SettingsCache::instance().personal().setCrashReportingMode(PersonalSettings::CrashReportingMode::AlwaysSend); } const QString preview = previewArea->toPlainText(); if (realSending && reporter != nullptr) { reporter->giveConsent(); if (reporter->sendReport(preview)) { accept(); return; } qCWarning(CrashReportLog) << "Failed to send crash report"; // Outcome mapping for the failure box: Save = store locally and close // as accepted (the follow-up info box shows the path, so the user // knows it was NOT sent); Discard = revoke consent and close as // rejected (dropped); Cancel/X = back to the dialog, neither sent nor // dropped. The Remember checkbox governs only the original // Send/Don't-send decision, so the Discard path below revokes and // closes via QDialog::reject() without touching the setting (calling // our reject() override here would wrongly persist NeverSend over the // AlwaysSend stored above when Remember was checked). QMessageBox failureBox(QMessageBox::Warning, tr("Send crash report"), tr("Sending the crash report failed. Save the report locally instead?"), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, this); failureBox.setDefaultButton(QMessageBox::Save); failureBox.setEscapeButton(QMessageBox::Cancel); const QMessageBox::StandardButton choice = static_cast(failureBox.exec()); if (choice == QMessageBox::Save) { const QString savedPath = writeMockReport(preview); if (savedPath.isEmpty()) { QMessageBox::warning(this, tr("Send crash report"), tr("Could not save the crash report locally.")); } else { QMessageBox::information(this, tr("Send crash report"), tr("Crash report saved locally to:\n%1").arg(savedPath)); } accept(); return; } if (choice == QMessageBox::Discard) { reporter->revokeConsent(); QDialog::reject(); return; } return; } const QString savedPath = writeMockReport(preview); if (savedPath.isEmpty()) { qCWarning(CrashReportLog) << "Could not save the mock crash report locally"; } else { qCInfo(CrashReportLog) << "Mock crash report saved locally to" << savedPath; } accept(); } void DlgCrashReport::actDontSend() { reject(); } void DlgCrashReport::reject() { if (rememberBox->isChecked()) { SettingsCache::instance().personal().setCrashReportingMode(PersonalSettings::CrashReportingMode::NeverSend); } if (realSending && reporter != nullptr) { reporter->revokeConsent(); } QDialog::reject(); }