Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
536c4a1
Import offline caching POC (#1461)
JoshuaMoelans Nov 27, 2025
5742eda
breakpad: delete .dmp
jpnurmi Jan 15, 2026
6c75ccf
Flatten cache/
jpnurmi Jan 15, 2026
8f3ffd5
Add tests
jpnurmi Jan 16, 2026
3410b6c
Respect has_breakpad
jpnurmi Jan 23, 2026
799d219
SENTRY_TRANSPORT=none
jpnurmi Jan 23, 2026
3c40494
Fix set_file_mtime on Windows
jpnurmi Jan 23, 2026
a37ac86
Present cache_max_age in seconds
jpnurmi Jan 23, 2026
20815bd
Present max_cache_size in bytes
jpnurmi Jan 23, 2026
ef96547
Tweak docs & signatures
jpnurmi Jan 23, 2026
e96d9d4
Fix warning
jpnurmi Jan 23, 2026
93aefb2
Fix test_unit::cache_max_age
jpnurmi Jan 24, 2026
f4936d5
Fix sign conversion warning on Windows
jpnurmi Jan 24, 2026
7eddee4
Add changelog entry for offline caching feature
jpnurmi Jan 24, 2026
8d40ec1
Fix sign conversion warning in CleanDatabase call
jpnurmi Jan 24, 2026
b28663f
Clarify test_integration_cache
jpnurmi Jan 26, 2026
ab62f90
Change cache_max_age type from uint64_t to time_t
jpnurmi Jan 27, 2026
7bc7856
Merge remote-tracking branch 'upstream/master' into jpnurmi/feat/offl…
jpnurmi Feb 2, 2026
9e09bd0
Update CHANGELOG.md
jpnurmi Feb 2, 2026
747edb1
Log warning when envelope caching fails
jpnurmi Feb 3, 2026
b1a4671
Revise crashpad_backend_prune_database
jpnurmi Feb 3, 2026
b101fce
Fix cache size calculation to exclude pruned files
jpnurmi Feb 3, 2026
2e1c63b
Add NULL checks after path allocations in cache handling
jpnurmi Feb 3, 2026
e0cb46e
Add NULL check after path clone in sentry__cleanup_cache
jpnurmi Feb 3, 2026
317968c
Fix cache size pruning to remove all older entries once limit hit
jpnurmi Feb 3, 2026
e30846f
Add INVALID_HANDLE_VALUE check and use TEST_ASSERT for set_file_mtime
jpnurmi Feb 4, 2026
1d565d6
Remove redundant conditional around sentry__path_free
jpnurmi Feb 4, 2026
76a5f81
Replace crashpad prune conditions with custom implementations
jpnurmi Feb 4, 2026
f0e5075
Fix size_t conversion warning on 32-bit Windows
jpnurmi Feb 4, 2026
25da5e1
Add cache_max_items option for Android & Cocoa compat
jpnurmi Feb 4, 2026
fc0f6cc
Merge branch 'master' into jpnurmi/feat/offline-caching
jpnurmi Feb 5, 2026
57cb740
Merge remote-tracking branch 'upstream/master' into jpnurmi/feat/offl…
jpnurmi Feb 5, 2026
fd3c257
Update CHANGELOG.md
jpnurmi Feb 5, 2026
8a17813
Default cache max size/age to 0 (disabled)
jpnurmi Feb 5, 2026
5ece6d5
fix(crashpad): Restore original prune limits when offline caching is …
jpnurmi Feb 6, 2026
6a80d77
Merge remote-tracking branch 'upstream/master' into jpnurmi/feat/offl…
jpnurmi Feb 9, 2026
dd0ef77
docs: mention default value in sentry_options_set_cache_keep
jpnurmi Feb 9, 2026
bbfb4d2
docs: clarify crashpad database pruning defaults
jpnurmi Feb 9, 2026
43450d2
refactor: merge crashpad prune conditions into a single class
jpnurmi Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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_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**:
Expand Down
6 changes: 6 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,12 @@ 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, 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);
}

if (has_arg(argc, argv, "enable-metrics")) {
sentry_options_set_enable_metrics(options, true);
Expand Down
48 changes: 48 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ extern "C" {
#include <inttypes.h>
#include <stdarg.h>
#include <stddef.h>
#include <time.h>

/* context type dependencies */
#ifdef _WIN32
Expand Down Expand Up @@ -1415,6 +1416,53 @@ SENTRY_API void sentry_options_set_symbolize_stacktraces(
SENTRY_API int sentry_options_get_symbolize_stacktraces(
const sentry_options_t *opts);

/**
* 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_items, cache_max_size,
* and cache_max_age options.
*
* Disabled by default.
*/
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 0 (no max size).
*/
SENTRY_API void sentry_options_set_cache_max_size(
sentry_options_t *opts, size_t bytes);

/**
* 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 0 (no max age).
*/
SENTRY_API void sentry_options_set_cache_max_age(
sentry_options_t *opts, time_t seconds);

/**
* 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.
*
Expand Down
67 changes: 59 additions & 8 deletions src/backends/sentry_backend_crashpad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -744,19 +744,70 @@ crashpad_backend_last_crash(sentry_backend_t *backend)
return crash_time;
}

class CachePruneCondition final : public crashpad::PruneCondition {
public:
CachePruneCondition(size_t max_items, size_t max_size, time_t max_age)
: max_items_(max_items)
, item_count_(0)
, max_size_(max_size)
, measured_size_(0)
, max_age_(max_age)
, oldest_report_time_(time(nullptr) - max_age)
{
}

bool
ShouldPruneReport(
const crashpad::CrashReportDatabase::Report &report) override
{
++item_count_;
measured_size_ += static_cast<size_t>(report.total_size);

bool by_items = max_items_ > 0 && item_count_ > max_items_;
bool by_size = max_size_ > 0 && measured_size_ > max_size_;
bool by_age
= max_age_ > 0 && report.creation_time < oldest_report_time_;
return by_items || by_size || by_age;
}

private:
const size_t max_items_;
size_t item_count_;
const size_t max_size_;
size_t measured_size_;
const time_t max_age_;
const time_t oldest_report_time_;
};

static void
crashpad_backend_prune_database(sentry_backend_t *backend)
{
auto *data = static_cast<crashpad_state_t *>(backend->data);

// We want to eagerly clean up reports older than 2 days, and limit the
// 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));
// For backwards compatibility, default to the parameters that were used
// before the offline caching API was introduced. We wanted to eagerly
// clean up reports older than 2 days, and limit the complete database
// to a maximum of 8M. That might still have been a lot for an embedded
// use-case, but minidumps on desktop can sometimes be quite large.
time_t max_age = 2 * 24 * 60 * 60; // 2 days
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe extracting these constants somewhere would make sense (instead of keeping them as crashpad-only defaults in a function)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are just some legacy defaults that were used by the crashpad backend before offline caching was introduced. These defaults are specific to the crashpad backend and are not used anywhere else. The idea is to retain full backwards compatibility by using the old legacy defaults unless the user opts in for offline caching. I updated the comments - I hope it's clear now :)

size_t max_size = 8 * 1024 * 1024; // 8 MB
size_t max_items = 0;

// When offline caching is enabled, the user has full control over these
// parameters via the cache_max_* options.
SENTRY_WITH_OPTIONS (options) {
if (options->cache_keep) {
max_age = options->cache_max_age;
max_size = options->cache_max_size;
max_items = options->cache_max_items;
}
}

if (max_age > 0) {
data->db->CleanDatabase(max_age);
}

CachePruneCondition condition(max_items, max_size, max_age);
crashpad::PruneCrashReportDatabase(data->db, &condition);
}

Expand Down
4 changes: 4 additions & 0 deletions src/sentry_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,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();
}
Expand Down
147 changes: 147 additions & 0 deletions src/sentry_database.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "sentry_session.h"
#include "sentry_uuid.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>

sentry_run_t *
Expand Down Expand Up @@ -237,6 +238,15 @@ 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 *cache_dir = NULL;
if (options->cache_keep) {
cache_dir = sentry__path_join_str(options->database_path, "cache");
if (cache_dir) {
sentry__path_create_dir_all(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) {
Expand Down Expand Up @@ -281,12 +291,25 @@ 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 (cache_dir) {
sentry_path_t *cached_file = sentry__path_join_str(
cache_dir, sentry__path_filename(file));
if (!cached_file
|| sentry__path_rename(file, cached_file) != 0) {
SENTRY_WARNF("failed to cache envelope \"%s\"",
sentry__path_filename(file));
}
sentry__path_free(cached_file);
continue;
}
}

sentry__path_remove(file);
}
sentry__pathiter_free(run_iter);

sentry__path_free(cache_dir);
sentry__path_remove_all(run_dir);
sentry__filelock_free(lock);
}
Expand All @@ -295,6 +318,130 @@ 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;
} cache_entry_t;

/**
* 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 (!cache_dir || !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);
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++;
}
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;

// Prune entries: iterate newest-to-oldest, accumulating size
// Remove if: too old OR accumulated size exceeds limit
size_t accumulated_size = 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;
} 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;
}
// Item count pruning
if (options->cache_max_items > 0 && i >= options->cache_max_items) {
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
Expand Down
6 changes: 6 additions & 0 deletions src/sentry_database.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
* `<database>/last_crash` file.
Expand Down
Loading
Loading