From 72ce9b1b08e04ea190fa5c31cf46cf89c33b0b1b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 1 Jul 2026 19:39:23 +0200 Subject: [PATCH 01/29] feat(internal): scope observer --- src/sentry_core.c | 36 ++- src/sentry_scope.c | 55 +++- src/sentry_scope.h | 78 ++++++ tests/unit/test_scope.c | 585 ++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 15 +- 5 files changed, 760 insertions(+), 9 deletions(-) diff --git a/src/sentry_core.c b/src/sentry_core.c index e169f7308f..dad081937d 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -899,6 +899,7 @@ sentry_set_release_n(const char *release, size_t release_len) scope->release = sentry__string_clone_n(release, release_len); sentry_value_set_by_key(scope->dynamic_sampling_context, "release", sentry_value_new_string(scope->release)); + SENTRY_SCOPE_NOTIFY(scope, set_release, release, release_len); } } @@ -917,6 +918,8 @@ sentry_set_environment_n(const char *environment, size_t environment_len) = sentry__string_clone_n(environment, environment_len); sentry_value_set_by_key(scope->dynamic_sampling_context, "environment", sentry_value_new_string(scope->environment)); + SENTRY_SCOPE_NOTIFY( + scope, set_environment, environment, environment_len); } } @@ -981,9 +984,7 @@ sentry_set_tag_n( void sentry_remove_tag(const char *key) { - SENTRY_WITH_SCOPE_MUT (scope) { - sentry_value_remove_by_key(scope->tags, key); - } + sentry_remove_tag_n(key, sentry__guarded_strlen(key)); } void @@ -991,6 +992,7 @@ sentry_remove_tag_n(const char *key, size_t key_len) { SENTRY_WITH_SCOPE_MUT (scope) { sentry_value_remove_by_key_n(scope->tags, key, key_len); + SENTRY_SCOPE_NOTIFY(scope, remove_tag, key, key_len); } } @@ -1015,6 +1017,8 @@ sentry_remove_extra(const char *key) { SENTRY_WITH_SCOPE_MUT (scope) { sentry_value_remove_by_key(scope->extra, key); + SENTRY_SCOPE_NOTIFY( + scope, remove_extra, key, sentry__guarded_strlen(key)); } } @@ -1023,6 +1027,7 @@ sentry_remove_extra_n(const char *key, size_t key_len) { SENTRY_WITH_SCOPE_MUT (scope) { sentry_value_remove_by_key_n(scope->extra, key, key_len); + SENTRY_SCOPE_NOTIFY(scope, remove_extra, key, key_len); } } @@ -1096,6 +1101,8 @@ sentry__set_propagation_context(const char *key, sentry_value_t value) { SENTRY_WITH_SCOPE_MUT (scope) { sentry_value_set_by_key(scope->propagation_context, key, value); + SENTRY_SCOPE_NOTIFY( + scope, set_context, key, sentry__guarded_strlen(key), value); } } @@ -1130,6 +1137,8 @@ sentry_remove_context(const char *key) { SENTRY_WITH_SCOPE_MUT (scope) { sentry_value_remove_by_key(scope->contexts, key); + SENTRY_SCOPE_NOTIFY( + scope, remove_context, key, sentry__guarded_strlen(key)); } } @@ -1138,6 +1147,7 @@ sentry_remove_context_n(const char *key, size_t key_len) { SENTRY_WITH_SCOPE_MUT (scope) { sentry_value_remove_by_key_n(scope->contexts, key, key_len); + SENTRY_SCOPE_NOTIFY(scope, remove_context, key, key_len); } } @@ -1174,6 +1184,7 @@ sentry_remove_fingerprint(void) SENTRY_WITH_SCOPE_MUT (scope) { sentry_value_decref(scope->fingerprint); scope->fingerprint = sentry_value_new_null(); + SENTRY_SCOPE_NOTIFY(scope, set_fingerprint, scope->fingerprint); } } @@ -1228,6 +1239,9 @@ sentry_regenerate_trace(void) generate_propagation_context(scope->propagation_context); scope->trace_managed = false; sentry__scope_update_dsc(scope, options); + SENTRY_SCOPE_NOTIFY(scope, set_context, "trace", + sentry__guarded_strlen("trace"), + sentry_value_get_by_key(scope->propagation_context, "trace")); } } } @@ -1242,6 +1256,8 @@ sentry_set_transaction(const char *transaction) if (scope->transaction_object) { sentry_transaction_set_name(scope->transaction_object, transaction); } + SENTRY_SCOPE_NOTIFY(scope, set_transaction, transaction, + sentry__guarded_strlen(transaction)); } } @@ -1257,6 +1273,8 @@ sentry_set_transaction_n(const char *transaction, size_t transaction_len) sentry_transaction_set_name_n( scope->transaction_object, transaction, transaction_len); } + SENTRY_SCOPE_NOTIFY( + scope, set_transaction, transaction, transaction_len); } } @@ -1910,6 +1928,9 @@ add_attachment(sentry_attachment_t *attachment) } SENTRY_WITH_SCOPE_MUT (scope) { attachment = sentry__attachments_add(&scope->attachments, attachment); + if (attachment) { + SENTRY_SCOPE_NOTIFY(scope, add_attachment, attachment); + } } return attachment; } @@ -1947,12 +1968,14 @@ sentry_clear_attachments(void) { SENTRY_WITH_OPTIONS (options) { SENTRY_WITH_SCOPE_MUT (scope) { - if (options->backend && options->backend->remove_attachment_func) { - for (sentry_attachment_t *it = scope->attachments; it; - it = it->next) { + for (sentry_attachment_t *it = scope->attachments; it; + it = it->next) { + if (options->backend + && options->backend->remove_attachment_func) { options->backend->remove_attachment_func( options->backend, it); } + SENTRY_SCOPE_NOTIFY(scope, remove_attachment, it); } sentry__attachments_free(scope->attachments); scope->attachments = NULL; @@ -1970,6 +1993,7 @@ sentry_remove_attachment(sentry_attachment_t *attachment) } } SENTRY_WITH_SCOPE_MUT (scope) { + SENTRY_SCOPE_NOTIFY(scope, remove_attachment, attachment); sentry__attachments_remove(&scope->attachments, attachment); } } diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 1a6f54a55a..43502ea6f0 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -88,6 +88,9 @@ init_scope(sentry_scope_t *scope) scope->transaction_object = NULL; scope->span = NULL; scope->trace_managed = true; + scope->observers = NULL; + scope->num_observers = 0; + scope->is_notifying = false; } static sentry_scope_t * @@ -127,6 +130,12 @@ cleanup_scope(sentry_scope_t *scope) sentry__attachments_free(scope->attachments); sentry__transaction_decref(scope->transaction_object); sentry__span_decref(scope->span); + for (size_t i = 0; i < scope->num_observers; i++) { + sentry_free(scope->observers[i]); + } + sentry_free(scope->observers); + scope->observers = NULL; + scope->num_observers = 0; } void @@ -169,6 +178,37 @@ sentry__scope_flush_unlock(void) } } +sentry_scope_observer_t * +sentry__scope_observer_new(void) +{ + return SENTRY_MAKE(sentry_scope_observer_t); +} + +void +sentry__scope_add_observer( + sentry_scope_t *scope, sentry_scope_observer_t *observer) +{ + if (!observer) { + return; + } + + size_t new_count = scope->num_observers + 1; + sentry_scope_observer_t **new_array + = sentry__calloc(new_count, sizeof(sentry_scope_observer_t *)); + if (!new_array) { + sentry_free(observer); + return; + } + if (scope->observers) { + memcpy(new_array, scope->observers, + scope->num_observers * sizeof(sentry_scope_observer_t *)); + sentry_free(scope->observers); + } + new_array[scope->num_observers] = observer; + scope->observers = new_array; + scope->num_observers = new_count; +} + sentry_scope_t * sentry_local_scope_new(void) { @@ -516,6 +556,7 @@ void sentry_scope_add_breadcrumb(sentry_scope_t *scope, sentry_value_t breadcrumb) { sentry__ringbuffer_append(scope->breadcrumbs, breadcrumb); + SENTRY_SCOPE_NOTIFY(scope, add_breadcrumb, breadcrumb); } void @@ -523,12 +564,14 @@ sentry_scope_set_user(sentry_scope_t *scope, sentry_value_t user) { sentry_value_decref(scope->user); scope->user = user; + SENTRY_SCOPE_NOTIFY(scope, set_user, user); } void sentry_scope_set_tag(sentry_scope_t *scope, const char *key, const char *value) { - sentry_value_set_by_key(scope->tags, key, sentry_value_new_string(value)); + sentry_scope_set_tag_n(scope, key, sentry__guarded_strlen(key), value, + sentry__guarded_strlen(value)); } void @@ -537,13 +580,14 @@ sentry_scope_set_tag_n(sentry_scope_t *scope, const char *key, size_t key_len, { sentry_value_set_by_key_n( scope->tags, key, key_len, sentry_value_new_string_n(value, value_len)); + SENTRY_SCOPE_NOTIFY(scope, set_tag, key, key_len, value, value_len); } void sentry_scope_set_extra( sentry_scope_t *scope, const char *key, sentry_value_t value) { - sentry_value_set_by_key(scope->extra, key, value); + sentry_scope_set_extra_n(scope, key, sentry__guarded_strlen(key), value); } void @@ -551,6 +595,7 @@ sentry_scope_set_extra_n(sentry_scope_t *scope, const char *key, size_t key_len, sentry_value_t value) { sentry_value_set_by_key_n(scope->extra, key, key_len, value); + SENTRY_SCOPE_NOTIFY(scope, set_extra, key, key_len, value); } void @@ -590,6 +635,8 @@ sentry_scope_set_context( sentry_scope_t *scope, const char *key, sentry_value_t value) { sentry_value_set_by_key(scope->contexts, key, value); + SENTRY_SCOPE_NOTIFY( + scope, set_context, key, sentry__guarded_strlen(key), value); } void @@ -597,6 +644,7 @@ sentry_scope_set_context_n(sentry_scope_t *scope, const char *key, size_t key_len, sentry_value_t value) { sentry_value_set_by_key_n(scope->contexts, key, key_len, value); + SENTRY_SCOPE_NOTIFY(scope, set_context, key, key_len, value); } void @@ -633,6 +681,7 @@ sentry__scope_set_fingerprint_va( sentry_value_decref(scope->fingerprint); scope->fingerprint = fingerprint_value; + SENTRY_SCOPE_NOTIFY(scope, set_fingerprint, fingerprint_value); } void @@ -687,12 +736,14 @@ sentry_scope_set_fingerprints( sentry_value_decref(scope->fingerprint); scope->fingerprint = fingerprints; + SENTRY_SCOPE_NOTIFY(scope, set_fingerprint, fingerprints); } void sentry_scope_set_level(sentry_scope_t *scope, sentry_level_t level) { scope->level = level; + SENTRY_SCOPE_NOTIFY(scope, set_level, level); } sentry_attachment_t * diff --git a/src/sentry_scope.h b/src/sentry_scope.h index 6047fa8035..47c56ee568 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -8,6 +8,45 @@ #include "sentry_session.h" #include "sentry_value.h" +/** + * Scope observer — one callback per scope property. + * + * Implementors set the function pointers they care about. NULL pointers are + * skipped. Callbacks are invoked while the scope lock is held. + * + * Ownership: the scope takes ownership of the observer pointer on + * registration; the caller must not free it after that point. + */ +typedef struct sentry_scope_observer_s { + void *data; + + void (*set_release)(void *data, const char *release, size_t release_len); + void (*set_environment)( + void *data, const char *environment, size_t environment_len); + void (*set_transaction)( + void *data, const char *transaction, size_t transaction_len); + void (*set_fingerprint)(void *data, sentry_value_t fingerprint); + void (*set_level)(void *data, sentry_level_t level); + void (*set_user)(void *data, sentry_value_t user); + + void (*add_breadcrumb)(void *data, sentry_value_t breadcrumb); + + void (*set_tag)(void *data, const char *key, size_t key_len, + const char *value, size_t value_len); + void (*remove_tag)(void *data, const char *key, size_t key_len); + + void (*set_extra)( + void *data, const char *key, size_t key_len, sentry_value_t value); + void (*remove_extra)(void *data, const char *key, size_t key_len); + + void (*set_context)( + void *data, const char *key, size_t key_len, sentry_value_t value); + void (*remove_context)(void *data, const char *key, size_t key_len); + + void (*add_attachment)(void *data, sentry_attachment_t *attachment); + void (*remove_attachment)(void *data, sentry_attachment_t *attachment); +} sentry_scope_observer_t; + /** * This represents the current scope. */ @@ -39,6 +78,10 @@ struct sentry_scope_s { sentry_transaction_t *transaction_object; sentry_span_t *span; bool trace_managed; + + sentry_scope_observer_t **observers; + size_t num_observers; + bool is_notifying; }; /** @@ -128,6 +171,41 @@ void sentry__scope_remove_attribute_n( for (sentry_scope_t *Scope = sentry__scope_lock(); Scope; \ sentry__scope_unlock(), Scope = NULL) +/** + * Allocate and zero-initialize a scope observer. + * + * Returns NULL on allocation failure. The caller sets whichever callback + * function pointers and the `data` pointer they need, then registers the + * observer with `sentry__scope_add_observer`, which takes ownership. + */ +sentry_scope_observer_t *sentry__scope_observer_new(void); + +/** + * Register a scope observer. + * + * Takes ownership of `observer`; the caller must not use or free it after + * this call. Must be called while holding the scope lock (i.e., inside + * SENTRY_WITH_SCOPE_MUT). Registration order is respected — observers are + * notified in registration order. + */ +void sentry__scope_add_observer( + sentry_scope_t *scope, sentry_scope_observer_t *observer); + +/** Re-entrancy guard: set while notifying observers. */ +#define SENTRY_SCOPE_NOTIFY(scope, callback, ...) \ + do { \ + if ((scope)->is_notifying) \ + break; \ + (scope)->is_notifying = true; \ + for (size_t _i = 0; _i < (scope)->num_observers; _i++) { \ + sentry_scope_observer_t *_observer = (scope)->observers[_i]; \ + if (_observer->callback) { \ + _observer->callback(_observer->data, __VA_ARGS__); \ + } \ + } \ + (scope)->is_notifying = false; \ + } while (0) + /** * Rebuilds the scope's dynamic sampling context (DSC) from the SDK options * and the current propagation context. The previous DSC is discarded. diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 0ac3951ff9..1f11da71e1 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1132,3 +1132,588 @@ SENTRY_TEST(scope_local_attributes) sentry_close(); } + +typedef struct { + sentry_value_t release; + sentry_value_t environment; + sentry_value_t transaction; + sentry_value_t fingerprint; + sentry_level_t level; + sentry_value_t user; + sentry_value_t breadcrumbs; + sentry_value_t tags; + sentry_value_t extras; + sentry_value_t contexts; + sentry_value_t attachments; + bool was_called; +} test_observer_data_t; + +static void +observe_set_release(void *data, const char *release, size_t release_len) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + d->release = sentry_value_new_string_n(release, release_len); + d->was_called = true; +} + +static void +observe_set_environment( + void *data, const char *environment, size_t environment_len) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + d->environment = sentry_value_new_string_n(environment, environment_len); + d->was_called = true; +} + +static void +observe_set_transaction( + void *data, const char *transaction, size_t transaction_len) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + d->transaction = sentry_value_new_string_n(transaction, transaction_len); + d->was_called = true; +} + +static void +observe_set_fingerprint(void *data, sentry_value_t fingerprint) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + if (!sentry_value_is_null(d->fingerprint)) { + sentry_value_decref(d->fingerprint); + } + sentry_value_incref(fingerprint); + d->fingerprint = fingerprint; + d->was_called = true; +} + +static void +observe_set_level(void *data, sentry_level_t level) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + d->level = level; + d->was_called = true; +} + +static void +observe_add_attachment(void *data, sentry_attachment_t *attachment) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + if (sentry_value_is_null(d->attachments)) { + d->attachments = sentry_value_new_list(); + } + sentry_value_t obj = sentry_value_new_object(); + const char *filename = sentry__attachment_get_filename(attachment); + if (filename) { + sentry_value_set_by_key( + obj, "filename", sentry_value_new_string(filename)); + } + if (attachment->buf) { + sentry_value_set_by_key(obj, "buf", + sentry_value_new_string_n(attachment->buf, attachment->buf_len)); + } + sentry_value_append(d->attachments, obj); + d->was_called = true; +} + +static void +observe_remove_attachment(void *data, sentry_attachment_t *attachment) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + if (sentry_value_is_null(d->attachments)) { + d->attachments = sentry_value_new_list(); + } + sentry_value_t obj = sentry_value_new_object(); + const char *filename = sentry__attachment_get_filename(attachment); + if (filename) { + sentry_value_set_by_key( + obj, "filename", sentry_value_new_string(filename)); + } + sentry_value_set_by_key(obj, "removed", sentry_value_new_bool(true)); + sentry_value_append(d->attachments, obj); + d->was_called = true; +} + +static void +observe_set_user(void *data, sentry_value_t user) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + d->user = user; + d->was_called = true; +} + +static void +observe_add_breadcrumb(void *data, sentry_value_t breadcrumb) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + if (sentry_value_is_null(d->breadcrumbs)) { + d->breadcrumbs = sentry_value_new_list(); + } + sentry_value_incref(breadcrumb); + sentry_value_append(d->breadcrumbs, breadcrumb); + d->was_called = true; +} + +static void +observe_set_tag(void *data, const char *key, size_t key_len, const char *value, + size_t value_len) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + if (sentry_value_is_null(d->tags)) { + d->tags = sentry_value_new_object(); + } + sentry_value_set_by_key_n( + d->tags, key, key_len, sentry_value_new_string_n(value, value_len)); + d->was_called = true; +} + +static void +observe_remove_tag(void *data, const char *key, size_t key_len) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + if (sentry_value_is_null(d->tags)) { + d->tags = sentry_value_new_object(); + } + sentry_value_set_by_key_n( + d->tags, key, key_len, sentry_value_new_string("(removed)")); + d->was_called = true; +} + +static void +observe_set_extra( + void *data, const char *key, size_t key_len, sentry_value_t value) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + if (sentry_value_is_null(d->extras)) { + d->extras = sentry_value_new_object(); + } + sentry_value_incref(value); + sentry_value_set_by_key_n(d->extras, key, key_len, value); + d->was_called = true; +} + +static void +observe_remove_extra(void *data, const char *key, size_t key_len) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + if (sentry_value_is_null(d->extras)) { + d->extras = sentry_value_new_object(); + } + sentry_value_set_by_key_n( + d->extras, key, key_len, sentry_value_new_string("(removed)")); + d->was_called = true; +} + +static void +observe_set_context( + void *data, const char *key, size_t key_len, sentry_value_t value) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + if (sentry_value_is_null(d->contexts)) { + d->contexts = sentry_value_new_object(); + } + sentry_value_incref(value); + sentry_value_set_by_key_n(d->contexts, key, key_len, value); + d->was_called = true; +} + +static void +observe_remove_context(void *data, const char *key, size_t key_len) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + if (sentry_value_is_null(d->contexts)) { + d->contexts = sentry_value_new_object(); + } + sentry_value_set_by_key_n( + d->contexts, key, key_len, sentry_value_new_string("(removed)")); + d->was_called = true; +} + +SENTRY_TEST(scope_observer_null) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d = { .tags = sentry_value_new_null() }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->set_tag = observe_set_tag; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + + sentry_set_tag("my-tag", "my-value"); + TEST_CHECK(d.was_called); + + d.was_called = false; + sentry_remove_tag("my-tag"); + TEST_CHECK(!d.was_called); + + sentry_value_decref(d.tags); + + sentry_close(); +} + +SENTRY_TEST(scope_observer_multiple) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d1 = { .tags = sentry_value_new_null() }; + test_observer_data_t d2 = { .tags = sentry_value_new_null() }; + sentry_scope_observer_t *observer1 = sentry__scope_observer_new(); + observer1->data = &d1; + observer1->set_tag = observe_set_tag; + + sentry_scope_observer_t *observer2 = sentry__scope_observer_new(); + observer2->data = &d2; + observer2->set_tag = observe_set_tag; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer1); + sentry__scope_add_observer(scope, observer2); + } + + sentry_set_tag("multi", "test"); + TEST_CHECK(d1.was_called); + TEST_CHECK(d2.was_called); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(d1.tags, "multi")), + "test"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(d2.tags, "multi")), + "test"); + + sentry_value_decref(d1.tags); + sentry_value_decref(d2.tags); + sentry_close(); +} + +SENTRY_TEST(scope_observer_release) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d = { 0 }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->set_release = observe_set_release; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + + sentry_set_release("my-release"); + TEST_CHECK(d.was_called); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(d.release), "my-release"); + + sentry_value_decref(d.release); + sentry_close(); +} + +SENTRY_TEST(scope_observer_environment) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d = { 0 }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->set_environment = observe_set_environment; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + + sentry_set_environment("my-env"); + TEST_CHECK(d.was_called); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(d.environment), "my-env"); + + sentry_value_decref(d.environment); + sentry_close(); +} + +SENTRY_TEST(scope_observer_transaction) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d = { 0 }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->set_transaction = observe_set_transaction; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + + sentry_set_transaction("my-transaction"); + TEST_CHECK(d.was_called); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(d.transaction), "my-transaction"); + + sentry_value_decref(d.transaction); + sentry_close(); +} + +SENTRY_TEST(scope_observer_fingerprint) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d = { 0 }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->set_fingerprint = observe_set_fingerprint; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + + sentry_set_fingerprint("my-fingerprint", NULL); + TEST_CHECK(d.was_called); + TEST_CHECK(!sentry_value_is_null(d.fingerprint)); + TEST_CHECK_JSON_VALUE(d.fingerprint, "[\"my-fingerprint\"]"); + + d.was_called = false; + sentry_remove_fingerprint(); + TEST_CHECK(d.was_called); + TEST_CHECK(sentry_value_is_null(d.fingerprint)); + + sentry_close(); +} + +SENTRY_TEST(scope_observer_level) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d = { 0 }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->set_level = observe_set_level; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + + sentry_set_level(SENTRY_LEVEL_WARNING); + TEST_CHECK(d.was_called); + TEST_CHECK(d.level == SENTRY_LEVEL_WARNING); + + sentry_close(); +} + +SENTRY_TEST(scope_observer_user) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d = { 0 }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->set_user = observe_set_user; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + + sentry_value_t user = sentry_value_new_object(); + sentry_value_set_by_key(user, "id", sentry_value_new_string("user123")); + sentry_set_user(user); + TEST_CHECK(d.was_called); + TEST_CHECK(!sentry_value_is_null(d.user)); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(d.user, "id")), + "user123"); + + d.was_called = false; + sentry_remove_user(); + TEST_CHECK(d.was_called); + TEST_CHECK(sentry_value_is_null(d.user)); + + sentry_close(); +} + +SENTRY_TEST(scope_observer_breadcrumbs) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d = { .breadcrumbs = sentry_value_new_null() }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->add_breadcrumb = observe_add_breadcrumb; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + + sentry_add_breadcrumb( + sentry_value_new_breadcrumb(NULL, "first breadcrumb")); + TEST_CHECK(d.was_called); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(d.breadcrumbs), 1); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_index(d.breadcrumbs, 0), "message")), + "first breadcrumb"); + + sentry_add_breadcrumb( + sentry_value_new_breadcrumb("warning", "second breadcrumb")); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(d.breadcrumbs), 2); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_index(d.breadcrumbs, 1), "message")), + "second breadcrumb"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_index(d.breadcrumbs, 1), "type")), + "warning"); + + sentry_value_decref(d.breadcrumbs); + sentry_close(); +} + +SENTRY_TEST(scope_observer_tags) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d = { .tags = sentry_value_new_null() }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->set_tag = observe_set_tag; + observer->remove_tag = observe_remove_tag; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + + sentry_set_tag("my-tag", "my-value"); + TEST_CHECK(d.was_called); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(d.tags, "my-tag")), + "my-value"); + TEST_CHECK_INT_EQUAL( + sentry_value_get_length(sentry_value_get_by_key(d.tags, "my-tag")), 8); + + d.was_called = false; + sentry_remove_tag("my-tag"); + TEST_CHECK(d.was_called); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(d.tags, "my-tag")), + "(removed)"); + + sentry_value_decref(d.tags); + sentry_close(); +} + +SENTRY_TEST(scope_observer_extras) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d = { .extras = sentry_value_new_null() }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->set_extra = observe_set_extra; + observer->remove_extra = observe_remove_extra; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + + sentry_value_t val = sentry_value_new_string("extra-value"); + sentry_set_extra("my-extra", val); + TEST_CHECK(d.was_called); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(d.extras, "my-extra")), + "extra-value"); + + d.was_called = false; + sentry_remove_extra("my-extra"); + TEST_CHECK(d.was_called); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(d.extras, "my-extra")), + "(removed)"); + + sentry_value_decref(d.extras); + sentry_close(); +} + +SENTRY_TEST(scope_observer_contexts) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d = { .contexts = sentry_value_new_null() }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->set_context = observe_set_context; + observer->remove_context = observe_remove_context; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + + sentry_value_t ctx = sentry_value_new_object(); + sentry_value_set_by_key(ctx, "type", sentry_value_new_string("device")); + sentry_set_context("my-context", ctx); + TEST_CHECK(d.was_called); + sentry_value_t received = sentry_value_get_by_key(d.contexts, "my-context"); + TEST_CHECK(!sentry_value_is_null(received)); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(received, "type")), + "device"); + + d.was_called = false; + sentry_remove_context("my-context"); + TEST_CHECK(d.was_called); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + d.contexts, "my-context")), + "(removed)"); + + sentry_value_decref(d.contexts); + sentry_close(); +} + +SENTRY_TEST(scope_observer_attachments) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d = { .attachments = sentry_value_new_null() }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->add_attachment = observe_add_attachment; + observer->remove_attachment = observe_remove_attachment; + + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + + sentry_attachment_t *attachment = sentry_attach_bytes("buf", 3, "test.txt"); + TEST_CHECK(d.was_called); + TEST_CHECK(attachment != NULL); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(d.attachments), 1); + sentry_value_t added = sentry_value_get_by_index(d.attachments, 0); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(added, "buf")), "buf"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(added, "filename")), + "test.txt"); + + d.was_called = false; + sentry_remove_attachment(attachment); + TEST_CHECK(d.was_called); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(d.attachments), 2); + sentry_value_t removed = sentry_value_get_by_index(d.attachments, 1); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(removed, "filename")), + "test.txt"); + TEST_CHECK( + sentry_value_is_true(sentry_value_get_by_key(removed, "removed"))); + + sentry_value_decref(d.attachments); + sentry_close(); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 86addc6da0..7e31f205d3 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -285,14 +285,27 @@ XX(sampling_decision) XX(sampling_transaction) XX(scope_breadcrumbs) XX(scope_contexts) -XX(scope_update_context) XX(scope_extra) XX(scope_fingerprint) XX(scope_fingerprint_n) XX(scope_global_attributes) XX(scope_level) XX(scope_local_attributes) +XX(scope_observer_attachments) +XX(scope_observer_breadcrumbs) +XX(scope_observer_contexts) +XX(scope_observer_environment) +XX(scope_observer_extras) +XX(scope_observer_fingerprint) +XX(scope_observer_level) +XX(scope_observer_multiple) +XX(scope_observer_null) +XX(scope_observer_release) +XX(scope_observer_tags) +XX(scope_observer_transaction) +XX(scope_observer_user) XX(scope_tags) +XX(scope_update_context) XX(scope_user) XX(scope_user_id) XX(scoped_txn) From 15f7e018df50f76289dbb987ece7e91536909a1c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 3 Jul 2026 19:14:03 +0200 Subject: [PATCH 02/29] sentry_scope_update_context: add missing notify --- src/sentry_scope.c | 1 + tests/unit/test_scope.c | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 43502ea6f0..ffd38d495a 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -667,6 +667,7 @@ sentry_scope_update_context_n(sentry_scope_t *scope, const char *key, sentry__value_merge_objects(value, context); sentry_value_set_by_key_n(scope->contexts, key, key_len, value); } + SENTRY_SCOPE_NOTIFY(scope, set_context, key, key_len, value); } void diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 1f11da71e1..9e965acb00 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1666,6 +1666,19 @@ SENTRY_TEST(scope_observer_contexts) sentry_value_as_string(sentry_value_get_by_key(received, "type")), "device"); + sentry_value_t update = sentry_value_new_object(); + sentry_value_set_by_key(update, "version", sentry_value_new_string("1.0")); + d.was_called = false; + sentry_update_context("my-context", update); + TEST_CHECK(d.was_called); + received = sentry_value_get_by_key(d.contexts, "my-context"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(received, "type")), + "device"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(received, "version")), + "1.0"); + d.was_called = false; sentry_remove_context("my-context"); TEST_CHECK(d.was_called); From dd7e4fcd5f6a7f68ad8737474c9e6030a03b1867 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 3 Jul 2026 19:19:38 +0200 Subject: [PATCH 03/29] sentry__ringbuffer_append returns -1 on failure --- src/sentry_scope.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sentry_scope.c b/src/sentry_scope.c index ffd38d495a..238298bb60 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -555,8 +555,9 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, void sentry_scope_add_breadcrumb(sentry_scope_t *scope, sentry_value_t breadcrumb) { - sentry__ringbuffer_append(scope->breadcrumbs, breadcrumb); - SENTRY_SCOPE_NOTIFY(scope, add_breadcrumb, breadcrumb); + if (sentry__ringbuffer_append(scope->breadcrumbs, breadcrumb) == 0) { + SENTRY_SCOPE_NOTIFY(scope, add_breadcrumb, breadcrumb); + } } void From 3470b4058b77d9db353589f573c3d724647fd2be Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 3 Jul 2026 19:38:49 +0200 Subject: [PATCH 04/29] don't notify duplicate attachments --- src/sentry_attachment.c | 10 ++++++---- src/sentry_attachment.h | 3 ++- src/sentry_core.c | 32 +++++++++++++++++++++++--------- tests/unit/test_scope.c | 21 +++++++++++++++++++++ 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/sentry_attachment.c b/src/sentry_attachment.c index 57d11bb4bc..c337a02f87 100644 --- a/src/sentry_attachment.c +++ b/src/sentry_attachment.c @@ -262,12 +262,12 @@ sentry__attachments_add_path(sentry_attachment_t **attachments_ptr, return sentry__attachments_add(attachments_ptr, attachment); } -void +bool sentry__attachments_remove( sentry_attachment_t **attachments_ptr, sentry_attachment_t *attachment) { if (!attachment) { - return; + return false; } sentry_attachment_t **next_ptr = attachments_ptr; @@ -275,12 +275,14 @@ sentry__attachments_remove( for (sentry_attachment_t *it = *attachments_ptr; it; it = it->next) { if (it == attachment) { *next_ptr = it->next; - sentry__attachment_free(it); - return; + it->next = NULL; + return true; } next_ptr = &it->next; } + + return false; } static sentry_attachment_t * diff --git a/src/sentry_attachment.h b/src/sentry_attachment.h index 0004164e76..0ddf96f35d 100644 --- a/src/sentry_attachment.h +++ b/src/sentry_attachment.h @@ -90,8 +90,9 @@ sentry_attachment_t *sentry__attachments_add_path( /** * Removes an attachment from the attachments list at `attachments_ptr`. + * Returns true if the attachment was found and removed. */ -void sentry__attachments_remove( +bool sentry__attachments_remove( sentry_attachment_t **attachments_ptr, sentry_attachment_t *attachment); /** diff --git a/src/sentry_core.c b/src/sentry_core.c index dad081937d..f189be2d71 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -1921,15 +1921,22 @@ sentry_capture_minidump_n(const char *path, size_t path_len) static sentry_attachment_t * add_attachment(sentry_attachment_t *attachment) { + if (!attachment) { + return NULL; + } + SENTRY_WITH_OPTIONS (options) { if (options->backend && options->backend->add_attachment_func) { options->backend->add_attachment_func(options->backend, attachment); } } SENTRY_WITH_SCOPE_MUT (scope) { - attachment = sentry__attachments_add(&scope->attachments, attachment); - if (attachment) { + sentry_attachment_t *new_attachment + = sentry__attachments_add(&scope->attachments, attachment); + if (attachment == new_attachment) { SENTRY_SCOPE_NOTIFY(scope, add_attachment, attachment); + } else { + attachment = new_attachment; // existing/duplicate } } return attachment; @@ -1986,16 +1993,23 @@ sentry_clear_attachments(void) void sentry_remove_attachment(sentry_attachment_t *attachment) { + if (!attachment) { + return; + } + SENTRY_WITH_OPTIONS (options) { - if (options->backend && options->backend->remove_attachment_func) { - options->backend->remove_attachment_func( - options->backend, attachment); + SENTRY_WITH_SCOPE_MUT (scope) { + if (sentry__attachments_remove(&scope->attachments, attachment)) { + if (options->backend + && options->backend->remove_attachment_func) { + options->backend->remove_attachment_func( + options->backend, attachment); + } + SENTRY_SCOPE_NOTIFY(scope, remove_attachment, attachment); + sentry__attachment_free(attachment); + } } } - SENTRY_WITH_SCOPE_MUT (scope) { - SENTRY_SCOPE_NOTIFY(scope, remove_attachment, attachment); - sentry__attachments_remove(&scope->attachments, attachment); - } } #ifdef SENTRY_PLATFORM_WINDOWS diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 9e965acb00..b45978e2eb 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1727,6 +1727,27 @@ SENTRY_TEST(scope_observer_attachments) TEST_CHECK( sentry_value_is_true(sentry_value_get_by_key(removed, "removed"))); + attachment = sentry_attach_file("test.txt"); + TEST_CHECK(d.was_called); + TEST_CHECK(attachment != NULL); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(d.attachments), 3); + + d.was_called = false; + sentry_attachment_t *duplicate = sentry_attach_file("test.txt"); + TEST_CHECK(duplicate == attachment); + TEST_CHECK(!d.was_called); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(d.attachments), 3); + + d.was_called = false; + sentry_remove_attachment(attachment); + TEST_CHECK(d.was_called); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(d.attachments), 4); + + d.was_called = false; + sentry_remove_attachment(duplicate); + TEST_CHECK(!d.was_called); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(d.attachments), 4); + sentry_value_decref(d.attachments); sentry_close(); } From 86ed17738564f6292b5ff9026469292a9da5c318 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 3 Jul 2026 19:46:13 +0200 Subject: [PATCH 05/29] sentry_transaction_start: notify --- src/sentry_core.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sentry_core.c b/src/sentry_core.c index f189be2d71..b94ee71fde 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -1347,6 +1347,10 @@ sentry_transaction_start_ts(sentry_transaction_context_t *opaque_tx_ctx, sentry_value_remove_by_key(tx, "parent_span_id"); sentry_value_remove_by_key(tx, "sampled"); sentry__scope_update_dsc(scope, options); + SENTRY_SCOPE_NOTIFY(scope, set_context, "trace", + sentry__guarded_strlen("trace"), + sentry_value_get_by_key( + scope->propagation_context, "trace")); } } } From d8f18f2a935afdd9a8ce008cb7cba3c5d6daec37 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 3 Jul 2026 20:17:07 +0200 Subject: [PATCH 06/29] notity scope-level attachment changes --- src/sentry_core.c | 8 +------- src/sentry_scope.c | 28 ++++++++++++++++++++++------ src/sentry_scope.h | 3 +++ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/sentry_core.c b/src/sentry_core.c index b94ee71fde..b8b4bd30f9 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -1935,13 +1935,7 @@ add_attachment(sentry_attachment_t *attachment) } } SENTRY_WITH_SCOPE_MUT (scope) { - sentry_attachment_t *new_attachment - = sentry__attachments_add(&scope->attachments, attachment); - if (attachment == new_attachment) { - SENTRY_SCOPE_NOTIFY(scope, add_attachment, attachment); - } else { - attachment = new_attachment; // existing/duplicate - } + attachment = sentry__scope_add_attachment(scope, attachment); } return attachment; } diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 238298bb60..f3ddef89d6 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -748,6 +748,22 @@ sentry_scope_set_level(sentry_scope_t *scope, sentry_level_t level) SENTRY_SCOPE_NOTIFY(scope, set_level, level); } +sentry_attachment_t * +sentry__scope_add_attachment( + sentry_scope_t *scope, sentry_attachment_t *attachment) +{ + if (!attachment) { + return NULL; + } + + sentry_attachment_t *added + = sentry__attachments_add(&scope->attachments, attachment); + if (added == attachment) { + SENTRY_SCOPE_NOTIFY(scope, add_attachment, attachment); + } + return added; +} + sentry_attachment_t * sentry_scope_attach_file(sentry_scope_t *scope, const char *path) { @@ -759,8 +775,8 @@ sentry_attachment_t * sentry_scope_attach_file_n( sentry_scope_t *scope, const char *path, size_t path_len) { - return sentry__attachments_add_path(&scope->attachments, - sentry__path_from_str_n(path, path_len), NULL, NULL); + return sentry__scope_add_attachment(scope, + sentry__attachment_from_path(sentry__path_from_str_n(path, path_len))); } sentry_attachment_t * @@ -775,7 +791,7 @@ sentry_attachment_t * sentry_scope_attach_bytes_n(sentry_scope_t *scope, const char *buf, size_t buf_len, const char *filename, size_t filename_len) { - return sentry__attachments_add(&scope->attachments, + return sentry__scope_add_attachment(scope, sentry__attachment_from_buffer( buf, buf_len, sentry__path_from_str_n(filename, filename_len))); } @@ -792,8 +808,8 @@ sentry_attachment_t * sentry_scope_attach_filew_n( sentry_scope_t *scope, const wchar_t *path, size_t path_len) { - return sentry__attachments_add_path(&scope->attachments, - sentry__path_from_wstr_n(path, path_len), NULL, NULL); + return sentry__scope_add_attachment(scope, + sentry__attachment_from_path(sentry__path_from_wstr_n(path, path_len))); } sentry_attachment_t * @@ -809,7 +825,7 @@ sentry_attachment_t * sentry_scope_attach_bytesw_n(sentry_scope_t *scope, const char *buf, size_t buf_len, const wchar_t *filename, size_t filename_len) { - return sentry__attachments_add(&scope->attachments, + return sentry__scope_add_attachment(scope, sentry__attachment_from_buffer( buf, buf_len, sentry__path_from_wstr_n(filename, filename_len))); } diff --git a/src/sentry_scope.h b/src/sentry_scope.h index 47c56ee568..f5199d922f 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -157,6 +157,9 @@ void sentry__scope_remove_attribute(sentry_scope_t *scope, const char *key); void sentry__scope_remove_attribute_n( sentry_scope_t *scope, const char *key, size_t key_len); +sentry_attachment_t *sentry__scope_add_attachment( + sentry_scope_t *scope, sentry_attachment_t *attachment); + /** * These are convenience macros to automatically lock/unlock the global scope * inside a code block. From b29b7ceb6dd68aae3e0873c85142e91fce812e2a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 3 Jul 2026 20:25:03 +0200 Subject: [PATCH 07/29] fix sentry_clear_attachments --- src/sentry_core.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sentry_core.c b/src/sentry_core.c index b8b4bd30f9..2341e790b6 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -1973,8 +1973,9 @@ sentry_clear_attachments(void) { SENTRY_WITH_OPTIONS (options) { SENTRY_WITH_SCOPE_MUT (scope) { - for (sentry_attachment_t *it = scope->attachments; it; - it = it->next) { + sentry_attachment_t *attachments = scope->attachments; + scope->attachments = NULL; + for (sentry_attachment_t *it = attachments; it; it = it->next) { if (options->backend && options->backend->remove_attachment_func) { options->backend->remove_attachment_func( @@ -1982,8 +1983,7 @@ sentry_clear_attachments(void) } SENTRY_SCOPE_NOTIFY(scope, remove_attachment, it); } - sentry__attachments_free(scope->attachments); - scope->attachments = NULL; + sentry__attachments_free(attachments); } } } From e64a4e8f38e37773bf68f76fa33ce5ba81f904cb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 11:35:15 +0200 Subject: [PATCH 08/29] add_observer: return bool --- src/sentry_scope.c | 7 ++++--- src/sentry_scope.h | 9 ++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sentry_scope.c b/src/sentry_scope.c index f3ddef89d6..04a31d49ff 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -184,12 +184,12 @@ sentry__scope_observer_new(void) return SENTRY_MAKE(sentry_scope_observer_t); } -void +bool sentry__scope_add_observer( sentry_scope_t *scope, sentry_scope_observer_t *observer) { if (!observer) { - return; + return false; } size_t new_count = scope->num_observers + 1; @@ -197,7 +197,7 @@ sentry__scope_add_observer( = sentry__calloc(new_count, sizeof(sentry_scope_observer_t *)); if (!new_array) { sentry_free(observer); - return; + return false; } if (scope->observers) { memcpy(new_array, scope->observers, @@ -207,6 +207,7 @@ sentry__scope_add_observer( new_array[scope->num_observers] = observer; scope->observers = new_array; scope->num_observers = new_count; + return true; } sentry_scope_t * diff --git a/src/sentry_scope.h b/src/sentry_scope.h index f5199d922f..56f633ba27 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -186,12 +186,11 @@ sentry_scope_observer_t *sentry__scope_observer_new(void); /** * Register a scope observer. * - * Takes ownership of `observer`; the caller must not use or free it after - * this call. Must be called while holding the scope lock (i.e., inside - * SENTRY_WITH_SCOPE_MUT). Registration order is respected — observers are - * notified in registration order. + * Takes ownership of `observer`; the caller must not free it after this call. + * Must be called while holding the scope lock. Registration order is respected + * — observers are notified in registration order. */ -void sentry__scope_add_observer( +bool sentry__scope_add_observer( sentry_scope_t *scope, sentry_scope_observer_t *observer); /** Re-entrancy guard: set while notifying observers. */ From cf407cf1daf730b7006febaed2d284f7e1edf44f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 11:35:41 +0200 Subject: [PATCH 09/29] add remove_observer --- src/sentry_scope.c | 26 ++++++++++++++++++++++++++ src/sentry_scope.h | 9 +++++++++ 2 files changed, 35 insertions(+) diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 04a31d49ff..54d123d97e 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -210,6 +210,32 @@ sentry__scope_add_observer( return true; } +void +sentry__scope_remove_observer( + sentry_scope_t *scope, sentry_scope_observer_t *observer) +{ + if (!observer || !scope->observers) { + return; + } + + for (size_t i = 0; i < scope->num_observers; i++) { + if (scope->observers[i] != observer) { + continue; + } + + sentry_free(observer); + for (size_t j = i + 1; j < scope->num_observers; j++) { + scope->observers[j - 1] = scope->observers[j]; + } + scope->num_observers--; + if (scope->num_observers == 0) { + sentry_free(scope->observers); + scope->observers = NULL; + } + return; + } +} + sentry_scope_t * sentry_local_scope_new(void) { diff --git a/src/sentry_scope.h b/src/sentry_scope.h index 56f633ba27..77cee536ec 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -193,6 +193,15 @@ sentry_scope_observer_t *sentry__scope_observer_new(void); bool sentry__scope_add_observer( sentry_scope_t *scope, sentry_scope_observer_t *observer); +/** + * Remove a scope observer. + * + * Frees `observer` if it is registered. Must be called while holding the scope + * lock. Does nothing if `observer` is NULL or not registered. + */ +void sentry__scope_remove_observer( + sentry_scope_t *scope, sentry_scope_observer_t *observer); + /** Re-entrancy guard: set while notifying observers. */ #define SENTRY_SCOPE_NOTIFY(scope, callback, ...) \ do { \ From ff1c49113528b9127dd2c743d563af331094e763 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 11:37:54 +0200 Subject: [PATCH 10/29] update tests --- tests/unit/test_scope.c | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index b45978e2eb..9b0c496177 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1370,8 +1370,8 @@ SENTRY_TEST(scope_observer_multiple) observer2->set_tag = observe_set_tag; SENTRY_WITH_SCOPE_MUT (scope) { - sentry__scope_add_observer(scope, observer1); - sentry__scope_add_observer(scope, observer2); + TEST_CHECK(sentry__scope_add_observer(scope, observer1)); + TEST_CHECK(sentry__scope_add_observer(scope, observer2)); } sentry_set_tag("multi", "test"); @@ -1384,6 +1384,19 @@ SENTRY_TEST(scope_observer_multiple) sentry_value_as_string(sentry_value_get_by_key(d2.tags, "multi")), "test"); + d1.was_called = false; + d2.was_called = false; + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_remove_observer(scope, observer1); + } + + sentry_set_tag("multi", "again"); + TEST_CHECK(!d1.was_called); + TEST_CHECK(d2.was_called); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(d2.tags, "multi")), + "again"); + sentry_value_decref(d1.tags); sentry_value_decref(d2.tags); sentry_close(); From e20cbcb4e6b51fa3ebf8eea7d1087a6adf65d3f2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 12:58:10 +0200 Subject: [PATCH 11/29] prevent array shift during notify iteration --- src/sentry_scope.c | 38 ++++++++++++++++++++++++ src/sentry_scope.h | 15 +++++----- tests/unit/test_scope.c | 65 +++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 4 files changed, 112 insertions(+), 7 deletions(-) diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 54d123d97e..6802168081 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -224,6 +224,11 @@ sentry__scope_remove_observer( } sentry_free(observer); + if (scope->is_notifying) { + // avoid shifting the array while SENTRY_SCOPE_NOTIFY is iterating + scope->observers[i] = NULL; + return; + } for (size_t j = i + 1; j < scope->num_observers; j++) { scope->observers[j - 1] = scope->observers[j]; } @@ -236,6 +241,39 @@ sentry__scope_remove_observer( } } +size_t +sentry__scope_begin_notify(sentry_scope_t *scope) +{ + if (scope->is_notifying) { + return 0; + } + scope->is_notifying = true; + return scope->num_observers; +} + +void +sentry__scope_end_notify(sentry_scope_t *scope) +{ + scope->is_notifying = false; + if (!scope->observers) { + return; + } + + // removals during notification leave tombstones to avoid shifting the array + // while it is being iterated + size_t j = 0; + for (size_t i = 0; i < scope->num_observers; i++) { + if (scope->observers[i]) { + scope->observers[j++] = scope->observers[i]; + } + } + scope->num_observers = j; + if (scope->num_observers == 0) { + sentry_free(scope->observers); + scope->observers = NULL; + } +} + sentry_scope_t * sentry_local_scope_new(void) { diff --git a/src/sentry_scope.h b/src/sentry_scope.h index 77cee536ec..1859a905e6 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -202,19 +202,20 @@ bool sentry__scope_add_observer( void sentry__scope_remove_observer( sentry_scope_t *scope, sentry_scope_observer_t *observer); -/** Re-entrancy guard: set while notifying observers. */ +size_t sentry__scope_begin_notify(sentry_scope_t *scope); +void sentry__scope_end_notify(sentry_scope_t *scope); + +/** Notify observers registered before this notification started. */ #define SENTRY_SCOPE_NOTIFY(scope, callback, ...) \ do { \ - if ((scope)->is_notifying) \ - break; \ - (scope)->is_notifying = true; \ - for (size_t _i = 0; _i < (scope)->num_observers; _i++) { \ + size_t _end = sentry__scope_begin_notify(scope); \ + for (size_t _i = 0; _i < _end && _i < (scope)->num_observers; _i++) { \ sentry_scope_observer_t *_observer = (scope)->observers[_i]; \ - if (_observer->callback) { \ + if (_observer && _observer->callback) { \ _observer->callback(_observer->data, __VA_ARGS__); \ } \ } \ - (scope)->is_notifying = false; \ + sentry__scope_end_notify(scope); \ } while (0) /** diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 9b0c496177..29014b21d2 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1148,6 +1148,12 @@ typedef struct { bool was_called; } test_observer_data_t; +typedef struct { + test_observer_data_t *self_data; + sentry_scope_observer_t *self; + sentry_scope_observer_t *added; +} reentrant_observer_data_t; + static void observe_set_release(void *data, const char *release, size_t release_len) { @@ -1266,6 +1272,18 @@ observe_set_tag(void *data, const char *key, size_t key_len, const char *value, d->was_called = true; } +static void +observe_set_tag_remove_self_and_add(void *data, const char *key, size_t key_len, + const char *value, size_t value_len) +{ + reentrant_observer_data_t *d = (reentrant_observer_data_t *)data; + observe_set_tag(d->self_data, key, key_len, value, value_len); + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_remove_observer(scope, d->self); + TEST_CHECK(sentry__scope_add_observer(scope, d->added)); + } +} + static void observe_remove_tag(void *data, const char *key, size_t key_len) { @@ -1402,6 +1420,53 @@ SENTRY_TEST(scope_observer_multiple) sentry_close(); } +SENTRY_TEST(scope_observer_mutate) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + test_observer_data_t d1 = { .tags = sentry_value_new_null() }; + test_observer_data_t d2 = { .tags = sentry_value_new_null() }; + test_observer_data_t d3 = { .tags = sentry_value_new_null() }; + reentrant_observer_data_t reentrant = { .self_data = &d1 }; + + sentry_scope_observer_t *observer1 = sentry__scope_observer_new(); + reentrant.self = observer1; + observer1->data = &reentrant; + observer1->set_tag = observe_set_tag_remove_self_and_add; + + sentry_scope_observer_t *observer2 = sentry__scope_observer_new(); + observer2->data = &d2; + observer2->set_tag = observe_set_tag; + + sentry_scope_observer_t *observer3 = sentry__scope_observer_new(); + reentrant.added = observer3; + observer3->data = &d3; + observer3->set_tag = observe_set_tag; + + SENTRY_WITH_SCOPE_MUT (scope) { + TEST_CHECK(sentry__scope_add_observer(scope, observer1)); + TEST_CHECK(sentry__scope_add_observer(scope, observer2)); + } + + sentry_set_tag("reentrant", "first"); + TEST_CHECK(d1.was_called); + TEST_CHECK(d2.was_called); + TEST_CHECK(!d3.was_called); + + d1.was_called = false; + d2.was_called = false; + sentry_set_tag("reentrant", "second"); + TEST_CHECK(!d1.was_called); + TEST_CHECK(d2.was_called); + TEST_CHECK(d3.was_called); + + sentry_value_decref(d1.tags); + sentry_value_decref(d2.tags); + sentry_value_decref(d3.tags); + sentry_close(); +} + SENTRY_TEST(scope_observer_release) { SENTRY_TEST_OPTIONS_NEW(options); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 7e31f205d3..965bda59ac 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -299,6 +299,7 @@ XX(scope_observer_extras) XX(scope_observer_fingerprint) XX(scope_observer_level) XX(scope_observer_multiple) +XX(scope_observer_mutate) XX(scope_observer_null) XX(scope_observer_release) XX(scope_observer_tags) From 48a2b1c8f4d66808f4da40966264151b02556c1d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 13:45:35 +0200 Subject: [PATCH 12/29] fix nested notify --- src/sentry_scope.c | 11 +++++------ src/sentry_scope.h | 2 +- tests/unit/test_scope.c | 8 +++++++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 6802168081..a79b756072 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -90,7 +90,7 @@ init_scope(sentry_scope_t *scope) scope->trace_managed = true; scope->observers = NULL; scope->num_observers = 0; - scope->is_notifying = false; + scope->is_notifying = 0; } static sentry_scope_t * @@ -244,17 +244,16 @@ sentry__scope_remove_observer( size_t sentry__scope_begin_notify(sentry_scope_t *scope) { - if (scope->is_notifying) { - return 0; - } - scope->is_notifying = true; + scope->is_notifying++; return scope->num_observers; } void sentry__scope_end_notify(sentry_scope_t *scope) { - scope->is_notifying = false; + if (--scope->is_notifying > 0) { + return; + } if (!scope->observers) { return; } diff --git a/src/sentry_scope.h b/src/sentry_scope.h index 1859a905e6..389ed75e5e 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -81,7 +81,7 @@ struct sentry_scope_s { sentry_scope_observer_t **observers; size_t num_observers; - bool is_notifying; + size_t is_notifying; }; /** diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 29014b21d2..2c0396f5d6 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1150,6 +1150,7 @@ typedef struct { typedef struct { test_observer_data_t *self_data; + test_observer_data_t *nested_data; sentry_scope_observer_t *self; sentry_scope_observer_t *added; } reentrant_observer_data_t; @@ -1282,6 +1283,9 @@ observe_set_tag_remove_self_and_add(void *data, const char *key, size_t key_len, sentry__scope_remove_observer(scope, d->self); TEST_CHECK(sentry__scope_add_observer(scope, d->added)); } + d->nested_data->was_called = false; + sentry_set_extra("nested", sentry_value_new_string("notify")); + TEST_CHECK(d->nested_data->was_called); } static void @@ -1428,7 +1432,8 @@ SENTRY_TEST(scope_observer_mutate) test_observer_data_t d1 = { .tags = sentry_value_new_null() }; test_observer_data_t d2 = { .tags = sentry_value_new_null() }; test_observer_data_t d3 = { .tags = sentry_value_new_null() }; - reentrant_observer_data_t reentrant = { .self_data = &d1 }; + reentrant_observer_data_t reentrant + = { .self_data = &d1, .nested_data = &d2 }; sentry_scope_observer_t *observer1 = sentry__scope_observer_new(); reentrant.self = observer1; @@ -1438,6 +1443,7 @@ SENTRY_TEST(scope_observer_mutate) sentry_scope_observer_t *observer2 = sentry__scope_observer_new(); observer2->data = &d2; observer2->set_tag = observe_set_tag; + observer2->set_extra = observe_set_extra; sentry_scope_observer_t *observer3 = sentry__scope_observer_new(); reentrant.added = observer3; From a32d6c0ef066cdc09cab774022cd9fe35751d48e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 13:58:14 +0200 Subject: [PATCH 13/29] test no flush --- tests/unit/test_scope.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 2c0396f5d6..0be25809cb 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1279,12 +1279,15 @@ observe_set_tag_remove_self_and_add(void *data, const char *key, size_t key_len, { reentrant_observer_data_t *d = (reentrant_observer_data_t *)data; observe_set_tag(d->self_data, key, key_len, value, value_len); - SENTRY_WITH_SCOPE_MUT (scope) { + SENTRY_WITH_SCOPE_MUT_NO_FLUSH (scope) { sentry__scope_remove_observer(scope, d->self); TEST_CHECK(sentry__scope_add_observer(scope, d->added)); } d->nested_data->was_called = false; - sentry_set_extra("nested", sentry_value_new_string("notify")); + SENTRY_WITH_SCOPE_MUT_NO_FLUSH (scope) { + sentry_scope_set_extra( + scope, "nested", sentry_value_new_string("notify")); + } TEST_CHECK(d->nested_data->was_called); } From f92daadccd02ba4a84ec78414b2ebdfc0e2aecc7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 14:07:24 +0200 Subject: [PATCH 14/29] was_notifying (tsan) --- src/sentry_scope.c | 4 ++++ tests/unit/test_scope.c | 5 +---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sentry_scope.c b/src/sentry_scope.c index a79b756072..8c75ee7962 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -168,7 +168,11 @@ sentry__scope_unlock(void) void sentry__scope_flush_unlock(void) { + bool was_notifying = g_scope.is_notifying > 0; sentry__scope_unlock(); + if (was_notifying) { + return; + } SENTRY_WITH_OPTIONS (options) { // we try to unlock the scope as soon as possible. The // backend will do its own `WITH_SCOPE` internally. diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 0be25809cb..56bfdf7d26 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1284,10 +1284,7 @@ observe_set_tag_remove_self_and_add(void *data, const char *key, size_t key_len, TEST_CHECK(sentry__scope_add_observer(scope, d->added)); } d->nested_data->was_called = false; - SENTRY_WITH_SCOPE_MUT_NO_FLUSH (scope) { - sentry_scope_set_extra( - scope, "nested", sentry_value_new_string("notify")); - } + sentry_set_extra("nested", sentry_value_new_string("notify")); TEST_CHECK(d->nested_data->was_called); } From 2d983e7cf9e27075247deb30abb5f86c21e82810 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 20:53:38 +0200 Subject: [PATCH 15/29] trace context --- src/sentry_core.c | 7 +++++ src/sentry_scope.c | 44 ++++++++++++++++++++------- src/sentry_scope.h | 7 +++++ tests/unit/test_scope.c | 66 +++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 5 files changed, 115 insertions(+), 10 deletions(-) diff --git a/src/sentry_core.c b/src/sentry_core.c index 2341e790b6..6d0f2a1ed1 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -1421,6 +1421,9 @@ sentry__transaction_finish_value( if (sentry__string_eq(tx_id, scope_tx_id)) { sentry__transaction_decref(scope->transaction_object); scope->transaction_object = NULL; + if (!scope->trace_managed) { + sentry__scope_notify_trace_context(scope); + } } } // if the SDK manages the trace (rather than the user or a downstream @@ -1433,6 +1436,7 @@ sentry__transaction_finish_value( sentry_value_set_by_key( sentry_value_get_by_key(scope->propagation_context, "trace"), "trace_id", txn_trace_id); + sentry__scope_notify_trace_context(scope); } } // The sampling decision should already be made for transactions @@ -1497,6 +1501,7 @@ sentry_set_transaction_object(sentry_transaction_t *tx) sentry__transaction_decref(scope->transaction_object); sentry__transaction_incref(tx); scope->transaction_object = tx; + sentry__scope_notify_trace_context(scope); } } @@ -1509,6 +1514,7 @@ sentry_set_span(sentry_span_t *span) sentry__span_decref(scope->span); sentry__span_incref(span); scope->span = span; + sentry__scope_notify_trace_context(scope); } } @@ -1670,6 +1676,7 @@ sentry_span_finish_ts(sentry_span_t *opaque_span, uint64_t timestamp) if (sentry__string_eq(span_id, scope_span_id)) { sentry__span_decref(scope->span); scope->span = NULL; + sentry__scope_notify_trace_context(scope); } } } diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 8c75ee7962..b197455ba5 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -458,6 +458,39 @@ get_span_or_transaction(const sentry_scope_t *scope) } } +static sentry_value_t +get_scope_trace_context(const sentry_scope_t *scope) +{ + sentry_value_t scoped_txn_or_span = get_span_or_transaction(scope); + sentry_value_t trace_context + = sentry__value_get_trace_context(scoped_txn_or_span); + if (sentry_value_is_null(trace_context)) { + return trace_context; + } + + sentry_value_t data = sentry_value_get_by_key(scoped_txn_or_span, "data"); + if (!sentry_value_is_null(data)) { + sentry_value_incref(data); + sentry_value_set_by_key(trace_context, "data", data); + } + return trace_context; +} + +void +sentry__scope_notify_trace_context(sentry_scope_t *scope) +{ + sentry_value_t trace_context = get_scope_trace_context(scope); + if (sentry_value_is_null(trace_context)) { + // Fall back to the propagation trace so observers see the same trace + // context as an event with scope applied. + trace_context + = sentry_value_get_by_key(scope->propagation_context, "trace"); + sentry_value_incref(trace_context); + } + SENTRY_SCOPE_NOTIFY(scope, set_trace_context, trace_context); + sentry_value_decref(trace_context); +} + #ifdef SENTRY_UNITTEST sentry_value_t sentry__scope_get_span_or_transaction(void) @@ -554,23 +587,14 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, // prep contexts sourced from scope; data about transaction on scope needs // to be extracted and inserted - sentry_value_t scoped_txn_or_span = sentry_value_new_null(); sentry_value_t scope_trace = sentry_value_new_null(); if (!is_transaction) { - scoped_txn_or_span = get_span_or_transaction(scope); - scope_trace = sentry__value_get_trace_context(scoped_txn_or_span); + scope_trace = get_scope_trace_context(scope); } if (!sentry_value_is_null(scope_trace)) { if (sentry_value_is_null(contexts)) { contexts = sentry_value_new_object(); } - sentry_value_t scoped_txn_or_span_data - = sentry_value_get_by_key(scoped_txn_or_span, "data"); - if (!sentry_value_is_null(scoped_txn_or_span_data)) { - sentry_value_incref(scoped_txn_or_span_data); - sentry_value_set_by_key( - scope_trace, "data", scoped_txn_or_span_data); - } sentry_value_set_by_key(contexts, "trace", scope_trace); } diff --git a/src/sentry_scope.h b/src/sentry_scope.h index 389ed75e5e..9d1f6b3d87 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -42,6 +42,7 @@ typedef struct sentry_scope_observer_s { void (*set_context)( void *data, const char *key, size_t key_len, sentry_value_t value); void (*remove_context)(void *data, const char *key, size_t key_len); + void (*set_trace_context)(void *data, sentry_value_t value); void (*add_attachment)(void *data, sentry_attachment_t *attachment); void (*remove_attachment)(void *data, sentry_attachment_t *attachment); @@ -225,6 +226,12 @@ void sentry__scope_end_notify(sentry_scope_t *scope); void sentry__scope_update_dsc( sentry_scope_t *scope, const sentry_options_t *options); +/** + * Notifies observers with the trace context derived from the active scoped span + * or transaction, falling back to the propagation context when neither is set. + */ +void sentry__scope_notify_trace_context(sentry_scope_t *scope); + /** * Replaces the scope's dynamic sampling context (DSC) with a verbatim copy * of the incoming object. Used when continuing an upstream trace: per the diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 56bfdf7d26..4d793fd03e 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1144,6 +1144,7 @@ typedef struct { sentry_value_t tags; sentry_value_t extras; sentry_value_t contexts; + sentry_value_t trace_contexts; sentry_value_t attachments; bool was_called; } test_observer_data_t; @@ -1350,6 +1351,18 @@ observe_remove_context(void *data, const char *key, size_t key_len) d->was_called = true; } +static void +observe_set_trace_context(void *data, sentry_value_t trace_context) +{ + test_observer_data_t *d = (test_observer_data_t *)data; + if (sentry_value_is_null(d->trace_contexts)) { + d->trace_contexts = sentry_value_new_list(); + } + sentry_value_incref(trace_context); + sentry_value_append(d->trace_contexts, trace_context); + d->was_called = true; +} + SENTRY_TEST(scope_observer_null) { SENTRY_TEST_OPTIONS_NEW(options); @@ -1774,6 +1787,59 @@ SENTRY_TEST(scope_observer_contexts) sentry_close(); } +SENTRY_TEST(scope_observer_trace_context) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_init(options); + + test_observer_data_t d = { .trace_contexts = sentry_value_new_null() }; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = &d; + observer->set_trace_context = observe_set_trace_context; + + SENTRY_WITH_SCOPE_MUT (scope) { + TEST_CHECK(sentry__scope_add_observer(scope, observer)); + } + + sentry_transaction_context_t *tx_ctx + = sentry_transaction_context_new("root", "op"); + sentry_transaction_t *tx + = sentry_transaction_start(tx_ctx, sentry_value_new_null()); + sentry_transaction_set_data(tx, "tx-data", sentry_value_new_string("root")); + sentry_set_transaction_object(tx); + + TEST_CHECK(d.was_called); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(d.trace_contexts), 1); + sentry_value_t trace = sentry_value_get_by_index(d.trace_contexts, 0); + TEST_CHECK( + !sentry_value_is_null(sentry_value_get_by_key(trace, "trace_id"))); + TEST_CHECK( + !sentry_value_is_null(sentry_value_get_by_key(trace, "span_id"))); + sentry_value_t data = sentry_value_get_by_key(trace, "data"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(data, "tx-data")), + "root"); + + sentry_span_t *child = sentry_transaction_start_child(tx, "child", "desc"); + sentry_span_set_data(child, "span-data", sentry_value_new_string("child")); + sentry_set_span(child); + + TEST_CHECK_INT_EQUAL(sentry_value_get_length(d.trace_contexts), 2); + trace = sentry_value_get_by_index(d.trace_contexts, 1); + data = sentry_value_get_by_key(trace, "data"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(data, "span-data")), + "child"); + + sentry_span_finish(child); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(d.trace_contexts), 3); + + sentry_transaction_finish(tx); + sentry_value_decref(d.trace_contexts); + sentry_close(); +} + SENTRY_TEST(scope_observer_attachments) { SENTRY_TEST_OPTIONS_NEW(options); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 965bda59ac..f16b745ae7 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -303,6 +303,7 @@ XX(scope_observer_mutate) XX(scope_observer_null) XX(scope_observer_release) XX(scope_observer_tags) +XX(scope_observer_trace_context) XX(scope_observer_transaction) XX(scope_observer_user) XX(scope_tags) From 7729a18a7ccb4df9aa125c4df19cff1e0717c947 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 1 Jul 2026 23:00:52 +0200 Subject: [PATCH 16/29] ref(crashpad): replace scope flushing with incremental IPC updates --- external/crashpad | 2 +- src/backends/sentry_backend_crashpad.cpp | 489 ++++++++++++++--------- 2 files changed, 297 insertions(+), 194 deletions(-) diff --git a/external/crashpad b/external/crashpad index 6a25bbac1c..460c37585e 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit 6a25bbac1cc8c7a49a37599a24d18eea214cff99 +Subproject commit 460c37585e99e31a0121bfe03f10e80f494e722f diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index bdfd50a9c7..d079391aba 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -5,7 +5,6 @@ extern "C" { #include "sentry_attachment.h" #include "sentry_backend.h" #include "sentry_core.h" -#include "sentry_cpu_relax.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_logger.h" @@ -128,12 +127,9 @@ typedef struct { crashpad::CrashReportDatabase *db; crashpad::CrashpadClient *client; sentry_path_t *event_path; - sentry_path_t *breadcrumb1_path; - sentry_path_t *breadcrumb2_path; sentry_path_t *external_report_path; - size_t num_breadcrumbs; - std::atomic crashed; - std::atomic scope_flush; + char *installation_id; + size_t max_breadcrumbs; sentry_uuid_t crash_event_id; } crashpad_state_t; @@ -145,6 +141,7 @@ crashpad_state_dtor(crashpad_state_t *state) { safe_delete(state->client); safe_delete(state->db); + sentry_free(state->installation_id); } static void @@ -249,6 +246,10 @@ flush_external_crash_report( return; } sentry__envelope_set_event_id(envelope, crash_event_id); + if (options->dsn && options->dsn->is_valid) { + sentry__envelope_set_header(envelope, "dsn", + sentry_value_new_string(sentry_options_get_dsn(options))); + } if (options->session) { sentry__envelope_add_session(envelope, options->session); } @@ -261,71 +262,7 @@ flush_external_crash_report( sentry_envelope_free(envelope); } -// This function is necessary for macOS since it has no `FirstChanceHandler`. -// but it is also necessary on Windows if the WER handler is enabled. -// This means we have to continuously flush the scope on -// every change so that `__sentry_event` is ready to upload when the crash -// happens. With platforms that have a `FirstChanceHandler` we can do that -// once in the handler. No need to share event- or crashpad-state mutation. -static void -crashpad_backend_flush_scope( - sentry_backend_t *backend, const sentry_options_t *options) -{ -#if defined(SENTRY_PLATFORM_LINUX) - (void)backend; - (void)options; -#else - auto *data = static_cast(backend->data); - bool expected = false; - - // - if (!data->event_path || data->crashed.load(std::memory_order_relaxed) - || !data->scope_flush.compare_exchange_strong( - expected, true, std::memory_order_acquire)) { - return; - } - - sentry_value_t event = sentry_value_new_object(); - sentry_value_set_by_key( - event, "event_id", sentry__value_new_uuid(&data->crash_event_id)); - // Since this will only be uploaded in case of a crash we must make this - // event fatal. - sentry_value_set_by_key( - event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); - - flush_scope_to_event(data->event_path, options, event); - if (data->external_report_path) { - flush_external_crash_report(options, &data->crash_event_id); - } - data->scope_flush.store(false, std::memory_order_release); -#endif -} - #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_WINDOWS) -static void -flush_scope_from_handler( - const sentry_options_t *options, sentry_value_t crash_event) -{ - auto state = static_cast(options->backend->data); - - // this blocks any further calls to `crashpad_backend_flush_scope` - state->crashed.store(true, std::memory_order_relaxed); - - // busy-wait until any in-progress scope flushes are finished - bool expected = false; - while (!state->scope_flush.compare_exchange_strong( - expected, true, std::memory_order_acquire)) { - expected = false; - sentry__cpu_relax(); - } - - // now we are the sole flusher and can flush into the crash event - flush_scope_to_event(state->event_path, options, crash_event); - if (state->external_report_path) { - flush_external_crash_report(options, &state->crash_event_id); - } -} - # ifdef SENTRY_PLATFORM_WINDOWS static bool crashpad_handler(EXCEPTION_POINTERS *ExceptionInfo) @@ -385,7 +322,7 @@ crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) should_dump = !sentry_value_is_null(crash_event); if (should_dump) { - flush_scope_from_handler(options, crash_event); + sentry_value_decref(crash_event); sentry__write_crash_marker(options); sentry__record_errors_on_current_session(1); @@ -609,6 +546,239 @@ process_completed_reports( } } +#if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_LINUX) \ + || defined(SENTRY_PLATFORM_MACOS) + +static void +send_scope_update(crashpad_state_t *data, sentry_value_t update) +{ + if (!data || !data->client) { + sentry_value_decref(update); + return; + } + + size_t mpack_size = 0; + char *mpack = sentry_value_to_msgpack(update, &mpack_size); + sentry_value_decref(update); + if (!mpack) { + return; + } + + data->client->UpdateScope(std::string(mpack, mpack_size)); + sentry_free(mpack); +} + +static sentry_value_t +scope_update_new(const char *op, const char *field) +{ + sentry_value_t update = sentry_value_new_object(); + sentry_value_set_by_key(update, "op", sentry_value_new_string(op)); + sentry_value_set_by_key(update, "field", sentry_value_new_string(field)); + return update; +} + +static void +send_scope_value_update(void *state, const char *field, sentry_value_t value) +{ + sentry_value_t update = scope_update_new("set", field); + sentry_value_set_by_key(update, "value", sentry__value_clone(value)); + send_scope_update(static_cast(state), update); +} + +static void +send_scope_string_update( + void *state, const char *field, const char *value, size_t value_len) +{ + sentry_value_t update = scope_update_new("set", field); + sentry_value_set_by_key( + update, "value", sentry_value_new_string_n(value, value_len)); + send_scope_update(static_cast(state), update); +} + +static void +send_scope_item_update(void *state, const char *field, const char *key, + size_t key_len, sentry_value_t value) +{ + sentry_value_t update = scope_update_new("set_item", field); + sentry_value_set_by_key( + update, "key", sentry_value_new_string_n(key, key_len)); + sentry_value_set_by_key(update, "value", sentry__value_clone(value)); + send_scope_update(static_cast(state), update); +} + +static void +send_scope_remove_item_update( + void *state, const char *field, const char *key, size_t key_len) +{ + sentry_value_t update = scope_update_new("remove_item", field); + sentry_value_set_by_key( + update, "key", sentry_value_new_string_n(key, key_len)); + send_scope_update(static_cast(state), update); +} + +static void +set_release(void *state, const char *release, size_t release_len) +{ + send_scope_string_update(state, "release", release, release_len); +} + +static void +set_environment(void *state, const char *environment, size_t environment_len) +{ + send_scope_string_update( + state, "environment", environment, environment_len); +} + +static void +set_transaction(void *state, const char *transaction, size_t transaction_len) +{ + send_scope_string_update( + state, "transaction", transaction, transaction_len); +} + +static void +set_fingerprint(void *state, sentry_value_t fingerprint) +{ + send_scope_value_update(state, "fingerprint", fingerprint); +} + +static void +set_level(void *state, sentry_level_t level) +{ + sentry_value_t update = scope_update_new("set", "level"); + sentry_value_set_by_key(update, "value", sentry__value_new_level(level)); + send_scope_update(static_cast(state), update); +} + +static void +set_user(void *state, sentry_value_t user) +{ + sentry_value_t update = scope_update_new("set", "user"); + sentry_value_t value = sentry__value_clone(user); + auto *data = static_cast(state); + if (data->installation_id + && sentry_value_get_type(value) == SENTRY_VALUE_TYPE_OBJECT + && sentry_value_is_null(sentry_value_get_by_key(value, "id"))) { + sentry_value_set_by_key( + value, "id", sentry_value_new_string(data->installation_id)); + } + sentry_value_set_by_key(update, "value", value); + send_scope_update(data, update); +} + +static void +set_tag(void *state, const char *key, size_t key_len, const char *value, + size_t value_len) +{ + sentry_value_t update = scope_update_new("set_item", "tags"); + sentry_value_set_by_key( + update, "key", sentry_value_new_string_n(key, key_len)); + sentry_value_set_by_key( + update, "value", sentry_value_new_string_n(value, value_len)); + send_scope_update(static_cast(state), update); +} + +static void +remove_tag(void *state, const char *key, size_t key_len) +{ + send_scope_remove_item_update(state, "tags", key, key_len); +} + +static void +set_extra(void *state, const char *key, size_t key_len, sentry_value_t value) +{ + send_scope_item_update(state, "extra", key, key_len, value); +} + +static void +remove_extra(void *state, const char *key, size_t key_len) +{ + send_scope_remove_item_update(state, "extra", key, key_len); +} + +static void +set_context(void *state, const char *key, size_t key_len, sentry_value_t value) +{ + send_scope_item_update(state, "contexts", key, key_len, value); +} + +static void +remove_context(void *state, const char *key, size_t key_len) +{ + send_scope_remove_item_update(state, "contexts", key, key_len); +} + +static void +add_breadcrumb(void *state, sentry_value_t breadcrumb) +{ + auto *data = static_cast(state); + if (!data->max_breadcrumbs) { + return; + } + + sentry_value_t update = scope_update_new("add_breadcrumb", "breadcrumbs"); + sentry_value_set_by_key(update, "value", sentry__value_clone(breadcrumb)); + sentry_value_set_by_key(update, "max", + sentry_value_new_uint64(static_cast(data->max_breadcrumbs))); + send_scope_update(data, update); +} + +static bool +ensure_unique_path(sentry_attachment_t *attachment) +{ + sentry_path_t *path = nullptr; + SENTRY_WITH_OPTIONS (options) { + path = sentry__path_unique(options->run->run_path, + sentry__path_filename(attachment->filename)); + } + if (!path) { + return false; + } + + sentry__path_free(attachment->path); + attachment->path = path; + return true; +} + +static void +add_attachment(void *state, sentry_attachment_t *attachment) +{ + auto *data = static_cast(state); + if (!data || !data->client) { + return; + } + + if (attachment->buf) { + if (!ensure_unique_path(attachment) + || sentry__path_write_buffer( + attachment->path, attachment->buf, attachment->buf_len) + != 0) { + SENTRY_WARNF("failed to write crashpad attachment \"%s\"", + attachment->path->path); + } + } + + data->client->AddAttachment( + base::FilePath(SENTRY_PATH_PLATFORM_STR(attachment->path))); +} + +static void +remove_attachment(void *state, sentry_attachment_t *attachment) +{ + auto *data = static_cast(state); + if (!data || !data->client) { + return; + } + data->client->RemoveAttachment( + base::FilePath(SENTRY_PATH_PLATFORM_STR(attachment->path))); + + if (attachment->buf && sentry__path_remove(attachment->path) != 0) { + SENTRY_WARNF("failed to remove crashpad attachment \"%s\"", + attachment->path->path); + } +} +#endif + static int crashpad_backend_startup( sentry_backend_t *backend, const sentry_options_t *options) @@ -655,6 +825,7 @@ crashpad_backend_startup( absolute_handler_path->path); sentry_path_t *current_run_folder = options->run->run_path; auto *data = static_cast(backend->data); + data->max_breadcrumbs = options->max_breadcrumbs; // pre-generate event ID for a potential future crash to be able to // associate feedback with the crash event. @@ -672,23 +843,14 @@ crashpad_backend_startup( attachments.emplace_back(SENTRY_PATH_PLATFORM_STR(attachment->path)); } - // and add the serialized event, and two rotating breadcrumb files - // as attachments and make sure the files exist + // and add the serialized event as an attachment and make sure it exists data->event_path = sentry__path_join_str(current_run_folder, "__sentry-event"); - data->breadcrumb1_path - = sentry__path_join_str(current_run_folder, "__sentry-breadcrumb1"); - data->breadcrumb2_path - = sentry__path_join_str(current_run_folder, "__sentry-breadcrumb2"); sentry__path_touch(data->event_path); - sentry__path_touch(data->breadcrumb1_path); - sentry__path_touch(data->breadcrumb2_path); - attachments.insert(attachments.end(), - { base::FilePath(SENTRY_PATH_PLATFORM_STR(data->event_path)), - base::FilePath(SENTRY_PATH_PLATFORM_STR(data->breadcrumb1_path)), - base::FilePath(SENTRY_PATH_PLATFORM_STR(data->breadcrumb2_path)) }); + attachments.push_back( + base::FilePath(SENTRY_PATH_PLATFORM_STR(data->event_path))); base::FilePath screenshot; if (options->attach_screenshot) { @@ -714,6 +876,31 @@ crashpad_backend_startup( } } + sentry_value_t initial_event + = sentry__value_new_event_with_id(&data->crash_event_id); + sentry_value_set_by_key( + initial_event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); + if (options->release) { + sentry_value_set_by_key(initial_event, "release", + sentry_value_new_string(options->release)); + } + if (options->dist) { + sentry_value_set_by_key( + initial_event, "dist", sentry_value_new_string(options->dist)); + } + if (options->environment) { + sentry_value_set_by_key(initial_event, "environment", + sentry_value_new_string(options->environment)); + } + flush_scope_to_event(data->event_path, options, initial_event); + if (data->external_report_path) { + flush_external_crash_report(options, &data->crash_event_id); + } + if (options->run && options->run->installation_id) { + data->installation_id + = sentry__string_clone(options->run->installation_id); + } + std::vector arguments { "--no-rate-limit" }; // Map sentry's log level to mini_chromium's LogSeverity. They diverge at @@ -828,6 +1015,32 @@ crashpad_backend_startup( crashpad::TriState::kEnabled); } +#if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_LINUX) \ + || defined(SENTRY_PLATFORM_MACOS) + { + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + observer->data = data; + observer->set_release = set_release; + observer->set_environment = set_environment; + observer->set_transaction = set_transaction; + observer->set_fingerprint = set_fingerprint; + observer->set_level = set_level; + observer->set_user = set_user; + observer->set_tag = set_tag; + observer->remove_tag = remove_tag; + observer->set_extra = set_extra; + observer->remove_extra = remove_extra; + observer->set_context = set_context; + observer->remove_context = remove_context; + observer->add_breadcrumb = add_breadcrumb; + observer->add_attachment = add_attachment; + observer->remove_attachment = remove_attachment; + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_add_observer(scope, observer); + } + } +#endif + return 0; } @@ -853,51 +1066,11 @@ crashpad_backend_shutdown(sentry_backend_t *backend) #endif } -static void -crashpad_backend_add_breadcrumb(sentry_backend_t *backend, - sentry_value_t breadcrumb, const sentry_options_t *options) -{ - auto *data = static_cast(backend->data); - - size_t max_breadcrumbs = options->max_breadcrumbs; - if (!max_breadcrumbs) { - return; - } - - bool first_breadcrumb = data->num_breadcrumbs % max_breadcrumbs == 0; - - const sentry_path_t *breadcrumb_file - = data->num_breadcrumbs % (max_breadcrumbs * 2) < max_breadcrumbs - ? data->breadcrumb1_path - : data->breadcrumb2_path; - data->num_breadcrumbs++; - if (!breadcrumb_file) { - return; - } - - size_t mpack_size; - char *mpack = sentry_value_to_msgpack(breadcrumb, &mpack_size); - if (!mpack) { - return; - } - - int rv = first_breadcrumb - ? sentry__path_write_buffer(breadcrumb_file, mpack, mpack_size) - : sentry__path_append_buffer(breadcrumb_file, mpack, mpack_size); - sentry_free(mpack); - - if (rv != 0) { - SENTRY_WARN("flushing breadcrumb to msgpack failed"); - } -} - static void crashpad_backend_free(sentry_backend_t *backend) { auto *data = static_cast(backend->data); sentry__path_free(data->event_path); - sentry__path_free(data->breadcrumb1_path); - sentry__path_free(data->breadcrumb2_path); sentry__path_free(data->external_report_path); delete data; } @@ -1016,66 +1189,6 @@ crashpad_backend_prune_database(sentry_backend_t *backend) crashpad::PruneCrashReportDatabase(data->db, &condition); } -#if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_LINUX) \ - || defined(SENTRY_PLATFORM_MACOS) -static bool -ensure_unique_path(sentry_attachment_t *attachment) -{ - sentry_path_t *path = nullptr; - SENTRY_WITH_OPTIONS (options) { - path = sentry__path_unique(options->run->run_path, - sentry__path_filename(attachment->filename)); - } - if (!path) { - return false; - } - - sentry__path_free(attachment->path); - attachment->path = path; - return true; -} - -static void -crashpad_backend_add_attachment( - sentry_backend_t *backend, sentry_attachment_t *attachment) -{ - auto *data = static_cast(backend->data); - if (!data || !data->client) { - return; - } - - if (attachment->buf) { - if (!ensure_unique_path(attachment) - || sentry__path_write_buffer( - attachment->path, attachment->buf, attachment->buf_len) - != 0) { - SENTRY_WARNF("failed to write crashpad attachment \"%s\"", - attachment->path->path); - } - } - - data->client->AddAttachment( - base::FilePath(SENTRY_PATH_PLATFORM_STR(attachment->path))); -} - -static void -crashpad_backend_remove_attachment( - sentry_backend_t *backend, sentry_attachment_t *attachment) -{ - auto *data = static_cast(backend->data); - if (!data || !data->client) { - return; - } - data->client->RemoveAttachment( - base::FilePath(SENTRY_PATH_PLATFORM_STR(attachment->path))); - - if (attachment->buf && sentry__path_remove(attachment->path) != 0) { - SENTRY_WARNF("failed to remove crashpad attachment \"%s\"", - attachment->path->path); - } -} -#endif - void sentry__backend_preload(void) { @@ -1094,23 +1207,13 @@ sentry__backend_new(void) sentry_free(backend); return nullptr; } - data->scope_flush = false; - data->crashed = false; - backend->startup_func = crashpad_backend_startup; backend->shutdown_func = crashpad_backend_shutdown; backend->except_func = crashpad_backend_except; backend->free_func = crashpad_backend_free; - backend->flush_scope_func = crashpad_backend_flush_scope; - backend->add_breadcrumb_func = crashpad_backend_add_breadcrumb; backend->user_consent_changed_func = crashpad_backend_user_consent_changed; backend->get_last_crash_func = crashpad_backend_last_crash; backend->prune_database_func = crashpad_backend_prune_database; -#if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_LINUX) \ - || defined(SENTRY_PLATFORM_MACOS) - backend->add_attachment_func = crashpad_backend_add_attachment; - backend->remove_attachment_func = crashpad_backend_remove_attachment; -#endif backend->data = data; backend->can_capture_after_shutdown = true; From e0419de61af4d3224ade7d28fa0b2efc015cfa56 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 3 Jul 2026 22:45:04 +0200 Subject: [PATCH 17/29] restore flush after on_crash/before_send --- src/backends/sentry_backend_crashpad.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index d079391aba..5c502050f2 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -322,7 +322,7 @@ crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) should_dump = !sentry_value_is_null(crash_event); if (should_dump) { - sentry_value_decref(crash_event); + flush_scope_to_event(state->event_path, options, crash_event); sentry__write_crash_marker(options); sentry__record_errors_on_current_session(1); From b47177cf0c9ff296449c87ced62f2ad549aa0958 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 3 Jul 2026 22:46:47 +0200 Subject: [PATCH 18/29] adapt report_to_envelope --- src/backends/sentry_backend_crashpad.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 5c502050f2..ce210bfc02 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -475,9 +475,13 @@ report_to_envelope(const crashpad::CrashReportDatabase::Report &report, } } if (envelope) { - sentry_value_set_by_key(event, "breadcrumbs", - sentry__value_merge_breadcrumbs( - breadcrumbs1, breadcrumbs2, options->max_breadcrumbs)); + sentry_value_t breadcrumbs = sentry__value_merge_breadcrumbs( + breadcrumbs1, breadcrumbs2, options->max_breadcrumbs); + if (!sentry_value_is_null(breadcrumbs)) { + sentry_value_set_by_key(event, "breadcrumbs", breadcrumbs); + } else { + sentry_value_decref(breadcrumbs); + } sentry__attachments_add_path(&attachments, minidump_path, SENTRY_ATTACHMENT_TYPE_MINIDUMP, nullptr); From 9e2ac0c9427ecb5e8aa2a1897384466c5a9e50ce Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 3 Jul 2026 22:48:15 +0200 Subject: [PATCH 19/29] restore flush_external_crash_report after on_crash/before_send --- src/backends/sentry_backend_crashpad.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index ce210bfc02..4179418615 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -323,6 +323,9 @@ crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) if (should_dump) { flush_scope_to_event(state->event_path, options, crash_event); + if (state->external_report_path) { + flush_external_crash_report(options, &state->crash_event_id); + } sentry__write_crash_marker(options); sentry__record_errors_on_current_session(1); From 7e019358afc75e21ffd583d05ce359366a536cc7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 3 Jul 2026 23:22:38 +0200 Subject: [PATCH 20/29] restore crashed flag --- src/backends/sentry_backend_crashpad.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 4179418615..2be5325b91 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -130,6 +130,7 @@ typedef struct { sentry_path_t *external_report_path; char *installation_id; size_t max_breadcrumbs; + std::atomic crashed; sentry_uuid_t crash_event_id; } crashpad_state_t; @@ -322,6 +323,7 @@ crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) should_dump = !sentry_value_is_null(crash_event); if (should_dump) { + state->crashed.store(true, std::memory_order_relaxed); flush_scope_to_event(state->event_path, options, crash_event); if (state->external_report_path) { flush_external_crash_report(options, &state->crash_event_id); @@ -559,7 +561,8 @@ process_completed_reports( static void send_scope_update(crashpad_state_t *data, sentry_value_t update) { - if (!data || !data->client) { + if (!data || !data->client + || data->crashed.load(std::memory_order_relaxed)) { sentry_value_decref(update); return; } @@ -571,7 +574,9 @@ send_scope_update(crashpad_state_t *data, sentry_value_t update) return; } - data->client->UpdateScope(std::string(mpack, mpack_size)); + if (!data->crashed.load(std::memory_order_relaxed)) { + data->client->UpdateScope(std::string(mpack, mpack_size)); + } sentry_free(mpack); } From 0f5002bd5dac7049ab732c0773f399b56a26403b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 09:41:32 +0200 Subject: [PATCH 21/29] restore scope_flush flag --- src/backends/sentry_backend_crashpad.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 2be5325b91..845c8150a4 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -5,6 +5,7 @@ extern "C" { #include "sentry_attachment.h" #include "sentry_backend.h" #include "sentry_core.h" +#include "sentry_cpu_relax.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_logger.h" @@ -131,6 +132,7 @@ typedef struct { char *installation_id; size_t max_breadcrumbs; std::atomic crashed; + std::atomic scope_flush; sentry_uuid_t crash_event_id; } crashpad_state_t; @@ -324,6 +326,12 @@ crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) if (should_dump) { state->crashed.store(true, std::memory_order_relaxed); + bool expected = false; + while (!state->scope_flush.compare_exchange_strong( + expected, true, std::memory_order_acquire)) { + expected = false; + sentry__cpu_relax(); + } flush_scope_to_event(state->event_path, options, crash_event); if (state->external_report_path) { flush_external_crash_report(options, &state->crash_event_id); @@ -567,10 +575,24 @@ send_scope_update(crashpad_state_t *data, sentry_value_t update) return; } + bool expected = false; + if (!data->scope_flush.compare_exchange_strong( + expected, true, std::memory_order_acquire)) { + sentry_value_decref(update); + return; + } + + if (data->crashed.load(std::memory_order_relaxed)) { + data->scope_flush.store(false, std::memory_order_release); + sentry_value_decref(update); + return; + } + size_t mpack_size = 0; char *mpack = sentry_value_to_msgpack(update, &mpack_size); sentry_value_decref(update); if (!mpack) { + data->scope_flush.store(false, std::memory_order_release); return; } @@ -578,6 +600,7 @@ send_scope_update(crashpad_state_t *data, sentry_value_t update) data->client->UpdateScope(std::string(mpack, mpack_size)); } sentry_free(mpack); + data->scope_flush.store(false, std::memory_order_release); } static sentry_value_t From 8c86052045eb40a8637a6f53d725d9517e78e0b9 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 09:44:02 +0200 Subject: [PATCH 22/29] restore flush_scope_from_handler --- src/backends/sentry_backend_crashpad.cpp | 39 +++++++++++++++++------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 845c8150a4..37396e4742 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -266,6 +266,30 @@ flush_external_crash_report( } #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_WINDOWS) +static void +flush_scope_from_handler( + const sentry_options_t *options, sentry_value_t crash_event) +{ + auto state = static_cast(options->backend->data); + + // this blocks any further calls to `send_scope_update` + state->crashed.store(true, std::memory_order_relaxed); + + // busy-wait until any in-progress scope updates are finished + bool expected = false; + while (!state->scope_flush.compare_exchange_strong( + expected, true, std::memory_order_acquire)) { + expected = false; + sentry__cpu_relax(); + } + + // now we are the sole flusher and can flush into the crash event + flush_scope_to_event(state->event_path, options, crash_event); + if (state->external_report_path) { + flush_external_crash_report(options, &state->crash_event_id); + } +} + # ifdef SENTRY_PLATFORM_WINDOWS static bool crashpad_handler(EXCEPTION_POINTERS *ExceptionInfo) @@ -325,17 +349,7 @@ crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) should_dump = !sentry_value_is_null(crash_event); if (should_dump) { - state->crashed.store(true, std::memory_order_relaxed); - bool expected = false; - while (!state->scope_flush.compare_exchange_strong( - expected, true, std::memory_order_acquire)) { - expected = false; - sentry__cpu_relax(); - } - flush_scope_to_event(state->event_path, options, crash_event); - if (state->external_report_path) { - flush_external_crash_report(options, &state->crash_event_id); - } + flush_scope_from_handler(options, crash_event); sentry__write_crash_marker(options); sentry__record_errors_on_current_session(1); @@ -1242,6 +1256,9 @@ sentry__backend_new(void) sentry_free(backend); return nullptr; } + data->scope_flush = false; + data->crashed = false; + backend->startup_func = crashpad_backend_startup; backend->shutdown_func = crashpad_backend_shutdown; backend->except_func = crashpad_backend_except; From f19439735b81f2788c10d4124a55f703d8b9f458 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 10:03:42 +0200 Subject: [PATCH 23/29] add test_crashpad_crash_before_send --- tests/test_integration_crashpad.py | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index b73f5e73e7..0c56f4d8a0 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -434,6 +434,37 @@ def test_crashpad_dumping_crash(cmake, httpserver, run_args, build_args): assert wait_for_file(minidump) +@pytest.mark.skipif( + sys.platform == "darwin", + reason="crashpad doesn't provide SetFirstChanceExceptionHandler on macOS", +) +def test_crashpad_crash_before_send(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"}) + + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + httpserver.expect_oneshot_request("/api/123456/minidump/").respond_with_data("OK") + + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + [ + "log", + "overflow-breadcrumbs", + "crashpad-wait-for-upload", + "crash", + "before-send", + ], + expect_failure=True, + env=env, + ) + + assert waiting.result + + attachments = assert_crashpad_upload(httpserver.log[0][0]) + assert attachments.event["adapted_by"] == "before_send" + + @pytest.mark.parametrize( "stack_size", [ From 0e7002a418b502f67fac17c22210102129a54087 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 10:21:42 +0200 Subject: [PATCH 24/29] clarifying comment --- src/backends/sentry_backend_crashpad.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 37396e4742..4e35682fba 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -925,6 +925,9 @@ crashpad_backend_startup( } } + // Seed __sentry-event with event data that is not sent as scope updates, + // such as the event ID and option-level metadata. Mutable scope state is + // sent via IPC and merged when Crashpad writes the report. sentry_value_t initial_event = sentry__value_new_event_with_id(&data->crash_event_id); sentry_value_set_by_key( From 3b7f38a9fa867e90a8dcdaa8c04a513ae3bee7fd Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 10:50:03 +0200 Subject: [PATCH 25/29] post-init --- src/backends/sentry_backend_crashpad.cpp | 72 ++++++++++++++++-------- src/sentry_backend.h | 1 + src/sentry_core.c | 12 ++-- 3 files changed, 58 insertions(+), 27 deletions(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 4e35682fba..e147c4b301 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -265,6 +265,54 @@ flush_external_crash_report( sentry_envelope_free(envelope); } +// This hook seeds __sentry-event after sentry_init() has initialized derived +// scope state. macOS has no FirstChanceHandler, and Windows WER can capture +// outside the normal first-chance path, so the attachment must already contain +// static event data. Mutable scope changes are sent to Crashpad via IPC and +// merged when the report is written. +static void +crashpad_backend_post_init( + sentry_backend_t *backend, const sentry_options_t *options) +{ + auto *data = static_cast(backend->data); + bool expected = false; + + if (!data->event_path || data->crashed.load(std::memory_order_relaxed) + || !data->scope_flush.compare_exchange_strong( + expected, true, std::memory_order_acquire)) { + return; + } + + sentry_value_t event = sentry_value_new_object(); + sentry_value_set_by_key( + event, "event_id", sentry__value_new_uuid(&data->crash_event_id)); + // Since this will only be uploaded in case of a crash we must make this + // event fatal. + sentry_value_set_by_key( + event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); + if (options->release) { + sentry_value_set_by_key( + event, "release", sentry_value_new_string(options->release)); + } + if (options->dist) { + sentry_value_set_by_key( + event, "dist", sentry_value_new_string(options->dist)); + } + if (options->environment) { + sentry_value_set_by_key(event, "environment", + sentry_value_new_string(options->environment)); + } + + // Seed __sentry-event with event data that is not sent as scope updates, + // such as the event ID and option-level metadata. Mutable scope state is + // sent via IPC and merged when Crashpad writes the report. + flush_scope_to_event(data->event_path, options, event); + if (data->external_report_path) { + flush_external_crash_report(options, &data->crash_event_id); + } + data->scope_flush.store(false, std::memory_order_release); +} + #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_WINDOWS) static void flush_scope_from_handler( @@ -925,29 +973,6 @@ crashpad_backend_startup( } } - // Seed __sentry-event with event data that is not sent as scope updates, - // such as the event ID and option-level metadata. Mutable scope state is - // sent via IPC and merged when Crashpad writes the report. - sentry_value_t initial_event - = sentry__value_new_event_with_id(&data->crash_event_id); - sentry_value_set_by_key( - initial_event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); - if (options->release) { - sentry_value_set_by_key(initial_event, "release", - sentry_value_new_string(options->release)); - } - if (options->dist) { - sentry_value_set_by_key( - initial_event, "dist", sentry_value_new_string(options->dist)); - } - if (options->environment) { - sentry_value_set_by_key(initial_event, "environment", - sentry_value_new_string(options->environment)); - } - flush_scope_to_event(data->event_path, options, initial_event); - if (data->external_report_path) { - flush_external_crash_report(options, &data->crash_event_id); - } if (options->run && options->run->installation_id) { data->installation_id = sentry__string_clone(options->run->installation_id); @@ -1263,6 +1288,7 @@ sentry__backend_new(void) data->crashed = false; backend->startup_func = crashpad_backend_startup; + backend->post_init_func = crashpad_backend_post_init; backend->shutdown_func = crashpad_backend_shutdown; backend->except_func = crashpad_backend_except; backend->free_func = crashpad_backend_free; diff --git a/src/sentry_backend.h b/src/sentry_backend.h index a55fc507a4..1ec3f0173f 100644 --- a/src/sentry_backend.h +++ b/src/sentry_backend.h @@ -13,6 +13,7 @@ */ struct sentry_backend_s { int (*startup_func)(sentry_backend_t *, const sentry_options_t *options); + void (*post_init_func)(sentry_backend_t *, const sentry_options_t *options); void (*shutdown_func)(sentry_backend_t *); void (*free_func)(sentry_backend_t *); void (*except_func)(sentry_backend_t *, const struct sentry_ucontext_s *); diff --git a/src/sentry_core.c b/src/sentry_core.c index 6d0f2a1ed1..a3fd430695 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -206,10 +206,9 @@ sentry_init(sentry_options_t *options) g_last_crash = sentry__has_crash_marker(options); g_options = options; - // *after* setting the global options, trigger a scope and consent flush, - // since at least crashpad needs that. At this point we also freeze the - // `client_sdk` in the `scope` because some downstream SDKs want to override - // it at runtime via the options interface. + // *after* setting the global options, initialize derived scope state. At + // this point we also freeze the `client_sdk` in the `scope` because some + // downstream SDKs want to override it at runtime via the options interface. SENTRY_WITH_SCOPE_MUT (scope) { if (options->sdk_name) { sentry_value_t sdk_name @@ -228,6 +227,9 @@ sentry_init(sentry_options_t *options) sentry__scope_update_dsc(scope, options); } + if (backend && backend->post_init_func) { + backend->post_init_func(backend, options); + } if (backend && backend->user_consent_changed_func) { backend->user_consent_changed_func(backend); } @@ -392,6 +394,8 @@ sentry_reinstall_backend(void) if (backend && backend->startup_func) { if (backend->startup_func(backend, options)) { rv = 1; + } else if (backend->post_init_func) { + backend->post_init_func(backend, options); } } } From 9db8011c823cad2e4c109a928c8ce1ec752f5dae Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 14:50:15 +0200 Subject: [PATCH 26/29] fix duplicate observer --- src/backends/sentry_backend_crashpad.cpp | 98 +++++++++++++++--------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index e147c4b301..8840fde5db 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -129,8 +129,8 @@ typedef struct { crashpad::CrashpadClient *client; sentry_path_t *event_path; sentry_path_t *external_report_path; - char *installation_id; size_t max_breadcrumbs; + sentry_scope_observer_t *scope_observer; std::atomic crashed; std::atomic scope_flush; sentry_uuid_t crash_event_id; @@ -144,7 +144,6 @@ crashpad_state_dtor(crashpad_state_t *state) { safe_delete(state->client); safe_delete(state->db); - sentry_free(state->installation_id); } static void @@ -752,15 +751,17 @@ set_user(void *state, sentry_value_t user) { sentry_value_t update = scope_update_new("set", "user"); sentry_value_t value = sentry__value_clone(user); - auto *data = static_cast(state); - if (data->installation_id - && sentry_value_get_type(value) == SENTRY_VALUE_TYPE_OBJECT + if (sentry_value_get_type(value) == SENTRY_VALUE_TYPE_OBJECT && sentry_value_is_null(sentry_value_get_by_key(value, "id"))) { - sentry_value_set_by_key( - value, "id", sentry_value_new_string(data->installation_id)); + SENTRY_WITH_OPTIONS (options) { + if (options->run && options->run->installation_id) { + sentry_value_set_by_key(value, "id", + sentry_value_new_string(options->run->installation_id)); + } + } } sentry_value_set_by_key(update, "value", value); - send_scope_update(data, update); + send_scope_update(static_cast(state), update); } static void @@ -874,6 +875,55 @@ remove_attachment(void *state, sentry_attachment_t *attachment) attachment->path->path); } } + +static void +add_scope_observer(crashpad_state_t *data) +{ + if (data->scope_observer) { + return; + } + + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + if (!observer) { + return; + } + + observer->data = data; + observer->set_release = set_release; + observer->set_environment = set_environment; + observer->set_transaction = set_transaction; + observer->set_fingerprint = set_fingerprint; + observer->set_level = set_level; + observer->set_user = set_user; + observer->set_tag = set_tag; + observer->remove_tag = remove_tag; + observer->set_extra = set_extra; + observer->remove_extra = remove_extra; + observer->set_context = set_context; + observer->remove_context = remove_context; + observer->add_breadcrumb = add_breadcrumb; + observer->add_attachment = add_attachment; + observer->remove_attachment = remove_attachment; + + SENTRY_WITH_SCOPE_MUT_NO_FLUSH (scope) { + if (sentry__scope_add_observer(scope, observer)) { + data->scope_observer = observer; + } + } +} + +static void +remove_scope_observer(crashpad_state_t *data) +{ + if (!data->scope_observer) { + return; + } + + SENTRY_WITH_SCOPE_MUT_NO_FLUSH (scope) { + sentry__scope_remove_observer(scope, data->scope_observer); + } + data->scope_observer = nullptr; +} #endif static int @@ -973,11 +1023,6 @@ crashpad_backend_startup( } } - if (options->run && options->run->installation_id) { - data->installation_id - = sentry__string_clone(options->run->installation_id); - } - std::vector arguments { "--no-rate-limit" }; // Map sentry's log level to mini_chromium's LogSeverity. They diverge at @@ -1094,28 +1139,7 @@ crashpad_backend_startup( #if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_LINUX) \ || defined(SENTRY_PLATFORM_MACOS) - { - sentry_scope_observer_t *observer = sentry__scope_observer_new(); - observer->data = data; - observer->set_release = set_release; - observer->set_environment = set_environment; - observer->set_transaction = set_transaction; - observer->set_fingerprint = set_fingerprint; - observer->set_level = set_level; - observer->set_user = set_user; - observer->set_tag = set_tag; - observer->remove_tag = remove_tag; - observer->set_extra = set_extra; - observer->remove_extra = remove_extra; - observer->set_context = set_context; - observer->remove_context = remove_context; - observer->add_breadcrumb = add_breadcrumb; - observer->add_attachment = add_attachment; - observer->remove_attachment = remove_attachment; - SENTRY_WITH_SCOPE_MUT (scope) { - sentry__scope_add_observer(scope, observer); - } - } + add_scope_observer(data); #endif return 0; @@ -1147,6 +1171,10 @@ static void crashpad_backend_free(sentry_backend_t *backend) { auto *data = static_cast(backend->data); +#if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_LINUX) \ + || defined(SENTRY_PLATFORM_MACOS) + remove_scope_observer(data); +#endif sentry__path_free(data->event_path); sentry__path_free(data->external_report_path); delete data; From 5d504b073bb48a5092fb87dfa5244cb8700cc427 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 14:54:02 +0200 Subject: [PATCH 27/29] begin/end flush --- src/backends/sentry_backend_crashpad.cpp | 78 ++++++++++++++++++------ 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 8840fde5db..a9c3c56879 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -264,6 +264,37 @@ flush_external_crash_report( sentry_envelope_free(envelope); } +static bool +begin_scope_flush(crashpad_state_t *data) +{ + if (data->crashed.load(std::memory_order_relaxed)) { + return false; + } + + bool expected = false; + while (!data->scope_flush.compare_exchange_strong( + expected, true, std::memory_order_acquire)) { + if (data->crashed.load(std::memory_order_relaxed)) { + return false; + } + expected = false; + sentry__cpu_relax(); + } + + if (data->crashed.load(std::memory_order_relaxed)) { + data->scope_flush.store(false, std::memory_order_release); + return false; + } + + return true; +} + +static void +end_scope_flush(crashpad_state_t *data) +{ + data->scope_flush.store(false, std::memory_order_release); +} + // This hook seeds __sentry-event after sentry_init() has initialized derived // scope state. macOS has no FirstChanceHandler, and Windows WER can capture // outside the normal first-chance path, so the attachment must already contain @@ -274,11 +305,8 @@ crashpad_backend_post_init( sentry_backend_t *backend, const sentry_options_t *options) { auto *data = static_cast(backend->data); - bool expected = false; - if (!data->event_path || data->crashed.load(std::memory_order_relaxed) - || !data->scope_flush.compare_exchange_strong( - expected, true, std::memory_order_acquire)) { + if (!data->event_path || data->crashed.load(std::memory_order_relaxed)) { return; } @@ -305,11 +333,35 @@ crashpad_backend_post_init( // Seed __sentry-event with event data that is not sent as scope updates, // such as the event ID and option-level metadata. Mutable scope state is // sent via IPC and merged when Crashpad writes the report. - flush_scope_to_event(data->event_path, options, event); + SENTRY_WITH_SCOPE (scope) { + // we want the scope without any modules or breadcrumbs + sentry__scope_apply_to_event(scope, options, event, SENTRY_SCOPE_NONE); + } + + if (!begin_scope_flush(data)) { + sentry_value_decref(event); + return; + } + + size_t mpack_size; + char *mpack = sentry_value_to_msgpack(event, &mpack_size); + sentry_value_decref(event); + if (!mpack) { + end_scope_flush(data); + return; + } + + int rv = sentry__path_write_buffer(data->event_path, mpack, mpack_size); + sentry_free(mpack); + + if (rv != 0) { + SENTRY_WARN("flushing scope to msgpack failed"); + } + end_scope_flush(data); + if (data->external_report_path) { flush_external_crash_report(options, &data->crash_event_id); } - data->scope_flush.store(false, std::memory_order_release); } #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_WINDOWS) @@ -636,15 +688,7 @@ send_scope_update(crashpad_state_t *data, sentry_value_t update) return; } - bool expected = false; - if (!data->scope_flush.compare_exchange_strong( - expected, true, std::memory_order_acquire)) { - sentry_value_decref(update); - return; - } - - if (data->crashed.load(std::memory_order_relaxed)) { - data->scope_flush.store(false, std::memory_order_release); + if (!begin_scope_flush(data)) { sentry_value_decref(update); return; } @@ -653,7 +697,7 @@ send_scope_update(crashpad_state_t *data, sentry_value_t update) char *mpack = sentry_value_to_msgpack(update, &mpack_size); sentry_value_decref(update); if (!mpack) { - data->scope_flush.store(false, std::memory_order_release); + end_scope_flush(data); return; } @@ -661,7 +705,7 @@ send_scope_update(crashpad_state_t *data, sentry_value_t update) data->client->UpdateScope(std::string(mpack, mpack_size)); } sentry_free(mpack); - data->scope_flush.store(false, std::memory_order_release); + end_scope_flush(data); } static sentry_value_t From 2bb02e79a808302b89228e1a248546305e99d6b8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 16:34:00 +0200 Subject: [PATCH 28/29] fix breadcrumbs --- external/crashpad | 2 +- src/backends/sentry_backend_crashpad.cpp | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/external/crashpad b/external/crashpad index 460c37585e..41801e4f85 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit 460c37585e99e31a0121bfe03f10e80f494e722f +Subproject commit 41801e4f85378c6fb5404e034bfa8bd4d868a6ed diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index a9c3c56879..f798a2b02d 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -331,11 +331,13 @@ crashpad_backend_post_init( } // Seed __sentry-event with event data that is not sent as scope updates, - // such as the event ID and option-level metadata. Mutable scope state is - // sent via IPC and merged when Crashpad writes the report. + // such as the event ID and option-level metadata. Existing breadcrumbs + // are included so they survive backend reinstall; later mutable scope + // state is sent via IPC and merged when Crashpad writes the report. SENTRY_WITH_SCOPE (scope) { - // we want the scope without any modules or breadcrumbs - sentry__scope_apply_to_event(scope, options, event, SENTRY_SCOPE_NONE); + // we want the scope without any modules + sentry__scope_apply_to_event( + scope, options, event, SENTRY_SCOPE_BREADCRUMBS); } if (!begin_scope_flush(data)) { From 1f4e5fc9cf4c934b6e1ddca091e4103bbbef7c28 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 4 Jul 2026 20:53:38 +0200 Subject: [PATCH 29/29] trace context --- external/crashpad | 2 +- src/backends/sentry_backend_crashpad.cpp | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/external/crashpad b/external/crashpad index 41801e4f85..44d4b1639f 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit 41801e4f85378c6fb5404e034bfa8bd4d868a6ed +Subproject commit 44d4b1639fb854d10f1c0a71e3f6c701212233a7 diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index f798a2b02d..4eb4b04f36 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -852,6 +852,20 @@ remove_context(void *state, const char *key, size_t key_len) send_scope_remove_item_update(state, "contexts", key, key_len); } +static void +set_trace_context(void *state, sentry_value_t trace_context) +{ + sentry_value_t update; + if (sentry_value_is_null(trace_context)) { + update = scope_update_new("remove", "trace_context"); + } else { + update = scope_update_new("set", "trace_context"); + sentry_value_set_by_key( + update, "value", sentry__value_clone(trace_context)); + } + send_scope_update(static_cast(state), update); +} + static void add_breadcrumb(void *state, sentry_value_t breadcrumb) { @@ -947,6 +961,7 @@ add_scope_observer(crashpad_state_t *data) observer->remove_extra = remove_extra; observer->set_context = set_context; observer->remove_context = remove_context; + observer->set_trace_context = set_trace_context; observer->add_breadcrumb = add_breadcrumb; observer->add_attachment = add_attachment; observer->remove_attachment = remove_attachment;