From 36db027476c040ea8fb28ecb1d6f7d0936039b46 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 10 Jul 2026 12:59:42 +0200 Subject: [PATCH 1/9] Add sentry_scope_new(), sentry_scope_free(), sentry_scope_clone() --- include/sentry.h | 30 ++++++++++++++++++- src/sentry_core.c | 4 ++- src/sentry_ringbuffer.c | 19 ++++++++++++ src/sentry_ringbuffer.h | 7 +++++ src/sentry_scope.c | 64 +++++++++++++++++++++++++++++++++++++++++ src/sentry_scope.h | 4 +++ 6 files changed, 126 insertions(+), 2 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index 441833d154..3f3f66445e 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2121,6 +2121,31 @@ typedef struct sentry_scope_s sentry_scope_t; */ SENTRY_API sentry_scope_t *sentry_local_scope_new(void); +/** + * Creates a user-owned scope. + * + * Unlike a local scope, a user-owned scope is applied but not freed by capture + * functions such as `sentry_capture_event_with_scope`, so the same scope can be + * mutated and reused across many captures. You must release it yourself with + * `sentry_scope_free`. + */ +SENTRY_API sentry_scope_t *sentry_scope_new(void); + +/** + * Frees a user-owned scope created via `sentry_scope_new` or + * `sentry_scope_clone`. + */ +SENTRY_API void sentry_scope_free(sentry_scope_t *scope); + +/** + * Creates a copy of a scope. + * + * Top-level fields are copied while nested values are shared by reference. The + * returned scope is user-owned regardless of the source's ownership. Release it + * with `sentry_scope_free`. + */ +SENTRY_API sentry_scope_t *sentry_scope_clone(const sentry_scope_t *scope); + /** * Sends a sentry event. * @@ -2133,7 +2158,10 @@ SENTRY_API sentry_uuid_t sentry_capture_event(sentry_value_t event); /** * Sends a sentry event with a local scope. * - * Takes ownership of `scope`. + * If `scope` is a local scope (`sentry_local_scope_new`), this takes ownership + * of it and frees it. If `scope` is user-owned (`sentry_scope_new` or + * `sentry_scope_clone`), it is applied but not freed, so it can be reused; free + * it yourself with `sentry_scope_free`. */ SENTRY_API sentry_uuid_t sentry_capture_event_with_scope( sentry_value_t event, sentry_scope_t *scope); diff --git a/src/sentry_core.c b/src/sentry_core.c index 509066ea23..a733ec3a4c 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -699,7 +699,9 @@ sentry__prepare_event(const sentry_options_t *options, sentry_value_t event, sentry_scope_mode_t mode = SENTRY_SCOPE_BREADCRUMBS; sentry__scope_apply_to_event(local_scope, options, event, mode); sentry__attachments_extend(&all_attachments, local_scope->attachments); - sentry__scope_free(local_scope); + if (!local_scope->user_owned) { + sentry__scope_free(local_scope); + } } SENTRY_WITH_SCOPE (scope) { diff --git a/src/sentry_ringbuffer.c b/src/sentry_ringbuffer.c index a24ed540b0..a89b1fd02d 100644 --- a/src/sentry_ringbuffer.c +++ b/src/sentry_ringbuffer.c @@ -28,6 +28,25 @@ sentry__ringbuffer_free(sentry_ringbuffer_t *rb) sentry_free(rb); } +sentry_ringbuffer_t * +sentry__ringbuffer_clone(const sentry_ringbuffer_t *rb) +{ + if (!rb) { + return NULL; + } + + sentry_ringbuffer_t *clone = SENTRY_MAKE(sentry_ringbuffer_t); + if (!clone) { + return NULL; + } + + clone->list = sentry__value_clone(rb->list); + clone->max_size = rb->max_size; + clone->start_idx = rb->start_idx; + + return clone; +} + int sentry__ringbuffer_append(sentry_ringbuffer_t *rb, sentry_value_t value) { diff --git a/src/sentry_ringbuffer.h b/src/sentry_ringbuffer.h index 4acf45f154..15407ebf44 100644 --- a/src/sentry_ringbuffer.h +++ b/src/sentry_ringbuffer.h @@ -30,6 +30,13 @@ sentry_ringbuffer_t *sentry__ringbuffer_new(size_t max_size); */ void sentry__ringbuffer_free(sentry_ringbuffer_t *rb); +/** + * Create a copy of a ringbuffer with the same contents and ring state. + * The contained values are shared by reference, like `sentry__value_clone`. + * Returns a pointer to the ringbuffer on success, NULL on failure. + */ +sentry_ringbuffer_t *sentry__ringbuffer_clone(const sentry_ringbuffer_t *rb); + /** * Append a sentry_value_t to the ringbuffer. * If the ringbuffer is full, the oldest value will be overwritten. diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 0df499c313..6ed2829265 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -88,6 +88,7 @@ init_scope(sentry_scope_t *scope) scope->transaction_object = NULL; scope->span = NULL; scope->trace_managed = true; + scope->user_owned = false; } static sentry_scope_t * @@ -192,6 +193,69 @@ sentry__scope_free(sentry_scope_t *scope) sentry_free(scope); } +sentry_scope_t * +sentry_scope_new(void) +{ + sentry_scope_t *scope = sentry_local_scope_new(); + if (scope) { + scope->user_owned = true; + } + return scope; +} + +void +sentry_scope_free(sentry_scope_t *scope) +{ + sentry__scope_free(scope); +} + +sentry_scope_t * +sentry_scope_clone(const sentry_scope_t *scope) +{ + if (!scope) { + return NULL; + } + + sentry_scope_t *clone = SENTRY_MAKE(sentry_scope_t); + if (!clone) { + return NULL; + } + memset(clone, 0, sizeof(sentry_scope_t)); + + clone->release = sentry__string_clone(scope->release); + clone->environment = sentry__string_clone(scope->environment); + clone->transaction = sentry__string_clone(scope->transaction); + clone->fingerprint = sentry__value_clone(scope->fingerprint); + clone->user = sentry__value_clone(scope->user); + clone->tags = sentry__value_clone(scope->tags); + clone->extra = sentry__value_clone(scope->extra); + clone->attributes = sentry__value_clone(scope->attributes); + clone->contexts = sentry__value_clone(scope->contexts); + clone->propagation_context + = sentry__value_clone(scope->propagation_context); + clone->breadcrumbs = sentry__ringbuffer_clone(scope->breadcrumbs); + clone->dynamic_sampling_context + = sentry__value_clone(scope->dynamic_sampling_context); + if (sentry_value_is_frozen(scope->dynamic_sampling_context)) { + sentry_value_freeze(clone->dynamic_sampling_context); + } + clone->level = scope->level; + clone->client_sdk = sentry__value_clone(scope->client_sdk); + sentry__attachments_extend(&clone->attachments, scope->attachments); + + clone->transaction_object = scope->transaction_object; + sentry__transaction_incref(clone->transaction_object); + clone->span = scope->span; + sentry__span_incref(clone->span); + clone->trace_managed = scope->trace_managed; + + // A clone is created to be owned and managed by the caller, so it is + // user-owned regardless of the source's ownership. + clone->user_owned = true; + + return clone; +} + void sentry__scope_freeze_dsc(sentry_scope_t *scope, sentry_value_t incoming) { diff --git a/src/sentry_scope.h b/src/sentry_scope.h index 6047fa8035..c610847721 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -39,6 +39,10 @@ struct sentry_scope_s { sentry_transaction_t *transaction_object; sentry_span_t *span; bool trace_managed; + + // Whether the user owns this scope's lifetime, meaning capture functions + // apply it without freeing it. + bool user_owned; }; /** From d540cd44ef95b0426e708425b4d28a12870b5836 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 10 Jul 2026 13:02:37 +0200 Subject: [PATCH 2/9] Update docs for other funcs --- include/sentry.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/include/sentry.h b/include/sentry.h index 3f3f66445e..096d9ef407 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2118,6 +2118,11 @@ typedef struct sentry_scope_s sentry_scope_t; /** * Creates a local scope. + * + * A local scope is a one-shot scope: the capture function it is passed to (such + * as `sentry_capture_event_with_scope`) takes ownership and frees it. To create + * a scope whose lifetime you manage and can reuse across captures, use + * `sentry_scope_new` instead. */ SENTRY_API sentry_scope_t *sentry_local_scope_new(void); @@ -2156,7 +2161,7 @@ SENTRY_API sentry_scope_t *sentry_scope_clone(const sentry_scope_t *scope); SENTRY_API sentry_uuid_t sentry_capture_event(sentry_value_t event); /** - * Sends a sentry event with a local scope. + * Sends a sentry event with a scope. * * If `scope` is a local scope (`sentry_local_scope_new`), this takes ownership * of it and frees it. If `scope` is user-owned (`sentry_scope_new` or From 6b45e7f8261ac018fea1e054c7e6b07ba613fd65 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 10 Jul 2026 13:31:54 +0200 Subject: [PATCH 3/9] Add tests --- tests/unit/test_scope.c | 190 ++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 7 +- 2 files changed, 196 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 79855c8b11..3ede18e934 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1213,3 +1213,193 @@ SENTRY_TEST(scope_local_attributes) sentry_close(); } + +SENTRY_TEST(scope_ownership) +{ + // `sentry_scope_new` marks a scope user-owned, `sentry_local_scope_new` + // does not. + sentry_scope_t *local_scope = sentry_local_scope_new(); + TEST_CHECK(!local_scope->user_owned); + sentry__scope_free(local_scope); + + sentry_scope_t *user_scope = sentry_scope_new(); + TEST_CHECK(user_scope->user_owned); + sentry_scope_free(user_scope); +} + +static size_t +scope_breadcrumb_count(const sentry_scope_t *scope) +{ + sentry_value_t breadcrumbs = sentry__ringbuffer_to_list(scope->breadcrumbs); + size_t count = sentry_value_get_length(breadcrumbs); + sentry_value_decref(breadcrumbs); + return count; +} + +SENTRY_TEST(scope_clone_independence) +{ + sentry_scope_t *scope = sentry_scope_new(); + sentry_scope_set_tag(scope, "shared", "original"); + sentry_scope_set_context( + scope, "device", sentry_value_new_string("original")); + sentry_scope_set_level(scope, SENTRY_LEVEL_WARNING); + sentry_scope_add_breadcrumb( + scope, sentry_value_new_breadcrumb(NULL, "first")); + + sentry_scope_t *clone = sentry_scope_clone(scope); + + // A clone is always user-owned. + TEST_CHECK(clone->user_owned); + + // The clone carries over the source's data. + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(clone->tags, "shared")), + "original"); + TEST_CHECK(clone->level == SENTRY_LEVEL_WARNING); + TEST_CHECK_INT_EQUAL(scope_breadcrumb_count(clone), 1); + + // Mutating the clone does not affect the source, and vice versa. + sentry_scope_set_tag(clone, "shared", "changed"); + sentry_scope_set_tag(clone, "clone_only", "yes"); + sentry_scope_add_breadcrumb( + clone, sentry_value_new_breadcrumb(NULL, "second")); + + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(scope->tags, "shared")), + "original"); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(scope->tags, "clone_only"))); + TEST_CHECK_INT_EQUAL(scope_breadcrumb_count(scope), 1); + TEST_CHECK_INT_EQUAL(scope_breadcrumb_count(clone), 2); + + sentry_scope_free(clone); + sentry_scope_free(scope); +} + +SENTRY_TEST(scope_clone_preserves_data) +{ + sentry_scope_t *scope = sentry_scope_new(); + + sentry_scope_set_tag(scope, "tag_key", "tag_value"); + sentry_scope_set_context(scope, "device", sentry_value_new_string("Xbox")); + sentry_scope_set_user( + scope, sentry_value_new_user("id-1", "alice", NULL, NULL)); + sentry_scope_set_extra( + scope, "extra_key", sentry_value_new_string("extra_value")); + sentry_scope_set_fingerprint(scope, "fp1", "fp2", NULL); + sentry_scope_set_level(scope, SENTRY_LEVEL_WARNING); + sentry__scope_set_attribute(scope, "attr_key", + sentry_value_new_attribute( + sentry_value_new_string("attr_value"), NULL)); + sentry_scope_add_breadcrumb( + scope, sentry_value_new_breadcrumb(NULL, "crumb")); + sentry_scope_attach_bytes(scope, "payload", 7, "file.bin"); + + sentry_scope_t *clone = sentry_scope_clone(scope); + + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(clone->tags, "tag_key")), + "tag_value"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + clone->contexts, "device")), + "Xbox"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + clone->user, "username")), + "alice"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + clone->extra, "extra_key")), + "extra_value"); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(clone->fingerprint), 2); + TEST_CHECK(clone->level == SENTRY_LEVEL_WARNING); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(clone->attributes, "attr_key"), "value")), + "attr_value"); + TEST_CHECK_INT_EQUAL(scope_breadcrumb_count(clone), 1); + + // Attachments are deep-copied into an independent list. + TEST_CHECK(clone->attachments != NULL); + TEST_CHECK(clone->attachments != scope->attachments); + TEST_CHECK_STRING_EQUAL( + sentry__attachment_get_filename(clone->attachments), "file.bin"); + TEST_CHECK_INT_EQUAL(clone->attachments->buf_len, 7); + + sentry_scope_free(clone); + sentry_scope_free(scope); +} + +SENTRY_TEST(scope_clone_shares_span) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_init(options); + + sentry_transaction_context_t *tx_ctx + = sentry_transaction_context_new("txn", NULL); + sentry_transaction_t *tx + = sentry_transaction_start(tx_ctx, sentry_value_new_null()); + sentry_set_transaction_object(tx); + + // Clone the scope while a transaction is active on it. + sentry_scope_t *clone = NULL; + sentry_transaction_t *scope_txn = NULL; + SENTRY_WITH_SCOPE (scope) { + scope_txn = scope->transaction_object; + clone = sentry_scope_clone(scope); + } + + // The active transaction is shared by reference, not dropped or duplicated. + TEST_CHECK(scope_txn != NULL); + TEST_CHECK(clone->transaction_object == scope_txn); + + // The shared reference keeps the transaction alive for the original: the + // clone can be freed and the transaction still finished safely. + sentry_scope_free(clone); + sentry_transaction_finish(tx); + + sentry_close(); +} + +static void +send_envelope_count(sentry_envelope_t *envelope, void *data) +{ + uint64_t *called = data; + *called += 1; + sentry_envelope_free(envelope); +} + +SENTRY_TEST(scope_capture_user_owned) +{ + uint64_t called = 0; + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_transport_t *transport = sentry_transport_new(send_envelope_count); + sentry_transport_set_state(transport, &called); + sentry_options_set_transport(options, transport); + sentry_init(options); + + // A user-owned scope survives being captured with, so it can be reused. + sentry_scope_t *scope = sentry_scope_new(); + sentry_scope_set_tag(scope, "run", "first"); + + sentry_capture_event_with_scope( + sentry_value_new_message_event(SENTRY_LEVEL_INFO, "logger", "one"), + scope); + + // The scope was applied but not freed, so reading and reusing it is safe + // (a use-after-free here would trip the sanitizers). + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(scope->tags, "run")), + "first"); + + sentry_scope_set_tag(scope, "run", "second"); + sentry_capture_event_with_scope( + sentry_value_new_message_event(SENTRY_LEVEL_INFO, "logger", "two"), + scope); + + sentry_scope_free(scope); + + TEST_CHECK_INT_EQUAL(called, 2); + + sentry_close(); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index dd693985b8..5e7a6960d6 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -284,16 +284,21 @@ XX(sampling_before_send) XX(sampling_decision) XX(sampling_transaction) XX(scope_breadcrumbs) +XX(scope_capture_user_owned) +XX(scope_clone_independence) +XX(scope_clone_preserves_data) +XX(scope_clone_shares_span) 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_ownership) XX(scope_propagation_context) XX(scope_tags) +XX(scope_update_context) XX(scope_user) XX(scope_user_id) XX(scoped_txn) From d90ac2c603cd227bdb8f977fbf58825b32e03938 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 10 Jul 2026 13:41:16 +0200 Subject: [PATCH 4/9] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d95bcbf04..335abfb0cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +**Features**: + +- Add reusable, user-owned scopes. `sentry_scope_new` creates a scope that `sentry_capture_event_with_scope` applies without consuming, so you can configure it once and reuse it across many captures instead of building a new local scope each time. `sentry_scope_clone` copies a scope, and `sentry_scope_free` releases it. ([#1855](https://github.com/getsentry/sentry-native/pull/1855)) + **Fixes**: - Apply the propagation context to events that already have contexts set, so that events captured with a local scope or with event-level contexts keep their trace. ([#1843](https://github.com/getsentry/sentry-native/pull/1843)) From c51035fce7d26e58ed42020da7dca8f7e0f54f62 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 10 Jul 2026 15:38:06 +0200 Subject: [PATCH 5/9] Promote sentry__scope_free() to public, remove forwarder --- src/sentry_core.c | 2 +- src/sentry_scope.c | 8 +------- src/sentry_scope.h | 5 ----- tests/unit/test_scope.c | 28 ++++++++++++++-------------- 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/sentry_core.c b/src/sentry_core.c index a733ec3a4c..d45f3c1848 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -700,7 +700,7 @@ sentry__prepare_event(const sentry_options_t *options, sentry_value_t event, sentry__scope_apply_to_event(local_scope, options, event, mode); sentry__attachments_extend(&all_attachments, local_scope->attachments); if (!local_scope->user_owned) { - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); } } diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 6ed2829265..d97069b8ae 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -183,7 +183,7 @@ sentry_local_scope_new(void) } void -sentry__scope_free(sentry_scope_t *scope) +sentry_scope_free(sentry_scope_t *scope) { if (!scope) { return; @@ -203,12 +203,6 @@ sentry_scope_new(void) return scope; } -void -sentry_scope_free(sentry_scope_t *scope) -{ - sentry__scope_free(scope); -} - sentry_scope_t * sentry_scope_clone(const sentry_scope_t *scope) { diff --git a/src/sentry_scope.h b/src/sentry_scope.h index c610847721..82ffbccec4 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -83,11 +83,6 @@ void sentry__scope_cleanup(void); */ void sentry__scope_flush_unlock(void); -/** - * Deallocates a (local) scope. - */ -void sentry__scope_free(sentry_scope_t *scope); - /** * This will merge the requested data which is in the given `scope` to the given * `event`. diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 3ede18e934..736698cebf 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -91,7 +91,7 @@ SENTRY_TEST(scope_contexts) TEST_CHECK_CONTEXT_EQUAL(event, "local", "local"); TEST_CHECK_CONTEXT_EQUAL(event, "scope", "local"); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); sentry_value_decref(event); } @@ -178,7 +178,7 @@ SENTRY_TEST(scope_update_context) sentry_value_as_string(sentry_value_get_by_key(ctx, "version")), "6.1"); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); } sentry_close(); @@ -238,7 +238,7 @@ SENTRY_TEST(scope_propagation_context) global_scope, options, event, SENTRY_SCOPE_NONE); TEST_CHECK_EVENT_TRACE_ID_EQUAL( event, "aaaabbbbccccddddeeeeffff00001111"); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); sentry_value_decref(event); } @@ -351,7 +351,7 @@ SENTRY_TEST(scope_extra) TEST_CHECK_EXTRA_EQUAL(event, "local", "local"); TEST_CHECK_EXTRA_EQUAL(event, "scope", "local"); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); sentry_value_decref(event); } @@ -435,7 +435,7 @@ SENTRY_TEST(scope_fingerprint) TEST_CHECK_JSON_VALUE(sentry_value_get_by_key(event, "fingerprint"), "[\"event1\",\"event2\"]"); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); sentry_value_decref(event); } @@ -546,7 +546,7 @@ SENTRY_TEST(scope_tags) TEST_CHECK_TAG_EQUAL(event, "local", "local"); TEST_CHECK_TAG_EQUAL(event, "scope", "local"); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); sentry_value_decref(event); } @@ -601,7 +601,7 @@ SENTRY_TEST(scope_user) TEST_CHECK_JSON_VALUE(sentry_value_get_by_key(event, "user"), "{\"id\":\"3\",\"username\":\"event\"}"); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); sentry_value_decref(event); } @@ -665,7 +665,7 @@ SENTRY_TEST(scope_user_id) sentry_scope_t *local_scope = sentry_local_scope_new(); sentry__scope_apply_to_event( local_scope, options, event, SENTRY_SCOPE_BREADCRUMBS); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); SENTRY_WITH_SCOPE (scope) { sentry__scope_apply_to_event(scope, options, event, SENTRY_SCOPE_ALL); } @@ -750,7 +750,7 @@ SENTRY_TEST(scope_level) local_scope, options, event, SENTRY_SCOPE_NONE); TEST_CHECK_LEVEL_EQUAL(event, "debug"); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); sentry_value_decref(event); } @@ -866,7 +866,7 @@ SENTRY_TEST(scope_breadcrumbs) TEST_CHECK_MESSAGE_EQUAL(result, 3, "event5"); TEST_CHECK_MESSAGE_EQUAL(result, 4, "local6"); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); sentry_value_decref(event); } @@ -1148,7 +1148,7 @@ SENTRY_TEST(scope_local_attributes) sentry_value_get_by_key(global_attributes, "global"), "value")), "global"); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); } // Test removing attributes from global scope @@ -1190,7 +1190,7 @@ SENTRY_TEST(scope_local_attributes) TEST_CHECK(sentry_value_is_null( sentry_value_get_by_key(local_attributes, "test_key"))); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); } // Test that invalid attributes are not set on local scope @@ -1208,7 +1208,7 @@ SENTRY_TEST(scope_local_attributes) sentry_value_get_by_key(local_attributes, "invalid"))); sentry_value_decref(invalid_attr); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); } sentry_close(); @@ -1220,7 +1220,7 @@ SENTRY_TEST(scope_ownership) // does not. sentry_scope_t *local_scope = sentry_local_scope_new(); TEST_CHECK(!local_scope->user_owned); - sentry__scope_free(local_scope); + sentry_scope_free(local_scope); sentry_scope_t *user_scope = sentry_scope_new(); TEST_CHECK(user_scope->user_owned); From 54e667089f1242a55c032f1d32d82727eee38974 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 10 Jul 2026 15:38:30 +0200 Subject: [PATCH 6/9] Correction for header doc --- include/sentry.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index 096d9ef407..391f18b254 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2137,8 +2137,8 @@ SENTRY_API sentry_scope_t *sentry_local_scope_new(void); SENTRY_API sentry_scope_t *sentry_scope_new(void); /** - * Frees a user-owned scope created via `sentry_scope_new` or - * `sentry_scope_clone`. + * Frees a scope created via `sentry_scope_new`, `sentry_scope_clone`, or + * `sentry_local_scope_new`. */ SENTRY_API void sentry_scope_free(sentry_scope_t *scope); From 427eb840e6856b9912a8c9fc9431b0821b3dc7b2 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 10 Jul 2026 15:46:04 +0200 Subject: [PATCH 7/9] Make sentry_scope_new as default impl, sentry_local_scope_new as specialization --- src/sentry_scope.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sentry_scope.c b/src/sentry_scope.c index d97069b8ae..0d8cf7c4d5 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -171,7 +171,7 @@ sentry__scope_flush_unlock(void) } sentry_scope_t * -sentry_local_scope_new(void) +sentry_scope_new(void) { sentry_scope_t *scope = SENTRY_MAKE(sentry_scope_t); if (!scope) { @@ -179,6 +179,7 @@ sentry_local_scope_new(void) } init_scope(scope); + scope->user_owned = true; return scope; } @@ -194,11 +195,11 @@ sentry_scope_free(sentry_scope_t *scope) } sentry_scope_t * -sentry_scope_new(void) +sentry_local_scope_new(void) { - sentry_scope_t *scope = sentry_local_scope_new(); + sentry_scope_t *scope = sentry_scope_new(); if (scope) { - scope->user_owned = true; + scope->user_owned = false; } return scope; } From 3ffe87e5df4ea76a37792da6475a9c0b53cd956e Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 10 Jul 2026 15:49:07 +0200 Subject: [PATCH 8/9] Drop memset 0 --- src/sentry_scope.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 0d8cf7c4d5..02214bc2c3 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -215,7 +215,6 @@ sentry_scope_clone(const sentry_scope_t *scope) if (!clone) { return NULL; } - memset(clone, 0, sizeof(sentry_scope_t)); clone->release = sentry__string_clone(scope->release); clone->environment = sentry__string_clone(scope->environment); From 26e884c1643389cd21c50d6809cb9a8db834ef59 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 10 Jul 2026 16:18:23 +0200 Subject: [PATCH 9/9] user_owned => one_shot --- src/sentry_core.c | 2 +- src/sentry_scope.c | 9 ++------- src/sentry_scope.h | 6 +++--- tests/unit/test_scope.c | 16 ++++++++-------- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/sentry_core.c b/src/sentry_core.c index d45f3c1848..a5c2d70cfa 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -699,7 +699,7 @@ sentry__prepare_event(const sentry_options_t *options, sentry_value_t event, sentry_scope_mode_t mode = SENTRY_SCOPE_BREADCRUMBS; sentry__scope_apply_to_event(local_scope, options, event, mode); sentry__attachments_extend(&all_attachments, local_scope->attachments); - if (!local_scope->user_owned) { + if (local_scope->one_shot) { sentry_scope_free(local_scope); } } diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 02214bc2c3..00439d6895 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -88,7 +88,7 @@ init_scope(sentry_scope_t *scope) scope->transaction_object = NULL; scope->span = NULL; scope->trace_managed = true; - scope->user_owned = false; + scope->one_shot = false; } static sentry_scope_t * @@ -179,7 +179,6 @@ sentry_scope_new(void) } init_scope(scope); - scope->user_owned = true; return scope; } @@ -199,7 +198,7 @@ sentry_local_scope_new(void) { sentry_scope_t *scope = sentry_scope_new(); if (scope) { - scope->user_owned = false; + scope->one_shot = true; } return scope; } @@ -243,10 +242,6 @@ sentry_scope_clone(const sentry_scope_t *scope) sentry__span_incref(clone->span); clone->trace_managed = scope->trace_managed; - // A clone is created to be owned and managed by the caller, so it is - // user-owned regardless of the source's ownership. - clone->user_owned = true; - return clone; } diff --git a/src/sentry_scope.h b/src/sentry_scope.h index 82ffbccec4..86f156a17d 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -40,9 +40,9 @@ struct sentry_scope_s { sentry_span_t *span; bool trace_managed; - // Whether the user owns this scope's lifetime, meaning capture functions - // apply it without freeing it. - bool user_owned; + // Whether this scope is single-use. A capture function frees a one-shot + // scope after applying it. + bool one_shot; }; /** diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 736698cebf..491fb74db0 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1216,14 +1216,14 @@ SENTRY_TEST(scope_local_attributes) SENTRY_TEST(scope_ownership) { - // `sentry_scope_new` marks a scope user-owned, `sentry_local_scope_new` - // does not. + // `sentry_local_scope_new` makes a one-shot scope, `sentry_scope_new` does + // not. sentry_scope_t *local_scope = sentry_local_scope_new(); - TEST_CHECK(!local_scope->user_owned); + TEST_CHECK(local_scope->one_shot); sentry_scope_free(local_scope); sentry_scope_t *user_scope = sentry_scope_new(); - TEST_CHECK(user_scope->user_owned); + TEST_CHECK(!user_scope->one_shot); sentry_scope_free(user_scope); } @@ -1248,8 +1248,8 @@ SENTRY_TEST(scope_clone_independence) sentry_scope_t *clone = sentry_scope_clone(scope); - // A clone is always user-owned. - TEST_CHECK(clone->user_owned); + // A clone is reusable, never one-shot. + TEST_CHECK(!clone->one_shot); // The clone carries over the source's data. TEST_CHECK_STRING_EQUAL( @@ -1399,7 +1399,7 @@ SENTRY_TEST(scope_capture_user_owned) sentry_scope_free(scope); - TEST_CHECK_INT_EQUAL(called, 2); - sentry_close(); + + TEST_CHECK_INT_EQUAL(called, 2); }