Skip to content
Open
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**:

- Cache consent-revoked envelopes to disk when `cache_keep` is enabled, so they can be sent when consent is later given. ([#1542](https://github.com/getsentry/sentry-native/pull/1542))

**Fixes**:

- macOS: cache VM regions for FP validation in the new unwinder. ([#1634](https://github.com/getsentry/sentry-native/pull/1634))
Expand Down
10 changes: 10 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,9 @@ 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, "require-user-consent")) {
sentry_options_set_require_user_consent(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
Expand Down Expand Up @@ -758,6 +761,10 @@ main(int argc, char **argv)
return EXIT_FAILURE;
}

if (has_arg(argc, argv, "user-consent-revoke")) {
sentry_user_consent_revoke();
}

if (has_arg(argc, argv, "set-global-attribute")) {
sentry_set_attribute("global.attribute.bool",
sentry_value_new_attribute(sentry_value_new_bool(true), NULL));
Expand Down Expand Up @@ -1049,6 +1056,9 @@ main(int argc, char **argv)
}
sentry_capture_event(event);
}
if (has_arg(argc, argv, "user-consent-give")) {
sentry_user_consent_give();
}
if (has_arg(argc, argv, "capture-exception")) {
sentry_value_t exc = sentry_value_new_exception(
"ParseIntError", "invalid digit found in string");
Expand Down
12 changes: 12 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,11 @@ SENTRY_API int sentry_options_get_auto_session_tracking(
* This disables uploads until the user has given the consent to the SDK.
* Consent itself is given with `sentry_user_consent_give` and
* `sentry_user_consent_revoke`.
*
* When combined with `cache_keep` or `http_retry`, envelopes captured
* while consent is revoked are written to the cache directory instead
* of being discarded. With `http_retry` enabled, cached envelopes are
* sent automatically once consent is given.
*/
SENTRY_API void sentry_options_set_require_user_consent(
sentry_options_t *opts, int val);
Expand Down Expand Up @@ -1505,6 +1510,10 @@ SENTRY_API int sentry_options_get_symbolize_stacktraces(
* subdirectory within the database directory. The cache is cleared on startup
* based on the cache_max_items, cache_max_size, and cache_max_age options.
*
* When combined with `sentry_options_set_require_user_consent`, envelopes
* captured while consent is revoked are also written to the cache. With
* `http_retry` enabled, they are sent once consent is given.
*
* Only applicable for HTTP transports.
*
* Disabled by default.
Expand Down Expand Up @@ -1920,6 +1929,9 @@ SENTRY_EXPERIMENTAL_API int sentry_reinstall_backend(void);

/**
* Gives user consent.
*
* Schedules a retry of any envelopes cached while consent was revoked,
* provided that `http_retry` is enabled.
*/
SENTRY_API void sentry_user_consent_give(void);

Expand Down
8 changes: 2 additions & 6 deletions src/sentry_batcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,7 @@ sentry__batcher_flush(sentry_batcher_t *batcher, bool crash_safe)
// crash
sentry__run_write_envelope(batcher->run, envelope);
sentry_envelope_free(envelope);
} else if (!batcher->user_consent
|| sentry__atomic_fetch(batcher->user_consent)
== SENTRY_USER_CONSENT_GIVEN) {
} else if (!sentry__run_should_skip_upload(batcher->run)) {
// Normal operation: use transport for HTTP transmission
sentry__transport_send_envelope(batcher->transport, envelope);
} else {
Expand Down Expand Up @@ -376,12 +374,10 @@ sentry__batcher_startup(
{
// dsn is incref'd because release() decref's it and may outlive options.
batcher->dsn = sentry__dsn_incref(options->dsn);
// transport, run, and user_consent are non-owning refs, safe because they
// transport and run are non-owning refs, safe because they
// are only accessed in flush() which is bound by the options lifetime.
batcher->transport = options->transport;
batcher->run = options->run;
batcher->user_consent
= options->require_user_consent ? (long *)&options->user_consent : NULL;

// Mark thread as starting before actually spawning so thread can transition
// to RUNNING. This prevents shutdown from thinking the thread was never
Expand Down
1 change: 0 additions & 1 deletion src/sentry_batcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ typedef struct {
sentry_dsn_t *dsn;
sentry_transport_t *transport;
sentry_run_t *run;
long *user_consent; // (atomic) NULL if consent not required
} sentry_batcher_t;

typedef struct {
Expand Down
33 changes: 20 additions & 13 deletions src/sentry_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,13 @@ load_user_consent(sentry_options_t *opts)
sentry__path_free(consent_path);
switch (contents ? contents[0] : 0) {
case '1':
opts->user_consent = SENTRY_USER_CONSENT_GIVEN;
opts->run->user_consent = SENTRY_USER_CONSENT_GIVEN;
break;
case '0':
opts->user_consent = SENTRY_USER_CONSENT_REVOKED;
opts->run->user_consent = SENTRY_USER_CONSENT_REVOKED;
break;
default:
opts->user_consent = SENTRY_USER_CONSENT_UNKNOWN;
opts->run->user_consent = SENTRY_USER_CONSENT_UNKNOWN;
break;
}
sentry_free(contents);
Expand All @@ -97,9 +97,7 @@ sentry__should_skip_upload(void)
{
bool skip = true;
SENTRY_WITH_OPTIONS (options) {
skip = options->require_user_consent
&& sentry__atomic_fetch((long *)&options->user_consent)
!= SENTRY_USER_CONSENT_GIVEN;
skip = sentry__run_should_skip_upload(options->run);
}
return skip;
}
Expand Down Expand Up @@ -205,6 +203,7 @@ sentry_init(sentry_options_t *options)
SENTRY_WARN("failed to initialize run directory");
goto fail;
}
options->run->require_user_consent = options->require_user_consent;

load_user_consent(options);

Expand Down Expand Up @@ -435,7 +434,7 @@ static void
set_user_consent(sentry_user_consent_t new_val)
{
SENTRY_WITH_OPTIONS (options) {
if (sentry__atomic_store((long *)&options->user_consent, new_val)
if (sentry__atomic_store(&options->run->user_consent, new_val)
!= new_val) {
if (options->backend
&& options->backend->user_consent_changed_func) {
Expand All @@ -447,6 +446,8 @@ set_user_consent(sentry_user_consent_t new_val)
switch (new_val) {
case SENTRY_USER_CONSENT_GIVEN:
sentry__path_write_buffer(consent_path, "1\n", 2);
// flush any envelopes cached while consent was revoked
sentry_transport_retry(options->transport);
break;
case SENTRY_USER_CONSENT_REVOKED:
sentry__path_write_buffer(consent_path, "0\n", 2);
Expand Down Expand Up @@ -484,7 +485,7 @@ sentry_user_consent_get(void)
sentry_user_consent_t rv = SENTRY_USER_CONSENT_UNKNOWN;
SENTRY_WITH_OPTIONS (options) {
rv = (sentry_user_consent_t)(int)sentry__atomic_fetch(
(long *)&options->user_consent);
&options->run->user_consent);
}
return rv;
}
Expand All @@ -503,13 +504,19 @@ void
sentry__capture_envelope(
sentry_transport_t *transport, sentry_envelope_t *envelope)
{
bool has_consent = !sentry__should_skip_upload();
if (!has_consent) {
SENTRY_INFO("discarding envelope due to missing user consent");
sentry_envelope_free(envelope);
if (!sentry__should_skip_upload()) {
sentry__transport_send_envelope(transport, envelope);
return;
}
sentry__transport_send_envelope(transport, envelope);
bool cached = false;
SENTRY_WITH_OPTIONS (options) {
if (options->cache_keep || options->http_retry) {
cached = sentry__run_write_cache(options->run, envelope, 0);
}
}
SENTRY_INFO(cached ? "caching envelope due to missing user consent"
: "discarding envelope due to missing user consent");
sentry_envelope_free(envelope);
}

void
Expand Down
4 changes: 4 additions & 0 deletions src/sentry_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
/**
* This function will check the user consent, and return `true` if uploads
* should *not* be sent to the sentry server, and be discarded instead.
*
* Note: This function acquires the options lock internally. Use
* `sentry__run_should_skip_upload` from worker threads that may run while
* the options are locked during SDK shutdown.
*/
bool sentry__should_skip_upload(void);

Expand Down
18 changes: 16 additions & 2 deletions src/sentry_database.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ sentry__run_new(const sentry_path_t *database_path)
}

run->refcount = 1;
run->require_user_consent = 0;
run->user_consent = SENTRY_USER_CONSENT_UNKNOWN;
run->uuid = uuid;
run->run_path = run_path;
run->session_path = session_path;
Expand All @@ -96,6 +98,14 @@ sentry__run_new(const sentry_path_t *database_path)
return NULL;
}

bool
sentry__run_should_skip_upload(sentry_run_t *run)
{
return sentry__atomic_fetch(&run->require_user_consent)
&& (sentry__atomic_fetch(&run->user_consent)
!= SENTRY_USER_CONSENT_GIVEN);
}

sentry_run_t *
sentry__run_incref(sentry_run_t *run)
{
Expand Down Expand Up @@ -426,7 +436,9 @@ 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 (envelope) {
sentry__capture_envelope(options->transport, envelope);
}
}

sentry__path_remove(file);
Expand All @@ -438,7 +450,9 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash)
}
sentry__pathiter_free(db_iter);

sentry__capture_envelope(options->transport, session_envelope);
if (session_envelope) {
sentry__capture_envelope(options->transport, session_envelope);
}
}

// Cache Pruning below is based on prune_crash_reports.cc from Crashpad
Expand Down
11 changes: 11 additions & 0 deletions src/sentry_database.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,19 @@ typedef struct sentry_run_s {
sentry_path_t *cache_path;
sentry_filelock_t *lock;
long refcount;
long require_user_consent; // (atomic) bool
long user_consent; // (atomic) sentry_user_consent_t
} sentry_run_t;

/**
* This function will check the user consent, and return `true` if uploads
* should *not* be sent to the sentry server, and be discarded instead.
*
* This is a lock-free variant of `sentry__should_skip_upload`, safe to call
* from worker threads while the options are locked during SDK shutdown.
*/
bool sentry__run_should_skip_upload(sentry_run_t *run);

/**
* This creates a new application run including its associated directory and
* lockfile:
Expand Down
1 change: 0 additions & 1 deletion src/sentry_options.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ sentry_options_new(void)
}
sentry_options_set_sdk_name(opts, SENTRY_SDK_NAME);
opts->max_breadcrumbs = SENTRY_BREADCRUMBS_MAX;
opts->user_consent = SENTRY_USER_CONSENT_UNKNOWN;
opts->auto_session_tracking = true;
opts->system_crash_reporter_enabled = false;
opts->attach_screenshot = false;
Expand Down
1 change: 0 additions & 1 deletion src/sentry_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ struct sentry_options_s {
struct sentry_backend_s *backend;
sentry_session_t *session;

long user_consent;
long refcount;
uint64_t shutdown_timeout;
sentry_handler_strategy_t handler_strategy;
Expand Down
4 changes: 4 additions & 0 deletions src/sentry_retry.c
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ size_t
sentry__retry_send(sentry_retry_t *retry, uint64_t before,
sentry_retry_send_func_t send_cb, void *data)
{
if (sentry__run_should_skip_upload(retry->run)) {
return 1; // keep the poll alive until consent is given
}

sentry_pathiter_t *piter
= sentry__path_iter_directory(retry->run->cache_path);
if (!piter) {
Expand Down
76 changes: 76 additions & 0 deletions tests/test_integration_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,79 @@ def test_cache_max_items_with_retry(cmake, backend, unreachable_dsn):
assert cache_dir.exists()
cache_files = list(cache_dir.glob("*.envelope"))
assert len(cache_files) <= 5


def test_cache_consent_revoke(cmake, unreachable_dsn):
"""With consent revoked and cache_keep, envelopes are cached to disk."""
tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})
cache_dir = tmp_path.joinpath(".sentry-native/cache")
env = dict(os.environ, SENTRY_DSN=unreachable_dsn)

run(
tmp_path,
"sentry_example",
[
"log",
"cache-keep",
"require-user-consent",
"user-consent-revoke",
"capture-event",
"flush",
],
env=env,
)

assert cache_dir.exists()
cache_files = list(cache_dir.glob("*.envelope"))
assert len(cache_files) == 1


def test_cache_consent_discard(cmake, unreachable_dsn):
"""With consent revoked but no cache_keep, envelopes are discarded."""
tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})
cache_dir = tmp_path.joinpath(".sentry-native/cache")
env = dict(os.environ, SENTRY_DSN=unreachable_dsn)

run(
tmp_path,
"sentry_example",
[
"log",
"require-user-consent",
"user-consent-revoke",
"capture-event",
"flush",
],
env=env,
)

assert not cache_dir.exists() or len(list(cache_dir.glob("*.envelope"))) == 0


def test_cache_consent_flush(cmake, httpserver):
"""Giving consent after capturing flushes cached envelopes immediately."""
from . import make_dsn

tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"})
cache_dir = tmp_path.joinpath(".sentry-native/cache")
env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver))

httpserver.expect_request("/api/123456/envelope/").respond_with_data("OK")

run(
tmp_path,
"sentry_example",
[
"log",
"http-retry",
"require-user-consent",
"user-consent-revoke",
"capture-event",
"user-consent-give",
"flush",
],
env=env,
)

assert len(httpserver.log) >= 1
assert not cache_dir.exists() or len(list(cache_dir.glob("*.envelope"))) == 0
Loading
Loading