From 1bdcc1e092587abe8cc3656c7f8c5c664951e1e7 Mon Sep 17 00:00:00 2001 From: Nathan White Date: Thu, 10 Sep 2026 09:18:02 -0700 Subject: [PATCH 1/5] feat: allow tags before crash handler startup --- CHANGELOG.md | 1 + CONTRIBUTING.md | 1 + examples/example.c | 14 +++ include/sentry.h | 12 +++ src/backends/sentry_backend_crashpad.cpp | 50 +++++++--- src/backends/sentry_backend_native.c | 6 ++ src/integrations/sentry_integration_wer.c | 10 ++ src/sentry_core.c | 13 +++ src/sentry_options.c | 15 +++ src/sentry_options.h | 1 + tests/test_integration_crashpad.py | 24 ++++- tests/test_integration_native.py | 3 +- tests/test_integration_wer.py | 5 + tests/unit/test_options.c | 109 ++++++++++++++++++++++ tests/unit/tests.inc | 3 + 15 files changed, 251 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eccd29e060..7eebec3715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ **Features**: +- Add `sentry_options_set_tags` for configuring tags before the crash backend is started, including out-of-process crash handlers. - Add `sentry_attachment_from_file/bytes` (and their wide-string variants) for creating attachment values that can be fully configured before they are added. ([#2079](https://github.com/getsentry/sentry-native/pull/2079)) - Add `sentry_add_attachment`, `sentry_scope_add_attachment`, and `sentry_hint_add_attachment` for adding configured attachments to the global scope, a specific scope, or a hint. ([#2079](https://github.com/getsentry/sentry-native/pull/2079)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a2665c05aa..8120e04bb1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -195,6 +195,7 @@ The example currently supports the following commands: - `clear-attachments`: Clears all attachments from the global scope. - `capture-user-feedback`: Captures a user feedback event. - `test-logger`: Sets up a test logger for integration tests that outputs in a format the integration tests can parse. +- `initial-tags`: Configures a test tag on the initial scope before SDK initialization. - `disable-logger-when-crashed`: Disables logging during crash handling. - `enable-logger-when-crashed`: Explicitly enables logging during crash handling (default behavior). - `test-logger-before-crash`: Outputs marker directly using printf for test parsing before crash. diff --git a/examples/example.c b/examples/example.c index a6e6d9ba3a..bb398ec1e0 100644 --- a/examples/example.c +++ b/examples/example.c @@ -182,6 +182,13 @@ on_crashed_last_run_callback(const sentry_envelope_t *envelope, void *user_data) const char *event_id = sentry_value_as_string( sentry_envelope_get_header(envelope, "event_id")); printf("CRASHED_LAST_RUN:%s\n", event_id ? event_id : ""); + sentry_value_t event = sentry_envelope_get_event(envelope); + sentry_value_t tags = sentry_value_get_by_key(event, "tags"); + const char *initial_tag = sentry_value_as_string( + sentry_value_get_by_key(tags, "test.initial-tag")); + if (initial_tag) { + printf("CRASHED_LAST_RUN_INITIAL_TAG:%s\n", initial_tag); + } fflush(stdout); } @@ -838,6 +845,13 @@ main(int argc, char **argv) sentry_options_set_crashpad_wait_for_upload(options, true); } + if (has_arg(argc, argv, "initial-tags")) { + sentry_value_t tags = sentry_value_new_object(); + sentry_value_set_by_key( + tags, "test.initial-tag", sentry_value_new_string("initial-value")); + sentry_options_set_tags(options, tags); + } + if (has_arg(argc, argv, "test-logger")) { // Set up the test logger for integration tests sentry_options_set_logger(options, test_logger_callback, NULL); diff --git a/include/sentry.h b/include/sentry.h index e44fbfe4b7..458b5bdd7c 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1426,6 +1426,18 @@ SENTRY_API sentry_options_t *sentry_options_new(void); */ SENTRY_API void sentry_options_free(sentry_options_t *opts); +/** + * Sets tags on the initial scope before the crash backend is started. + * + * The tags must be an object with string values. Non-string values are + * ignored. Calling this function again replaces the previously configured + * initial tags. + * + * The function takes ownership of `tags`. + */ +SENTRY_API void sentry_options_set_tags( + sentry_options_t *opts, sentry_value_t tags); + /** * Sets a transport. */ diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 6a1e0e7049..4d2e18f49a 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -229,9 +229,24 @@ static int write_attachment(crashpad_state_t *state, const base::FilePath &path, const char *data, size_t size) { - if (path.empty() || !state || !state->client) { + if (path.empty() || !state) { return 1; } + if (!state->client) { +#ifdef SENTRY_PLATFORM_WINDOWS + sentry_path_t *sentry_path + = sentry__path_from_wstr(path.value().c_str()); +#else + sentry_path_t *sentry_path + = sentry__path_from_str(path.value().c_str()); +#endif + if (!sentry_path) { + return 1; + } + int rv = sentry__path_write_buffer(sentry_path, data, size); + sentry__path_free(sentry_path); + return rv; + } return state->client->WriteAttachment( path, base::as_bytes(base::make_span(data, size))) ? 0 @@ -346,6 +361,22 @@ to_sentry_level(logging::LogSeverity severity) return SENTRY_LEVEL_DEBUG; } +static void +flush_scope_attachments(crashpad_state_t *data, const sentry_options_t *options) +{ + sentry_value_t event = sentry_value_new_object(); + sentry_value_set_by_key( + event, "event_id", sentry__value_new_uuid(&data->crash_event_id)); + sentry_value_set_by_key( + event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); + + flush_scope_to_event(data, data->event_path, options, event); + if (!data->external_report_path.empty()) { + flush_external_crash_report( + data, data->external_report_path, options, &data->crash_event_id); + } +} + // 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 @@ -371,19 +402,7 @@ crashpad_backend_flush_scope( 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, data->event_path, options, event); - if (!data->external_report_path.empty()) { - flush_external_crash_report( - data, data->external_report_path, options, &data->crash_event_id); - } + flush_scope_attachments(data, options); data->scope_flush.store(false, std::memory_order_release); #endif } @@ -913,6 +932,9 @@ crashpad_backend_startup( } } + // Persist the preloaded scope before Crashpad starts handling crashes. + flush_scope_attachments(data, options); + std::vector arguments { "--no-rate-limit" }; sentry_path_t *log_path = sentry__path_join_str(current_run_folder, "crashpad-handler.log"); diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 0061f63d0d..5d84acb0c9 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -213,6 +213,9 @@ typedef struct { volatile long crashed; } native_backend_state_t; +static void native_backend_flush_scope( + sentry_backend_t *backend, const sentry_options_t *options); + static bool native_backend_process_old_run(sentry_backend_t *backend, const sentry_options_t *options, const sentry_path_t *run_path) @@ -810,6 +813,9 @@ native_backend_startup( } #endif + // Persist the preloaded scope before any crash handler becomes active. + native_backend_flush_scope(backend, options); + // Install crash handlers (signal handlers on Linux/macOS, Mach exception // handler on iOS) #if defined(SENTRY_PLATFORM_IOS) diff --git a/src/integrations/sentry_integration_wer.c b/src/integrations/sentry_integration_wer.c index 1b9ee7ea85..58f337b7d9 100644 --- a/src/integrations/sentry_integration_wer.c +++ b/src/integrations/sentry_integration_wer.c @@ -131,6 +131,15 @@ wer_attachment_path(const sentry_attachment_t *attachment) return absolute_path; } +static int +wer_sync_tag(const char *key, sentry_value_t value, void *data) +{ + if (sentry_value_get_type(value) == SENTRY_VALUE_TYPE_STRING) { + wer_set_tag(data, key, sentry_value_as_string(value)); + } + return 0; +} + static void wer_add_attachment(void *UNUSED(data), sentry_attachment_t *attachment) { @@ -238,6 +247,7 @@ register_wer( if (sentry__scope_add_observer(scope, observer)) { wer_data->scope = scope; wer_data->observer = observer; + sentry_value_foreach_key_value(scope->tags, wer_sync_tag, wer_data); for (sentry_attachment_t *attachment = scope->attachments; attachment; attachment = attachment->next) { wer_add_attachment(wer_data, attachment); diff --git a/src/sentry_core.c b/src/sentry_core.c index bdd7b78e4e..6caa163923 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -152,6 +152,7 @@ sentry_init(sentry_options_t *options) { // pre-init here, so we can consistently use bailing out to :fail sentry_transport_t *transport = NULL; + bool initial_scope_tags_applied = false; SENTRY__MUTEX_INIT_DYN_ONCE(g_options_lock); // Stop the app hang watchdog before locking options. The watchdog thread @@ -248,6 +249,15 @@ sentry_init(sentry_options_t *options) sentry__init_cached_kernel32_functions(); #endif + if (!sentry_value_is_null(options->initial_scope_tags)) { + sentry_value_t tags = options->initial_scope_tags; + options->initial_scope_tags = sentry_value_new_null(); + SENTRY_WITH_SCOPE_MUT_NO_FLUSH (scope) { + sentry_scope_set_tags(scope, tags); + } + initial_scope_tags_applied = true; + } + // and then we will start the backend, since it requires a valid run sentry_backend_t *backend = options->backend; if (backend && backend->startup_func) { @@ -342,6 +352,9 @@ sentry_init(sentry_options_t *options) sentry__transport_shutdown(transport, 0); } sentry_options_free(options); + if (initial_scope_tags_applied) { + sentry__scope_cleanup(); + } sentry__mutex_unlock(&g_options_lock); return 1; } diff --git a/src/sentry_options.c b/src/sentry_options.c index 5e642e058a..f9ecde8450 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -42,6 +42,7 @@ sentry_options_new(void) return NULL; } opts->database_path = sentry__path_from_str(".sentry-native"); + opts->initial_scope_tags = sentry_value_new_null(); // we assume the DSN to be ASCII only sentry_options_set_dsn(opts, getenv("SENTRY_DSN")); const char *debug = getenv("SENTRY_DEBUG"); @@ -186,6 +187,7 @@ sentry_options_free(sentry_options_t *opts) sentry__path_free(opts->database_path); sentry__path_free(opts->handler_path); sentry__path_free(opts->external_crash_reporter); + sentry_value_decref(opts->initial_scope_tags); sentry_transport_free(opts->transport); sentry__backend_free(opts->backend); sentry__attachments_free(opts->attachments); @@ -198,6 +200,19 @@ sentry_options_free(sentry_options_t *opts) sentry_free(opts); } +void +sentry_options_set_tags(sentry_options_t *opts, sentry_value_t tags) +{ + sentry_value_decref(opts->initial_scope_tags); + if (sentry_value_get_type(tags) == SENTRY_VALUE_TYPE_OBJECT) { + opts->initial_scope_tags = tags; + } else { + SENTRY_WARN("initial tags must be an object"); + sentry_value_decref(tags); + opts->initial_scope_tags = sentry_value_new_null(); + } +} + void sentry_options_set_transport( sentry_options_t *opts, sentry_transport_t *transport) diff --git a/src/sentry_options.h b/src/sentry_options.h index d6169ad6c6..a7f5831bd8 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -35,6 +35,7 @@ struct sentry_options_s { sentry_path_t *database_path; sentry_path_t *handler_path; sentry_path_t *external_crash_reporter; + sentry_value_t initial_scope_tags; sentry_logger_t logger; size_t max_breadcrumbs; bool debug; diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index bc046d054b..64a1a6e7d2 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -97,7 +97,7 @@ def test_crashpad_on_crashed_last_run(cmake): run( tmp_path, "sentry_example", - ["log", "crash"], + [*args, "initial-tags", "crash"], expect_failure=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -125,6 +125,7 @@ def test_crashpad_on_crashed_last_run(cmake): ] assert len(callbacks) == 1 assert len(callbacks[0].partition(b":")[2]) == 36 + assert b"CRASHED_LAST_RUN_INITIAL_TAG:initial-value" in restarted.stdout restarted_again = run( tmp_path, @@ -169,6 +170,27 @@ def test_crashpad_codeview(cmake, httpserver): assert any(identifier) +@pytest.mark.skipif(sys.platform != "win32", reason="fast-fail is Windows-only") +def test_crashpad_initial_tags_fastfail(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"}) + + httpserver.expect_oneshot_request("/api/123456/minidump/").respond_with_data("OK") + + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + ["initial-tags", "crashpad-wait-for-upload", "fastfail"], + expect_failure=True, + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + + assert waiting.result + assert len(httpserver.log) == 1 + attachments = assert_crashpad_upload(httpserver.log[0][0]) + assert attachments.event["tags"]["test.initial-tag"] == "initial-value" + + def _setup_crashpad_proxy_test(cmake, httpserver, proxy): if proxy: proxy_process, port = start_proxy(proxy) diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 040cd6910a..a6ffd2c686 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -89,7 +89,7 @@ def test_native_on_crashed_last_run(cmake, httpserver): run_crash( tmp_path, "sentry_example", - [*args, "crash"], + [*args, "initial-tags", "crash"], env=env, wait_for_daemon=True, stdout=subprocess.PIPE, @@ -126,6 +126,7 @@ def test_native_on_crashed_last_run(cmake, httpserver): if line.startswith(b"CRASHED_LAST_RUN:") ] assert callbacks == [f"CRASHED_LAST_RUN:{event_id}".encode()] + assert b"CRASHED_LAST_RUN_INITIAL_TAG:initial-value" in restarted.stdout assert len(httpserver.log) == 1 assert not list(db_dir.glob("*.run")) assert not list(db_dir.glob("*.run*.lock")) diff --git a/tests/test_integration_wer.py b/tests/test_integration_wer.py index 7cdae44d75..ede04af0d6 100644 --- a/tests/test_integration_wer.py +++ b/tests/test_integration_wer.py @@ -139,12 +139,14 @@ def assert_sentry_event(httpserver, backend, crash_arg): assert httpserver.log[0][0].path == "/api/123456/minidump/" attachments = assert_crashpad_upload(httpserver.log[0][0]) assert attachments.event["event_id"] + assert attachments.event["tags"]["test.initial-tag"] == "initial-value" return attachments.event envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) event = envelope.get_event() assert event is not None assert event["event_id"] + assert event["tags"]["test.initial-tag"] == "initial-value" assert_event_meta(event, integrations=[backend, "wer"]) if backend == "inproc": @@ -262,6 +264,7 @@ def run_wer_crash(cmake, backend, crash_arg, httpserver=None, appx=False): if appx: run_args.append("appx") run_args.append(crash_arg) + run_args.append("initial-tags") if backend == "crashpad": run_args.append("crashpad-wait-for-upload") @@ -363,6 +366,8 @@ def run_wer_crash(cmake, backend, crash_arg, httpserver=None, appx=False): def test_wer_custom_metadata(cmake, backend): report = run_wer_crash(cmake, backend, "crash") + assert "test.initial-tag" in report + assert "initial-value" in report assert "expected-tag" in report assert "some value" in report assert "not-expected-tag" not in report diff --git a/tests/unit/test_options.c b/tests/unit/test_options.c index bf9e8ca10f..2d7b443cfe 100644 --- a/tests/unit/test_options.c +++ b/tests/unit/test_options.c @@ -1,7 +1,116 @@ +#include "sentry_alloc.h" +#include "sentry_backend.h" #include "sentry_options.h" +#include "sentry_scope.h" #include "sentry_testsupport.h" #include +#include + +static int +startup_with_initial_tags( + sentry_backend_t *backend, const sentry_options_t *UNUSED(options)) +{ + bool *found = backend->data; + SENTRY_WITH_SCOPE (scope) { + const char *value = sentry_value_as_string( + sentry_value_get_by_key(scope->tags, "initial")); + *found = value && strcmp(value, "value") == 0; + } + return 0; +} + +static int +startup_failure_with_initial_tags( + sentry_backend_t *backend, const sentry_options_t *options) +{ + startup_with_initial_tags(backend, options); + return 1; +} + +SENTRY_TEST(options_initial_tags_before_backend_startup) +{ + SENTRY_TEST_OPTIONS_NEW(options); + + bool found = false; + sentry_backend_t *backend = SENTRY_MAKE(sentry_backend_t); + TEST_ASSERT(!!backend); + backend->data = &found; + backend->startup_func = startup_with_initial_tags; + sentry_options_set_backend(options, backend); + + sentry_value_t tags = sentry_value_new_object(); + sentry_value_set_by_key(tags, "initial", sentry_value_new_string("value")); + sentry_value_set_by_key(tags, "invalid", sentry_value_new_int32(42)); + sentry_options_set_tags(options, tags); + + sentry_init(options); + TEST_CHECK(found); + + SENTRY_WITH_SCOPE (scope) { + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + scope->tags, "initial")), + "value"); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(scope->tags, "invalid"))); + } + + sentry_close(); +} + +SENTRY_TEST(options_initial_tags_rollback_after_startup_failure) +{ + SENTRY_TEST_OPTIONS_NEW(options); + + bool found = false; + sentry_backend_t *backend = SENTRY_MAKE(sentry_backend_t); + TEST_ASSERT(!!backend); + backend->data = &found; + backend->startup_func = startup_failure_with_initial_tags; + sentry_options_set_backend(options, backend); + + sentry_value_t tags = sentry_value_new_object(); + sentry_value_set_by_key(tags, "initial", sentry_value_new_string("value")); + sentry_options_set_tags(options, tags); + + TEST_CHECK(sentry_init(options) != 0); + TEST_CHECK(found); + + SENTRY_WITH_SCOPE (scope) { + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(scope->tags, "initial"))); + } +} + +SENTRY_TEST(options_initial_tags_replace) +{ + SENTRY_TEST_OPTIONS_NEW(options); + + sentry_value_t first = sentry_value_new_object(); + sentry_value_incref(first); + sentry_options_set_tags(options, first); + TEST_CHECK_INT_EQUAL(sentry_value_refcount(first), 2); + + sentry_value_t second = sentry_value_new_object(); + sentry_value_incref(second); + sentry_options_set_tags(options, second); + TEST_CHECK_INT_EQUAL(sentry_value_refcount(first), 1); + TEST_CHECK_INT_EQUAL(sentry_value_refcount(second), 2); + + sentry_options_set_tags(options, sentry_value_new_list()); + TEST_CHECK(sentry_value_is_null(options->initial_scope_tags)); + TEST_CHECK_INT_EQUAL(sentry_value_refcount(second), 1); + + sentry_value_decref(first); + sentry_value_decref(second); + + sentry_value_t final = sentry_value_new_object(); + sentry_value_incref(final); + sentry_options_set_tags(options, final); + sentry_options_free(options); + TEST_CHECK_INT_EQUAL(sentry_value_refcount(final), 1); + sentry_value_decref(final); +} SENTRY_TEST(options_sdk_name_defaults) { diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 8a719ca9b1..e592902798 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -280,6 +280,9 @@ XX(on_crashed_last_run_cache) XX(options_crash_reporting_mode_clamp) XX(options_crash_reporting_mode_default) XX(options_crash_reporting_mode_set_get) +XX(options_initial_tags_before_backend_startup) +XX(options_initial_tags_replace) +XX(options_initial_tags_rollback_after_startup_failure) XX(options_logger_enabled_when_crashed_default) XX(options_minidump_flags) XX(options_sample_rate) From 6a2766267829e26fd00da02dfc4e77cea5219c3a Mon Sep 17 00:00:00 2001 From: Nathan White Date: Thu, 10 Sep 2026 09:26:34 -0700 Subject: [PATCH 2/5] docs: clarify initial tags option --- include/sentry.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index 458b5bdd7c..e5d3b70d43 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1427,11 +1427,11 @@ SENTRY_API sentry_options_t *sentry_options_new(void); SENTRY_API void sentry_options_free(sentry_options_t *opts); /** - * Sets tags on the initial scope before the crash backend is started. + * Configures tags to add to the initial scope before the crash backend is + * started. * - * The tags must be an object with string values. Non-string values are - * ignored. Calling this function again replaces the previously configured - * initial tags. + * `tags` must be an object. Values that are not strings are ignored. Calling + * this function again replaces the previously configured tags. * * The function takes ownership of `tags`. */ From 60135dfc3140e8083af80f2d3a17ccd1f89d2e4b Mon Sep 17 00:00:00 2001 From: Nathan White Date: Thu, 10 Sep 2026 09:28:04 -0700 Subject: [PATCH 3/5] Update CHANGELOG.md with new features and deprecations --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eebec3715..95162effc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ **Features**: -- Add `sentry_options_set_tags` for configuring tags before the crash backend is started, including out-of-process crash handlers. +- Add `sentry_options_set_tags` for configuring tags before the crash backend is started, including out-of-process crash handlers. ([#2087](https://github.com/getsentry/sentry-native/pull/2087)) - Add `sentry_attachment_from_file/bytes` (and their wide-string variants) for creating attachment values that can be fully configured before they are added. ([#2079](https://github.com/getsentry/sentry-native/pull/2079)) - Add `sentry_add_attachment`, `sentry_scope_add_attachment`, and `sentry_hint_add_attachment` for adding configured attachments to the global scope, a specific scope, or a hint. ([#2079](https://github.com/getsentry/sentry-native/pull/2079)) From 32852b92b8f2e392573c135a219d22123b48f148 Mon Sep 17 00:00:00 2001 From: Nathan White Date: Thu, 10 Sep 2026 16:53:03 -0700 Subject: [PATCH 4/5] test: gate initial tags fast-fail on WER --- tests/test_integration_crashpad.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index 64a1a6e7d2..d93c5c8e53 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -170,7 +170,11 @@ def test_crashpad_codeview(cmake, httpserver): assert any(identifier) -@pytest.mark.skipif(sys.platform != "win32", reason="fast-fail is Windows-only") +@pytest.mark.skipif( + sys.platform != "win32" or bool(os.environ.get("TEST_MINGW")), + reason="fast-fail is only available in MSVC Windows builds", +) +@pytest.mark.with_wer def test_crashpad_initial_tags_fastfail(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"}) From a5d884767ccc896471e9e0b81f0e910e1c8c92a4 Mon Sep 17 00:00:00 2001 From: Nathan White Date: Fri, 11 Sep 2026 09:46:05 -0700 Subject: [PATCH 5/5] feat: generalize initial scope configuration --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 3 +- examples/example.c | 46 +++++++- include/sentry.h | 40 ++++--- src/backends/sentry_backend_crashpad.cpp | 74 +++++++++++-- src/backends/sentry_backend_native.c | 44 ++++++++ src/sentry_core.c | 66 ++++++------ src/sentry_options.c | 15 +-- src/sentry_options.h | 3 +- tests/assertions.py | 14 ++- tests/test_integration_crashpad.py | 27 ++++- tests/test_integration_native.py | 25 ++++- tests/test_integration_wer.py | 4 +- tests/unit/test_options.c | 127 ++++++++++++++--------- tests/unit/tests.inc | 6 +- 15 files changed, 359 insertions(+), 137 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95162effc6..4515040c80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ **Features**: -- Add `sentry_options_set_tags` for configuring tags before the crash backend is started, including out-of-process crash handlers. ([#2087](https://github.com/getsentry/sentry-native/pull/2087)) +- Add `sentry_options_set_initial_scope` for configuring scope data before the crash backend is started, including out-of-process crash handlers. ([#2087](https://github.com/getsentry/sentry-native/pull/2087)) - Add `sentry_attachment_from_file/bytes` (and their wide-string variants) for creating attachment values that can be fully configured before they are added. ([#2079](https://github.com/getsentry/sentry-native/pull/2079)) - Add `sentry_add_attachment`, `sentry_scope_add_attachment`, and `sentry_hint_add_attachment` for adding configured attachments to the global scope, a specific scope, or a hint. ([#2079](https://github.com/getsentry/sentry-native/pull/2079)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8120e04bb1..08f3b73ad8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -195,7 +195,8 @@ The example currently supports the following commands: - `clear-attachments`: Clears all attachments from the global scope. - `capture-user-feedback`: Captures a user feedback event. - `test-logger`: Sets up a test logger for integration tests that outputs in a format the integration tests can parse. -- `initial-tags`: Configures a test tag on the initial scope before SDK initialization. +- `initial-scope`: Configures test data on the initial scope before SDK initialization. +- `initial-scope-payload`: Adds a breadcrumb and attachments to the initial scope test data. - `disable-logger-when-crashed`: Disables logging during crash handling. - `enable-logger-when-crashed`: Explicitly enables logging during crash handling (default behavior). - `test-logger-before-crash`: Outputs marker directly using printf for test parsing before crash. diff --git a/examples/example.c b/examples/example.c index bb398ec1e0..59d13e3228 100644 --- a/examples/example.c +++ b/examples/example.c @@ -175,6 +175,29 @@ on_crash_callback( return event; } +static sentry_value_t create_debug_crumb(const char *message); + +static void +configure_initial_scope(sentry_scope_t *scope, void *user_data) +{ + sentry_scope_set_tag(scope, "test.initial-tag", "initial-value"); + + sentry_value_t context = sentry_value_new_object(); + sentry_value_set_by_key(context, "foo", sentry_value_new_string("bar")); + sentry_scope_set_context(scope, "initial", context); + + sentry_value_t user + = sentry_value_new_user("1", "user", "initial@example.com", NULL); + sentry_scope_set_user(scope, user); + + if (user_data && *(bool *)user_data) { + sentry_scope_add_breadcrumb( + scope, create_debug_crumb("initial scope breadcrumb")); + sentry_scope_attach_file(scope, "CMakeCache.txt"); + sentry_scope_attach_bytes(scope, "\xc0\xff\xee", 3, "bytes.bin"); + } +} + static void on_crashed_last_run_callback(const sentry_envelope_t *envelope, void *user_data) { @@ -189,6 +212,20 @@ on_crashed_last_run_callback(const sentry_envelope_t *envelope, void *user_data) if (initial_tag) { printf("CRASHED_LAST_RUN_INITIAL_TAG:%s\n", initial_tag); } + sentry_value_t contexts = sentry_value_get_by_key(event, "contexts"); + sentry_value_t initial_context + = sentry_value_get_by_key(contexts, "initial"); + const char *context_value = sentry_value_as_string( + sentry_value_get_by_key(initial_context, "foo")); + if (context_value) { + printf("CRASHED_LAST_RUN_INITIAL_CONTEXT:%s\n", context_value); + } + sentry_value_t user = sentry_value_get_by_key(event, "user"); + const char *user_id + = sentry_value_as_string(sentry_value_get_by_key(user, "id")); + if (user_id) { + printf("CRASHED_LAST_RUN_INITIAL_USER:%s\n", user_id); + } fflush(stdout); } @@ -845,11 +882,10 @@ main(int argc, char **argv) sentry_options_set_crashpad_wait_for_upload(options, true); } - if (has_arg(argc, argv, "initial-tags")) { - sentry_value_t tags = sentry_value_new_object(); - sentry_value_set_by_key( - tags, "test.initial-tag", sentry_value_new_string("initial-value")); - sentry_options_set_tags(options, tags); + bool initial_scope_payload = has_arg(argc, argv, "initial-scope-payload"); + if (has_arg(argc, argv, "initial-scope")) { + sentry_options_set_initial_scope( + options, configure_initial_scope, &initial_scope_payload); } if (has_arg(argc, argv, "test-logger")) { diff --git a/include/sentry.h b/include/sentry.h index e5d3b70d43..c820520f60 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -918,6 +918,14 @@ SENTRY_API void sentry_capture_envelope(sentry_envelope_t *envelope); struct sentry_options_s; typedef struct sentry_options_s sentry_options_t; +/** + * A Sentry Scope. + * + * See https://develop.sentry.dev/sdk/telemetry/scopes/ + */ +struct sentry_scope_s; +typedef struct sentry_scope_s sentry_scope_t; + /** * This represents an interface for user-defined transports. * @@ -1427,16 +1435,26 @@ SENTRY_API sentry_options_t *sentry_options_new(void); SENTRY_API void sentry_options_free(sentry_options_t *opts); /** - * Configures tags to add to the initial scope before the crash backend is - * started. + * Type of the callback used to configure the initial scope. * - * `tags` must be an object. Values that are not strings are ignored. Calling - * this function again replaces the previously configured tags. + * The callback is invoked synchronously once during `sentry_init`, after + * option-derived defaults are applied and before the crash backend is started. + * The scope is borrowed. Configure it with `sentry_scope_*` functions rather + * than global scope functions such as `sentry_set_tag`. + */ +typedef void (*sentry_initial_scope_function_t)( + sentry_scope_t *scope, void *user_data); + +/** + * Sets the callback used to configure the initial scope. * - * The function takes ownership of `tags`. + * Calling this function again replaces the previously configured callback. + * Passing `NULL` for `func` disables initial scope configuration. The SDK does + * not take ownership of `user_data`, which must remain valid until the callback + * is invoked. */ -SENTRY_API void sentry_options_set_tags( - sentry_options_t *opts, sentry_value_t tags); +SENTRY_API void sentry_options_set_initial_scope(sentry_options_t *opts, + sentry_initial_scope_function_t func, void *user_data); /** * Sets a transport. @@ -2506,14 +2524,6 @@ SENTRY_API sentry_user_consent_t sentry_user_consent_get(void); */ SENTRY_API int sentry_user_consent_is_required(void); -/** - * A sentry Scope. - * - * See https://develop.sentry.dev/sdk/telemetry/scopes/ - */ -struct sentry_scope_s; -typedef struct sentry_scope_s sentry_scope_t; - /** * Creates a local scope. * diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 4d2e18f49a..6f32b5020b 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -149,6 +149,9 @@ typedef struct { sentry_uuid_t crash_event_id; } crashpad_state_t; +static void crashpad_backend_add_breadcrumb(sentry_backend_t *backend, + sentry_value_t breadcrumb, const sentry_options_t *options); + /** * Correctly destruct C++ members of the crashpad state. */ @@ -257,9 +260,24 @@ static int append_attachment(crashpad_state_t *state, const base::FilePath &path, const char *data, size_t size) { - if (path.empty() || !state || !state->client) { + if (path.empty() || !state) { return 1; } + if (!state->client) { +#ifdef SENTRY_PLATFORM_WINDOWS + sentry_path_t *sentry_path + = sentry__path_from_wstr(path.value().c_str()); +#else + sentry_path_t *sentry_path + = sentry__path_from_str(path.value().c_str()); +#endif + if (!sentry_path) { + return 1; + } + int rv = sentry__path_append_buffer(sentry_path, data, size); + sentry__path_free(sentry_path); + return rv; + } return state->client->AppendAttachment( path, base::as_bytes(base::make_span(data, size))) ? 0 @@ -377,6 +395,42 @@ flush_scope_attachments(crashpad_state_t *data, const sentry_options_t *options) } } +static sentry_path_t * +prepare_initial_attachment( + sentry_attachment_t *attachment, const sentry_path_t *run_path) +{ + size_t bytes_len = 0; + const char *bytes = sentry__attachment_get_bytes(attachment, &bytes_len); + if (bytes && !attachment->path) { + attachment->path = sentry__path_unique( + run_path, sentry__attachment_get_filename(attachment)); + } + + sentry_path_t *path = sentry__attachment_make_path(attachment); + if (bytes + && (!path || sentry__path_write_buffer(path, bytes, bytes_len) != 0)) { + SENTRY_WARN("failed to prepare initial scope attachment"); + } + return path; +} + +static void +preload_scope_breadcrumbs( + sentry_backend_t *backend, const sentry_options_t *options) +{ + sentry_value_t breadcrumbs = sentry_value_new_null(); + SENTRY_WITH_SCOPE (scope) { + breadcrumbs = sentry__ringbuffer_to_list(scope->breadcrumbs); + } + + size_t breadcrumb_count = sentry_value_get_length(breadcrumbs); + for (size_t i = 0; i < breadcrumb_count; i++) { + crashpad_backend_add_breadcrumb( + backend, sentry_value_get_by_index(breadcrumbs, i), options); + } + sentry_value_decref(breadcrumbs); +} + // 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 @@ -875,13 +929,16 @@ crashpad_backend_startup( std::map annotations; std::vector attachments; - // register attachments - for (sentry_attachment_t *attachment = options->attachments; attachment; - attachment = attachment->next) { - sentry_path_t *path = sentry__attachment_make_path(attachment); - if (path) { - attachments.emplace_back(SENTRY_PATH_PLATFORM_STR(path)); - sentry__path_free(path); + // register attachments from the finalized initial scope + SENTRY_WITH_SCOPE (scope) { + for (sentry_attachment_t *attachment = scope->attachments; attachment; + attachment = attachment->next) { + sentry_path_t *path + = prepare_initial_attachment(attachment, current_run_folder); + if (path) { + attachments.emplace_back(SENTRY_PATH_PLATFORM_STR(path)); + sentry__path_free(path); + } } } @@ -933,6 +990,7 @@ crashpad_backend_startup( } // Persist the preloaded scope before Crashpad starts handling crashes. + preload_scope_breadcrumbs(backend, options); flush_scope_attachments(data, options); std::vector arguments { "--no-rate-limit" }; diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 5d84acb0c9..40dd79f56a 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -215,6 +215,49 @@ typedef struct { static void native_backend_flush_scope( sentry_backend_t *backend, const sentry_options_t *options); +static void native_backend_add_breadcrumb(sentry_backend_t *backend, + sentry_value_t breadcrumb, const sentry_options_t *options); + +static void +native_backend_prepare_initial_attachment( + sentry_attachment_t *attachment, const sentry_path_t *run_path) +{ + size_t bytes_len = 0; + const char *bytes = sentry__attachment_get_bytes(attachment, &bytes_len); + if (!bytes) { + return; + } + + if (!attachment->path) { + attachment->path = sentry__path_unique( + run_path, sentry__attachment_get_filename(attachment)); + } + if (!attachment->path + || sentry__path_write_buffer(attachment->path, bytes, bytes_len) != 0) { + SENTRY_WARN("failed to prepare initial scope attachment"); + } +} + +static void +native_backend_preload_scope(sentry_backend_t *backend, + const sentry_options_t *options, const sentry_path_t *run_path) +{ + sentry_value_t breadcrumbs = sentry_value_new_null(); + SENTRY_WITH_SCOPE (scope) { + for (sentry_attachment_t *attachment = scope->attachments; attachment; + attachment = attachment->next) { + native_backend_prepare_initial_attachment(attachment, run_path); + } + breadcrumbs = sentry__ringbuffer_to_list(scope->breadcrumbs); + } + + size_t breadcrumb_count = sentry_value_get_length(breadcrumbs); + for (size_t i = 0; i < breadcrumb_count; i++) { + native_backend_add_breadcrumb( + backend, sentry_value_get_by_index(breadcrumbs, i), options); + } + sentry_value_decref(breadcrumbs); +} static bool native_backend_process_old_run(sentry_backend_t *backend, @@ -814,6 +857,7 @@ native_backend_startup( #endif // Persist the preloaded scope before any crash handler becomes active. + native_backend_preload_scope(backend, options, run_path); native_backend_flush_scope(backend, options); // Install crash handlers (signal handlers on Linux/macOS, Mach exception diff --git a/src/sentry_core.c b/src/sentry_core.c index 6caa163923..1d8ed42544 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -152,7 +152,7 @@ sentry_init(sentry_options_t *options) { // pre-init here, so we can consistently use bailing out to :fail sentry_transport_t *transport = NULL; - bool initial_scope_tags_applied = false; + bool initial_scope_prepared = false; SENTRY__MUTEX_INIT_DYN_ONCE(g_options_lock); // Stop the app hang watchdog before locking options. The watchdog thread @@ -249,39 +249,7 @@ sentry_init(sentry_options_t *options) sentry__init_cached_kernel32_functions(); #endif - if (!sentry_value_is_null(options->initial_scope_tags)) { - sentry_value_t tags = options->initial_scope_tags; - options->initial_scope_tags = sentry_value_new_null(); - SENTRY_WITH_SCOPE_MUT_NO_FLUSH (scope) { - sentry_scope_set_tags(scope, tags); - } - initial_scope_tags_applied = true; - } - - // and then we will start the backend, since it requires a valid run - sentry_backend_t *backend = options->backend; - if (backend && backend->startup_func) { - SENTRY_DEBUG("starting backend"); - if (backend->startup_func(backend, options) != 0) { - SENTRY_WARN("failed to initialize backend"); - goto fail; - } - } - if (backend && backend->get_last_crash_func) { - last_crash = backend->get_last_crash_func(backend); - } - - g_last_crash = sentry__has_crash_marker(options); - if (g_last_crash && !options->retain_crash_marker) { - sentry__clear_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. - SENTRY_WITH_SCOPE_MUT (scope) { + SENTRY_WITH_SCOPE_MUT_NO_FLUSH (scope) { if (options->sdk_name) { sentry_value_t sdk_name = sentry_value_new_string(options->sdk_name); @@ -311,8 +279,36 @@ sentry_init(sentry_options_t *options) sentry__ringbuffer_set_max_size( scope->breadcrumbs, options->max_breadcrumbs); + if (options->initial_scope_func) { + options->initial_scope_func(scope, options->initial_scope_data); + } + sentry__scope_update_dsc(scope, options); + } + initial_scope_prepared = true; + + // and then we will start the backend, since it requires a valid run + sentry_backend_t *backend = options->backend; + if (backend && backend->startup_func) { + SENTRY_DEBUG("starting backend"); + if (backend->startup_func(backend, options) != 0) { + SENTRY_WARN("failed to initialize backend"); + goto fail; + } + } + if (backend && backend->get_last_crash_func) { + last_crash = backend->get_last_crash_func(backend); + } + + g_last_crash = sentry__has_crash_marker(options); + if (g_last_crash && !options->retain_crash_marker) { + sentry__clear_crash_marker(options); + } + g_options = options; + // *after* setting the global options, register integrations and trigger a + // scope and consent flush, since at least crashpad needs that. + SENTRY_WITH_SCOPE_MUT (scope) { register_integrations(scope, options); } if (backend && backend->user_consent_changed_func) { @@ -352,7 +348,7 @@ sentry_init(sentry_options_t *options) sentry__transport_shutdown(transport, 0); } sentry_options_free(options); - if (initial_scope_tags_applied) { + if (initial_scope_prepared) { sentry__scope_cleanup(); } sentry__mutex_unlock(&g_options_lock); diff --git a/src/sentry_options.c b/src/sentry_options.c index f9ecde8450..d1671a715b 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -42,7 +42,6 @@ sentry_options_new(void) return NULL; } opts->database_path = sentry__path_from_str(".sentry-native"); - opts->initial_scope_tags = sentry_value_new_null(); // we assume the DSN to be ASCII only sentry_options_set_dsn(opts, getenv("SENTRY_DSN")); const char *debug = getenv("SENTRY_DEBUG"); @@ -187,7 +186,6 @@ sentry_options_free(sentry_options_t *opts) sentry__path_free(opts->database_path); sentry__path_free(opts->handler_path); sentry__path_free(opts->external_crash_reporter); - sentry_value_decref(opts->initial_scope_tags); sentry_transport_free(opts->transport); sentry__backend_free(opts->backend); sentry__attachments_free(opts->attachments); @@ -201,16 +199,11 @@ sentry_options_free(sentry_options_t *opts) } void -sentry_options_set_tags(sentry_options_t *opts, sentry_value_t tags) +sentry_options_set_initial_scope(sentry_options_t *opts, + sentry_initial_scope_function_t func, void *user_data) { - sentry_value_decref(opts->initial_scope_tags); - if (sentry_value_get_type(tags) == SENTRY_VALUE_TYPE_OBJECT) { - opts->initial_scope_tags = tags; - } else { - SENTRY_WARN("initial tags must be an object"); - sentry_value_decref(tags); - opts->initial_scope_tags = sentry_value_new_null(); - } + opts->initial_scope_func = func; + opts->initial_scope_data = user_data; } void diff --git a/src/sentry_options.h b/src/sentry_options.h index a7f5831bd8..3c3f92039e 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -35,7 +35,8 @@ struct sentry_options_s { sentry_path_t *database_path; sentry_path_t *handler_path; sentry_path_t *external_crash_reporter; - sentry_value_t initial_scope_tags; + sentry_initial_scope_function_t initial_scope_func; + void *initial_scope_data; sentry_logger_t logger; size_t max_breadcrumbs; bool debug; diff --git a/tests/assertions.py b/tests/assertions.py index 0471a105dd..d67943c518 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -596,13 +596,21 @@ def assert_overflowing_breadcrumb(attachments): assert_breadcrumb_inner(attachments.breadcrumb1) -def assert_crashpad_upload(req, expect_attachment=False, expect_view_hierarchy=False): +def assert_crashpad_upload( + req, + expect_attachment=False, + expect_view_hierarchy=False, + expect_breadcrumbs=True, + expect_default_scope=True, +): multipart = gzip.decompress(req.get_data()) msg = email.message_from_bytes(bytes(str(req.headers), encoding="utf8") + multipart) attachments = _load_crashpad_attachments(msg) - assert_overflowing_breadcrumb(attachments) - assert_event_meta(attachments.event, integrations=["crashpad"]) + if expect_breadcrumbs: + assert_overflowing_breadcrumb(attachments) + if expect_default_scope: + assert_event_meta(attachments.event, integrations=["crashpad"]) if expect_attachment: assert attachments.cmake_cache > 0 assert attachments.bytes_bin == b"\xc0\xff\xee" diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index d93c5c8e53..a356bc3778 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -97,7 +97,7 @@ def test_crashpad_on_crashed_last_run(cmake): run( tmp_path, "sentry_example", - [*args, "initial-tags", "crash"], + [*args, "initial-scope", "no-setup", "crash"], expect_failure=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -126,6 +126,8 @@ def test_crashpad_on_crashed_last_run(cmake): assert len(callbacks) == 1 assert len(callbacks[0].partition(b":")[2]) == 36 assert b"CRASHED_LAST_RUN_INITIAL_TAG:initial-value" in restarted.stdout + assert b"CRASHED_LAST_RUN_INITIAL_CONTEXT:bar" in restarted.stdout + assert b"CRASHED_LAST_RUN_INITIAL_USER:1" in restarted.stdout restarted_again = run( tmp_path, @@ -175,7 +177,7 @@ def test_crashpad_codeview(cmake, httpserver): reason="fast-fail is only available in MSVC Windows builds", ) @pytest.mark.with_wer -def test_crashpad_initial_tags_fastfail(cmake, httpserver): +def test_crashpad_initial_scope_fastfail(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"}) httpserver.expect_oneshot_request("/api/123456/minidump/").respond_with_data("OK") @@ -184,15 +186,32 @@ def test_crashpad_initial_tags_fastfail(cmake, httpserver): run( tmp_path, "sentry_example", - ["initial-tags", "crashpad-wait-for-upload", "fastfail"], + [ + "initial-scope", + "initial-scope-payload", + "crashpad-wait-for-upload", + "no-setup", + "fastfail", + ], expect_failure=True, env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), ) assert waiting.result assert len(httpserver.log) == 1 - attachments = assert_crashpad_upload(httpserver.log[0][0]) + attachments = assert_crashpad_upload( + httpserver.log[0][0], + expect_attachment=True, + expect_breadcrumbs=False, + expect_default_scope=False, + ) + assert any( + breadcrumb.get("message") == "initial scope breadcrumb" + for breadcrumb in attachments.breadcrumb1 + ) assert attachments.event["tags"]["test.initial-tag"] == "initial-value" + assert attachments.event["contexts"]["initial"]["foo"] == "bar" + assert attachments.event["user"]["id"] == "1" def _setup_crashpad_proxy_test(cmake, httpserver, proxy): diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index a6ffd2c686..4d01d38987 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -89,7 +89,13 @@ def test_native_on_crashed_last_run(cmake, httpserver): run_crash( tmp_path, "sentry_example", - [*args, "initial-tags", "crash"], + [ + *args, + "initial-scope", + "initial-scope-payload", + "no-setup", + "crash", + ], env=env, wait_for_daemon=True, stdout=subprocess.PIPE, @@ -100,6 +106,21 @@ def test_native_on_crashed_last_run(cmake, httpserver): crash_envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) assert_native_crash(crash_envelope) + crash_event = crash_envelope.get_event() + assert any( + breadcrumb.get("message") == "initial scope breadcrumb" + for breadcrumb in crash_event["breadcrumbs"] + ) + assert any( + item.headers.get("filename") == "CMakeCache.txt" + and b"This is the CMakeCache file." in item.payload.bytes + for item in crash_envelope + ) + assert any( + item.headers.get("filename") == "bytes.bin" + and item.payload.bytes == b"\xc0\xff\xee" + for item in crash_envelope + ) event_id = crash_envelope.headers["event_id"] assert_crash_timestamp(has_files, tmp_path) @@ -127,6 +148,8 @@ def test_native_on_crashed_last_run(cmake, httpserver): ] assert callbacks == [f"CRASHED_LAST_RUN:{event_id}".encode()] assert b"CRASHED_LAST_RUN_INITIAL_TAG:initial-value" in restarted.stdout + assert b"CRASHED_LAST_RUN_INITIAL_CONTEXT:bar" in restarted.stdout + assert b"CRASHED_LAST_RUN_INITIAL_USER:1" in restarted.stdout assert len(httpserver.log) == 1 assert not list(db_dir.glob("*.run")) assert not list(db_dir.glob("*.run*.lock")) diff --git a/tests/test_integration_wer.py b/tests/test_integration_wer.py index ede04af0d6..cd0f0967cf 100644 --- a/tests/test_integration_wer.py +++ b/tests/test_integration_wer.py @@ -140,6 +140,7 @@ def assert_sentry_event(httpserver, backend, crash_arg): attachments = assert_crashpad_upload(httpserver.log[0][0]) assert attachments.event["event_id"] assert attachments.event["tags"]["test.initial-tag"] == "initial-value" + assert attachments.event["contexts"]["initial"]["foo"] == "bar" return attachments.event envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) @@ -147,6 +148,7 @@ def assert_sentry_event(httpserver, backend, crash_arg): assert event is not None assert event["event_id"] assert event["tags"]["test.initial-tag"] == "initial-value" + assert event["contexts"]["initial"]["foo"] == "bar" assert_event_meta(event, integrations=[backend, "wer"]) if backend == "inproc": @@ -264,7 +266,7 @@ def run_wer_crash(cmake, backend, crash_arg, httpserver=None, appx=False): if appx: run_args.append("appx") run_args.append(crash_arg) - run_args.append("initial-tags") + run_args.append("initial-scope") if backend == "crashpad": run_args.append("crashpad-wait-for-upload") diff --git a/tests/unit/test_options.c b/tests/unit/test_options.c index 2d7b443cfe..2ecdfbbdd8 100644 --- a/tests/unit/test_options.c +++ b/tests/unit/test_options.c @@ -7,74 +7,117 @@ #include #include +typedef struct { + bool configured; + bool defaults_found; + bool found; +} initial_scope_state_t; + +static void +configure_initial_scope(sentry_scope_t *scope, void *user_data) +{ + initial_scope_state_t *state = user_data; + state->configured = true; + state->defaults_found = scope->release + && strcmp(scope->release, "option-release") == 0 && scope->environment + && strcmp(scope->environment, "option-environment") == 0 + && scope->breadcrumbs->max_size == 17; + + sentry_scope_set_tag(scope, "initial", "value"); + sentry_scope_set_environment(scope, "initial-environment"); + + sentry_value_t context = sentry_value_new_object(); + sentry_value_set_by_key(context, "foo", sentry_value_new_string("bar")); + sentry_scope_set_context(scope, "initial", context); + + sentry_value_t user + = sentry_value_new_user("1", "user", "initial@example.com", NULL); + sentry_scope_set_user(scope, user); +} + static int -startup_with_initial_tags( +startup_with_initial_scope( sentry_backend_t *backend, const sentry_options_t *UNUSED(options)) { - bool *found = backend->data; + initial_scope_state_t *state = backend->data; SENTRY_WITH_SCOPE (scope) { - const char *value = sentry_value_as_string( + const char *tag = sentry_value_as_string( sentry_value_get_by_key(scope->tags, "initial")); - *found = value && strcmp(value, "value") == 0; + sentry_value_t context + = sentry_value_get_by_key(scope->contexts, "initial"); + const char *context_value + = sentry_value_as_string(sentry_value_get_by_key(context, "foo")); + const char *user_id = sentry_value_as_string( + sentry_value_get_by_key(scope->user, "id")); + state->found = state->configured && state->defaults_found && tag + && strcmp(tag, "value") == 0 && scope->environment + && strcmp(scope->environment, "initial-environment") == 0 + && context_value && strcmp(context_value, "bar") == 0 && user_id + && strcmp(user_id, "1") == 0; } return 0; } static int -startup_failure_with_initial_tags( +startup_failure_with_initial_scope( sentry_backend_t *backend, const sentry_options_t *options) { - startup_with_initial_tags(backend, options); + startup_with_initial_scope(backend, options); return 1; } -SENTRY_TEST(options_initial_tags_before_backend_startup) +static void +mark_initial_scope(sentry_scope_t *UNUSED(scope), void *user_data) +{ + *(bool *)user_data = true; +} + +SENTRY_TEST(options_initial_scope_before_backend_startup) { SENTRY_TEST_OPTIONS_NEW(options); - bool found = false; + initial_scope_state_t state = { 0 }; sentry_backend_t *backend = SENTRY_MAKE(sentry_backend_t); TEST_ASSERT(!!backend); - backend->data = &found; - backend->startup_func = startup_with_initial_tags; + backend->data = &state; + backend->startup_func = startup_with_initial_scope; sentry_options_set_backend(options, backend); - sentry_value_t tags = sentry_value_new_object(); - sentry_value_set_by_key(tags, "initial", sentry_value_new_string("value")); - sentry_value_set_by_key(tags, "invalid", sentry_value_new_int32(42)); - sentry_options_set_tags(options, tags); + sentry_options_set_release(options, "option-release"); + sentry_options_set_environment(options, "option-environment"); + sentry_options_set_max_breadcrumbs(options, 17); + sentry_options_set_initial_scope(options, configure_initial_scope, &state); sentry_init(options); - TEST_CHECK(found); + TEST_CHECK(state.found); SENTRY_WITH_SCOPE (scope) { TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( scope->tags, "initial")), "value"); - TEST_CHECK(sentry_value_is_null( - sentry_value_get_by_key(scope->tags, "invalid"))); } sentry_close(); } -SENTRY_TEST(options_initial_tags_rollback_after_startup_failure) +SENTRY_TEST(options_initial_scope_rollback_after_startup_failure) { SENTRY_TEST_OPTIONS_NEW(options); - bool found = false; + initial_scope_state_t state = { 0 }; sentry_backend_t *backend = SENTRY_MAKE(sentry_backend_t); TEST_ASSERT(!!backend); - backend->data = &found; - backend->startup_func = startup_failure_with_initial_tags; + backend->data = &state; + backend->startup_func = startup_failure_with_initial_scope; sentry_options_set_backend(options, backend); - sentry_value_t tags = sentry_value_new_object(); - sentry_value_set_by_key(tags, "initial", sentry_value_new_string("value")); - sentry_options_set_tags(options, tags); + sentry_options_set_release(options, "option-release"); + sentry_options_set_environment(options, "option-environment"); + sentry_options_set_max_breadcrumbs(options, 17); + sentry_options_set_initial_scope(options, configure_initial_scope, &state); TEST_CHECK(sentry_init(options) != 0); - TEST_CHECK(found); + TEST_CHECK(state.found); SENTRY_WITH_SCOPE (scope) { TEST_CHECK(sentry_value_is_null( @@ -82,34 +125,22 @@ SENTRY_TEST(options_initial_tags_rollback_after_startup_failure) } } -SENTRY_TEST(options_initial_tags_replace) +SENTRY_TEST(options_initial_scope_replace) { SENTRY_TEST_OPTIONS_NEW(options); - sentry_value_t first = sentry_value_new_object(); - sentry_value_incref(first); - sentry_options_set_tags(options, first); - TEST_CHECK_INT_EQUAL(sentry_value_refcount(first), 2); - - sentry_value_t second = sentry_value_new_object(); - sentry_value_incref(second); - sentry_options_set_tags(options, second); - TEST_CHECK_INT_EQUAL(sentry_value_refcount(first), 1); - TEST_CHECK_INT_EQUAL(sentry_value_refcount(second), 2); - - sentry_options_set_tags(options, sentry_value_new_list()); - TEST_CHECK(sentry_value_is_null(options->initial_scope_tags)); - TEST_CHECK_INT_EQUAL(sentry_value_refcount(second), 1); + bool first_called = false; + bool second_called = false; + sentry_options_set_initial_scope( + options, mark_initial_scope, &first_called); + sentry_options_set_initial_scope( + options, mark_initial_scope, &second_called); - sentry_value_decref(first); - sentry_value_decref(second); + TEST_CHECK(sentry_init(options) == 0); + TEST_CHECK(!first_called); + TEST_CHECK(second_called); - sentry_value_t final = sentry_value_new_object(); - sentry_value_incref(final); - sentry_options_set_tags(options, final); - sentry_options_free(options); - TEST_CHECK_INT_EQUAL(sentry_value_refcount(final), 1); - sentry_value_decref(final); + sentry_close(); } SENTRY_TEST(options_sdk_name_defaults) diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index e592902798..692fa21a65 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -280,9 +280,9 @@ XX(on_crashed_last_run_cache) XX(options_crash_reporting_mode_clamp) XX(options_crash_reporting_mode_default) XX(options_crash_reporting_mode_set_get) -XX(options_initial_tags_before_backend_startup) -XX(options_initial_tags_replace) -XX(options_initial_tags_rollback_after_startup_failure) +XX(options_initial_scope_before_backend_startup) +XX(options_initial_scope_replace) +XX(options_initial_scope_rollback_after_startup_failure) XX(options_logger_enabled_when_crashed_default) XX(options_minidump_flags) XX(options_sample_rate)