From 536c4a1af0b56de493c58333813b00998b33c07b Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:53:38 +0100 Subject: [PATCH 01/31] Import offline caching POC (#1461) Before "revert separate caching folder implementation" https://github.com/getsentry/sentry-native/pull/1461 --- examples/example.c | 4 + include/sentry.h | 21 +++ src/backends/sentry_backend_breakpad.cpp | 4 +- src/backends/sentry_backend_crashpad.cpp | 13 +- src/path/sentry_path_unix.c | 8 ++ src/path/sentry_path_windows.c | 12 ++ src/sentry_core.c | 4 + src/sentry_database.c | 168 ++++++++++++++++++++++- src/sentry_database.h | 6 + src/sentry_options.c | 27 ++++ src/sentry_options.h | 4 + src/sentry_path.h | 7 + 12 files changed, 271 insertions(+), 7 deletions(-) diff --git a/examples/example.c b/examples/example.c index 87f8fdd19..be909fb62 100644 --- a/examples/example.c +++ b/examples/example.c @@ -503,6 +503,10 @@ main(int argc, char **argv) if (has_arg(argc, argv, "log-attributes")) { sentry_options_set_logs_with_attributes(options, true); } + if (has_arg(argc, argv, "cache-keep")) { + sentry_options_set_cache_keep(options, true); + sentry_options_set_cache_max_size(options, 1000 * 8); + } if (0 != sentry_init(options)) { return EXIT_FAILURE; diff --git a/include/sentry.h b/include/sentry.h index 6fb9a9bf6..f180f60c2 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1375,6 +1375,27 @@ SENTRY_API void sentry_options_set_symbolize_stacktraces( SENTRY_API int sentry_options_get_symbolize_stacktraces( const sentry_options_t *opts); +/** + * Sets whether we should keep files cached even when sent successfully. + * The database will be cleared based on cache_max_size and cache_max_age + */ +SENTRY_API void sentry_options_set_cache_keep( + sentry_options_t *opts, int enabled); +/** + * Sets the maximum size (kb)/age (days) for the cache folder. + * On startup, we check new->old entries, and remove those that go over either + * boundary. + */ +SENTRY_API void sentry_options_set_cache_max_size( + sentry_options_t *opts, size_t size); +SENTRY_API void sentry_options_set_cache_max_age( + sentry_options_t *opts, int age); + +/** + * Gets the caching mode for crash reports. + */ +SENTRY_API int sentry_options_get_cache_keep(const sentry_options_t *opts); + /** * Adds a new attachment to be sent along. * diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index 86eb3bec4..7bd65728f 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -197,7 +197,9 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, // now that the envelope was written, we can remove the temporary // minidump file - sentry__path_remove(dump_path); + if (!options->cache_keep) { + sentry__path_remove(dump_path); + } sentry__path_free(dump_path); } else { SENTRY_DEBUG("event was discarded by the `on_crash` hook"); diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 781481359..2dbd18b8f 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -745,11 +745,14 @@ crashpad_backend_prune_database(sentry_backend_t *backend) // complete database to a maximum of 8M. That might still be a lot for // an embedded use-case, but minidumps on desktop can sometimes be quite // large. - data->db->CleanDatabase(60 * 60 * 24 * 2); - crashpad::BinaryPruneCondition condition(crashpad::BinaryPruneCondition::OR, - new crashpad::DatabaseSizePruneCondition(1024 * 8), - new crashpad::AgePruneCondition(2)); - crashpad::PruneCrashReportDatabase(data->db, &condition); + SENTRY_WITH_OPTIONS (options) { + data->db->CleanDatabase(60 * 60 * 24 * options->cache_max_age); + crashpad::BinaryPruneCondition condition( + crashpad::BinaryPruneCondition::OR, + new crashpad::DatabaseSizePruneCondition(options->cache_max_size), + new crashpad::AgePruneCondition(options->cache_max_age)); + crashpad::PruneCrashReportDatabase(data->db, &condition); + } } #if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_LINUX) diff --git a/src/path/sentry_path_unix.c b/src/path/sentry_path_unix.c index 7b7f63fa3..e831a2b22 100644 --- a/src/path/sentry_path_unix.c +++ b/src/path/sentry_path_unix.c @@ -324,6 +324,14 @@ sentry__path_remove(const sentry_path_t *path) return 1; } +int +sentry__path_rename(const sentry_path_t *src, const sentry_path_t *dst) +{ + int status; + EINTR_RETRY(rename(src->path, dst->path), &status); + return status == 0 ? 0 : 1; +} + int sentry__path_create_dir_all(const sentry_path_t *path) { diff --git a/src/path/sentry_path_windows.c b/src/path/sentry_path_windows.c index 5b76497f4..bd8269a6e 100644 --- a/src/path/sentry_path_windows.c +++ b/src/path/sentry_path_windows.c @@ -511,6 +511,18 @@ sentry__path_remove(const sentry_path_t *path) return removal_success ? 0 : !is_last_error_path_not_found(); } +int +sentry__path_rename(const sentry_path_t *src, const sentry_path_t *dst) +{ + wchar_t *src_w = src->path_w; + wchar_t *dst_w = dst->path_w; + if (!src_w || !dst_w) { + return 1; + } + // MOVEFILE_REPLACE_EXISTING allows overwriting the destination if it exists + return MoveFileExW(src_w, dst_w, MOVEFILE_REPLACE_EXISTING) ? 0 : 1; +} + int sentry__path_create_dir_all(const sentry_path_t *path) { diff --git a/src/sentry_core.c b/src/sentry_core.c index 465379813..9dfcff3ee 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -288,6 +288,10 @@ sentry_init(sentry_options_t *options) backend->prune_database_func(backend); } + if (options->cache_keep) { + sentry__cleanup_cache(options); + } + if (options->auto_session_tracking) { sentry_start_session(); } diff --git a/src/sentry_database.c b/src/sentry_database.c index 32cf8ba57..8c0a68d1e 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -6,6 +6,7 @@ #include "sentry_session.h" #include "sentry_uuid.h" #include +#include #include sentry_run_t * @@ -237,9 +238,26 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) if (strcmp(options->run->run_path->path, run_dir->path) == 0) { continue; } + sentry_path_t *cached_run_dir = NULL; + if (options->cache_keep) { + sentry_path_t *cache_dir + = sentry__path_join_str(options->database_path, "cache"); + sentry__path_create_dir_all(cache_dir); + cached_run_dir = sentry__path_join_str( + cache_dir, sentry__path_filename(run_dir)); + sentry__path_create_dir_all(cached_run_dir); + sentry__path_free(cache_dir); + } + sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); const sentry_path_t *file; while (run_iter && (file = sentry__pathiter_next(run_iter)) != NULL) { + sentry_path_t *cached_file = NULL; + if (options->cache_keep) { + cached_file = sentry__path_join_str( + cached_run_dir, sentry__path_filename(file)); + } + if (sentry__path_filename_matches(file, "session.json")) { if (!session_envelope) { session_envelope = sentry__envelope_new(); @@ -283,10 +301,19 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry__capture_envelope(options->transport, envelope); } - sentry__path_remove(file); + if (options->cache_keep) { + sentry__path_rename(file, cached_file); + sentry__path_free(cached_file); + } else { + sentry__path_remove(file); + } } sentry__pathiter_free(run_iter); + if (options->cache_keep) { + sentry__path_free(cached_run_dir); + } + sentry__path_remove_all(run_dir); sentry__filelock_free(lock); } @@ -295,6 +322,145 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry__capture_envelope(options->transport, session_envelope); } +// Cache Pruning below is based on prune_crash_reports.cc from Crashpad + +/** + * A cache entry with its metadata for sorting and pruning decisions. + */ +typedef struct { + sentry_path_t *path; + time_t mtime; + size_t size_in_kb; +} cache_entry_t; + +/** + * Calculate the total size of a directory (sum of all files). + */ +static size_t +get_directory_size_in_kb(const sentry_path_t *dir) +{ + size_t total_bytes = 0; + sentry_pathiter_t *iter = sentry__path_iter_directory(dir); + if (!iter) { + return 0; + } + const sentry_path_t *entry; + while ((entry = sentry__pathiter_next(iter)) != NULL) { + if (sentry__path_is_file(entry)) { + total_bytes += sentry__path_get_size(entry); + } + } + sentry__pathiter_free(iter); + // Round up to next KB boundary + return (total_bytes + 1023) / 1024; +} + +/** + * Comparison function to sort cache entries by mtime, newest first. + */ +static int +compare_cache_entries_newest_first(const void *a, const void *b) +{ + const cache_entry_t *entry_a = (const cache_entry_t *)a; + const cache_entry_t *entry_b = (const cache_entry_t *)b; + // Newest first: if b is newer, return positive (b comes before a) + if (entry_b->mtime > entry_a->mtime) { + return 1; + } + if (entry_b->mtime < entry_a->mtime) { + return -1; + } + return 0; +} + +void +sentry__cleanup_cache(const sentry_options_t *options) +{ + if (!options->database_path) { + return; + } + + sentry_path_t *cache_dir + = sentry__path_join_str(options->database_path, "cache"); + if (!sentry__path_is_dir(cache_dir)) { + sentry__path_free(cache_dir); + return; + } + + // First pass: collect all cache entries with their metadata + size_t entries_capacity = 16; + size_t entries_count = 0; + cache_entry_t *entries + = sentry_malloc(sizeof(cache_entry_t) * entries_capacity); + if (!entries) { + sentry__path_free(cache_dir); + return; + } + + sentry_pathiter_t *iter = sentry__path_iter_directory(cache_dir); + const sentry_path_t *entry; + while (iter && (entry = sentry__pathiter_next(iter)) != NULL) { + if (!sentry__path_is_dir(entry)) { + continue; + } + + // Grow array if needed + if (entries_count >= entries_capacity) { + entries_capacity *= 2; + cache_entry_t *new_entries + = sentry_malloc(sizeof(cache_entry_t) * entries_capacity); + if (!new_entries) { + break; + } + memcpy(new_entries, entries, sizeof(cache_entry_t) * entries_count); + sentry_free(entries); + entries = new_entries; + } + + entries[entries_count].path = sentry__path_clone(entry); + entries[entries_count].mtime = sentry__path_get_mtime(entry); + entries[entries_count].size_in_kb = get_directory_size_in_kb(entry); + entries_count++; + } + sentry__pathiter_free(iter); + + // Sort by mtime, newest first (like crashpad) + // This ensures we keep the newest entries when pruning by size + qsort(entries, entries_count, sizeof(cache_entry_t), + compare_cache_entries_newest_first); + + // Calculate the age threshold + time_t now = time(NULL); + time_t oldest_allowed = now - (options->cache_max_age * 24 * 60 * 60); + + // Prune entries: iterate newest-to-oldest, accumulating size + // Remove if: too old OR accumulated size exceeds limit + size_t accumulated_size_kb = 0; + for (size_t i = 0; i < entries_count; i++) { + bool should_prune = false; + + // Age-based pruning + if (options->cache_max_age > 0 && entries[i].mtime < oldest_allowed) { + should_prune = true; + } + + // Size-based pruning (accumulate size as we go, like crashpad) + accumulated_size_kb += entries[i].size_in_kb; + if (options->cache_max_size > 0 + && accumulated_size_kb > (size_t)options->cache_max_size) { + should_prune = true; + } + + if (should_prune) { + sentry__path_remove_all(entries[i].path); + } + sentry__path_free(entries[i].path); + } + + sentry_free(entries); + sentry__path_free(cache_dir); +} + static const char *g_last_crash_filename = "last_crash"; bool diff --git a/src/sentry_database.h b/src/sentry_database.h index 0cc688907..791c30d9f 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -78,6 +78,12 @@ bool sentry__run_clear_session(const sentry_run_t *run); void sentry__process_old_runs( const sentry_options_t *options, uint64_t last_crash); +/** + * Cleans up the cache based on options.max_cache_size and + * options.max_cache_age. + */ +void sentry__cleanup_cache(const sentry_options_t *options); + /** * This will write the current ISO8601 formatted timestamp into the * `/last_crash` file. diff --git a/src/sentry_options.c b/src/sentry_options.c index b9b6ea4c6..431778894 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -53,6 +53,9 @@ sentry_options_new(void) opts->enable_logging_when_crashed = true; opts->propagate_traceparent = false; opts->crashpad_limit_stack_capture_to_sp = false; + opts->cache_keep = false; + opts->cache_max_age = 2; + opts->cache_max_size = 1024 * 8; opts->symbolize_stacktraces = // AIX doesn't have reliable debug IDs for server-side symbolication, // and the diversity of Android makes it infeasible to have access to debug @@ -475,6 +478,30 @@ sentry_options_get_symbolize_stacktraces(const sentry_options_t *opts) return opts->symbolize_stacktraces; } +void +sentry_options_set_cache_keep(sentry_options_t *opts, int enabled) +{ + opts->cache_keep = !!enabled; +} + +void +sentry_options_set_cache_max_size(sentry_options_t *opts, size_t size) +{ + opts->cache_max_size = size; +} + +void +sentry_options_set_cache_max_age(sentry_options_t *opts, int age) +{ + opts->cache_max_age = age; +} + +int +sentry_options_get_cache_keep(const sentry_options_t *opts) +{ + return opts->cache_keep; +} + void sentry_options_set_system_crash_reporter_enabled( sentry_options_t *opts, int enabled) diff --git a/src/sentry_options.h b/src/sentry_options.h index 280703d40..306566a11 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -45,6 +45,10 @@ struct sentry_options_s { bool enable_logging_when_crashed; bool propagate_traceparent; bool crashpad_limit_stack_capture_to_sp; + bool cache_keep; + + int cache_max_age; // TODO in days? + size_t cache_max_size; // TODO in kb? sentry_attachment_t *attachments; sentry_run_t *run; diff --git a/src/sentry_path.h b/src/sentry_path.h index cd5a3a917..484a6f531 100644 --- a/src/sentry_path.h +++ b/src/sentry_path.h @@ -152,6 +152,13 @@ int sentry__path_remove(const sentry_path_t *path); */ int sentry__path_remove_all(const sentry_path_t *path); +/** + * Rename/move the file or directory from `src` to `dst`. + * This will overwrite `dst` if it already exists. + * Returns 0 on success. + */ +int sentry__path_rename(const sentry_path_t *src, const sentry_path_t *dst); + /** * This will create the directory referred to by `path`, and any non-existing * parent directory. From 5742eda07b465452e04b789e2c07df81fe419366 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 15 Jan 2026 09:53:51 +0100 Subject: [PATCH 02/31] breakpad: delete .dmp --- src/backends/sentry_backend_breakpad.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index 7bd65728f..86eb3bec4 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -197,9 +197,7 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, // now that the envelope was written, we can remove the temporary // minidump file - if (!options->cache_keep) { - sentry__path_remove(dump_path); - } + sentry__path_remove(dump_path); sentry__path_free(dump_path); } else { SENTRY_DEBUG("event was discarded by the `on_crash` hook"); From 6c75ccf71ba6756f670df43d0e6b9dd7984d786c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 15 Jan 2026 10:32:03 +0100 Subject: [PATCH 03/31] Flatten cache/ --- src/sentry_database.c | 57 ++++++++++++++----------------------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 8c0a68d1e..890ef987b 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -238,26 +238,16 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) if (strcmp(options->run->run_path->path, run_dir->path) == 0) { continue; } - sentry_path_t *cached_run_dir = NULL; + + sentry_path_t *cache_dir = NULL; if (options->cache_keep) { - sentry_path_t *cache_dir - = sentry__path_join_str(options->database_path, "cache"); + cache_dir = sentry__path_join_str(options->database_path, "cache"); sentry__path_create_dir_all(cache_dir); - cached_run_dir = sentry__path_join_str( - cache_dir, sentry__path_filename(run_dir)); - sentry__path_create_dir_all(cached_run_dir); - sentry__path_free(cache_dir); } sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); const sentry_path_t *file; while (run_iter && (file = sentry__pathiter_next(run_iter)) != NULL) { - sentry_path_t *cached_file = NULL; - if (options->cache_keep) { - cached_file = sentry__path_join_str( - cached_run_dir, sentry__path_filename(file)); - } - if (sentry__path_filename_matches(file, "session.json")) { if (!session_envelope) { session_envelope = sentry__envelope_new(); @@ -299,19 +289,22 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) } else if (sentry__path_ends_with(file, ".envelope")) { sentry_envelope_t *envelope = sentry__envelope_from_path(file); sentry__capture_envelope(options->transport, envelope); - } - if (options->cache_keep) { - sentry__path_rename(file, cached_file); - sentry__path_free(cached_file); - } else { - sentry__path_remove(file); + if (options->cache_keep) { + sentry_path_t *cached_file = sentry__path_join_str( + cache_dir, sentry__path_filename(file)); + sentry__path_rename(file, cached_file); + sentry__path_free(cached_file); + continue; + } } + + sentry__path_remove(file); } sentry__pathiter_free(run_iter); if (options->cache_keep) { - sentry__path_free(cached_run_dir); + sentry__path_free(cache_dir); } sentry__path_remove_all(run_dir); @@ -333,26 +326,12 @@ typedef struct { size_t size_in_kb; } cache_entry_t; -/** - * Calculate the total size of a directory (sum of all files). - */ static size_t -get_directory_size_in_kb(const sentry_path_t *dir) +get_file_size_in_kb(const sentry_path_t *path) { - size_t total_bytes = 0; - sentry_pathiter_t *iter = sentry__path_iter_directory(dir); - if (!iter) { - return 0; - } - const sentry_path_t *entry; - while ((entry = sentry__pathiter_next(iter)) != NULL) { - if (sentry__path_is_file(entry)) { - total_bytes += sentry__path_get_size(entry); - } - } - sentry__pathiter_free(iter); + size_t bytes = sentry__path_get_size(path); // Round up to next KB boundary - return (total_bytes + 1023) / 1024; + return (bytes + 1023) / 1024; } /** @@ -400,7 +379,7 @@ sentry__cleanup_cache(const sentry_options_t *options) sentry_pathiter_t *iter = sentry__path_iter_directory(cache_dir); const sentry_path_t *entry; while (iter && (entry = sentry__pathiter_next(iter)) != NULL) { - if (!sentry__path_is_dir(entry)) { + if (sentry__path_is_dir(entry)) { continue; } @@ -419,7 +398,7 @@ sentry__cleanup_cache(const sentry_options_t *options) entries[entries_count].path = sentry__path_clone(entry); entries[entries_count].mtime = sentry__path_get_mtime(entry); - entries[entries_count].size_in_kb = get_directory_size_in_kb(entry); + entries[entries_count].size_in_kb = get_file_size_in_kb(entry); entries_count++; } sentry__pathiter_free(iter); From 8f3ffd5134eef3feb35bb6ff4b6cc51a489b5d94 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 16 Jan 2026 12:03:27 +0100 Subject: [PATCH 04/31] Add tests --- examples/example.c | 1 + tests/test_integration_cache.py | 102 ++++++++++++++++++ tests/unit/CMakeLists.txt | 1 + tests/unit/test_cache.c | 180 ++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 3 + 5 files changed, 287 insertions(+) create mode 100644 tests/test_integration_cache.py create mode 100644 tests/unit/test_cache.c diff --git a/examples/example.c b/examples/example.c index be909fb62..2c86750fa 100644 --- a/examples/example.c +++ b/examples/example.c @@ -506,6 +506,7 @@ main(int argc, char **argv) if (has_arg(argc, argv, "cache-keep")) { sentry_options_set_cache_keep(options, true); sentry_options_set_cache_max_size(options, 1000 * 8); + sentry_options_set_cache_max_age(options, 5); } if (0 != sentry_init(options)) { diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py new file mode 100644 index 000000000..c4f23daab --- /dev/null +++ b/tests/test_integration_cache.py @@ -0,0 +1,102 @@ +import os +import time +import pytest + +from . import run +from .conditions import has_files + +pytestmark = pytest.mark.skipif(not has_files, reason="tests need local filesystem") + + +@pytest.mark.parametrize("cache_keep", [True, False]) +@pytest.mark.parametrize("backend", ["inproc", "breakpad"]) +def test_cache_keep(cmake, backend, cache_keep): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) + + run( + tmp_path, + "sentry_example", + ["log", "crash"] + (["cache-keep"] if cache_keep else []), + expect_failure=True, + ) + + cache_dir = tmp_path.joinpath(".sentry-native/cache") + assert not cache_dir.exists() or len(list(cache_dir.glob("*.envelope"))) == 0 + + run( + tmp_path, + "sentry_example", + ["log", "no-setup"] + (["cache-keep"] if cache_keep else []), + ) + + assert cache_dir.exists() or cache_keep is False + if cache_keep: + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + + +@pytest.mark.parametrize("backend", ["inproc", "breakpad"]) +def test_cache_max_size(cmake, backend): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) + cache_dir = tmp_path.joinpath(".sentry-native/cache") + + # 5 x 2mb + for i in range(5): + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "crash"], + expect_failure=True, + ) + + if cache_dir.exists(): + cache_files = list(cache_dir.glob("*.envelope")) + for f in cache_files: + with open(f, "r+b") as file: + file.truncate(2048 * 1000) + + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "no-setup"], + ) + + # max 8mb + assert cache_dir.exists() + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) <= 4 + assert sum(f.stat().st_size for f in cache_files) <= 8 * 1000 * 1024 + + +@pytest.mark.parametrize("backend", ["inproc", "breakpad"]) +def test_cache_max_age(cmake, backend): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) + cache_dir = tmp_path.joinpath(".sentry-native/cache") + + for i in range(5): + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "crash"], + expect_failure=True, + ) + + # 2,4,6,8,10 days old + assert cache_dir.exists() + cache_files = list(cache_dir.glob("*.envelope")) + for i, f in enumerate(cache_files): + mtime = time.time() - ((i + 1) * 2 * 24 * 60 * 60) + os.utime(str(f), (mtime, mtime)) + + # 0 days old + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "no-setup"], + ) + + # max 5 days + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 3 + for f in cache_files: + assert time.time() - f.stat().st_mtime <= 5 * 24 * 60 * 60 diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index b6c8dc0fc..dfdd4ec62 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -23,6 +23,7 @@ add_executable(sentry_test_unit sentry_testsupport.h test_attachments.c test_basic.c + test_cache.c test_consent.c test_concurrency.c test_embedded_info.c diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c new file mode 100644 index 000000000..71736b0c0 --- /dev/null +++ b/tests/unit/test_cache.c @@ -0,0 +1,180 @@ +#include "sentry_core.h" +#include "sentry_database.h" +#include "sentry_envelope.h" +#include "sentry_options.h" +#include "sentry_path.h" +#include "sentry_testsupport.h" +#include "sentry_uuid.h" +#include "sentry_value.h" + +#ifdef SENTRY_PLATFORM_WINDOWS +# include +#else +# include +#endif + +static int +set_file_mtime(const sentry_path_t *path, time_t mtime) +{ +#ifdef SENTRY_PLATFORM_WINDOWS + HANDLE h = CreateFileW(path->path_w, FILE_WRITE_ATTRIBUTES, 0, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + ULARGE_INTEGER ft + = { (uint64_t)(mtime * 10000000ULL + 116444736000000000ULL) }; + BOOL rv = SetFileTime(h, NULL, NULL, (FILETIME *)&ft); + CloseHandle(h); + return rv ? 0 : -1; +#else + struct utimbuf times = { .modtime = mtime, .actime = mtime }; + return utime(path->path, ×); +#endif +} + +SENTRY_TEST(cache_keep) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_cache_keep(options, true); + sentry_init(options); + + sentry_path_t *cache_path + = sentry__path_join_str(options->database_path, "cache"); + TEST_ASSERT(!!cache_path); + TEST_ASSERT(sentry__path_remove_all(cache_path) == 0); + + sentry_path_t *old_run_path + = sentry__path_join_str(options->database_path, "old.run"); + TEST_ASSERT(!!old_run_path); + TEST_ASSERT(sentry__path_create_dir_all(old_run_path) == 0); + + sentry_envelope_t *envelope = sentry__envelope_new(); + TEST_ASSERT(!!envelope); + sentry_uuid_t event_id = sentry_uuid_new_v4(); + sentry_value_t event = sentry__value_new_event_with_id(&event_id); + sentry__envelope_add_event(envelope, event); + + char *envelope_filename = sentry__uuid_as_filename(&event_id, ".envelope"); + TEST_ASSERT(!!envelope_filename); + sentry_path_t *old_envelope_path + = sentry__path_join_str(old_run_path, envelope_filename); + TEST_ASSERT( + sentry_envelope_write_to_path(envelope, old_envelope_path) == 0); + sentry_envelope_free(envelope); + + sentry_path_t *cached_envelope_path + = sentry__path_join_str(cache_path, envelope_filename); + TEST_ASSERT(!!cached_envelope_path); + + TEST_ASSERT(sentry__path_is_file(old_envelope_path)); + TEST_ASSERT(!sentry__path_is_file(cached_envelope_path)); + + sentry__process_old_runs(options, 0); + + TEST_ASSERT(!sentry__path_is_file(old_envelope_path)); + TEST_ASSERT(sentry__path_is_file(cached_envelope_path)); + + sentry__path_free(old_envelope_path); + sentry__path_free(cached_envelope_path); + sentry__path_free(old_run_path); + sentry__path_free(cache_path); + sentry_free(envelope_filename); + sentry_close(); +} + +SENTRY_TEST(cache_max_size) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_cache_keep(options, true); + sentry_options_set_cache_max_size(options, 10); // 10 kb + sentry_init(options); + + sentry_path_t *cache_path + = sentry__path_join_str(options->database_path, "cache"); + TEST_ASSERT(!!cache_path); + TEST_ASSERT(sentry__path_remove_all(cache_path) == 0); + TEST_ASSERT(sentry__path_create_dir_all(cache_path) == 0); + + // 10 x 5 kb files + for (int i = 0; i < 10; i++) { + sentry_uuid_t event_id = sentry_uuid_new_v4(); + char *filename = sentry__uuid_as_filename(&event_id, ".envelope"); + TEST_ASSERT(!!filename); + sentry_path_t *filepath = sentry__path_join_str(cache_path, filename); + sentry_free(filename); + + sentry_filewriter_t *fw = sentry__filewriter_new(filepath); + TEST_ASSERT(!!fw); + for (int j = 0; j < 500; j++) { + sentry__filewriter_write(fw, "0123456789", 10); + } + TEST_CHECK_INT_EQUAL(sentry__filewriter_byte_count(fw), 5000); + sentry__filewriter_free(fw); + sentry__path_free(filepath); + } + + sentry__cleanup_cache(options); + + int cache_count = 0; + size_t cache_size = 0; + sentry_pathiter_t *iter = sentry__path_iter_directory(cache_path); + const sentry_path_t *entry; + while (iter && (entry = sentry__pathiter_next(iter)) != NULL) { + cache_count++; + cache_size += sentry__path_get_size(entry); + } + sentry__pathiter_free(iter); + + TEST_CHECK_INT_EQUAL(cache_count, 2); + TEST_CHECK(cache_size <= 10 * 1024); + + sentry__path_free(cache_path); + sentry_close(); +} + +SENTRY_TEST(cache_max_age) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_cache_keep(options, true); + sentry_options_set_cache_max_age(options, 3); // 3 days + sentry_init(options); + + sentry_path_t *cache_path + = sentry__path_join_str(options->database_path, "cache"); + TEST_ASSERT(!!cache_path); + TEST_ASSERT(sentry__path_remove_all(cache_path) == 0); + TEST_ASSERT(sentry__path_create_dir_all(cache_path) == 0); + + // 10 files, 0-9 days old + time_t now = time(NULL); + for (int i = 0; i < 10; i++) { + sentry_uuid_t event_id = sentry_uuid_new_v4(); + char *filename = sentry__uuid_as_filename(&event_id, ".envelope"); + TEST_ASSERT(!!filename); + sentry_path_t *filepath = sentry__path_join_str(cache_path, filename); + sentry_free(filename); + + TEST_ASSERT(sentry__path_touch(filepath) == 0); + time_t mtime = now - (i * 24 * 60 * 60); // N days ago + TEST_CHECK(set_file_mtime(filepath, mtime) == 0); + sentry__path_free(filepath); + } + + sentry__cleanup_cache(options); + + int cache_count = 0; + sentry_pathiter_t *iter = sentry__path_iter_directory(cache_path); + const sentry_path_t *entry; + while (iter && (entry = sentry__pathiter_next(iter)) != NULL) { + cache_count++; + time_t mtime = sentry__path_get_mtime(entry); + TEST_CHECK(now - mtime <= (3 * 24 * 60 * 60)); + } + sentry__pathiter_free(iter); + + TEST_CHECK_INT_EQUAL(cache_count, 4); + + sentry__path_free(cache_path); + sentry_close(); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 43f531a52..e0c18a74c 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -27,6 +27,9 @@ XX(basic_write_envelope_to_file) XX(bgworker_flush) XX(breadcrumb_without_type_or_message_still_valid) XX(build_id_parser) +XX(cache_keep) +XX(cache_max_age) +XX(cache_max_size) XX(capture_minidump_basic) XX(capture_minidump_invalid_path) XX(capture_minidump_null_path) From 3410b6c34f48d23fd17b31f77006f0737786d64b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 23 Jan 2026 14:43:38 +0100 Subject: [PATCH 05/31] Respect has_breakpad --- tests/test_integration_cache.py | 41 +++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py index c4f23daab..bf6300d2f 100644 --- a/tests/test_integration_cache.py +++ b/tests/test_integration_cache.py @@ -3,13 +3,24 @@ import pytest from . import run -from .conditions import has_files +from .conditions import has_breakpad, has_files pytestmark = pytest.mark.skipif(not has_files, reason="tests need local filesystem") @pytest.mark.parametrize("cache_keep", [True, False]) -@pytest.mark.parametrize("backend", ["inproc", "breakpad"]) +@pytest.mark.parametrize( + "backend", + [ + "inproc", + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad, reason="breakpad backend not available" + ), + ), + ], +) def test_cache_keep(cmake, backend, cache_keep): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) @@ -35,7 +46,18 @@ def test_cache_keep(cmake, backend, cache_keep): assert len(cache_files) == 1 -@pytest.mark.parametrize("backend", ["inproc", "breakpad"]) +@pytest.mark.parametrize( + "backend", + [ + "inproc", + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad, reason="breakpad backend not available" + ), + ), + ], +) def test_cache_max_size(cmake, backend): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") @@ -68,7 +90,18 @@ def test_cache_max_size(cmake, backend): assert sum(f.stat().st_size for f in cache_files) <= 8 * 1000 * 1024 -@pytest.mark.parametrize("backend", ["inproc", "breakpad"]) +@pytest.mark.parametrize( + "backend", + [ + "inproc", + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad, reason="breakpad backend not available" + ), + ), + ], +) def test_cache_max_age(cmake, backend): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") From 799d2190fe3df4247fa283059b04441989f3f3c8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 23 Jan 2026 14:44:36 +0100 Subject: [PATCH 06/31] SENTRY_TRANSPORT=none --- tests/test_integration_cache.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py index bf6300d2f..119e7aaf3 100644 --- a/tests/test_integration_cache.py +++ b/tests/test_integration_cache.py @@ -22,7 +22,9 @@ ], ) def test_cache_keep(cmake, backend, cache_keep): - tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) + tmp_path = cmake( + ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} + ) run( tmp_path, @@ -59,7 +61,9 @@ def test_cache_keep(cmake, backend, cache_keep): ], ) def test_cache_max_size(cmake, backend): - tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) + tmp_path = cmake( + ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} + ) cache_dir = tmp_path.joinpath(".sentry-native/cache") # 5 x 2mb @@ -103,7 +107,9 @@ def test_cache_max_size(cmake, backend): ], ) def test_cache_max_age(cmake, backend): - tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) + tmp_path = cmake( + ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} + ) cache_dir = tmp_path.joinpath(".sentry-native/cache") for i in range(5): From 3c40494e3b905bee3471249a7a8b1bdedcc113ab Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 23 Jan 2026 15:41:27 +0100 Subject: [PATCH 07/31] Fix set_file_mtime on Windows --- tests/unit/test_cache.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index 71736b0c0..b87953b6f 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -19,9 +19,10 @@ set_file_mtime(const sentry_path_t *path, time_t mtime) #ifdef SENTRY_PLATFORM_WINDOWS HANDLE h = CreateFileW(path->path_w, FILE_WRITE_ATTRIBUTES, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - ULARGE_INTEGER ft - = { (uint64_t)(mtime * 10000000ULL + 116444736000000000ULL) }; - BOOL rv = SetFileTime(h, NULL, NULL, (FILETIME *)&ft); + // 100 ns intervals since January 1, 1601 (UTC) + uint64_t t = ((uint64_t)mtime * 10000000ULL) + 116444736000000000ULL; + FILETIME ft = { (DWORD)t, (DWORD)(t >> 32) }; + BOOL rv = SetFileTime(h, NULL, NULL, &ft); CloseHandle(h); return rv ? 0 : -1; #else From a37ac869f1ebb4d6ea46a35824044857a2e25ae3 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 23 Jan 2026 16:32:53 +0100 Subject: [PATCH 08/31] Present cache_max_age in seconds --- examples/example.c | 2 +- include/sentry.h | 8 ++++++-- src/backends/sentry_backend_crashpad.cpp | 5 +++-- src/sentry_database.c | 2 +- src/sentry_options.c | 4 ++-- src/sentry_options.h | 2 +- tests/unit/test_cache.c | 4 ++-- 7 files changed, 16 insertions(+), 11 deletions(-) diff --git a/examples/example.c b/examples/example.c index 2c86750fa..48f8cb78b 100644 --- a/examples/example.c +++ b/examples/example.c @@ -506,7 +506,7 @@ main(int argc, char **argv) if (has_arg(argc, argv, "cache-keep")) { sentry_options_set_cache_keep(options, true); sentry_options_set_cache_max_size(options, 1000 * 8); - sentry_options_set_cache_max_age(options, 5); + sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60); } if (0 != sentry_init(options)) { diff --git a/include/sentry.h b/include/sentry.h index f180f60c2..1d2eacf37 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1382,14 +1382,18 @@ SENTRY_API int sentry_options_get_symbolize_stacktraces( SENTRY_API void sentry_options_set_cache_keep( sentry_options_t *opts, int enabled); /** - * Sets the maximum size (kb)/age (days) for the cache folder. + * Sets the maximum size (kb) for the cache folder. * On startup, we check new->old entries, and remove those that go over either * boundary. */ SENTRY_API void sentry_options_set_cache_max_size( sentry_options_t *opts, size_t size); +/** + * Sets the maximum age (in seconds) for cache entries. + * Cache entries older than this value will be removed. + */ SENTRY_API void sentry_options_set_cache_max_age( - sentry_options_t *opts, int age); + sentry_options_t *opts, uint64_t age); /** * Gets the caching mode for crash reports. diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 2dbd18b8f..a132d9c9d 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -746,11 +746,12 @@ crashpad_backend_prune_database(sentry_backend_t *backend) // an embedded use-case, but minidumps on desktop can sometimes be quite // large. SENTRY_WITH_OPTIONS (options) { - data->db->CleanDatabase(60 * 60 * 24 * options->cache_max_age); + data->db->CleanDatabase(options->cache_max_age); crashpad::BinaryPruneCondition condition( crashpad::BinaryPruneCondition::OR, new crashpad::DatabaseSizePruneCondition(options->cache_max_size), - new crashpad::AgePruneCondition(options->cache_max_age)); + new crashpad::AgePruneCondition( + options->cache_max_age / (24 * 60 * 60))); crashpad::PruneCrashReportDatabase(data->db, &condition); } } diff --git a/src/sentry_database.c b/src/sentry_database.c index 890ef987b..c752bc77f 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -410,7 +410,7 @@ sentry__cleanup_cache(const sentry_options_t *options) // Calculate the age threshold time_t now = time(NULL); - time_t oldest_allowed = now - (options->cache_max_age * 24 * 60 * 60); + time_t oldest_allowed = now - options->cache_max_age; // Prune entries: iterate newest-to-oldest, accumulating size // Remove if: too old OR accumulated size exceeds limit diff --git a/src/sentry_options.c b/src/sentry_options.c index 431778894..79241723e 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -54,7 +54,7 @@ sentry_options_new(void) opts->propagate_traceparent = false; opts->crashpad_limit_stack_capture_to_sp = false; opts->cache_keep = false; - opts->cache_max_age = 2; + opts->cache_max_age = 2 * 24 * 60 * 60; opts->cache_max_size = 1024 * 8; opts->symbolize_stacktraces = // AIX doesn't have reliable debug IDs for server-side symbolication, @@ -491,7 +491,7 @@ sentry_options_set_cache_max_size(sentry_options_t *opts, size_t size) } void -sentry_options_set_cache_max_age(sentry_options_t *opts, int age) +sentry_options_set_cache_max_age(sentry_options_t *opts, uint64_t age) { opts->cache_max_age = age; } diff --git a/src/sentry_options.h b/src/sentry_options.h index 306566a11..61a4c1084 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -47,7 +47,7 @@ struct sentry_options_s { bool crashpad_limit_stack_capture_to_sp; bool cache_keep; - int cache_max_age; // TODO in days? + uint64_t cache_max_age; size_t cache_max_size; // TODO in kb? sentry_attachment_t *attachments; diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index b87953b6f..8e1347c47 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -138,7 +138,7 @@ SENTRY_TEST(cache_max_age) SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_cache_keep(options, true); - sentry_options_set_cache_max_age(options, 3); // 3 days + sentry_options_set_cache_max_age(options, 3 * 24 * 60 * 60); // 3 days sentry_init(options); sentry_path_t *cache_path @@ -147,7 +147,7 @@ SENTRY_TEST(cache_max_age) TEST_ASSERT(sentry__path_remove_all(cache_path) == 0); TEST_ASSERT(sentry__path_create_dir_all(cache_path) == 0); - // 10 files, 0-9 days old + // 10 files, 0-9 days ago time_t now = time(NULL); for (int i = 0; i < 10; i++) { sentry_uuid_t event_id = sentry_uuid_new_v4(); From 20815bddeaba7614d2c284167faddc0ff77f127f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 23 Jan 2026 16:56:27 +0100 Subject: [PATCH 09/31] Present max_cache_size in bytes --- examples/example.c | 2 +- include/sentry.h | 2 +- src/backends/sentry_backend_crashpad.cpp | 3 ++- src/sentry_database.c | 18 +++++------------- src/sentry_options.c | 2 +- src/sentry_options.h | 2 +- tests/test_integration_cache.py | 8 ++++---- tests/unit/test_cache.c | 2 +- 8 files changed, 16 insertions(+), 23 deletions(-) diff --git a/examples/example.c b/examples/example.c index 48f8cb78b..e11610306 100644 --- a/examples/example.c +++ b/examples/example.c @@ -505,7 +505,7 @@ main(int argc, char **argv) } if (has_arg(argc, argv, "cache-keep")) { sentry_options_set_cache_keep(options, true); - sentry_options_set_cache_max_size(options, 1000 * 8); + sentry_options_set_cache_max_size(options, 4 * 1024 * 1024); sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60); } diff --git a/include/sentry.h b/include/sentry.h index 1d2eacf37..9c6acbaf2 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1382,7 +1382,7 @@ SENTRY_API int sentry_options_get_symbolize_stacktraces( SENTRY_API void sentry_options_set_cache_keep( sentry_options_t *opts, int enabled); /** - * Sets the maximum size (kb) for the cache folder. + * Sets the maximum size (in bytes) for the cache folder. * On startup, we check new->old entries, and remove those that go over either * boundary. */ diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index a132d9c9d..dd8aa43fc 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -749,7 +749,8 @@ crashpad_backend_prune_database(sentry_backend_t *backend) data->db->CleanDatabase(options->cache_max_age); crashpad::BinaryPruneCondition condition( crashpad::BinaryPruneCondition::OR, - new crashpad::DatabaseSizePruneCondition(options->cache_max_size), + new crashpad::DatabaseSizePruneCondition( + options->cache_max_size / 1024), new crashpad::AgePruneCondition( options->cache_max_age / (24 * 60 * 60))); crashpad::PruneCrashReportDatabase(data->db, &condition); diff --git a/src/sentry_database.c b/src/sentry_database.c index c752bc77f..bfa61bf8d 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -323,17 +323,9 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) typedef struct { sentry_path_t *path; time_t mtime; - size_t size_in_kb; + size_t size; } cache_entry_t; -static size_t -get_file_size_in_kb(const sentry_path_t *path) -{ - size_t bytes = sentry__path_get_size(path); - // Round up to next KB boundary - return (bytes + 1023) / 1024; -} - /** * Comparison function to sort cache entries by mtime, newest first. */ @@ -398,7 +390,7 @@ sentry__cleanup_cache(const sentry_options_t *options) entries[entries_count].path = sentry__path_clone(entry); entries[entries_count].mtime = sentry__path_get_mtime(entry); - entries[entries_count].size_in_kb = get_file_size_in_kb(entry); + entries[entries_count].size = sentry__path_get_size(entry); entries_count++; } sentry__pathiter_free(iter); @@ -414,7 +406,7 @@ sentry__cleanup_cache(const sentry_options_t *options) // Prune entries: iterate newest-to-oldest, accumulating size // Remove if: too old OR accumulated size exceeds limit - size_t accumulated_size_kb = 0; + size_t accumulated_size = 0; for (size_t i = 0; i < entries_count; i++) { bool should_prune = false; @@ -424,9 +416,9 @@ sentry__cleanup_cache(const sentry_options_t *options) } // Size-based pruning (accumulate size as we go, like crashpad) - accumulated_size_kb += entries[i].size_in_kb; + accumulated_size += entries[i].size; if (options->cache_max_size > 0 - && accumulated_size_kb > (size_t)options->cache_max_size) { + && accumulated_size > options->cache_max_size) { should_prune = true; } diff --git a/src/sentry_options.c b/src/sentry_options.c index 79241723e..87fdf0afb 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -55,7 +55,7 @@ sentry_options_new(void) opts->crashpad_limit_stack_capture_to_sp = false; opts->cache_keep = false; opts->cache_max_age = 2 * 24 * 60 * 60; - opts->cache_max_size = 1024 * 8; + opts->cache_max_size = 8 * 1024 * 1024; opts->symbolize_stacktraces = // AIX doesn't have reliable debug IDs for server-side symbolication, // and the diversity of Android makes it infeasible to have access to debug diff --git a/src/sentry_options.h b/src/sentry_options.h index 61a4c1084..75018a8be 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -48,7 +48,7 @@ struct sentry_options_s { bool cache_keep; uint64_t cache_max_age; - size_t cache_max_size; // TODO in kb? + size_t cache_max_size; sentry_attachment_t *attachments; sentry_run_t *run; diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py index 119e7aaf3..fc717e72b 100644 --- a/tests/test_integration_cache.py +++ b/tests/test_integration_cache.py @@ -79,7 +79,7 @@ def test_cache_max_size(cmake, backend): cache_files = list(cache_dir.glob("*.envelope")) for f in cache_files: with open(f, "r+b") as file: - file.truncate(2048 * 1000) + file.truncate(2 * 1024 * 1024) run( tmp_path, @@ -87,11 +87,11 @@ def test_cache_max_size(cmake, backend): ["log", "cache-keep", "no-setup"], ) - # max 8mb + # max 4mb assert cache_dir.exists() cache_files = list(cache_dir.glob("*.envelope")) - assert len(cache_files) <= 4 - assert sum(f.stat().st_size for f in cache_files) <= 8 * 1000 * 1024 + assert len(cache_files) <= 2 + assert sum(f.stat().st_size for f in cache_files) <= 4 * 1024 * 1024 @pytest.mark.parametrize( diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index 8e1347c47..b089e38ac 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -87,7 +87,7 @@ SENTRY_TEST(cache_max_size) SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_cache_keep(options, true); - sentry_options_set_cache_max_size(options, 10); // 10 kb + sentry_options_set_cache_max_size(options, 10 * 1024); // 10 kb sentry_init(options); sentry_path_t *cache_path From ef96547061258c2e2051eb108fa9e7465656cc14 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 23 Jan 2026 17:14:15 +0100 Subject: [PATCH 10/31] Tweak docs & signatures --- include/sentry.h | 24 +++++++++++++++--------- src/sentry_options.c | 8 ++++---- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index 9c6acbaf2..0e528312b 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1376,24 +1376,30 @@ SENTRY_API int sentry_options_get_symbolize_stacktraces( const sentry_options_t *opts); /** - * Sets whether we should keep files cached even when sent successfully. - * The database will be cleared based on cache_max_size and cache_max_age + * Enables or disables storing envelopes in a persistent cache. + * + * When enabled, envelopes are written to a `cache/` subdirectory within the + * database directory and retained regardless of send success or failure. + * The cache is cleared on startup based on the cache_max_size and cache_max_age + * options. */ SENTRY_API void sentry_options_set_cache_keep( sentry_options_t *opts, int enabled); + /** - * Sets the maximum size (in bytes) for the cache folder. - * On startup, we check new->old entries, and remove those that go over either - * boundary. + * Sets the maximum size (in bytes) for the cache directory. + * On startup, cached entries are removed from oldest to newest until the + * directory size is within the max size limit. */ SENTRY_API void sentry_options_set_cache_max_size( - sentry_options_t *opts, size_t size); + sentry_options_t *opts, size_t bytes); + /** - * Sets the maximum age (in seconds) for cache entries. - * Cache entries older than this value will be removed. + * Sets the maximum age (in seconds) for cache entries in the cache directory. + * On startup, cached entries exceeding the max age limit are removed. */ SENTRY_API void sentry_options_set_cache_max_age( - sentry_options_t *opts, uint64_t age); + sentry_options_t *opts, uint64_t seconds); /** * Gets the caching mode for crash reports. diff --git a/src/sentry_options.c b/src/sentry_options.c index 87fdf0afb..b68b2d3b6 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -485,15 +485,15 @@ sentry_options_set_cache_keep(sentry_options_t *opts, int enabled) } void -sentry_options_set_cache_max_size(sentry_options_t *opts, size_t size) +sentry_options_set_cache_max_size(sentry_options_t *opts, size_t bytes) { - opts->cache_max_size = size; + opts->cache_max_size = bytes; } void -sentry_options_set_cache_max_age(sentry_options_t *opts, uint64_t age) +sentry_options_set_cache_max_age(sentry_options_t *opts, uint64_t seconds) { - opts->cache_max_age = age; + opts->cache_max_age = seconds; } int From e96d9d40ecaaea0353b8f51272766cc78915431b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 23 Jan 2026 18:41:02 +0100 Subject: [PATCH 11/31] Fix warning --- src/backends/sentry_backend_crashpad.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index dd8aa43fc..007926197 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -752,7 +752,7 @@ crashpad_backend_prune_database(sentry_backend_t *backend) new crashpad::DatabaseSizePruneCondition( options->cache_max_size / 1024), new crashpad::AgePruneCondition( - options->cache_max_age / (24 * 60 * 60))); + static_cast(options->cache_max_age / (24 * 60 * 60)))); crashpad::PruneCrashReportDatabase(data->db, &condition); } } From 93aefb2172dc36940f813157f6ade5034bb62aa6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 24 Jan 2026 11:53:54 +0100 Subject: [PATCH 12/31] Fix test_unit::cache_max_age --- tests/unit/test_cache.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index b089e38ac..712860afb 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -138,7 +138,7 @@ SENTRY_TEST(cache_max_age) SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_cache_keep(options, true); - sentry_options_set_cache_max_age(options, 3 * 24 * 60 * 60); // 3 days + sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60); // 5 days sentry_init(options); sentry_path_t *cache_path @@ -147,9 +147,9 @@ SENTRY_TEST(cache_max_age) TEST_ASSERT(sentry__path_remove_all(cache_path) == 0); TEST_ASSERT(sentry__path_create_dir_all(cache_path) == 0); - // 10 files, 0-9 days ago + // 0,2,4,6,8 days ago time_t now = time(NULL); - for (int i = 0; i < 10; i++) { + for (int i = 0; i < 5; i++) { sentry_uuid_t event_id = sentry_uuid_new_v4(); char *filename = sentry__uuid_as_filename(&event_id, ".envelope"); TEST_ASSERT(!!filename); @@ -157,7 +157,7 @@ SENTRY_TEST(cache_max_age) sentry_free(filename); TEST_ASSERT(sentry__path_touch(filepath) == 0); - time_t mtime = now - (i * 24 * 60 * 60); // N days ago + time_t mtime = now - (i * 2 * 24 * 60 * 60); // N days ago TEST_CHECK(set_file_mtime(filepath, mtime) == 0); sentry__path_free(filepath); } @@ -170,11 +170,11 @@ SENTRY_TEST(cache_max_age) while (iter && (entry = sentry__pathiter_next(iter)) != NULL) { cache_count++; time_t mtime = sentry__path_get_mtime(entry); - TEST_CHECK(now - mtime <= (3 * 24 * 60 * 60)); + TEST_CHECK(now - mtime <= (5 * 24 * 60 * 60)); } sentry__pathiter_free(iter); - TEST_CHECK_INT_EQUAL(cache_count, 4); + TEST_CHECK_INT_EQUAL(cache_count, 3); sentry__path_free(cache_path); sentry_close(); From f4936d59f42b1d9d84acc3b82779a26dfded38dc Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 24 Jan 2026 13:02:02 +0100 Subject: [PATCH 13/31] Fix sign conversion warning on Windows Cast cache_max_age to time_t to avoid implicit signedness conversion between uint64_t and time_t. Co-Authored-By: Claude Opus 4.5 --- src/sentry_database.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index bfa61bf8d..0e88d48f4 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -402,7 +402,7 @@ sentry__cleanup_cache(const sentry_options_t *options) // Calculate the age threshold time_t now = time(NULL); - time_t oldest_allowed = now - options->cache_max_age; + time_t oldest_allowed = now - (time_t)options->cache_max_age; // Prune entries: iterate newest-to-oldest, accumulating size // Remove if: too old OR accumulated size exceeds limit From 7eddee44af90da27880d20a93f8ec2937cfbe238 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 24 Jan 2026 13:12:26 +0100 Subject: [PATCH 14/31] Add changelog entry for offline caching feature Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 022429752..6dd652639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +**Features**: + +- Add new offline caching options to persist envelopes locally, currently supported with the `inproc` and `breakpad` backends: `sentry_options_set_cache_keep`, `sentry_options_set_cache_max_size`, and `sentry_options_set_cache_max_age`. ([#1490](https://github.com/getsentry/sentry-native/pull/1490)) + **Fixes**: - Crashpad: namespace mpack to avoid ODR violation. ([#1476](https://github.com/getsentry/sentry-native/pull/1476), [crashpad#143](https://github.com/getsentry/crashpad/pull/143)) From 8d40ec189b43d8fb5b014e3e6d3b5f5c43a736ec Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 24 Jan 2026 13:50:56 +0100 Subject: [PATCH 15/31] Fix sign conversion warning in CleanDatabase call Add explicit static_cast() for cache_max_age to fix -Wsign-conversion warning on Windows with clang-cl. Co-Authored-By: Claude Opus 4.5 --- src/backends/sentry_backend_crashpad.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 007926197..230a31f95 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -746,7 +746,7 @@ crashpad_backend_prune_database(sentry_backend_t *backend) // an embedded use-case, but minidumps on desktop can sometimes be quite // large. SENTRY_WITH_OPTIONS (options) { - data->db->CleanDatabase(options->cache_max_age); + data->db->CleanDatabase(static_cast(options->cache_max_age)); crashpad::BinaryPruneCondition condition( crashpad::BinaryPruneCondition::OR, new crashpad::DatabaseSizePruneCondition( From b28663f5680ece14b1850932a206ab29f7784348 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 26 Jan 2026 10:25:22 +0100 Subject: [PATCH 16/31] Clarify test_integration_cache --- tests/test_integration_cache.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py index fc717e72b..2eca8dfd3 100644 --- a/tests/test_integration_cache.py +++ b/tests/test_integration_cache.py @@ -25,7 +25,9 @@ def test_cache_keep(cmake, backend, cache_keep): tmp_path = cmake( ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} ) + cache_dir = tmp_path.joinpath(".sentry-native/cache") + # capture run( tmp_path, "sentry_example", @@ -33,9 +35,9 @@ def test_cache_keep(cmake, backend, cache_keep): expect_failure=True, ) - cache_dir = tmp_path.joinpath(".sentry-native/cache") assert not cache_dir.exists() or len(list(cache_dir.glob("*.envelope"))) == 0 + # cache run( tmp_path, "sentry_example", From ab62f90b922fc5f604a5ee8c80ef53260eebf875 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 27 Jan 2026 13:54:32 +0100 Subject: [PATCH 17/31] Change cache_max_age type from uint64_t to time_t For consistency with time-related operations throughout the codebase. This adds a time.h dependency to the public header, but it's a lightweight standard C header available since C89. Co-Authored-By: Claude Opus 4.5 --- include/sentry.h | 3 ++- src/backends/sentry_backend_crashpad.cpp | 2 +- src/sentry_database.c | 2 +- src/sentry_options.c | 2 +- src/sentry_options.h | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index 0e528312b..c026f31f1 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -165,6 +165,7 @@ extern "C" { #include #include #include +#include /* context type dependencies */ #ifdef _WIN32 @@ -1399,7 +1400,7 @@ SENTRY_API void sentry_options_set_cache_max_size( * On startup, cached entries exceeding the max age limit are removed. */ SENTRY_API void sentry_options_set_cache_max_age( - sentry_options_t *opts, uint64_t seconds); + sentry_options_t *opts, time_t seconds); /** * Gets the caching mode for crash reports. diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 230a31f95..007926197 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -746,7 +746,7 @@ crashpad_backend_prune_database(sentry_backend_t *backend) // an embedded use-case, but minidumps on desktop can sometimes be quite // large. SENTRY_WITH_OPTIONS (options) { - data->db->CleanDatabase(static_cast(options->cache_max_age)); + data->db->CleanDatabase(options->cache_max_age); crashpad::BinaryPruneCondition condition( crashpad::BinaryPruneCondition::OR, new crashpad::DatabaseSizePruneCondition( diff --git a/src/sentry_database.c b/src/sentry_database.c index 0e88d48f4..bfa61bf8d 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -402,7 +402,7 @@ sentry__cleanup_cache(const sentry_options_t *options) // Calculate the age threshold time_t now = time(NULL); - time_t oldest_allowed = now - (time_t)options->cache_max_age; + time_t oldest_allowed = now - options->cache_max_age; // Prune entries: iterate newest-to-oldest, accumulating size // Remove if: too old OR accumulated size exceeds limit diff --git a/src/sentry_options.c b/src/sentry_options.c index b68b2d3b6..e86d7852e 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -491,7 +491,7 @@ sentry_options_set_cache_max_size(sentry_options_t *opts, size_t bytes) } void -sentry_options_set_cache_max_age(sentry_options_t *opts, uint64_t seconds) +sentry_options_set_cache_max_age(sentry_options_t *opts, time_t seconds) { opts->cache_max_age = seconds; } diff --git a/src/sentry_options.h b/src/sentry_options.h index 75018a8be..e91ddfbe7 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -47,7 +47,7 @@ struct sentry_options_s { bool crashpad_limit_stack_capture_to_sp; bool cache_keep; - uint64_t cache_max_age; + time_t cache_max_age; size_t cache_max_size; sentry_attachment_t *attachments; From 9e09bd013ac464110a6e55427a37ced792f6b2b4 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 2 Feb 2026 16:40:06 +0100 Subject: [PATCH 18/31] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f591565b3..37f29574f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Features**: + +- Add new offline caching options to persist envelopes locally, currently supported with the `inproc` and `breakpad` backends: `sentry_options_set_cache_keep`, `sentry_options_set_cache_max_size`, and `sentry_options_set_cache_max_age`. ([#1490](https://github.com/getsentry/sentry-native/pull/1490)) + ## 0.12.5 **Features**: @@ -12,10 +18,6 @@ ## 0.12.4 -**Features**: - -- Add new offline caching options to persist envelopes locally, currently supported with the `inproc` and `breakpad` backends: `sentry_options_set_cache_keep`, `sentry_options_set_cache_max_size`, and `sentry_options_set_cache_max_age`. ([#1490](https://github.com/getsentry/sentry-native/pull/1490)) - **Fixes**: - Crashpad: namespace mpack to avoid ODR violation. ([#1476](https://github.com/getsentry/sentry-native/pull/1476), [crashpad#143](https://github.com/getsentry/crashpad/pull/143)) From 747edb1f6ced5b8308b7ed5367c90f48eaf0b78f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 3 Feb 2026 12:16:59 +0100 Subject: [PATCH 19/31] Log warning when envelope caching fails Co-Authored-By: Claude Opus 4.5 --- src/sentry_database.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index bfa61bf8d..a32a05564 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -293,7 +293,10 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) if (options->cache_keep) { sentry_path_t *cached_file = sentry__path_join_str( cache_dir, sentry__path_filename(file)); - sentry__path_rename(file, cached_file); + if (sentry__path_rename(file, cached_file) != 0) { + SENTRY_WARNF("failed to cache envelope \"%s\"", + sentry__path_filename(file)); + } sentry__path_free(cached_file); continue; } From b1a4671acc439a87d8a6bd5666adb9f9685bb89c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 3 Feb 2026 12:34:21 +0100 Subject: [PATCH 20/31] Revise crashpad_backend_prune_database --- src/backends/sentry_backend_crashpad.cpp | 31 ++++++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index f073b0279..4910369c1 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -750,14 +750,29 @@ crashpad_backend_prune_database(sentry_backend_t *backend) // an embedded use-case, but minidumps on desktop can sometimes be quite // large. SENTRY_WITH_OPTIONS (options) { - data->db->CleanDatabase(options->cache_max_age); - crashpad::BinaryPruneCondition condition( - crashpad::BinaryPruneCondition::OR, - new crashpad::DatabaseSizePruneCondition( - options->cache_max_size / 1024), - new crashpad::AgePruneCondition( - static_cast(options->cache_max_age / (24 * 60 * 60)))); - crashpad::PruneCrashReportDatabase(data->db, &condition); + if (options->cache_max_age > 0) { + data->db->CleanDatabase(options->cache_max_age); + } + + size_t max_kb = options->cache_max_size / 1024; + int max_days + = static_cast(options->cache_max_age / (24 * 60 * 60)); + + std::unique_ptr condition; + if (max_kb > 0 && max_days > 0) { + condition = std::make_unique( + crashpad::BinaryPruneCondition::OR, + new crashpad::DatabaseSizePruneCondition(max_kb), + new crashpad::AgePruneCondition(max_days)); + } else if (max_kb > 0) { + condition = std::make_unique( + max_kb); + } else if (max_days > 0) { + condition = std::make_unique(max_days); + } + if (condition) { + crashpad::PruneCrashReportDatabase(data->db, condition.get()); + } } } From b101fce6fbcd92dafe5d6369d38b493bb8ab3e89 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 3 Feb 2026 12:53:25 +0100 Subject: [PATCH 21/31] Fix cache size calculation to exclude pruned files Subtract file size from accumulated_size when a file is pruned (by age or size). Previously, pruned files' sizes were still counted, causing subsequent valid files to be incorrectly pruned when both cache_max_age and cache_max_size were set. Co-Authored-By: Claude Opus 4.5 --- src/sentry_database.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry_database.c b/src/sentry_database.c index a32a05564..b4fe52cfd 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -426,6 +426,7 @@ sentry__cleanup_cache(const sentry_options_t *options) } if (should_prune) { + accumulated_size -= entries[i].size; sentry__path_remove_all(entries[i].path); } sentry__path_free(entries[i].path); From 2e1c63b103bb6f728e6485316ec9a5341dae66aa Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 3 Feb 2026 13:00:18 +0100 Subject: [PATCH 22/31] Add NULL checks after path allocations in cache handling Co-Authored-By: Claude Opus 4.5 --- src/sentry_database.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index b4fe52cfd..7162d9d8c 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -242,7 +242,9 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry_path_t *cache_dir = NULL; if (options->cache_keep) { cache_dir = sentry__path_join_str(options->database_path, "cache"); - sentry__path_create_dir_all(cache_dir); + if (cache_dir) { + sentry__path_create_dir_all(cache_dir); + } } sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); @@ -290,10 +292,11 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry_envelope_t *envelope = sentry__envelope_from_path(file); sentry__capture_envelope(options->transport, envelope); - if (options->cache_keep) { + if (cache_dir) { sentry_path_t *cached_file = sentry__path_join_str( cache_dir, sentry__path_filename(file)); - if (sentry__path_rename(file, cached_file) != 0) { + if (!cached_file + || sentry__path_rename(file, cached_file) != 0) { SENTRY_WARNF("failed to cache envelope \"%s\"", sentry__path_filename(file)); } @@ -356,7 +359,7 @@ sentry__cleanup_cache(const sentry_options_t *options) sentry_path_t *cache_dir = sentry__path_join_str(options->database_path, "cache"); - if (!sentry__path_is_dir(cache_dir)) { + if (!cache_dir || !sentry__path_is_dir(cache_dir)) { sentry__path_free(cache_dir); return; } From e0cb46e2dcf6d6d8fc04338aaf6eb3cd826f4dfe Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 3 Feb 2026 13:13:12 +0100 Subject: [PATCH 23/31] Add NULL check after path clone in sentry__cleanup_cache Co-Authored-By: Claude Opus 4.5 --- src/sentry_database.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sentry_database.c b/src/sentry_database.c index 7162d9d8c..59385016d 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -395,6 +395,9 @@ sentry__cleanup_cache(const sentry_options_t *options) } entries[entries_count].path = sentry__path_clone(entry); + if (!entries[entries_count].path) { + break; + } entries[entries_count].mtime = sentry__path_get_mtime(entry); entries[entries_count].size = sentry__path_get_size(entry); entries_count++; From 317968c1434665600fe7f2cd9ceb1066eb04d35e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 3 Feb 2026 13:34:15 +0100 Subject: [PATCH 24/31] Fix cache size pruning to remove all older entries once limit hit Move size accumulation into else branch so age-pruned files don't count toward size limit. Remove the size subtraction on prune which caused bin-packing behavior (keeping older smaller files while removing newer larger ones). Add test for size-based pruning order. Co-Authored-By: Claude Opus 4.5 --- src/sentry_database.c | 15 +++++----- tests/unit/test_cache.c | 64 +++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 59385016d..e4c9e8bf7 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -422,17 +422,16 @@ sentry__cleanup_cache(const sentry_options_t *options) // Age-based pruning if (options->cache_max_age > 0 && entries[i].mtime < oldest_allowed) { should_prune = true; - } - - // Size-based pruning (accumulate size as we go, like crashpad) - accumulated_size += entries[i].size; - if (options->cache_max_size > 0 - && accumulated_size > options->cache_max_size) { - should_prune = true; + } else { + // Size-based pruning (accumulate size as we go, like crashpad) + accumulated_size += entries[i].size; + if (options->cache_max_size > 0 + && accumulated_size > options->cache_max_size) { + should_prune = true; + } } if (should_prune) { - accumulated_size -= entries[i].size; sentry__path_remove_all(entries[i].path); } sentry__path_free(entries[i].path); diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index 712860afb..ca170d9d1 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -179,3 +179,67 @@ SENTRY_TEST(cache_max_age) sentry__path_free(cache_path); sentry_close(); } + +SENTRY_TEST(cache_max_size_and_age) +{ + // Verify size pruning keeps newer entries, removes all older once limit + // hit. A (5KB), B (6KB), C (3KB) newest-to-oldest, max_size=10KB A+B=11KB + // exceeds limit -> B pruned, C (older) also pruned + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_cache_keep(options, true); + sentry_options_set_cache_max_size(options, 10 * 1024); // 10 KB + sentry_init(options); + + sentry_path_t *cache_path + = sentry__path_join_str(options->database_path, "cache"); + TEST_ASSERT(!!cache_path); + TEST_ASSERT(sentry__path_remove_all(cache_path) == 0); + TEST_ASSERT(sentry__path_create_dir_all(cache_path) == 0); + + time_t now = time(NULL); + + // A (newest, 5KB), B (middle, 6KB), C (oldest, 3KB) + struct { + const char *name; + size_t size; + time_t age; + } files[] = { + { "a.envelope", 5 * 1024, 0 }, // newest + { "b.envelope", 6 * 1024, 60 }, // 1 min old + { "c.envelope", 3 * 1024, 120 }, // 2 min old + }; + + for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); i++) { + sentry_path_t *filepath + = sentry__path_join_str(cache_path, files[i].name); + TEST_ASSERT(!!filepath); + + sentry_filewriter_t *fw = sentry__filewriter_new(filepath); + TEST_ASSERT(!!fw); + for (size_t j = 0; j < files[i].size / 10; j++) { + sentry__filewriter_write(fw, "0123456789", 10); + } + sentry__filewriter_free(fw); + + TEST_CHECK(set_file_mtime(filepath, now - files[i].age) == 0); + sentry__path_free(filepath); + } + + sentry__cleanup_cache(options); + + // Verify: A kept, B and C removed (once limit hit, all older removed) + sentry_path_t *a_path = sentry__path_join_str(cache_path, "a.envelope"); + sentry_path_t *b_path = sentry__path_join_str(cache_path, "b.envelope"); + sentry_path_t *c_path = sentry__path_join_str(cache_path, "c.envelope"); + + TEST_CHECK(sentry__path_is_file(a_path)); // newest, kept + TEST_CHECK(!sentry__path_is_file(b_path)); // size-pruned + TEST_CHECK(!sentry__path_is_file(c_path)); // older than B, also pruned + + sentry__path_free(a_path); + sentry__path_free(b_path); + sentry__path_free(c_path); + sentry__path_free(cache_path); + sentry_close(); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 513881ca4..c3b1530b1 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -30,6 +30,7 @@ XX(build_id_parser) XX(cache_keep) XX(cache_max_age) XX(cache_max_size) +XX(cache_max_size_and_age) XX(capture_minidump_basic) XX(capture_minidump_invalid_path) XX(capture_minidump_null_path) From e30846f2910c515b49989a149c9ab05477204f63 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Feb 2026 10:29:27 +0100 Subject: [PATCH 25/31] Add INVALID_HANDLE_VALUE check and use TEST_ASSERT for set_file_mtime - Check CreateFileW return value before calling SetFileTime/CloseHandle - Change TEST_CHECK to TEST_ASSERT since mtime setup is a precondition Co-Authored-By: Claude Opus 4.5 --- tests/unit/test_cache.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index ca170d9d1..2d474a143 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -19,6 +19,9 @@ set_file_mtime(const sentry_path_t *path, time_t mtime) #ifdef SENTRY_PLATFORM_WINDOWS HANDLE h = CreateFileW(path->path_w, FILE_WRITE_ATTRIBUTES, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (h == INVALID_HANDLE_VALUE) { + return -1; + } // 100 ns intervals since January 1, 1601 (UTC) uint64_t t = ((uint64_t)mtime * 10000000ULL) + 116444736000000000ULL; FILETIME ft = { (DWORD)t, (DWORD)(t >> 32) }; @@ -158,7 +161,7 @@ SENTRY_TEST(cache_max_age) TEST_ASSERT(sentry__path_touch(filepath) == 0); time_t mtime = now - (i * 2 * 24 * 60 * 60); // N days ago - TEST_CHECK(set_file_mtime(filepath, mtime) == 0); + TEST_ASSERT(set_file_mtime(filepath, mtime) == 0); sentry__path_free(filepath); } @@ -222,7 +225,7 @@ SENTRY_TEST(cache_max_size_and_age) } sentry__filewriter_free(fw); - TEST_CHECK(set_file_mtime(filepath, now - files[i].age) == 0); + TEST_ASSERT(set_file_mtime(filepath, now - files[i].age) == 0); sentry__path_free(filepath); } From 1d565d6db37d01bb0839754f76b3ab8eab8376a8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Feb 2026 10:31:32 +0100 Subject: [PATCH 26/31] Remove redundant conditional around sentry__path_free cache_dir is NULL when cache_keep is false, and sentry__path_free handles NULL safely. Co-Authored-By: Claude Opus 4.5 --- src/sentry_database.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index e4c9e8bf7..8dc6d4b23 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -309,10 +309,7 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) } sentry__pathiter_free(run_iter); - if (options->cache_keep) { - sentry__path_free(cache_dir); - } - + sentry__path_free(cache_dir); sentry__path_remove_all(run_dir); sentry__filelock_free(lock); } From 76a5f81673e48ef4e92eaac7de041144de00a5d2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Feb 2026 11:24:46 +0100 Subject: [PATCH 27/31] Replace crashpad prune conditions with custom implementations Crashpad's AgePruneCondition and DatabaseSizePruneCondition only accept days and KB respectively, causing precision loss for sub-day ages and sub-KB sizes. Replace them with MaxAgePruneCondition (seconds) and MaxSizePruneCondition (bytes) that handle zero as "no limit" internally. Co-Authored-By: Claude Opus 4.5 --- src/backends/sentry_backend_crashpad.cpp | 67 +++++++++++++++++------- 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 4910369c1..0c4bd7452 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -740,6 +740,49 @@ crashpad_backend_last_crash(sentry_backend_t *backend) return crash_time; } +// seconds-based alternative to crashpad::AgePruneCondition (days) +class MaxAgePruneCondition final : public crashpad::PruneCondition { +public: + explicit MaxAgePruneCondition(time_t max_age) + : max_age_(max_age) + , oldest_report_time_(time(nullptr) - max_age) + { + } + + bool + ShouldPruneReport( + const crashpad::CrashReportDatabase::Report &report) override + { + return max_age_ > 0 && report.creation_time < oldest_report_time_; + } + +private: + const time_t max_age_; + const time_t oldest_report_time_; +}; + +// bytes-based alternative to crashpad::DatabaseSizePruneCondition (kb) +class MaxSizePruneCondition final : public crashpad::PruneCondition { +public: + explicit MaxSizePruneCondition(size_t max_size) + : max_size_(max_size) + , measured_size_(0) + { + } + + bool + ShouldPruneReport( + const crashpad::CrashReportDatabase::Report &report) override + { + measured_size_ += report.total_size; + return max_size_ > 0 && measured_size_ > max_size_; + } + +private: + const size_t max_size_; + size_t measured_size_; +}; + static void crashpad_backend_prune_database(sentry_backend_t *backend) { @@ -754,25 +797,11 @@ crashpad_backend_prune_database(sentry_backend_t *backend) data->db->CleanDatabase(options->cache_max_age); } - size_t max_kb = options->cache_max_size / 1024; - int max_days - = static_cast(options->cache_max_age / (24 * 60 * 60)); - - std::unique_ptr condition; - if (max_kb > 0 && max_days > 0) { - condition = std::make_unique( - crashpad::BinaryPruneCondition::OR, - new crashpad::DatabaseSizePruneCondition(max_kb), - new crashpad::AgePruneCondition(max_days)); - } else if (max_kb > 0) { - condition = std::make_unique( - max_kb); - } else if (max_days > 0) { - condition = std::make_unique(max_days); - } - if (condition) { - crashpad::PruneCrashReportDatabase(data->db, condition.get()); - } + crashpad::BinaryPruneCondition condition( + crashpad::BinaryPruneCondition::OR, + new MaxSizePruneCondition(options->cache_max_size), + new MaxAgePruneCondition(options->cache_max_age)); + crashpad::PruneCrashReportDatabase(data->db, &condition); } } From f0e5075d6d2b28a849c3b35752d9345379001544 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Feb 2026 12:05:45 +0100 Subject: [PATCH 28/31] Fix size_t conversion warning on 32-bit Windows Co-Authored-By: Claude Opus 4.5 --- src/backends/sentry_backend_crashpad.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 0c4bd7452..0e6538bcc 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -774,7 +774,7 @@ class MaxSizePruneCondition final : public crashpad::PruneCondition { ShouldPruneReport( const crashpad::CrashReportDatabase::Report &report) override { - measured_size_ += report.total_size; + measured_size_ += static_cast(report.total_size); return max_size_ > 0 && measured_size_ > max_size_; } From 25da5e151f78bfe4693fb8c161a6b09b4812a96e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Feb 2026 16:40:55 +0100 Subject: [PATCH 29/31] Add cache_max_items option for Android & Cocoa compat Co-Authored-By: Claude Opus 4.5 --- examples/example.c | 1 + include/sentry.h | 14 ++++++++ src/backends/sentry_backend_crashpad.cpp | 26 ++++++++++++-- src/sentry_database.c | 4 +++ src/sentry_options.c | 7 ++++ src/sentry_options.h | 1 + tests/test_integration_cache.py | 38 ++++++++++++++++++++ tests/unit/test_cache.c | 44 ++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 9 files changed, 134 insertions(+), 2 deletions(-) diff --git a/examples/example.c b/examples/example.c index 75bcc1612..8e825d57b 100644 --- a/examples/example.c +++ b/examples/example.c @@ -507,6 +507,7 @@ main(int argc, char **argv) sentry_options_set_cache_keep(options, true); sentry_options_set_cache_max_size(options, 4 * 1024 * 1024); sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60); + sentry_options_set_cache_max_items(options, 5); } if (0 != sentry_init(options)) { diff --git a/include/sentry.h b/include/sentry.h index 4052758b3..8158482c1 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1391,6 +1391,8 @@ SENTRY_API void sentry_options_set_cache_keep( * Sets the maximum size (in bytes) for the cache directory. * On startup, cached entries are removed from oldest to newest until the * directory size is within the max size limit. + * + * Defaults to 8 MB (8 * 1024 * 1024). */ SENTRY_API void sentry_options_set_cache_max_size( sentry_options_t *opts, size_t bytes); @@ -1398,10 +1400,22 @@ SENTRY_API void sentry_options_set_cache_max_size( /** * Sets the maximum age (in seconds) for cache entries in the cache directory. * On startup, cached entries exceeding the max age limit are removed. + * + * Defaults to 2 days (2 * 24 * 60 * 60). */ SENTRY_API void sentry_options_set_cache_max_age( sentry_options_t *opts, time_t seconds); +/** + * Sets the maximum number of items in the cache directory. + * On startup, cached entries are removed from oldest to newest until the + * directory contains at most the specified number of items. + * + * Defaults to 30. + */ +SENTRY_API void sentry_options_set_cache_max_items( + sentry_options_t *opts, size_t items); + /** * Gets the caching mode for crash reports. */ diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 0e6538bcc..bf6a08bd6 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -783,6 +783,25 @@ class MaxSizePruneCondition final : public crashpad::PruneCondition { size_t measured_size_; }; +class MaxItemsPruneCondition final : public crashpad::PruneCondition { +public: + explicit MaxItemsPruneCondition(size_t max_items) + : max_items_(max_items) + , item_count_(0) + { + } + + bool + ShouldPruneReport(const crashpad::CrashReportDatabase::Report &) override + { + return max_items_ > 0 && ++item_count_ > max_items_; + } + +private: + const size_t max_items_; + size_t item_count_; +}; + static void crashpad_backend_prune_database(sentry_backend_t *backend) { @@ -799,8 +818,11 @@ crashpad_backend_prune_database(sentry_backend_t *backend) crashpad::BinaryPruneCondition condition( crashpad::BinaryPruneCondition::OR, - new MaxSizePruneCondition(options->cache_max_size), - new MaxAgePruneCondition(options->cache_max_age)); + new MaxItemsPruneCondition(options->cache_max_items), + new crashpad::BinaryPruneCondition( + crashpad::BinaryPruneCondition::OR, + new MaxSizePruneCondition(options->cache_max_size), + new MaxAgePruneCondition(options->cache_max_age))); crashpad::PruneCrashReportDatabase(data->db, &condition); } } diff --git a/src/sentry_database.c b/src/sentry_database.c index 8dc6d4b23..45f8b8eb1 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -426,6 +426,10 @@ sentry__cleanup_cache(const sentry_options_t *options) && accumulated_size > options->cache_max_size) { should_prune = true; } + // Item count pruning + if (options->cache_max_items > 0 && i >= options->cache_max_items) { + should_prune = true; + } } if (should_prune) { diff --git a/src/sentry_options.c b/src/sentry_options.c index e86d7852e..2349dccb9 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -56,6 +56,7 @@ sentry_options_new(void) opts->cache_keep = false; opts->cache_max_age = 2 * 24 * 60 * 60; opts->cache_max_size = 8 * 1024 * 1024; + opts->cache_max_items = 30; opts->symbolize_stacktraces = // AIX doesn't have reliable debug IDs for server-side symbolication, // and the diversity of Android makes it infeasible to have access to debug @@ -496,6 +497,12 @@ sentry_options_set_cache_max_age(sentry_options_t *opts, time_t seconds) opts->cache_max_age = seconds; } +void +sentry_options_set_cache_max_items(sentry_options_t *opts, size_t items) +{ + opts->cache_max_items = items; +} + int sentry_options_get_cache_keep(const sentry_options_t *opts) { diff --git a/src/sentry_options.h b/src/sentry_options.h index e91ddfbe7..5f7b052bd 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -49,6 +49,7 @@ struct sentry_options_s { time_t cache_max_age; size_t cache_max_size; + size_t cache_max_items; sentry_attachment_t *attachments; sentry_run_t *run; diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py index 2eca8dfd3..aff10fa9f 100644 --- a/tests/test_integration_cache.py +++ b/tests/test_integration_cache.py @@ -141,3 +141,41 @@ def test_cache_max_age(cmake, backend): assert len(cache_files) == 3 for f in cache_files: assert time.time() - f.stat().st_mtime <= 5 * 24 * 60 * 60 + + +@pytest.mark.parametrize( + "backend", + [ + "inproc", + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad, reason="breakpad backend not available" + ), + ), + ], +) +def test_cache_max_items(cmake, backend): + tmp_path = cmake( + ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} + ) + cache_dir = tmp_path.joinpath(".sentry-native/cache") + + for i in range(6): + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "crash"], + expect_failure=True, + ) + + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "no-setup"], + ) + + # max 5 items + assert cache_dir.exists() + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 5 diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index 2d474a143..5a9e41a26 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -183,6 +183,50 @@ SENTRY_TEST(cache_max_age) sentry_close(); } +SENTRY_TEST(cache_max_items) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_cache_keep(options, true); + sentry_options_set_cache_max_items(options, 5); + sentry_init(options); + + sentry_path_t *cache_path + = sentry__path_join_str(options->database_path, "cache"); + TEST_ASSERT(!!cache_path); + TEST_ASSERT(sentry__path_remove_all(cache_path) == 0); + TEST_ASSERT(sentry__path_create_dir_all(cache_path) == 0); + + time_t now = time(NULL); + for (int i = 0; i < 10; i++) { + sentry_uuid_t event_id = sentry_uuid_new_v4(); + char *filename = sentry__uuid_as_filename(&event_id, ".envelope"); + TEST_ASSERT(!!filename); + sentry_path_t *filepath = sentry__path_join_str(cache_path, filename); + sentry_free(filename); + + TEST_ASSERT(sentry__path_touch(filepath) == 0); + time_t mtime = now - (i * 60); + TEST_ASSERT(set_file_mtime(filepath, mtime) == 0); + sentry__path_free(filepath); + } + + sentry__cleanup_cache(options); + + int cache_count = 0; + sentry_pathiter_t *iter = sentry__path_iter_directory(cache_path); + const sentry_path_t *entry; + while (iter && (entry = sentry__pathiter_next(iter)) != NULL) { + cache_count++; + } + sentry__pathiter_free(iter); + + TEST_CHECK_INT_EQUAL(cache_count, 5); + + sentry__path_free(cache_path); + sentry_close(); +} + SENTRY_TEST(cache_max_size_and_age) { // Verify size pruning keeps newer entries, removes all older once limit diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index c3b1530b1..6c6d2454b 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -29,6 +29,7 @@ XX(breadcrumb_without_type_or_message_still_valid) XX(build_id_parser) XX(cache_keep) XX(cache_max_age) +XX(cache_max_items) XX(cache_max_size) XX(cache_max_size_and_age) XX(capture_minidump_basic) From fd3c2577e8e1b27abd921d7b4a5569e065c58871 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 5 Feb 2026 13:28:05 +0100 Subject: [PATCH 30/31] Update CHANGELOG.md --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b90a9c4cb..50ead0447 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,16 @@ # Changelog +## Unreleased + +**Features**: + +- Add new offline caching options to persist envelopes locally, currently supported with the `inproc` and `breakpad` backends: `sentry_options_set_cache_keep`, `sentry_options_set_cache_max_items`, `sentry_options_set_cache_max_size`, and `sentry_options_set_cache_max_age`. ([#1490](https://github.com/getsentry/sentry-native/pull/1490)) + ## 0.12.6 **Features**: - Add support for metrics. It is currently experimental, and one can enable it by setting `sentry_options_set_enable_metrics`. When enabled, you can record a metric using `sentry_metrics_count()`, `sentry_metrics_gauge()`, or `sentry_metrics_distribution()`. Metrics can be filtered by setting the `before_send_metric` hook. ([#1498](https://github.com/getsentry/sentry-native/pull/1498)) -- Add new offline caching options to persist envelopes locally, currently supported with the `inproc` and `breakpad` backends: `sentry_options_set_cache_keep`, `sentry_options_set_cache_max_size`, and `sentry_options_set_cache_max_age`. ([#1490](https://github.com/getsentry/sentry-native/pull/1490)) ## 0.12.5 From 8a17813fe5c68e913667cbc60dd6609451c9272f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 5 Feb 2026 15:20:51 +0100 Subject: [PATCH 31/31] Default cache max size/age to 0 (disabled) Re-order cache_max_items before max_size/max_age for visibility as the most relevant default limit. Co-Authored-By: Claude Opus 4.5 --- examples/example.c | 4 ++-- include/sentry.h | 28 ++++++++++++++-------------- src/sentry_options.c | 16 ++++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/example.c b/examples/example.c index 7ce8947cc..3627e0e53 100644 --- a/examples/example.c +++ b/examples/example.c @@ -575,8 +575,8 @@ main(int argc, char **argv) } if (has_arg(argc, argv, "cache-keep")) { sentry_options_set_cache_keep(options, true); - sentry_options_set_cache_max_size(options, 4 * 1024 * 1024); - sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60); + sentry_options_set_cache_max_size(options, 4 * 1024 * 1024); // 4 MB + sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60); // 5 days sentry_options_set_cache_max_items(options, 5); } diff --git a/include/sentry.h b/include/sentry.h index e412c7473..16caba3fc 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1421,18 +1421,28 @@ SENTRY_API int sentry_options_get_symbolize_stacktraces( * * When enabled, envelopes are written to a `cache/` subdirectory within the * database directory and retained regardless of send success or failure. - * The cache is cleared on startup based on the cache_max_size and cache_max_age - * options. + * The cache is cleared on startup based on the cache_max_items, cache_max_size, + * and cache_max_age options. */ SENTRY_API void sentry_options_set_cache_keep( sentry_options_t *opts, int enabled); +/** + * Sets the maximum number of items in the cache directory. + * On startup, cached entries are removed from oldest to newest until the + * directory contains at most the specified number of items. + * + * Defaults to 30. + */ +SENTRY_API void sentry_options_set_cache_max_items( + sentry_options_t *opts, size_t items); + /** * Sets the maximum size (in bytes) for the cache directory. * On startup, cached entries are removed from oldest to newest until the * directory size is within the max size limit. * - * Defaults to 8 MB (8 * 1024 * 1024). + * Defaults to 0 (no max size). */ SENTRY_API void sentry_options_set_cache_max_size( sentry_options_t *opts, size_t bytes); @@ -1441,21 +1451,11 @@ SENTRY_API void sentry_options_set_cache_max_size( * Sets the maximum age (in seconds) for cache entries in the cache directory. * On startup, cached entries exceeding the max age limit are removed. * - * Defaults to 2 days (2 * 24 * 60 * 60). + * Defaults to 0 (no max age). */ SENTRY_API void sentry_options_set_cache_max_age( sentry_options_t *opts, time_t seconds); -/** - * Sets the maximum number of items in the cache directory. - * On startup, cached entries are removed from oldest to newest until the - * directory contains at most the specified number of items. - * - * Defaults to 30. - */ -SENTRY_API void sentry_options_set_cache_max_items( - sentry_options_t *opts, size_t items); - /** * Gets the caching mode for crash reports. */ diff --git a/src/sentry_options.c b/src/sentry_options.c index 2285bb673..fcb37ee1a 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -54,8 +54,8 @@ sentry_options_new(void) opts->propagate_traceparent = false; opts->crashpad_limit_stack_capture_to_sp = false; opts->cache_keep = false; - opts->cache_max_age = 2 * 24 * 60 * 60; - opts->cache_max_size = 8 * 1024 * 1024; + opts->cache_max_age = 0; + opts->cache_max_size = 0; opts->cache_max_items = 30; opts->symbolize_stacktraces = // AIX doesn't have reliable debug IDs for server-side symbolication, @@ -486,21 +486,21 @@ sentry_options_set_cache_keep(sentry_options_t *opts, int enabled) } void -sentry_options_set_cache_max_size(sentry_options_t *opts, size_t bytes) +sentry_options_set_cache_max_items(sentry_options_t *opts, size_t items) { - opts->cache_max_size = bytes; + opts->cache_max_items = items; } void -sentry_options_set_cache_max_age(sentry_options_t *opts, time_t seconds) +sentry_options_set_cache_max_size(sentry_options_t *opts, size_t bytes) { - opts->cache_max_age = seconds; + opts->cache_max_size = bytes; } void -sentry_options_set_cache_max_items(sentry_options_t *opts, size_t items) +sentry_options_set_cache_max_age(sentry_options_t *opts, time_t seconds) { - opts->cache_max_items = items; + opts->cache_max_age = seconds; } int