Skip to content

Commit 18d314f

Browse files
jpnurmiclaude
andcommitted
feat(cache): cache consent-revoked envelopes
When user consent is revoked, write envelopes to <database>/cache/ instead of discarding them, so they are picked up by the retry module when consent is later given. Caching is conditional on the cache_keep option being enabled. - Modify sentry__capture_envelope() to cache instead of discard - Guard sentry__capture_envelope() against NULL session_envelope in process_old_runs - Add consent check to sentry__retry_send() via stored require_user_consent bool and db->user_consent atomic read Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 814b822 commit 18d314f

File tree

8 files changed

+232
-7
lines changed

8 files changed

+232
-7
lines changed

examples/example.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,9 @@ main(int argc, char **argv)
694694
if (has_arg(argc, argv, "log-attributes")) {
695695
sentry_options_set_logs_with_attributes(options, true);
696696
}
697+
if (has_arg(argc, argv, "require-user-consent")) {
698+
sentry_options_set_require_user_consent(options, true);
699+
}
697700
if (has_arg(argc, argv, "cache-keep")) {
698701
sentry_options_set_cache_keep(options, true);
699702
sentry_options_set_cache_max_size(options, 4 * 1024 * 1024); // 4 MB
@@ -758,6 +761,13 @@ main(int argc, char **argv)
758761
return EXIT_FAILURE;
759762
}
760763

764+
if (has_arg(argc, argv, "user-consent-give")) {
765+
sentry_user_consent_give();
766+
}
767+
if (has_arg(argc, argv, "user-consent-revoke")) {
768+
sentry_user_consent_revoke();
769+
}
770+
761771
if (has_arg(argc, argv, "set-global-attribute")) {
762772
sentry_set_attribute("global.attribute.bool",
763773
sentry_value_new_attribute(sentry_value_new_bool(true), NULL));

src/sentry_core.c

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -502,13 +502,19 @@ void
502502
sentry__capture_envelope(
503503
sentry_transport_t *transport, sentry_envelope_t *envelope)
504504
{
505-
bool has_consent = !sentry__should_skip_upload();
506-
if (!has_consent) {
507-
SENTRY_INFO("discarding envelope due to missing user consent");
508-
sentry_envelope_free(envelope);
505+
if (!sentry__should_skip_upload()) {
506+
sentry__transport_send_envelope(transport, envelope);
509507
return;
510508
}
511-
sentry__transport_send_envelope(transport, envelope);
509+
bool cached = false;
510+
SENTRY_WITH_OPTIONS (options) {
511+
if (options->cache_keep) {
512+
cached = sentry__run_write_cache(options->run, envelope, 0);
513+
}
514+
}
515+
SENTRY_INFO(cached ? "caching envelope due to missing user consent"
516+
: "discarding envelope due to missing user consent");
517+
sentry_envelope_free(envelope);
512518
}
513519

514520
void

src/sentry_database.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,9 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash)
438438
}
439439
sentry__pathiter_free(db_iter);
440440

441-
sentry__capture_envelope(options->transport, session_envelope);
441+
if (session_envelope) {
442+
sentry__capture_envelope(options->transport, session_envelope);
443+
}
442444
}
443445

444446
// Cache Pruning below is based on prune_crash_reports.cc from Crashpad

src/sentry_retry.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ typedef enum {
2828
struct sentry_retry_s {
2929
sentry_run_t *run;
3030
bool cache_keep;
31+
long *user_consent;
3132
uint64_t startup_time;
3233
volatile long state;
3334
volatile long scheduled;
@@ -48,6 +49,8 @@ sentry__retry_new(const sentry_options_t *options)
4849
sentry__mutex_init(&retry->sealed_lock);
4950
retry->run = sentry__run_incref(options->run);
5051
retry->cache_keep = options->cache_keep;
52+
retry->user_consent
53+
= options->require_user_consent ? (long *)&options->user_consent : NULL;
5154
retry->startup_time = sentry__usec_time() / 1000;
5255
return retry;
5356
}
@@ -142,6 +145,12 @@ size_t
142145
sentry__retry_send(sentry_retry_t *retry, uint64_t before,
143146
sentry_retry_send_func_t send_cb, void *data)
144147
{
148+
if (retry->user_consent
149+
&& sentry__atomic_fetch(retry->user_consent)
150+
!= SENTRY_USER_CONSENT_GIVEN) {
151+
return 0;
152+
}
153+
145154
sentry_pathiter_t *piter
146155
= sentry__path_iter_directory(retry->run->cache_path);
147156
if (!piter) {

tests/test_integration_cache.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,76 @@ def test_cache_max_items_with_retry(cmake, backend, unreachable_dsn):
263263
assert cache_dir.exists()
264264
cache_files = list(cache_dir.glob("*.envelope"))
265265
assert len(cache_files) <= 5
266+
267+
268+
def test_cache_consent_revoked(cmake, unreachable_dsn):
269+
"""With consent revoked and cache_keep, envelopes are cached to disk."""
270+
tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})
271+
cache_dir = tmp_path.joinpath(".sentry-native/cache")
272+
env = dict(os.environ, SENTRY_DSN=unreachable_dsn)
273+
274+
run(
275+
tmp_path,
276+
"sentry_example",
277+
[
278+
"log",
279+
"cache-keep",
280+
"require-user-consent",
281+
"user-consent-revoke",
282+
"capture-event",
283+
"flush",
284+
],
285+
env=env,
286+
)
287+
288+
assert cache_dir.exists()
289+
cache_files = list(cache_dir.glob("*.envelope"))
290+
assert len(cache_files) == 1
291+
292+
293+
def test_cache_consent_revoked_no_cache_keep(cmake, unreachable_dsn):
294+
"""With consent revoked but no cache_keep, envelopes are discarded."""
295+
tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})
296+
cache_dir = tmp_path.joinpath(".sentry-native/cache")
297+
env = dict(os.environ, SENTRY_DSN=unreachable_dsn)
298+
299+
run(
300+
tmp_path,
301+
"sentry_example",
302+
[
303+
"log",
304+
"require-user-consent",
305+
"user-consent-revoke",
306+
"capture-event",
307+
"flush",
308+
],
309+
env=env,
310+
)
311+
312+
assert not cache_dir.exists() or len(list(cache_dir.glob("*.envelope"))) == 0
313+
314+
315+
def test_cache_consent_given_sends(cmake, httpserver):
316+
"""With consent given, envelopes are sent normally."""
317+
from . import make_dsn
318+
319+
tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})
320+
env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver))
321+
322+
httpserver.expect_request("/api/123456/envelope/").respond_with_data("OK")
323+
324+
run(
325+
tmp_path,
326+
"sentry_example",
327+
[
328+
"log",
329+
"cache-keep",
330+
"require-user-consent",
331+
"user-consent-give",
332+
"capture-event",
333+
"flush",
334+
],
335+
env=env,
336+
)
337+
338+
assert len(httpserver.log) >= 1

tests/unit/test_cache.c

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#include "sentry_envelope.h"
44
#include "sentry_options.h"
55
#include "sentry_path.h"
6-
#include "sentry_string.h"
6+
#include "sentry_retry.h"
77
#include "sentry_testsupport.h"
88
#include "sentry_uuid.h"
99
#include "sentry_value.h"
@@ -310,6 +310,83 @@ SENTRY_TEST(cache_max_items_with_retry)
310310
sentry_close();
311311
}
312312

313+
SENTRY_TEST(cache_consent_revoked)
314+
{
315+
#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS)
316+
SKIP_TEST();
317+
#endif
318+
SENTRY_TEST_OPTIONS_NEW(options);
319+
sentry_options_set_dsn(options, "https://foo@sentry.invalid/42");
320+
sentry_options_set_cache_keep(options, true);
321+
sentry_options_set_require_user_consent(options, true);
322+
sentry_init(options);
323+
sentry_user_consent_revoke();
324+
325+
sentry_path_t *cache_path
326+
= sentry__path_join_str(options->database_path, "cache");
327+
TEST_ASSERT(!!cache_path);
328+
sentry__path_remove_all(cache_path);
329+
330+
sentry_capture_event(
331+
sentry_value_new_message_event(SENTRY_LEVEL_INFO, "test", "revoked"));
332+
333+
int count = 0;
334+
bool is_retry_format = false;
335+
sentry_pathiter_t *iter = sentry__path_iter_directory(cache_path);
336+
const sentry_path_t *entry;
337+
while (iter && (entry = sentry__pathiter_next(iter)) != NULL) {
338+
if (sentry__path_ends_with(entry, ".envelope")) {
339+
count++;
340+
uint64_t ts;
341+
int attempt;
342+
const char *uuid;
343+
is_retry_format = sentry__parse_cache_filename(
344+
sentry__path_filename(entry), &ts, &attempt, &uuid);
345+
}
346+
}
347+
sentry__pathiter_free(iter);
348+
TEST_CHECK_INT_EQUAL(count, 1);
349+
TEST_CHECK(is_retry_format);
350+
351+
sentry__path_free(cache_path);
352+
sentry_close();
353+
}
354+
355+
SENTRY_TEST(cache_consent_revoked_nocache)
356+
{
357+
#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS)
358+
SKIP_TEST();
359+
#endif
360+
SENTRY_TEST_OPTIONS_NEW(options);
361+
sentry_options_set_dsn(options, "https://foo@sentry.invalid/42");
362+
sentry_options_set_cache_keep(options, false);
363+
sentry_options_set_require_user_consent(options, true);
364+
sentry_init(options);
365+
sentry_user_consent_revoke();
366+
367+
sentry_path_t *cache_path
368+
= sentry__path_join_str(options->database_path, "cache");
369+
TEST_ASSERT(!!cache_path);
370+
sentry__path_remove_all(cache_path);
371+
372+
sentry_capture_event(
373+
sentry_value_new_message_event(SENTRY_LEVEL_INFO, "test", "revoked"));
374+
375+
int count = 0;
376+
sentry_pathiter_t *iter = sentry__path_iter_directory(cache_path);
377+
const sentry_path_t *entry;
378+
while (iter && (entry = sentry__pathiter_next(iter)) != NULL) {
379+
if (sentry__path_ends_with(entry, ".envelope")) {
380+
count++;
381+
}
382+
}
383+
sentry__pathiter_free(iter);
384+
TEST_CHECK_INT_EQUAL(count, 0);
385+
386+
sentry__path_free(cache_path);
387+
sentry_close();
388+
}
389+
313390
SENTRY_TEST(cache_max_size_and_age)
314391
{
315392
#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS)

tests/unit/test_retry.c

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,3 +489,48 @@ SENTRY_TEST(retry_trigger)
489489
sentry__retry_free(retry);
490490
sentry_close();
491491
}
492+
493+
SENTRY_TEST(retry_consent)
494+
{
495+
#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS)
496+
SKIP_TEST();
497+
#endif
498+
SENTRY_TEST_OPTIONS_NEW(options);
499+
sentry_options_set_dsn(options, "https://foo@sentry.invalid/42");
500+
sentry_options_set_http_retry(options, true);
501+
sentry_options_set_require_user_consent(options, true);
502+
sentry_init(options);
503+
sentry_user_consent_revoke();
504+
505+
sentry_retry_t *retry = sentry__retry_new(options);
506+
TEST_ASSERT(!!retry);
507+
508+
const sentry_path_t *cache_path = options->run->cache_path;
509+
sentry__path_remove_all(cache_path);
510+
sentry__path_create_dir_all(cache_path);
511+
512+
uint64_t old_ts
513+
= sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0);
514+
sentry_uuid_t event_id = sentry_uuid_new_v4();
515+
write_retry_file(options->run, old_ts, 0, &event_id);
516+
517+
TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1);
518+
519+
// consent revoked: retry_send returns 0 without calling send_cb
520+
retry_test_ctx_t ctx = { 200, 0 };
521+
size_t remaining = sentry__retry_send(retry, 0, test_send_cb, &ctx);
522+
TEST_CHECK_INT_EQUAL(ctx.count, 0);
523+
TEST_CHECK_INT_EQUAL(remaining, 0);
524+
TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1);
525+
526+
// give consent: retry_send sends and removes the file
527+
sentry_user_consent_give();
528+
ctx = (retry_test_ctx_t) { 200, 0 };
529+
remaining = sentry__retry_send(retry, UINT64_MAX, test_send_cb, &ctx);
530+
TEST_CHECK_INT_EQUAL(ctx.count, 1);
531+
TEST_CHECK_INT_EQUAL(remaining, 0);
532+
TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0);
533+
534+
sentry__retry_free(retry);
535+
sentry_close();
536+
}

tests/unit/tests.inc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ XX(bgworker_flush)
4040
XX(bgworker_task_delay)
4141
XX(breadcrumb_without_type_or_message_still_valid)
4242
XX(build_id_parser)
43+
XX(cache_consent_revoked)
44+
XX(cache_consent_revoked_nocache)
4345
XX(cache_keep)
4446
XX(cache_max_age)
4547
XX(cache_max_items)
@@ -197,6 +199,7 @@ XX(read_write_envelope_to_invalid_path)
197199
XX(recursive_paths)
198200
XX(retry_backoff)
199201
XX(retry_cache)
202+
XX(retry_consent)
200203
XX(retry_filename)
201204
XX(retry_make_cache_path)
202205
XX(retry_result)

0 commit comments

Comments
 (0)