diff --git a/CHANGELOG.md b/CHANGELOG.md index 692fc5ee8..702297a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ **Features**: +- 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. These functions consume and freeze the attachment value. ([#2079](https://github.com/getsentry/sentry-native/pull/2079), [#1974](https://github.com/getsentry/sentry-native/pull/1974)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a2665c05a..08f3b73ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -195,6 +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-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 b3b44f61e..5a16a1cfa 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) { @@ -182,6 +205,27 @@ 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); + } + 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); } @@ -838,6 +882,12 @@ main(int argc, char **argv) sentry_options_set_crashpad_wait_for_upload(options, true); } + 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")) { // 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 9a5b2442b..7ee3d72c7 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. * @@ -1426,6 +1434,28 @@ SENTRY_API sentry_options_t *sentry_options_new(void); */ SENTRY_API void sentry_options_free(sentry_options_t *opts); +/** + * Type of the callback used to configure the initial scope. + * + * 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. + * + * 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_initial_scope(sentry_options_t *opts, + sentry_initial_scope_function_t func, void *user_data); + /** * Sets a transport. */ @@ -2494,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 83d99961b..179f9b572 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -150,6 +150,11 @@ 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); +static sentry_path_t *make_attachment_path( + const sentry_path_t *run_path, sentry_value_t attachment); + /** * Correctly destruct C++ members of the crashpad state. */ @@ -230,9 +235,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 @@ -243,9 +263,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 @@ -347,6 +382,63 @@ 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); + } +} + +static sentry_path_t * +prepare_initial_attachment( + sentry_value_t attachment, const sentry_path_t *run_path) +{ + size_t bytes_len = 0; + const char *bytes = sentry__attachment_get_bytes(attachment, &bytes_len); + sentry_path_t *path = make_attachment_path(run_path, attachment); + if (!path) { + return nullptr; + } + if (bytes) { + sentry_path_t *dir = sentry__path_dir(path); + int rv = dir ? sentry__path_create_dir_all(dir) : 1; + sentry__path_free(dir); + if (rv != 0 || sentry__path_write_buffer(path, bytes, bytes_len) != 0) { + SENTRY_WARN("failed to prepare initial scope attachment"); + sentry__path_remove(path); + sentry__path_free(path); + return nullptr; + } + } + 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 @@ -372,19 +464,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 } @@ -859,15 +939,18 @@ crashpad_backend_startup( std::map annotations; std::vector attachments; - // register attachments - size_t num_attachments = sentry_value_get_length(options->attachments); - for (size_t i = 0; i < num_attachments; i++) { - sentry_value_t attachment - = sentry_value_get_by_index(options->attachments, i); - 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) { + size_t num_attachments = sentry_value_get_length(scope->attachments); + for (size_t i = 0; i < num_attachments; i++) { + sentry_value_t attachment + = sentry_value_get_by_index(scope->attachments, i); + 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); + } } } @@ -918,6 +1001,10 @@ 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" }; 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 19b6fb6eb..5be667c35 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -213,6 +213,35 @@ 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 void native_backend_add_breadcrumb(sentry_backend_t *backend, + sentry_value_t breadcrumb, const sentry_options_t *options); +static void native_backend_add_attachment(sentry_backend_t *backend, + sentry_value_t attachment, const sentry_options_t *options); + +static void +native_backend_preload_scope( + sentry_backend_t *backend, const sentry_options_t *options) +{ + sentry_value_t breadcrumbs = sentry_value_new_null(); + SENTRY_WITH_SCOPE (scope) { + size_t attachment_count = sentry_value_get_length(scope->attachments); + for (size_t i = 0; i < attachment_count; i++) { + native_backend_add_attachment(backend, + sentry_value_get_by_index(scope->attachments, i), options); + } + 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, const sentry_options_t *options, const sentry_path_t *run_path) @@ -810,6 +839,10 @@ native_backend_startup( } #endif + // Persist the preloaded scope before any crash handler becomes active. + native_backend_preload_scope(backend, options); + 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 23f9af316..62342d6d9 100644 --- a/src/integrations/sentry_integration_wer.c +++ b/src/integrations/sentry_integration_wer.c @@ -131,6 +131,15 @@ wer_attachment_path(sentry_value_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_value_t attachment) { @@ -239,6 +248,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); size_t len = sentry_value_get_length(scope->attachments); for (size_t i = 0; i < len; i++) { wer_add_attachment( diff --git a/src/sentry_core.c b/src/sentry_core.c index ab9f3f256..ae85084c8 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_prepared = false; SENTRY__MUTEX_INIT_DYN_ONCE(g_options_lock); // Stop the app hang watchdog before locking options. The watchdog thread @@ -248,30 +249,7 @@ sentry_init(sentry_options_t *options) sentry__init_cached_kernel32_functions(); #endif - // 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); @@ -302,8 +280,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) { @@ -343,6 +349,9 @@ sentry_init(sentry_options_t *options) sentry__transport_shutdown(transport, 0); } sentry_options_free(options); + if (initial_scope_prepared) { + sentry__scope_cleanup(); + } sentry__mutex_unlock(&g_options_lock); return 1; } diff --git a/src/sentry_options.c b/src/sentry_options.c index cf0c35eeb..3cac39b3e 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -199,6 +199,14 @@ sentry_options_free(sentry_options_t *opts) sentry_free(opts); } +void +sentry_options_set_initial_scope(sentry_options_t *opts, + sentry_initial_scope_function_t func, void *user_data) +{ + opts->initial_scope_func = func; + opts->initial_scope_data = user_data; +} + 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 062e4ba31..43e58b60a 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -35,6 +35,8 @@ struct sentry_options_s { sentry_path_t *database_path; sentry_path_t *handler_path; sentry_path_t *external_crash_reporter; + 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 0471a105d..d67943c51 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 bc046d054..a356bc377 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-scope", "no-setup", "crash"], expect_failure=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -125,6 +125,9 @@ 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, @@ -169,6 +172,48 @@ def test_crashpad_codeview(cmake, httpserver): assert any(identifier) +@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_scope_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-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], + 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): if proxy: proxy_process, port = start_proxy(proxy) diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index affcde75e..c06352b05 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, "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) @@ -126,6 +147,9 @@ 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 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 7cdae44d7..cd0f0967c 100644 --- a/tests/test_integration_wer.py +++ b/tests/test_integration_wer.py @@ -139,12 +139,16 @@ 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" + assert attachments.event["contexts"]["initial"]["foo"] == "bar" 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["contexts"]["initial"]["foo"] == "bar" assert_event_meta(event, integrations=[backend, "wer"]) if backend == "inproc": @@ -262,6 +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-scope") if backend == "crashpad": run_args.append("crashpad-wait-for-upload") @@ -363,6 +368,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 bf9e8ca10..2ecdfbbdd 100644 --- a/tests/unit/test_options.c +++ b/tests/unit/test_options.c @@ -1,7 +1,147 @@ +#include "sentry_alloc.h" +#include "sentry_backend.h" #include "sentry_options.h" +#include "sentry_scope.h" #include "sentry_testsupport.h" #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_scope( + sentry_backend_t *backend, const sentry_options_t *UNUSED(options)) +{ + initial_scope_state_t *state = backend->data; + SENTRY_WITH_SCOPE (scope) { + const char *tag = sentry_value_as_string( + sentry_value_get_by_key(scope->tags, "initial")); + 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_scope( + sentry_backend_t *backend, const sentry_options_t *options) +{ + startup_with_initial_scope(backend, options); + return 1; +} + +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); + + initial_scope_state_t state = { 0 }; + sentry_backend_t *backend = SENTRY_MAKE(sentry_backend_t); + TEST_ASSERT(!!backend); + backend->data = &state; + backend->startup_func = startup_with_initial_scope; + sentry_options_set_backend(options, backend); + + 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(state.found); + + SENTRY_WITH_SCOPE (scope) { + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + scope->tags, "initial")), + "value"); + } + + sentry_close(); +} + +SENTRY_TEST(options_initial_scope_rollback_after_startup_failure) +{ + SENTRY_TEST_OPTIONS_NEW(options); + + initial_scope_state_t state = { 0 }; + sentry_backend_t *backend = SENTRY_MAKE(sentry_backend_t); + TEST_ASSERT(!!backend); + backend->data = &state; + backend->startup_func = startup_failure_with_initial_scope; + sentry_options_set_backend(options, backend); + + 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(state.found); + + SENTRY_WITH_SCOPE (scope) { + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(scope->tags, "initial"))); + } +} + +SENTRY_TEST(options_initial_scope_replace) +{ + SENTRY_TEST_OPTIONS_NEW(options); + + 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); + + TEST_CHECK(sentry_init(options) == 0); + TEST_CHECK(!first_called); + TEST_CHECK(second_called); + + sentry_close(); +} SENTRY_TEST(options_sdk_name_defaults) { diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 4565226d3..6a1e9ad3c 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -282,6 +282,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_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)