Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
5 changes: 5 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,11 @@ 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);
sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60);
}

if (0 != sentry_init(options)) {
return EXIT_FAILURE;
Expand Down
32 changes: 32 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 @@ -1375,6 +1376,37 @@ 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_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 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 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.
*/
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
15 changes: 10 additions & 5 deletions src/backends/sentry_backend_crashpad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -745,11 +745,16 @@ 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(options->cache_max_age);
crashpad::BinaryPruneCondition condition(
crashpad::BinaryPruneCondition::OR,
new crashpad::DatabaseSizePruneCondition(
options->cache_max_size / 1024),
new crashpad::AgePruneCondition(
static_cast<int>(options->cache_max_age / (24 * 60 * 60))));
crashpad::PruneCrashReportDatabase(data->db, &condition);
}
}

#if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_LINUX)
Expand Down
8 changes: 8 additions & 0 deletions src/path/sentry_path_unix.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
12 changes: 12 additions & 0 deletions src/path/sentry_path_windows.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
4 changes: 4 additions & 0 deletions src/sentry_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
137 changes: 137 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,13 @@ 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");
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 +289,24 @@ 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_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(cache_dir);
}

sentry__path_remove_all(run_dir);
sentry__filelock_free(lock);
}
Expand All @@ -295,6 +315,123 @@ 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 (!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 = 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;
}

// 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) {
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
27 changes: 27 additions & 0 deletions src/sentry_options.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 * 24 * 60 * 60;
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
Expand Down Expand Up @@ -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 bytes)
{
opts->cache_max_size = bytes;
}

void
sentry_options_set_cache_max_age(sentry_options_t *opts, time_t seconds)
{
opts->cache_max_age = seconds;
}

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)
Expand Down
4 changes: 4 additions & 0 deletions src/sentry_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

time_t cache_max_age;
size_t cache_max_size;

sentry_attachment_t *attachments;
sentry_run_t *run;
Expand Down
7 changes: 7 additions & 0 deletions src/sentry_path.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading