From 6452b5339e482bf14e6cee190b9e00e1af0a42a3 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 7 May 2026 15:40:32 +0200 Subject: [PATCH 1/8] feat: Add cache keep modes Add enum values for offline and always-on cache modes while keeping the existing cache_keep setter as the public entry point. Ensure envelopes kept with SENTRY_CACHE_KEEP_ALWAYS use non-retry cache filenames so they are not picked up by retry processing. Co-Authored-By: OpenAI Codex --- examples/example.c | 8 +++- include/sentry.h | 33 ++++++++++++++--- src/backends/native/sentry_crash_context.h | 2 +- src/sentry_core.c | 7 +++- src/sentry_options.c | 10 ++++- src/sentry_options.h | 2 +- src/sentry_retry.c | 26 +++++++++++-- src/transports/sentry_http_transport.c | 10 +++-- tests/test_integration_cache.py | 43 +++++++++++++++++----- tests/test_integration_crashpad.py | 23 ++++++++---- tests/unit/test_retry.c | 43 ++++++++++++++++++++++ tests/unit/tests.inc | 1 + 12 files changed, 173 insertions(+), 35 deletions(-) diff --git a/examples/example.c b/examples/example.c index 54cf5cd96..3a88b8a46 100644 --- a/examples/example.c +++ b/examples/example.c @@ -723,12 +723,18 @@ main(int argc, char **argv) if (has_arg(argc, argv, "require-user-consent")) { sentry_options_set_require_user_consent(options, true); } - if (has_arg(argc, argv, "cache-keep")) { + if (has_arg(argc, argv, "cache-keep") + || has_arg(argc, argv, "cache-keep-always")) { + // true corresponds to SENTRY_CACHE_KEEP_OFFLINE for backwards + // compatibility with older SDK versions that don't have the enum sentry_options_set_cache_keep(options, true); sentry_options_set_cache_max_size(options, 16 * 1024 * 1024); // 16 MB sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60); // 5 days sentry_options_set_cache_max_items(options, 5); } + if (has_arg(argc, argv, "cache-keep-always")) { + sentry_options_set_cache_keep(options, SENTRY_CACHE_KEEP_ALWAYS); + } if (has_arg(argc, argv, "http-retry")) { sentry_options_set_http_retry(options, true); } diff --git a/include/sentry.h b/include/sentry.h index 210381223..a43bbeeb9 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1076,6 +1076,28 @@ typedef enum { SENTRY_CRASH_REPORTING_MODE_NATIVE_WITH_MINIDUMP = 2, } sentry_crash_reporting_mode_t; +/** + * Controls if and when envelopes are kept in the persistent cache. + */ +typedef enum { + /** Do not keep envelopes in the persistent cache. */ + SENTRY_CACHE_KEEP_NONE = 0, + + /** + * Envelopes that cannot be uploaded immediately are written to a `cache/` + * subdirectory within the database directory. This includes network + * failures and envelopes captured while user consent is revoked. + */ + SENTRY_CACHE_KEEP_OFFLINE = 1, + + /** + * Envelopes are written to the cache regardless of the upload result. Kept + * entries use non-retry filenames and are not picked up by the + * retry queue. + */ + SENTRY_CACHE_KEEP_ALWAYS = 2, +} sentry_cache_keep_t; + /** * Creates a new options struct. * Can be freed with `sentry_options_free`. @@ -1509,12 +1531,11 @@ SENTRY_API int sentry_options_get_symbolize_stacktraces( const sentry_options_t *opts); /** - * Enables or disables storing envelopes that fail to send in a persistent - * cache. + * Configures if and when envelopes are kept in a persistent cache. * - * When enabled, envelopes that fail to send are written to a `cache/` - * subdirectory within the database directory. The cache is cleared on startup - * based on the cache_max_items, cache_max_size, and cache_max_age options. + * Kept envelopes are written to a `cache/` subdirectory within the database + * directory. The cache is cleared on startup based on the cache_max_items, + * cache_max_size, and cache_max_age options. * * When combined with `sentry_options_set_require_user_consent`, envelopes * captured while consent is revoked are also written to the cache. With @@ -1522,7 +1543,7 @@ SENTRY_API int sentry_options_get_symbolize_stacktraces( * * Only applicable for HTTP transports. * - * Disabled by default. + * `SENTRY_CACHE_KEEP_NONE` by default. */ SENTRY_API void sentry_options_set_cache_keep( sentry_options_t *opts, int enabled); diff --git a/src/backends/native/sentry_crash_context.h b/src/backends/native/sentry_crash_context.h index da386da9a..be17c298a 100644 --- a/src/backends/native/sentry_crash_context.h +++ b/src/backends/native/sentry_crash_context.h @@ -270,7 +270,7 @@ typedef struct { int crash_reporting_mode; // sentry_crash_reporting_mode_t bool debug_enabled; // Debug logging enabled in parent process bool attach_screenshot; // Screenshot attachment enabled in parent process - bool cache_keep; + sentry_cache_keep_t cache_keep; bool require_user_consent; bool enable_large_attachments; uint64_t shutdown_timeout; diff --git a/src/sentry_core.c b/src/sentry_core.c index 881448739..fdd1aa10d 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -493,7 +493,8 @@ sentry__capture_envelope(sentry_transport_t *transport, } bool cached = false; if (options->cache_keep || options->http_retry) { - cached = sentry__run_write_cache(options->run, envelope, 0); + int retry_count = options->http_retry ? 0 : -1; + cached = sentry__run_write_cache(options->run, envelope, retry_count); if (cached && !sentry__run_should_skip_upload(options->run)) { // consent given meanwhile -> trigger retry to avoid waiting // until the next retry poll @@ -1683,6 +1684,10 @@ sentry__launch_external_crash_reporter( return false; } + if (options->cache_keep == SENTRY_CACHE_KEEP_ALWAYS) { + sentry__run_write_cache(options->run, envelope, -1); + } + sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); char *envelope_filename = sentry__uuid_as_filename(&event_id, ".envelope"); if (!envelope_filename) { diff --git a/src/sentry_options.c b/src/sentry_options.c index 100015cfe..8731f27f1 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -66,7 +66,7 @@ sentry_options_new(void) opts->crashpad_limit_stack_capture_to_sp = false; opts->enable_metrics = true; opts->enable_logs = true; - opts->cache_keep = false; + opts->cache_keep = SENTRY_CACHE_KEEP_NONE; opts->cache_max_age = 0; opts->cache_max_size = 0; opts->cache_max_items = 30; @@ -512,7 +512,13 @@ sentry_options_get_symbolize_stacktraces(const sentry_options_t *opts) void sentry_options_set_cache_keep(sentry_options_t *opts, int enabled) { - opts->cache_keep = !!enabled; + // Clamp to valid range + if (enabled < SENTRY_CACHE_KEEP_NONE) { + enabled = SENTRY_CACHE_KEEP_NONE; + } else if (enabled > SENTRY_CACHE_KEEP_ALWAYS) { + enabled = SENTRY_CACHE_KEEP_ALWAYS; + } + opts->cache_keep = (sentry_cache_keep_t)enabled; } void diff --git a/src/sentry_options.h b/src/sentry_options.h index 0a063a3aa..c8b75d2c2 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -47,7 +47,7 @@ struct sentry_options_s { bool enable_logging_when_crashed; bool propagate_traceparent; bool crashpad_limit_stack_capture_to_sp; - bool cache_keep; + sentry_cache_keep_t cache_keep; time_t cache_max_age; size_t cache_max_size; diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 7daa10e37..bfc240a8f 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -27,7 +27,7 @@ typedef enum { struct sentry_retry_s { sentry_run_t *run; - bool cache_keep; + sentry_cache_keep_t cache_keep; uint64_t startup_time; volatile long state; volatile long scheduled; @@ -92,7 +92,8 @@ compare_retry_items(const void *a, const void *b) } static bool -handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) +handle_result(sentry_retry_t *retry, const retry_item_t *item, + const sentry_envelope_t *envelope, int status_code) { // Only network failures (status_code < 0) trigger retries. HTTP responses // including 5xx (500, 502, 503, 504) are discarded: @@ -127,13 +128,28 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) // cache on last attempt if (exhausted && retry->cache_keep && status_code < 0) { + if (retry->cache_keep == SENTRY_CACHE_KEEP_ALWAYS) { + sentry_path_t *cached_path + = sentry__run_make_cache_path(retry->run, 0, -1, item->uuid); + bool cached = cached_path && sentry__path_is_file(cached_path); + sentry__path_free(cached_path); + if (cached) { + sentry__path_remove(item->path); + return false; + } + } if (!sentry__run_move_cache(retry->run, item->path, -1)) { sentry__cache_remove_envelope(item->path); } return false; } - sentry__cache_remove_envelope(item->path); + if (retry->cache_keep == SENTRY_CACHE_KEEP_ALWAYS + && sentry__run_write_cache(retry->run, envelope, -1)) { + sentry__path_remove(item->path); + } else { + sentry__cache_remove_envelope(item->path); + } return false; } @@ -216,8 +232,10 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, SENTRY_DEBUGF("retrying envelope (%d/%d)", items[i].count + 1, SENTRY_RETRY_ATTEMPTS); int status_code = send_cb(envelope, data); + bool keep_retry + = handle_result(retry, &items[i], envelope, status_code); sentry_envelope_free(envelope); - if (!handle_result(retry, &items[i], status_code)) { + if (!keep_retry) { total--; } // stop on network failure to avoid wasting time on a dead diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 0b07118b1..f0d6fc7c2 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -37,7 +37,7 @@ typedef struct { sentry_http_send_func_t send_func; void (*shutdown_client)(void *client); sentry_retry_t *retry; - bool cache_keep; + sentry_cache_keep_t cache_keep; sentry_run_t *run; bool send_client_reports; } http_transport_state_t; @@ -688,8 +688,8 @@ http_send_task(void *_envelope, void *_state) sentry_value_t ref_paths = collect_attachment_refs(envelope); int status_code = http_send_envelope(envelope, state); + const sentry_envelope_t *ref_owner = NULL; if (status_code < 0) { - const sentry_envelope_t *ref_owner = NULL; if (sentry_value_get_length(ref_paths) > 0 && status_code == RESULT_SHUTDOWN && sentry__run_write_envelope(state->run, envelope)) { @@ -711,7 +711,11 @@ http_send_task(void *_envelope, void *_state) } prune_attachment_refs(state->run, ref_paths, ref_owner); } else { - prune_attachment_refs(state->run, ref_paths, NULL); + if (state->cache_keep == SENTRY_CACHE_KEEP_ALWAYS + && sentry__run_write_cache(state->run, envelope, -1)) { + ref_owner = envelope; + } + prune_attachment_refs(state->run, ref_paths, ref_owner); } sentry_value_decref(ref_paths); diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py index 46d2b4ddd..7d5e4c01b 100644 --- a/tests/test_integration_cache.py +++ b/tests/test_integration_cache.py @@ -2,7 +2,7 @@ import time import pytest -from . import run +from . import make_dsn, run from .conditions import has_breakpad, has_files, has_http, is_qemu pytestmark = [ @@ -11,7 +11,14 @@ ] -@pytest.mark.parametrize("cache_keep", [True, False]) +@pytest.mark.parametrize( + "cache_args,expect_cache", + [ + ([], False), + (["cache-keep"], True), + (["cache-keep-always"], True), + ], +) @pytest.mark.parametrize( "backend", [ @@ -24,7 +31,7 @@ ), ], ) -def test_cache_keep(cmake, backend, cache_keep, unreachable_dsn): +def test_cache_keep(cmake, backend, cache_args, expect_cache, unreachable_dsn): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") env = dict(os.environ, SENTRY_DSN=unreachable_dsn) @@ -33,8 +40,7 @@ def test_cache_keep(cmake, backend, cache_keep, unreachable_dsn): run( tmp_path, "sentry_example", - ["log", "no-http-retry", "flush", "crash"] - + (["cache-keep"] if cache_keep else []), + ["log", "no-http-retry", "flush", "crash"] + cache_args, expect_failure=True, env=env, ) @@ -45,13 +51,12 @@ def test_cache_keep(cmake, backend, cache_keep, unreachable_dsn): run( tmp_path, "sentry_example", - ["log", "no-http-retry", "flush", "no-setup"] - + (["cache-keep"] if cache_keep else []), + ["log", "no-http-retry", "flush", "no-setup"] + cache_args, env=env, ) - assert cache_dir.exists() or cache_keep is False - if cache_keep: + assert cache_dir.exists() or expect_cache is False + if expect_cache: cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 if backend != "inproc": @@ -60,6 +65,26 @@ def test_cache_keep(cmake, backend, cache_keep, unreachable_dsn): assert cache_files[0].stem == dmp_files[0].stem +def test_cache_keep_always(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) + cache_dir = tmp_path.joinpath(".sentry-native/cache") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + ["log", "cache-keep-always", "flush", "capture-event"], + env=env, + ) + assert waiting.result + + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert len(cache_files[0].stem) == 36 + + @pytest.mark.parametrize( "backend", [ diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index c91724ee6..bbd378174 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -815,8 +815,15 @@ def test_crashpad_external_crash_reporter_wer(cmake, httpserver, run_args): test_crashpad_external_crash_reporter(cmake, httpserver, run_args) -@pytest.mark.parametrize("cache_keep", [True, False]) -def test_crashpad_cache_keep(cmake, httpserver, cache_keep): +@pytest.mark.parametrize( + "cache_args,expect_cache", + [ + ([], False), + (["cache-keep"], True), + (["cache-keep-always"], True), + ], +) +def test_crashpad_cache_keep(cmake, httpserver, cache_args, expect_cache): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") @@ -827,7 +834,7 @@ def test_crashpad_cache_keep(cmake, httpserver, cache_keep): run( tmp_path, "sentry_example", - ["log", "crash"] + (["cache-keep"] if cache_keep else []), + ["log", "crash"] + cache_args, expect_failure=True, env=env, ) @@ -839,7 +846,7 @@ def test_crashpad_cache_keep(cmake, httpserver, cache_keep): run( tmp_path, "sentry_example", - ["log", "no-setup"] + (["cache-keep"] if cache_keep else []), + ["log", "no-setup"] + cache_args, env=env, ) @@ -847,12 +854,12 @@ def test_crashpad_cache_keep(cmake, httpserver, cache_keep): run( tmp_path, "sentry_example", - ["log", "no-setup"] + (["cache-keep"] if cache_keep else []), + ["log", "no-setup"] + cache_args, env=env, ) - assert cache_dir.exists() or cache_keep is False - if cache_keep: + assert cache_dir.exists() or expect_cache is False + if expect_cache: cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 with open(cache_files[0], "rb") as f: @@ -864,6 +871,8 @@ def test_crashpad_cache_keep(cmake, httpserver, cache_keep): dmp_files = list(cache_dir.glob("*.dmp")) assert len(dmp_files) == 1 assert cache_files[0].stem == dmp_files[0].stem + else: + assert not cache_dir.exists() or len(list(cache_dir.glob("*.envelope"))) == 0 def test_crashpad_cache_consent(cmake, httpserver): diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 68f8762bf..e45035768 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -425,6 +425,49 @@ SENTRY_TEST(retry_cache) sentry_close(); } +SENTRY_TEST(retry_cache_keep_always) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_http_retry(options, false); + sentry_options_set_cache_keep(options, SENTRY_CACHE_KEEP_ALWAYS); + sentry_init(options); + + sentry_retry_t *retry = sentry__retry_new(options); + TEST_ASSERT(!!retry); + + const sentry_path_t *cache_path = options->run->cache_path; + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); + + uint64_t old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(0); + sentry_uuid_t event_id = sentry_uuid_new_v4(); + write_retry_file(options->run, old_ts, 0, &event_id); + + char uuid_str[37]; + sentry_uuid_as_string(&event_id, uuid_str); + char cache_name[46]; + snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", uuid_str); + sentry_path_t *cached = sentry__path_join_str(cache_path, cache_name); + + char sib_name[128]; + snprintf(sib_name, sizeof(sib_name), "%.36s-payload.bin", uuid_str); + sentry_path_t *sib_path = sentry__path_join_str(cache_path, sib_name); + TEST_ASSERT(sentry__path_write_buffer(sib_path, "data", 4) == 0); + + retry_test_ctx_t ctx = { 200, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + TEST_CHECK(sentry__path_is_file(cached)); + TEST_CHECK(sentry__path_is_file(sib_path)); + + sentry__retry_free(retry); + sentry__path_free(sib_path); + sentry__path_free(cached); + sentry_close(); +} + static int retry_func_calls = 0; static void diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 8be3d7181..0fba923b3 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -227,6 +227,7 @@ XX(read_write_envelope_to_invalid_path) XX(recursive_paths) XX(retry_backoff) XX(retry_cache) +XX(retry_cache_keep_always) XX(retry_consent) XX(retry_filename) XX(retry_make_cache_path) From 1d084ca20844fd1421b0521485ad99c76e8534f6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 7 May 2026 17:42:24 +0200 Subject: [PATCH 2/8] fix(transport): Preserve keep-always retry attachments When HTTP retry re-sends an envelope with attachment refs, the callback pruned sibling cache files before the retry layer archived keep-always envelopes. Keep referenced siblings so the cached envelope does not point at missing files. Co-Authored-By: Codex --- src/transports/sentry_http_transport.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index f0d6fc7c2..c9ca44582 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -635,7 +635,7 @@ retry_send_cb(sentry_envelope_t *envelope, void *_state) bool reported = add_client_report(envelope, state, &report); sentry_value_t ref_paths = collect_attachment_refs(envelope); int status_code = http_send_envelope(envelope, state); - if (status_code < 0) { + if (status_code < 0 || state->cache_keep == SENTRY_CACHE_KEEP_ALWAYS) { prune_attachment_refs(state->run, ref_paths, envelope); } else { prune_attachment_refs(state->run, ref_paths, NULL); From fa6b9ba8d9b9bf7fd8e87554ae5b92522e4cdebf Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 7 May 2026 18:13:48 +0200 Subject: [PATCH 3/8] fix(native): Retry consent-cached crash envelopes Pass the app http_retry setting to the crash daemon so consent-revoked crash envelopes are cached in retry format. Keep daemon retry polling disabled and leave restart-time retries to the app process. Co-Authored-By: Codex --- src/backends/native/sentry_crash_context.h | 1 + src/backends/native/sentry_crash_daemon.c | 4 ++++ src/backends/sentry_backend_native.c | 1 + 3 files changed, 6 insertions(+) diff --git a/src/backends/native/sentry_crash_context.h b/src/backends/native/sentry_crash_context.h index be17c298a..70ea066ab 100644 --- a/src/backends/native/sentry_crash_context.h +++ b/src/backends/native/sentry_crash_context.h @@ -273,6 +273,7 @@ typedef struct { sentry_cache_keep_t cache_keep; bool require_user_consent; bool enable_large_attachments; + bool http_retry; uint64_t shutdown_timeout; // Atomic user consent (sentry_user_consent_t), updated whenever user diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index ec8b052ea..683608eb3 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -3351,6 +3351,10 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, if (options->transport) { SENTRY_DEBUG("Starting transport"); sentry__transport_startup(options->transport, options); + // Set http_retry after transport startup to keep daemon-side retry + // polling disabled, while letting capture cache consent-revoked + // envelopes in retry format for the app to send on restart. + options->http_retry = ipc->shmem->http_retry; } else { SENTRY_WARN("No transport available"); } diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index d9bbcf038..c360988a7 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -195,6 +195,7 @@ native_backend_startup( ctx->cache_keep = options->cache_keep; ctx->require_user_consent = options->require_user_consent; ctx->enable_large_attachments = options->enable_large_attachments; + ctx->http_retry = options->http_retry; ctx->shutdown_timeout = options->shutdown_timeout; sentry__atomic_store( &ctx->user_consent, sentry__atomic_fetch(&options->run->user_consent)); From f0bee6582b691cd3b7aeefb25261123118d911a3 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 7 May 2026 18:31:57 +0200 Subject: [PATCH 4/8] fix(native): Stabilize cache keep IPC field Store cache_keep as an int in the crash IPC context like other enum-backed fields. Cast at the app and daemon boundaries so enum size flags cannot change the shared-memory layout. Co-Authored-By: Codex --- src/backends/native/sentry_crash_context.h | 2 +- src/backends/native/sentry_crash_daemon.c | 2 +- src/backends/sentry_backend_native.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backends/native/sentry_crash_context.h b/src/backends/native/sentry_crash_context.h index 70ea066ab..3abb76186 100644 --- a/src/backends/native/sentry_crash_context.h +++ b/src/backends/native/sentry_crash_context.h @@ -270,7 +270,7 @@ typedef struct { int crash_reporting_mode; // sentry_crash_reporting_mode_t bool debug_enabled; // Debug logging enabled in parent process bool attach_screenshot; // Screenshot attachment enabled in parent process - sentry_cache_keep_t cache_keep; + int cache_keep; // sentry_cache_keep_t bool require_user_consent; bool enable_large_attachments; bool http_retry; diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 683608eb3..8d4d75026 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -3289,7 +3289,7 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, // Use debug logging and screenshot settings from parent process sentry_options_set_debug(options, ipc->shmem->debug_enabled); options->attach_screenshot = ipc->shmem->attach_screenshot; - options->cache_keep = ipc->shmem->cache_keep; + options->cache_keep = (sentry_cache_keep_t)ipc->shmem->cache_keep; options->enable_large_attachments = ipc->shmem->enable_large_attachments; options->http_retry = false; options->shutdown_timeout = ipc->shmem->shutdown_timeout; diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index c360988a7..c5d07fb19 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -192,7 +192,7 @@ native_backend_startup( // Pass debug logging setting to daemon ctx->debug_enabled = options->debug; ctx->attach_screenshot = options->attach_screenshot; - ctx->cache_keep = options->cache_keep; + ctx->cache_keep = (int)options->cache_keep; ctx->require_user_consent = options->require_user_consent; ctx->enable_large_attachments = options->enable_large_attachments; ctx->http_retry = options->http_retry; From 85d99ad98bbec382d09dd7b6b9e2225c71b829db Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 7 May 2026 18:53:19 +0200 Subject: [PATCH 5/8] !!enabled --- src/sentry_options.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sentry_options.c b/src/sentry_options.c index 8731f27f1..037c7dae1 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -512,9 +512,8 @@ sentry_options_get_symbolize_stacktraces(const sentry_options_t *opts) void sentry_options_set_cache_keep(sentry_options_t *opts, int enabled) { - // Clamp to valid range if (enabled < SENTRY_CACHE_KEEP_NONE) { - enabled = SENTRY_CACHE_KEEP_NONE; + enabled = SENTRY_CACHE_KEEP_OFFLINE; } else if (enabled > SENTRY_CACHE_KEEP_ALWAYS) { enabled = SENTRY_CACHE_KEEP_ALWAYS; } From d9d9e51c79dd74969111710917f631a9beeafd76 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 7 May 2026 18:56:07 +0200 Subject: [PATCH 6/8] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b85251c3a..b98d1dacb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ **Features**: - Auto-populate `event.user.id` with a persistent per-installation UUID when no explicit user ID is set. ([#1661](https://github.com/getsentry/sentry-native/pull/1661)) +- Add cache keep modes, including `SENTRY_CACHE_KEEP_ALWAYS` to cache envelopes regardless of upload result. ([#1707](https://github.com/getsentry/sentry-native/pull/1707)) ## 0.14.0 From 8532f488241094202c7eacb05d55a741bb16872a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 11 May 2026 11:14:12 +0200 Subject: [PATCH 7/8] rename enabled->mode --- include/sentry.h | 3 +-- src/sentry_options.c | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index a43bbeeb9..1ab78ad49 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1545,8 +1545,7 @@ SENTRY_API int sentry_options_get_symbolize_stacktraces( * * `SENTRY_CACHE_KEEP_NONE` by default. */ -SENTRY_API void sentry_options_set_cache_keep( - sentry_options_t *opts, int enabled); +SENTRY_API void sentry_options_set_cache_keep(sentry_options_t *opts, int mode); /** * Sets the maximum number of items in the cache directory. diff --git a/src/sentry_options.c b/src/sentry_options.c index 037c7dae1..bd9c57a26 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -510,14 +510,14 @@ sentry_options_get_symbolize_stacktraces(const sentry_options_t *opts) } void -sentry_options_set_cache_keep(sentry_options_t *opts, int enabled) +sentry_options_set_cache_keep(sentry_options_t *opts, int mode) { - if (enabled < SENTRY_CACHE_KEEP_NONE) { - enabled = SENTRY_CACHE_KEEP_OFFLINE; - } else if (enabled > SENTRY_CACHE_KEEP_ALWAYS) { - enabled = SENTRY_CACHE_KEEP_ALWAYS; + if (mode < SENTRY_CACHE_KEEP_NONE) { + mode = SENTRY_CACHE_KEEP_OFFLINE; + } else if (mode > SENTRY_CACHE_KEEP_ALWAYS) { + mode = SENTRY_CACHE_KEEP_ALWAYS; } - opts->cache_keep = (sentry_cache_keep_t)enabled; + opts->cache_keep = (sentry_cache_keep_t)mode; } void From 509b6210346e35650add0b8d0c9062504cabb6cc Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 11 May 2026 11:42:45 +0200 Subject: [PATCH 8/8] test(retry): Cover keep-always duplicate cache path Add retry coverage for the keep-always path that sees an existing bare cache envelope after retries are exhausted. Verify the retry file is removed without overwriting the cached envelope. Co-Authored-By: OpenAI Codex --- tests/unit/test_retry.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index e45035768..81a7bc383 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -462,6 +462,27 @@ SENTRY_TEST(retry_cache_keep_always) TEST_CHECK(sentry__path_is_file(cached)); TEST_CHECK(sentry__path_is_file(sib_path)); + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); + TEST_ASSERT(sentry__path_write_buffer(cached, "cached", 6) == 0); + + old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(5); + write_retry_file(options->run, old_ts, 5, &event_id); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 2); + + ctx = (retry_test_ctx_t) { -1, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + TEST_CHECK(sentry__path_is_file(cached)); + + size_t cached_len = 0; + char *cached_buf = sentry__path_read_to_buffer(cached, &cached_len); + TEST_ASSERT(!!cached_buf); + TEST_CHECK_INT_EQUAL(cached_len, 6); + TEST_CHECK_STRING_EQUAL(cached_buf, "cached"); + sentry_free(cached_buf); + sentry__retry_free(retry); sentry__path_free(sib_path); sentry__path_free(cached);