diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d95bcbf0..335abfb0c 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)) diff --git a/include/sentry.h b/include/sentry.h index 441833d15..391f18b25 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2118,9 +2118,39 @@ 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); +/** + * 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 scope created via `sentry_scope_new`, `sentry_scope_clone`, or + * `sentry_local_scope_new`. + */ +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. * @@ -2131,9 +2161,12 @@ SENTRY_API sentry_scope_t *sentry_local_scope_new(void); 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. * - * 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 509066ea2..a5c2d70cf 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->one_shot) { + sentry_scope_free(local_scope); + } } SENTRY_WITH_SCOPE (scope) { diff --git a/src/sentry_ringbuffer.c b/src/sentry_ringbuffer.c index a24ed540b..a89b1fd02 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 4acf45f15..15407ebf4 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 0df499c31..00439d689 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->one_shot = false; } static sentry_scope_t * @@ -170,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) { @@ -182,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; @@ -192,6 +193,58 @@ sentry__scope_free(sentry_scope_t *scope) sentry_free(scope); } +sentry_scope_t * +sentry_local_scope_new(void) +{ + sentry_scope_t *scope = sentry_scope_new(); + if (scope) { + scope->one_shot = true; + } + return 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; + } + + 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; + + 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 6047fa803..86f156a17 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 this scope is single-use. A capture function frees a one-shot + // scope after applying it. + bool one_shot; }; /** @@ -79,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 79855c8b1..491fb74db 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,8 +1208,198 @@ 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(); } + +SENTRY_TEST(scope_ownership) +{ + // `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->one_shot); + sentry_scope_free(local_scope); + + sentry_scope_t *user_scope = sentry_scope_new(); + TEST_CHECK(!user_scope->one_shot); + 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 reusable, never one-shot. + TEST_CHECK(!clone->one_shot); + + // 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); + + sentry_close(); + + TEST_CHECK_INT_EQUAL(called, 2); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index dd693985b..5e7a6960d 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)