From dd5e15061c1759f0ec2d0637c8ccc6809cb76a45 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 15 Apr 2026 13:35:38 +0200 Subject: [PATCH] feat(crashpad): consent-aware offline caching Wires UploadsPaused alongside UploadsEnabled from the consent handler: when consent is revoked and cache_keep or http_retry is enabled, pause crashpad uploads so pending reports are retained rather than skipped to completed. On consent transitions that re-enable uploads, call RequestRetry() so the handler wakes immediately instead of waiting for its next periodic scan. The crashpad submodule bump pulls in the matching UploadsPaused setting and RequestRetry IPC additions on the handler side. An integration test exercises the full flow: a crash captured while consent is revoked stays in crashpad's pending queue (no upload, not moved to completed), and giving consent in a subsequent run drains the pending report to the server. Co-Authored-By: Claude Opus 4.6 (1M context) --- external/crashpad | 2 +- src/backends/sentry_backend_crashpad.cpp | 14 +++++- tests/test_integration_crashpad.py | 56 ++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/external/crashpad b/external/crashpad index 5e6960d833..38617eb5a0 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit 5e6960d83323f20276b911c354fd1b540e39106c +Subproject commit 38617eb5a0799acade5dc4312f206e0e43642566 diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index cd965bf237..815acd5a4e 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -154,7 +154,19 @@ crashpad_backend_user_consent_changed(sentry_backend_t *backend) if (!data->db || !data->db->GetSettings()) { return; } - data->db->GetSettings()->SetUploadsEnabled(!sentry__should_skip_upload()); + const bool enabled = !sentry__should_skip_upload(); + bool paused = false; + if (!enabled) { + SENTRY_WITH_OPTIONS (options) { + paused = options->cache_keep || options->http_retry; + } + } + auto *settings = data->db->GetSettings(); + settings->SetUploadsEnabled(enabled); + settings->SetUploadsPaused(paused); + if (enabled && data->client) { + data->client->RequestRetry(); + } } #ifdef SENTRY_PLATFORM_WINDOWS diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index adaf3ccff7..1c4c61e4c8 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -866,6 +866,62 @@ def test_crashpad_cache_keep(cmake, httpserver, cache_keep): assert cache_files[0].stem == dmp_files[0].stem +def test_crashpad_cache_consent(cmake, httpserver): + """Crash while consent is revoked + cache_keep: the minidump stays in + crashpad's pending queue (no upload, not moved to completed). Once + consent is given, RequestRetry() wakes the handler and the minidump + is uploaded.""" + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"}) + db_path = tmp_path.joinpath(".sentry-native") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + # 1) Crash with consent revoked. No upload should happen. + run( + tmp_path, + "sentry_example", + [ + "log", + "require-user-consent", + "user-consent-revoke", + "cache-keep", + "crash", + ], + expect_failure=True, + env=env, + ) + + # Give the (out-of-process) handler a moment to write the report. + time.sleep(1) + assert len(httpserver.log) == 0 + + # On Linux/macOS the database lays out pending/ and completed/ as + # separate directories; on Windows a single reports/ dir tracks state + # in metadata, so only assert the dir-based invariants off-Windows. + if sys.platform != "win32": + pending_dir = db_path.joinpath("pending") + completed_dir = db_path.joinpath("completed") + assert pending_dir.exists() + assert len(list(pending_dir.glob("*.dmp"))) >= 1 + assert not completed_dir.exists() or not any(completed_dir.glob("*.dmp")) + + # 2) Give consent. The handler should wake via RequestRetry() and upload. + httpserver.expect_oneshot_request("/api/123456/minidump/").respond_with_data("OK") + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + [ + "log", + "require-user-consent", + "user-consent-give", + "cache-keep", + "no-setup", + ], + env=env, + ) + assert waiting.result + + def test_crashpad_cache_max_size(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"}) cache_dir = tmp_path.joinpath(".sentry-native/cache")