diff --git a/cockatrice/src/client/crash/crash_reporter.cpp b/cockatrice/src/client/crash/crash_reporter.cpp index 07a0389c7..cba84994c 100644 --- a/cockatrice/src/client/crash/crash_reporter.cpp +++ b/cockatrice/src/client/crash/crash_reporter.cpp @@ -614,3 +614,19 @@ QString CrashReporter::redactCockatriceUrls(const QString &line) redacted.append(line.mid(pos)); return redacted; } + +void CrashReporter::addBreadcrumb(const char *category, const char *message) +{ +#if COCKATRICE_WITH_CRASH_REPORTING + if (category == nullptr || message == nullptr) { + return; + } + sentry_value_t crumb = sentry_value_new_breadcrumb("default", message); + sentry_value_set_by_key(crumb, "category", sentry_value_new_string(category)); + sentry_value_set_by_key(crumb, "level", sentry_value_new_string("info")); + sentry_add_breadcrumb(crumb); +#else + Q_UNUSED(category); + Q_UNUSED(message); +#endif +} diff --git a/cockatrice/src/client/crash/crash_reporter.h b/cockatrice/src/client/crash/crash_reporter.h index aecc6ade3..f06103188 100644 --- a/cockatrice/src/client/crash/crash_reporter.h +++ b/cockatrice/src/client/crash/crash_reporter.h @@ -167,6 +167,17 @@ public: static QString sanitizeLogLine(const QString &line); static QString redactCockatriceUrls(const QString &line); + /** + * @brief Record a fixed-text lifecycle breadcrumb on the active Sentry scope. + * + * Native crashes whose stack walk loses the in-app frames (e.g. a QMap lookup + * that dies inside Qt's string comparison) otherwise arrive with no hint of + * what the client was doing. Only pass compile-time string literals: no user + * names, card names, paths or URLs, so no personal data can leak through this + * channel. Thread-safe; a no-op in OFF builds or before init(). + */ + static void addBreadcrumb(const char *category, const char *message); + private: bool ready = false; QString sentryDatabasePath; diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index 943b4d5de..b7f4614c9 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -1,5 +1,6 @@ #include "user_list_widget.h" +#include "../../../../client/crash/crash_reporter.h" #include "../../../../client/settings/cache_settings.h" #include "../../interface/pixel_map_generator.h" #include "../../interface/theme_manager.h" @@ -1221,6 +1222,7 @@ void UserListWidget::processUserInfo(Section section, const ServerInfo_User &use bool UserListWidget::deleteUser(const QString &userName) { + CrashReporter::addBreadcrumb("users", "user list row removed"); if (sectioned) { // The user may own several rows (one per section). Drop them all. bool removed = false; diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 7647b8ca8..c5cb7fda6 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -1,5 +1,6 @@ #include "tab_supervisor.h" +#include "../../../client/crash/crash_reporter.h" #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" #include "../../intents/contexts/context_connect_to_server.h" @@ -593,6 +594,7 @@ void TabSupervisor::startLocal(const QList &_clients) */ void TabSupervisor::stop() { + CrashReporter::addBreadcrumb("tabs", "tab supervisor stopping"); if ((!client) && localClients.isEmpty()) { return; } @@ -1003,6 +1005,7 @@ void TabSupervisor::updateLatencyTooltip(const LatencyTracker::Stats &stats) void TabSupervisor::gameJoined(const Event_GameJoined &event) { + CrashReporter::addBreadcrumb("tabs", "game joined"); QMap roomGameTypes; TabRoom *room = roomTabs.value(event.game_info().room_id()); if (room) { @@ -1055,6 +1058,7 @@ void TabSupervisor::gameLeft(TabGame *tab) void TabSupervisor::addRoomTab(const ServerInfo_Room &info, bool setCurrent) { + CrashReporter::addBreadcrumb("tabs", "room tab added"); auto *tab = new TabRoom(this, client, userInfo, info); connect(tab, &TabRoom::maximizeClient, this, &TabSupervisor::maximizeMainWindow); connect(tab, &TabRoom::roomClosing, this, &TabSupervisor::roomLeft); @@ -1472,6 +1476,7 @@ void TabSupervisor::actShowPopup(const QString &message) void TabSupervisor::processUserLeft(const QString &userName) { + CrashReporter::addBreadcrumb("users", "user left"); TabMessage *tab = messageTabs.value(userName); if (tab) { tab->processUserLeft(); diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index b4dc0610f..e82867f3f 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -427,6 +427,23 @@ int main(int argc, char *argv[]) CardDatabaseManager::setCardDatabasePathProvider(&SettingsCache::instance()); CardDatabaseManager::setCardSetPriorityController(&SettingsCache::instance().cardDatabase()); + // Lifecycle breadcrumbs for native crash reports (see COCKATRICE-DESKTOP-1R): the live + // card maps are replaced wholesale on reset, so knowing whether a crash followed a reload + // narrows down which QMap was being read. Direct connections: the SDK call is thread-safe + // and loading signals may come from a QtConcurrent worker. + { + CardDatabase *crumbDb = CardDatabaseManager::getInstance(); + QObject::connect( + crumbDb, &CardDatabase::cardDatabaseReset, crumbDb, + [] { CrashReporter::addBreadcrumb("card_db", "card database snapshot swapped in"); }, Qt::DirectConnection); + QObject::connect( + crumbDb, &CardDatabase::cardDatabaseLoadingFinished, crumbDb, + [] { CrashReporter::addBreadcrumb("card_db", "card database loading finished"); }, Qt::DirectConnection); + QObject::connect( + crumbDb, &CardDatabase::cardDatabaseLoadingFailed, crumbDb, + [] { CrashReporter::addBreadcrumb("card_db", "card database loading failed"); }, Qt::DirectConnection); + } + qCInfo(MainLog) << "Starting main program"; // Front-load the card database before constructing the main window. The diff --git a/tests/crash_reporter_test.cpp b/tests/crash_reporter_test.cpp index e44a34a60..208812a4b 100644 --- a/tests/crash_reporter_test.cpp +++ b/tests/crash_reporter_test.cpp @@ -418,6 +418,16 @@ TEST(CrashReporterTest, BeginSessionEndSessionRoundTripReportsClean) reporter.endSession(); } +TEST(CrashReporterTest, AddBreadcrumbIsSafeWithoutInitAndWithNullArguments) +{ + // Breadcrumbs are emitted from GUI slots and QtConcurrent workers regardless of whether + // the SDK was initialised (empty DSN, Never consent, OFF builds); none of these may crash. + CrashReporter::addBreadcrumb("card_db", "card database snapshot swapped in"); + CrashReporter::addBreadcrumb(nullptr, "message"); + CrashReporter::addBreadcrumb("category", nullptr); + SUCCEED(); +} + } // namespace int main(int argc, char **argv)