[Crash] Add lifecycle breadcrumbs for unattributable native crashes

Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
This commit is contained in:
Lukas Brübach 2026-11-08 21:44:02 +00:00
parent 40ebd22549
commit 59db732abd
6 changed files with 61 additions and 0 deletions

View file

@ -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
}

View file

@ -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;

View file

@ -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;

View file

@ -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<AbstractClient *> &_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<int, QString> 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();

View file

@ -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

View file

@ -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)