Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 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
683e521
WIP: feat(crashpad): offline caching
jpnurmi Jan 23, 2026
d03d9fb
fix(tests): add httpserver.wait to crashpad cache tests
jpnurmi Jan 26, 2026
badab46
chore: update changelog
jpnurmi Jan 26, 2026
ceb4912
fix(crashpad): skip processing completed reports when cache is disabled
jpnurmi Jan 26, 2026
4031e45
fix(crashpad): skip report conversion when event data is missing
jpnurmi Feb 4, 2026
74713f6
fix changelog
jpnurmi Feb 10, 2026
bd2bafe
fix(crashpad): missing DSN header in cached envelopes
jpnurmi Feb 10, 2026
f5e6a7a
fix(crashpad): check return value of sentry__envelope_add_event
jpnurmi Feb 10, 2026
0c291b1
refactor(crashpad): inline report_minidump_path
jpnurmi Feb 10, 2026
30e3320
fix(crashpad): skip caching already cached reports
jpnurmi Feb 10, 2026
8a22d2c
fix(crashpad): decref event when sentry__envelope_add_event fails
jpnurmi Feb 10, 2026
7c72f1c
fix(crashpad): delete completed report when cache file already exists
jpnurmi Feb 10, 2026
97b2723
docs(crashpad): add comments to report_to_envelope and process_comple…
jpnurmi Feb 10, 2026
709d7f2
Merge branch 'master' into jpnurmi/feat/crashpad-offline-caching
jpnurmi Feb 11, 2026
2985e08
Merge remote-tracking branch 'upstream/master' into jpnurmi/feat/cras…
jpnurmi Feb 11, 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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

**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))
- Add new offline caching options to persist envelopes locally: `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), [#1493](https://github.com/getsentry/sentry-native/pull/1493))

**Fixes**:

Expand Down
163 changes: 163 additions & 0 deletions src/backends/sentry_backend_crashpad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ extern "C" {
#include "sentry_screenshot.h"
#include "sentry_sync.h"
#include "sentry_transport.h"
#include "sentry_value.h"
#ifdef SENTRY_PLATFORM_LINUX
# include "sentry_unix_pageallocator.h"
#endif
Expand Down Expand Up @@ -436,6 +437,167 @@ sentry__crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context)
}
#endif

static sentry_value_t
read_msgpack_file(const sentry_path_t *path)
{
size_t size;
char *data = sentry__path_read_to_buffer(path, &size);
if (!data) {
return sentry_value_new_null();
}
sentry_value_t value = sentry__value_from_msgpack(data, size);
sentry_free(data);
return value;
}

static sentry_path_t *
report_attachments_dir(const crashpad::CrashReportDatabase::Report &report,
const sentry_options_t *options)
{
sentry_path_t *attachments_root
= sentry__path_join_str(options->database_path, "attachments");
if (!attachments_root) {
return nullptr;
}

sentry_path_t *attachments_dir = sentry__path_join_str(
attachments_root, report.uuid.ToString().c_str());

sentry__path_free(attachments_root);
return attachments_dir;
}

// Converts a completed crashpad report into a sentry envelope by reading the
// event, breadcrumbs, and attachments from the report's attachments directory.
static sentry_envelope_t *
report_to_envelope(const crashpad::CrashReportDatabase::Report &report,
const sentry_options_t *options)
{
#ifdef SENTRY_PLATFORM_WINDOWS
sentry_path_t *minidump_path
= sentry__path_from_wstr(report.file_path.value().c_str());
#else
sentry_path_t *minidump_path
= sentry__path_from_str(report.file_path.value().c_str());
#endif
sentry_path_t *attachments_dir = report_attachments_dir(report, options);

if (!minidump_path || !attachments_dir) {
sentry__path_free(minidump_path);
sentry__path_free(attachments_dir);
return nullptr;
}

sentry_value_t event = sentry_value_new_null();
sentry_value_t breadcrumbs1 = sentry_value_new_null();
sentry_value_t breadcrumbs2 = sentry_value_new_null();
sentry_attachment_t *attachments = nullptr;

sentry_pathiter_t *iter = sentry__path_iter_directory(attachments_dir);
if (iter) {
const sentry_path_t *path;
while ((path = sentry__pathiter_next(iter)) != nullptr) {
const char *filename = sentry__path_filename(path);
if (strcmp(filename, "__sentry-event") == 0) {
event = read_msgpack_file(path);
} else if (strcmp(filename, "__sentry-breadcrumb1") == 0) {
breadcrumbs1 = read_msgpack_file(path);
} else if (strcmp(filename, "__sentry-breadcrumb2") == 0) {
breadcrumbs2 = read_msgpack_file(path);
} else {
sentry__attachments_add_path(&attachments,
sentry__path_clone(path), ATTACHMENT, nullptr);
}
}
sentry__pathiter_free(iter);
}
sentry__path_free(attachments_dir);

sentry_envelope_t *envelope = nullptr;
if (!sentry_value_is_null(event)) {
envelope = sentry__envelope_new();
if (envelope && options->dsn && options->dsn->is_valid) {
sentry__envelope_set_header(envelope, "dsn",
sentry_value_new_string(sentry_options_get_dsn(options)));
}
}
if (envelope) {
sentry_value_set_by_key(event, "breadcrumbs",
sentry__value_merge_breadcrumbs(
breadcrumbs1, breadcrumbs2, options->max_breadcrumbs));
sentry__attachments_add_path(
&attachments, minidump_path, MINIDUMP, nullptr);

if (sentry__envelope_add_event(envelope, event)) {
sentry__envelope_add_attachments(envelope, attachments);
} else {
sentry_value_decref(event);
sentry_envelope_free(envelope);
envelope = nullptr;
}
} else {
sentry__path_free(minidump_path);
sentry_value_decref(event);
}

sentry_value_decref(breadcrumbs1);
sentry_value_decref(breadcrumbs2);
sentry__attachments_free(attachments);

return envelope;
}

// Caches completed crashpad reports as sentry envelopes and removes them from
// the crashpad database. Called during startup before the handler is started.
static void
process_completed_reports(
crashpad_state_t *state, const sentry_options_t *options)
{
if (!state || !state->db || !options || !options->cache_keep) {
return;
}

std::vector<crashpad::CrashReportDatabase::Report> reports;
if (state->db->GetCompletedReports(&reports)
!= crashpad::CrashReportDatabase::kNoError
|| reports.empty()) {
return;
}

SENTRY_DEBUGF("caching %zu completed reports", reports.size());

sentry_path_t *cache_dir
= sentry__path_join_str(options->database_path, "cache");
if (!cache_dir || sentry__path_create_dir_all(cache_dir) != 0) {
SENTRY_WARN("failed to create cache dir");
sentry__path_free(cache_dir);
return;
}

for (const auto &report : reports) {
std::string filename = report.uuid.ToString() + ".envelope";
sentry_envelope_t *envelope = report_to_envelope(report, options);
if (!envelope) {
SENTRY_WARNF("failed to convert \"%s\"", filename.c_str());
continue;
}
sentry_path_t *out_path
= sentry__path_join_str(cache_dir, filename.c_str());
if (!out_path
|| (!sentry__path_is_file(out_path)
&& sentry_envelope_write_to_path(envelope, out_path) != 0)) {
SENTRY_WARNF("failed to cache \"%s\"", filename.c_str());
} else if (state->db->DeleteReport(report.uuid)
!= crashpad::CrashReportDatabase::kNoError) {
SENTRY_WARNF("failed to delete \"%s\"", filename.c_str());
}
Copy link

Choose a reason for hiding this comment

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

Partial cache file causes report deletion and data loss

Medium Severity

If sentry_envelope_write_to_path fails but leaves a partial/corrupt file on disk, the next startup will see sentry__path_is_file(out_path) return true, skip rewriting the envelope, and proceed to call DeleteReport — permanently deleting the Crashpad report while only a corrupt envelope remains in cache. The sentry__path_is_file check doesn't validate the file's integrity, so a half-written file from a previous failed attempt is treated as a successfully cached envelope.

Fix in Cursor Fix in Web

Copy link
Collaborator Author

@jpnurmi jpnurmi Feb 11, 2026

Choose a reason for hiding this comment

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

Not sure... To validate the file's integrity, we would either have to store the checksum somewhere or parse the previously written envelope. 🤔

The lack of atomic (temp+rename) file writes could bite in several places in sentry-native. Another scenario: crash during envelope write -> truncated .envelope on disk -> next sentry_init() calls sentry__process_old_runs() -> sentry__envelope_from_path() reads truncated buffer as raw payload -> corrupted envelope sent to Sentry.

sentry__path_free(out_path);
sentry_envelope_free(envelope);
}

sentry__path_free(cache_dir);
}

static int
crashpad_backend_startup(
sentry_backend_t *backend, const sentry_options_t *options)
Expand Down Expand Up @@ -549,6 +711,7 @@ crashpad_backend_startup(
// Initialize database first, flushing the consent later on as part of
// `sentry_init` will persist the upload flag.
data->db = crashpad::CrashReportDatabase::Initialize(database).release();
process_completed_reports(data, options);
data->client = new crashpad::CrashpadClient;
char *minidump_url
= sentry__dsn_get_minidump_url(options->dsn, options->user_agent);
Expand Down
2 changes: 1 addition & 1 deletion src/sentry_envelope.c
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ sentry_envelope_free(sentry_envelope_t *envelope)
sentry_free(envelope);
}

static void
void
sentry__envelope_set_header(
sentry_envelope_t *envelope, const char *key, sentry_value_t value)
{
Expand Down
6 changes: 6 additions & 0 deletions src/sentry_envelope.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ sentry_envelope_item_t *sentry__envelope_add_from_buffer(
sentry_envelope_t *envelope, const char *buf, size_t buf_len,
const char *type);

/**
* This sets an explicit header for the given envelope.
*/
void sentry__envelope_set_header(
sentry_envelope_t *envelope, const char *key, sentry_value_t value);

/**
* This sets an explicit header for the given envelope item.
*/
Expand Down
Loading
Loading