From 3c6fd49203bd5da3206f55cd83550f82f85c7773 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 29 May 2026 16:39:40 +0200 Subject: [PATCH 1/6] refactor --- src/backends/native/sentry_crash_daemon.c | 26 ++-- src/backends/sentry_backend_native.c | 147 +++++++++------------- 2 files changed, 74 insertions(+), 99 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index be660d4326..ff42856b09 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -2119,14 +2119,21 @@ build_stacktrace_from_ctx(const sentry_crash_context_t *ctx) } /** - * Build native crash event with exception, mechanism, and debug_meta + * Build a native event from the scope-complete base event, adding the + * caller-specified framing (level, mechanism) plus threads and debug_meta. + * The base event (contexts, tags, user, breadcrumbs, ...) is identical + * regardless of event type; the caller states what this event is. * * @param ctx Crash context - * @param event_file_path Path to event file from parent process + * @param event_file_path Path to base event file from parent process + * @param level Event level (e.g. "fatal") + * @param mechanism_type Exception mechanism type (e.g. "signalhandler") + * @param handled Whether the mechanism was handled */ static sentry_value_t -build_native_crash_event( - const sentry_crash_context_t *ctx, const char *event_file_path) +build_native_event(const sentry_crash_context_t *ctx, + const char *event_file_path, const char *level, + const char *mechanism_type, bool handled) { // Read base event from parent's file sentry_value_t event = sentry_value_new_null(); @@ -2152,8 +2159,7 @@ build_native_crash_event( sentry_value_set_by_key( event, "platform", sentry_value_new_string("native")); - // Set level to fatal - sentry_value_set_by_key(event, "level", sentry_value_new_string("fatal")); + sentry_value_set_by_key(event, "level", sentry_value_new_string(level)); // Build exception const char *signal_name = "UNKNOWN"; @@ -2175,10 +2181,11 @@ build_native_crash_event( // Add mechanism sentry_value_t mechanism = sentry_value_new_object(); sentry_value_set_by_key( - mechanism, "type", sentry_value_new_string("signalhandler")); + mechanism, "type", sentry_value_new_string(mechanism_type)); sentry_value_set_by_key( mechanism, "synthetic", sentry_value_new_bool(true)); - sentry_value_set_by_key(mechanism, "handled", sentry_value_new_bool(false)); + sentry_value_set_by_key( + mechanism, "handled", sentry_value_new_bool(handled)); // Add signal metadata sentry_value_t meta = sentry_value_new_object(); @@ -2477,7 +2484,8 @@ write_envelope_with_native_stacktrace(const sentry_options_t *options, // Build native crash event (always include threads with names) SENTRY_DEBUGF("write_envelope_with_native_stacktrace: minidump_path=%s", minidump_path ? minidump_path : "(null)"); - sentry_value_t event = build_native_crash_event(ctx, event_file_path); + sentry_value_t event = build_native_event( + ctx, event_file_path, "fatal", "signalhandler", false); // Serialize event to JSON size_t event_size = 0; diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 3c3e508434..bea2986b1a 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -767,9 +767,61 @@ native_backend_write_attachments(const sentry_path_t *event_path) } } +#if defined(SENTRY_PLATFORM_WINDOWS) +// Sentry's symbolicator needs `contexts.device.arch` to process PE modules. If +// the scope already carries a device context with arch (host SDKs like Unity +// provide one), leave it; otherwise synthesize a minimal one so native-only +// consumers still symbolicate. +static void +native_backend_ensure_device_arch(sentry_value_t event) +{ + sentry_value_t contexts = sentry_value_get_by_key(event, "contexts"); + if (sentry_value_is_null(contexts)) { + contexts = sentry_value_new_object(); + sentry_value_set_by_key(event, "contexts", contexts); + } + sentry_value_t device = sentry_value_get_by_key(contexts, "device"); + if (sentry_value_is_null(device)) { + device = sentry_value_new_object(); + sentry_value_set_by_key( + device, "type", sentry_value_new_string("device")); + sentry_value_set_by_key(contexts, "device", device); + } + if (!sentry_value_is_null(sentry_value_get_by_key(device, "arch"))) { + return; + } +# if defined(_M_AMD64) + sentry_value_set_by_key(device, "arch", sentry_value_new_string("x86_64")); +# elif defined(_M_IX86) + sentry_value_set_by_key(device, "arch", sentry_value_new_string("x86")); +# elif defined(_M_ARM64) + sentry_value_set_by_key(device, "arch", sentry_value_new_string("arm64")); +# endif +} +#endif + +// Applies the full scope to `event`: contexts (os, device, gpu, app, runtime, +// plus SDK-specific entries such as the Unity context), user, tags, extra, +// fingerprint, release/dist/env, sdk metadata, and breadcrumbs - plus the +// Windows device.arch fallback. Single source of truth for the base event the +// daemon reads, shared by the continuous scope flush and the crash handler so +// both write an identical base regardless of which one wins the race. +static void +native_backend_apply_scope( + sentry_value_t event, const sentry_options_t *options) +{ + SENTRY_WITH_SCOPE (scope) { + sentry__scope_apply_to_event( + scope, options, event, SENTRY_SCOPE_BREADCRUMBS); + } +#if defined(SENTRY_PLATFORM_WINDOWS) + native_backend_ensure_device_arch(event); +#endif +} + static void native_backend_flush_scope( - sentry_backend_t *backend, const sentry_options_t *UNUSED(options)) + sentry_backend_t *backend, const sentry_options_t *options) { native_backend_state_t *state = (native_backend_state_t *)backend->data; if (!state || !state->event_path) { @@ -784,65 +836,11 @@ native_backend_flush_scope( return; } - // Create event with current scope + // Keep the on-disk base event complete and current, so the daemon has the + // full scope even if a crash beats the in-process handler to the file. sentry_value_t event = sentry_value_new_object(); - sentry_value_set_by_key( - event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); - - // Apply scope with contexts (includes OS, device info from Sentry) - SENTRY_WITH_SCOPE (scope) { - // Get contexts from scope (includes OS info) - sentry_value_t os_context - = sentry_value_get_by_key(scope->contexts, "os"); - if (!sentry_value_is_null(os_context)) { - sentry_value_t event_contexts = sentry_value_new_object(); - sentry_value_set_by_key(event_contexts, "os", os_context); - sentry_value_incref(os_context); + native_backend_apply_scope(event, options); -#if defined(SENTRY_PLATFORM_WINDOWS) - // Add device context with arch for Windows native events - // This is required for Sentry's symbolicator to process PE modules - sentry_value_t device_context = sentry_value_new_object(); - sentry_value_set_by_key( - device_context, "type", sentry_value_new_string("device")); -# if defined(_M_AMD64) - sentry_value_set_by_key( - device_context, "arch", sentry_value_new_string("x86_64")); -# elif defined(_M_IX86) - sentry_value_set_by_key( - device_context, "arch", sentry_value_new_string("x86")); -# elif defined(_M_ARM64) - sentry_value_set_by_key( - device_context, "arch", sentry_value_new_string("arm64")); -# endif - sentry_value_set_by_key(event_contexts, "device", device_context); -#endif - - sentry_value_set_by_key(event, "contexts", event_contexts); - } - - // Also copy other scope data (user, tags, extra, etc.) - sentry_value_t user = scope->user; - if (sentry_value_get_type(user) == SENTRY_VALUE_TYPE_OBJECT - && sentry_value_get_length(user) > 0) { - sentry_value_set_by_key(event, "user", user); - sentry_value_incref(user); - } - - sentry_value_t tags = scope->tags; - if (!sentry_value_is_null(tags)) { - sentry_value_set_by_key(event, "tags", tags); - sentry_value_incref(tags); - } - - sentry_value_t extra = scope->extra; - if (!sentry_value_is_null(extra)) { - sentry_value_set_by_key(event, "extra", extra); - sentry_value_incref(extra); - } - } - - // Serialize to JSON (so it can be deserialized on next start) size_t json_len = 0; char *json_str = sentry__value_to_json(event, &json_len); sentry_value_decref(event); @@ -1024,38 +1022,7 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) } if (should_handle) { - // Apply scope to event including breadcrumbs - SENTRY_WITH_SCOPE (scope) { - sentry__scope_apply_to_event( - scope, options, event, SENTRY_SCOPE_BREADCRUMBS); - } - -#if defined(SENTRY_PLATFORM_WINDOWS) - // Add device context with arch for Windows native events - // This is required for Sentry's symbolicator to process PE - // modules - sentry_value_t contexts - = sentry_value_get_by_key(event, "contexts"); - if (sentry_value_is_null(contexts)) { - contexts = sentry_value_new_object(); - sentry_value_set_by_key(event, "contexts", contexts); - } - sentry_value_t device_context = sentry_value_new_object(); - sentry_value_set_by_key( - device_context, "type", sentry_value_new_string("device")); -# if defined(_M_AMD64) - sentry_value_set_by_key( - device_context, "arch", sentry_value_new_string("x86_64")); -# elif defined(_M_IX86) - sentry_value_set_by_key( - device_context, "arch", sentry_value_new_string("x86")); -# elif defined(_M_ARM64) - sentry_value_set_by_key( - device_context, "arch", sentry_value_new_string("arm64")); -# endif - sentry_value_set_by_key(contexts, "device", device_context); - -#endif + native_backend_apply_scope(event, options); #ifndef SENTRY_SCREENSHOT_NONE // The screenshot is captured by the daemon out-of-process, so From a26c756e6e76aa1eed23344d6f1b783440b86114 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 1 Jun 2026 10:11:04 +0200 Subject: [PATCH 2/6] linter --- src/backends/native/sentry_crash_daemon.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index ff42856b09..7be96c4685 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -2132,8 +2132,8 @@ build_stacktrace_from_ctx(const sentry_crash_context_t *ctx) */ static sentry_value_t build_native_event(const sentry_crash_context_t *ctx, - const char *event_file_path, const char *level, - const char *mechanism_type, bool handled) + const char *event_file_path, const char *level, const char *mechanism_type, + bool handled) { // Read base event from parent's file sentry_value_t event = sentry_value_new_null(); From 0668a9345174b35f896fe3024ec0f54a188e0a6d Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 1 Jun 2026 14:10:42 +0200 Subject: [PATCH 3/6] restored default fatal behaviour --- src/backends/sentry_backend_native.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index bea2986b1a..d0e4d15c21 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -839,6 +839,9 @@ native_backend_flush_scope( // Keep the on-disk base event complete and current, so the daemon has the // full scope even if a crash beats the in-process handler to the file. sentry_value_t event = sentry_value_new_object(); + // Default to `FATAL` for all paths, i.e. minidump mode. + sentry_value_set_by_key( + event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); native_backend_apply_scope(event, options); size_t json_len = 0; From caeb8fbe142f03269756cb17478b327020766b22 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 1 Jun 2026 14:13:16 +0200 Subject: [PATCH 4/6] fix naming --- src/backends/sentry_backend_native.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index d0e4d15c21..3c5bb62c6a 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -773,7 +773,7 @@ native_backend_write_attachments(const sentry_path_t *event_path) // provide one), leave it; otherwise synthesize a minimal one so native-only // consumers still symbolicate. static void -native_backend_ensure_device_arch(sentry_value_t event) +ensure_device_arch(sentry_value_t event) { sentry_value_t contexts = sentry_value_get_by_key(event, "contexts"); if (sentry_value_is_null(contexts)) { @@ -807,7 +807,7 @@ native_backend_ensure_device_arch(sentry_value_t event) // daemon reads, shared by the continuous scope flush and the crash handler so // both write an identical base regardless of which one wins the race. static void -native_backend_apply_scope( +apply_scope( sentry_value_t event, const sentry_options_t *options) { SENTRY_WITH_SCOPE (scope) { @@ -815,7 +815,7 @@ native_backend_apply_scope( scope, options, event, SENTRY_SCOPE_BREADCRUMBS); } #if defined(SENTRY_PLATFORM_WINDOWS) - native_backend_ensure_device_arch(event); + ensure_device_arch(event); #endif } @@ -842,7 +842,7 @@ native_backend_flush_scope( // Default to `FATAL` for all paths, i.e. minidump mode. sentry_value_set_by_key( event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); - native_backend_apply_scope(event, options); + apply_scope(event, options); size_t json_len = 0; char *json_str = sentry__value_to_json(event, &json_len); @@ -1025,7 +1025,7 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) } if (should_handle) { - native_backend_apply_scope(event, options); + apply_scope(event, options); #ifndef SENTRY_SCREENSHOT_NONE // The screenshot is captured by the daemon out-of-process, so From 0a3a6466ad0f063b2bf87dffb2616a9db0b426ab Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 1 Jun 2026 16:40:45 +0200 Subject: [PATCH 5/6] perf(native): keep breadcrumbs off the per-mutation scope flush native_backend_flush_scope runs on every scope mutation (set_tag, set_context, set_user, ...). Folding breadcrumbs into the flushed base event re-serialized the entire breadcrumb ring on each of those calls - prohibitive on a hot path such as a 60fps game main thread. Give apply_scope a scope-mode argument: the continuous flush now passes SENTRY_SCOPE_NONE, while the crash handler still passes SENTRY_SCOPE_BREADCRUMBS to capture them at crash time (the process's last chance to record them). This matches the pre-existing behavior before breadcrumbs were added to the shared flush path. Co-Authored-By: Claude --- src/backends/sentry_backend_native.c | 39 ++++++++++++++++++---------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 3c5bb62c6a..f2b391002d 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -800,19 +800,24 @@ ensure_device_arch(sentry_value_t event) } #endif -// Applies the full scope to `event`: contexts (os, device, gpu, app, runtime, -// plus SDK-specific entries such as the Unity context), user, tags, extra, -// fingerprint, release/dist/env, sdk metadata, and breadcrumbs - plus the -// Windows device.arch fallback. Single source of truth for the base event the -// daemon reads, shared by the continuous scope flush and the crash handler so -// both write an identical base regardless of which one wins the race. +// Applies the scope to `event`: contexts (os, device, gpu, app, runtime, plus +// SDK-specific entries such as the Unity context), user, tags, extra, +// fingerprint, release/dist/env, sdk metadata - plus the Windows device.arch +// fallback. Shared by the continuous scope flush and the crash handler so both +// write an identical base regardless of which one wins the race. +// +// `mode` controls the expensive, list-shaped parts. The crash handler passes +// SENTRY_SCOPE_BREADCRUMBS to capture them at crash time, but the continuous +// flush passes SENTRY_SCOPE_NONE: it runs on *every* scope mutation, so folding +// the breadcrumb buffer in there would re-serialize the whole ring on every +// set_tag/set_context/... - prohibitive on a hot path such as a 60fps main +// thread. static void -apply_scope( - sentry_value_t event, const sentry_options_t *options) +apply_scope(sentry_value_t event, const sentry_options_t *options, + sentry_scope_mode_t mode) { SENTRY_WITH_SCOPE (scope) { - sentry__scope_apply_to_event( - scope, options, event, SENTRY_SCOPE_BREADCRUMBS); + sentry__scope_apply_to_event(scope, options, event, mode); } #if defined(SENTRY_PLATFORM_WINDOWS) ensure_device_arch(event); @@ -836,13 +841,17 @@ native_backend_flush_scope( return; } - // Keep the on-disk base event complete and current, so the daemon has the - // full scope even if a crash beats the in-process handler to the file. + // Keep the on-disk base event current, so the daemon has the full scope + // even if a crash beats the in-process handler to the file. Breadcrumbs are + // deliberately excluded here (SENTRY_SCOPE_NONE): they are flushed + // incrementally to the breadcrumb ring files and the crash handler captures + // them at crash time. This keeps the per-mutation flush off the breadcrumb + // serialization cost. sentry_value_t event = sentry_value_new_object(); // Default to `FATAL` for all paths, i.e. minidump mode. sentry_value_set_by_key( event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); - apply_scope(event, options); + apply_scope(event, options, SENTRY_SCOPE_NONE); size_t json_len = 0; char *json_str = sentry__value_to_json(event, &json_len); @@ -1025,7 +1034,9 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) } if (should_handle) { - apply_scope(event, options); + // At crash time we capture breadcrumbs (unlike the continuous + // flush) - this is the process's last chance to record them. + apply_scope(event, options, SENTRY_SCOPE_BREADCRUMBS); #ifndef SENTRY_SCREENSHOT_NONE // The screenshot is captured by the daemon out-of-process, so From 0a57db69d5e63db67b4e543bafae24b5d0dd7ed4 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 1 Jun 2026 18:31:44 +0200 Subject: [PATCH 6/6] minified change --- src/backends/native/sentry_crash_daemon.c | 5 +-- src/backends/sentry_backend_native.c | 52 ++++++++--------------- 2 files changed, 18 insertions(+), 39 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 7be96c4685..a2e03f7d33 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -2119,10 +2119,7 @@ build_stacktrace_from_ctx(const sentry_crash_context_t *ctx) } /** - * Build a native event from the scope-complete base event, adding the - * caller-specified framing (level, mechanism) plus threads and debug_meta. - * The base event (contexts, tags, user, breadcrumbs, ...) is identical - * regardless of event type; the caller states what this event is. + * Build a native event and set the level, mechanism, and handled state * * @param ctx Crash context * @param event_file_path Path to base event file from parent process diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index f2b391002d..7768fe0304 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -800,30 +800,6 @@ ensure_device_arch(sentry_value_t event) } #endif -// Applies the scope to `event`: contexts (os, device, gpu, app, runtime, plus -// SDK-specific entries such as the Unity context), user, tags, extra, -// fingerprint, release/dist/env, sdk metadata - plus the Windows device.arch -// fallback. Shared by the continuous scope flush and the crash handler so both -// write an identical base regardless of which one wins the race. -// -// `mode` controls the expensive, list-shaped parts. The crash handler passes -// SENTRY_SCOPE_BREADCRUMBS to capture them at crash time, but the continuous -// flush passes SENTRY_SCOPE_NONE: it runs on *every* scope mutation, so folding -// the breadcrumb buffer in there would re-serialize the whole ring on every -// set_tag/set_context/... - prohibitive on a hot path such as a 60fps main -// thread. -static void -apply_scope(sentry_value_t event, const sentry_options_t *options, - sentry_scope_mode_t mode) -{ - SENTRY_WITH_SCOPE (scope) { - sentry__scope_apply_to_event(scope, options, event, mode); - } -#if defined(SENTRY_PLATFORM_WINDOWS) - ensure_device_arch(event); -#endif -} - static void native_backend_flush_scope( sentry_backend_t *backend, const sentry_options_t *options) @@ -841,17 +817,18 @@ native_backend_flush_scope( return; } - // Keep the on-disk base event current, so the daemon has the full scope - // even if a crash beats the in-process handler to the file. Breadcrumbs are - // deliberately excluded here (SENTRY_SCOPE_NONE): they are flushed - // incrementally to the breadcrumb ring files and the crash handler captures - // them at crash time. This keeps the per-mutation flush off the breadcrumb - // serialization cost. + // Create event with current scope sentry_value_t event = sentry_value_new_object(); - // Default to `FATAL` for all paths, i.e. minidump mode. sentry_value_set_by_key( event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); - apply_scope(event, options, SENTRY_SCOPE_NONE); + + // Apply scope with contexts + SENTRY_WITH_SCOPE (scope) { + sentry__scope_apply_to_event(scope, options, event, SENTRY_SCOPE_NONE); + } +#if defined(SENTRY_PLATFORM_WINDOWS) + ensure_device_arch(event); +#endif size_t json_len = 0; char *json_str = sentry__value_to_json(event, &json_len); @@ -1034,9 +1011,14 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) } if (should_handle) { - // At crash time we capture breadcrumbs (unlike the continuous - // flush) - this is the process's last chance to record them. - apply_scope(event, options, SENTRY_SCOPE_BREADCRUMBS); + // Apply scope to event including breadcrumbs + SENTRY_WITH_SCOPE (scope) { + sentry__scope_apply_to_event( + scope, options, event, SENTRY_SCOPE_BREADCRUMBS); + } +#if defined(SENTRY_PLATFORM_WINDOWS) + ensure_device_arch(event); +#endif #ifndef SENTRY_SCREENSHOT_NONE // The screenshot is captured by the daemon out-of-process, so