From d670df8267276b91ed5ad82cadd4f2ecee9eb682 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 8 May 2026 15:55:36 +0300 Subject: [PATCH 1/2] feat: allow downstream Xbox SDK to supply replay clip capture implementation --- include/sentry.h | 20 ++++++++++ src/CMakeLists.txt | 9 +++++ src/backends/native/sentry_crash_context.h | 2 + src/backends/native/sentry_crash_daemon.c | 43 ++++++++++++++++++++++ src/backends/sentry_backend_breakpad.cpp | 13 +++++++ src/backends/sentry_backend_inproc.c | 13 +++++++ src/backends/sentry_backend_native.c | 2 + src/replay_clip/sentry_replay_clip.c | 7 ++++ src/replay_clip/sentry_replay_clip_none.c | 10 +++++ src/sentry_options.c | 15 ++++++++ src/sentry_options.h | 2 + src/sentry_replay_clip.h | 27 ++++++++++++++ 12 files changed, 163 insertions(+) create mode 100644 src/replay_clip/sentry_replay_clip.c create mode 100644 src/replay_clip/sentry_replay_clip_none.c create mode 100644 src/sentry_replay_clip.h diff --git a/include/sentry.h b/include/sentry.h index 210381223..d37edbc68 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1636,6 +1636,26 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_before_screenshot( sentry_options_t *opts, sentry_before_screenshot_function_t func, void *user_data); +/** + * Enables capturing a short retroactive video clip on crash. Currently only + * supported on Xbox via the OS-managed game recording ring. The clip is + * attached to the crash envelope as `replay-clip.mp4`. + * + * Set the duration via `sentry_options_set_replay_clip_duration_ms` (default + * 5000 ms). Disabled by default. Must be set before `sentry_init`. + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_attach_replay_clip( + sentry_options_t *opts, int val); + +/** + * Sets the requested duration of the retroactive replay clip in milliseconds. + * + * The resulting clip can be shorter than the requested duration if it hasn't + * accumulated enough buffered frames yet. Defaults to 5000 ms. + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_replay_clip_duration_ms( + sentry_options_t *opts, uint32_t duration_ms); + /** * Sets the path to the crashpad handler if the crashpad backend is used. * diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 35bd378ad..b97d70f35 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -44,6 +44,7 @@ sentry_target_sources_cwd(sentry sentry_scope.c sentry_scope.h sentry_screenshot.h + sentry_replay_clip.h sentry_session.c sentry_session.h sentry_slice.c @@ -65,6 +66,7 @@ sentry_target_sources_cwd(sentry sentry_tracing.h path/sentry_path.c screenshot/sentry_screenshot.c + replay_clip/sentry_replay_clip.c transports/sentry_disk_transport.c transports/sentry_disk_transport.h transports/sentry_function_transport.c @@ -280,3 +282,10 @@ elseif(NOT WIN32) screenshot/sentry_screenshot_none.c ) endif() + +# replay clip +if(NOT XBOX) + sentry_target_sources_cwd(sentry + replay_clip/sentry_replay_clip_none.c + ) +endif() diff --git a/src/backends/native/sentry_crash_context.h b/src/backends/native/sentry_crash_context.h index da386da9a..c08804e3d 100644 --- a/src/backends/native/sentry_crash_context.h +++ b/src/backends/native/sentry_crash_context.h @@ -270,6 +270,8 @@ typedef struct { int crash_reporting_mode; // sentry_crash_reporting_mode_t bool debug_enabled; // Debug logging enabled in parent process bool attach_screenshot; // Screenshot attachment enabled in parent process + bool attach_replay_clip; // Replay clip attachment enabled in parent process + uint32_t replay_clip_duration_ms; // Requested replay clip duration in ms bool cache_keep; bool require_user_consent; bool enable_large_attachments; diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index ec8b052ea..269b361c5 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -12,6 +12,7 @@ #include "sentry_options.h" #include "sentry_path.h" #include "sentry_process.h" +#include "sentry_replay_clip.h" #include "sentry_screenshot.h" #include "sentry_string.h" #include "sentry_symbolizer.h" @@ -2504,6 +2505,17 @@ write_envelope_with_native_stacktrace(const sentry_options_t *options, } } + // Add replay clip attachment if captured by the daemon + if (ctx->attach_replay_clip && run_folder) { + sentry_path_t *clip_path + = sentry__path_join_str(run_folder, "replay-clip.mp4"); + if (clip_path) { + write_attachment_to_envelope( + fd, clip_path->path, "replay-clip.mp4", "video/mp4"); + sentry__path_free(clip_path); + } + } + #if defined(SENTRY_PLATFORM_UNIX) close(fd); #elif defined(SENTRY_PLATFORM_WINDOWS) @@ -2740,6 +2752,17 @@ write_envelope_with_minidump(const sentry_options_t *options, } } + // Add replay clip attachment if captured by the daemon + if (ctx->attach_replay_clip && run_folder) { + sentry_path_t *clip_path + = sentry__path_join_str(run_folder, "replay-clip.mp4"); + if (clip_path) { + write_attachment_to_envelope( + fd, clip_path->path, "replay-clip.mp4", "video/mp4"); + sentry__path_free(clip_path); + } + } + #if defined(SENTRY_PLATFORM_UNIX) close(fd); #elif defined(SENTRY_PLATFORM_WINDOWS) @@ -2922,6 +2945,24 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) sentry__path_free(screenshot_path); } } + + // Capture replay clip if enabled. Like screenshot, this runs out-of-process + // because the underlying OS APIs are not signal-safe. + if (ctx->attach_replay_clip && run_folder) { + SENTRY_DEBUG("Capturing replay clip"); + sentry_path_t *clip_path + = sentry__path_join_str(run_folder, "replay-clip.mp4"); + if (clip_path) { + if (sentry__replay_clip_capture(clip_path, + ctx->replay_clip_duration_ms, + (uint32_t)ctx->crashed_pid)) { + SENTRY_DEBUG("Replay clip captured successfully"); + } else { + SENTRY_DEBUG("Replay clip capture failed"); + } + sentry__path_free(clip_path); + } + } #endif // On Linux, capture modules and threads from /proc for native mode @@ -3289,6 +3330,8 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, // Use debug logging and screenshot settings from parent process sentry_options_set_debug(options, ipc->shmem->debug_enabled); options->attach_screenshot = ipc->shmem->attach_screenshot; + options->attach_replay_clip = ipc->shmem->attach_replay_clip; + options->replay_clip_duration_ms = ipc->shmem->replay_clip_duration_ms; options->cache_keep = ipc->shmem->cache_keep; options->enable_large_attachments = ipc->shmem->enable_large_attachments; options->http_retry = false; diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index 3f0f4cdea..971771b10 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -16,6 +16,7 @@ extern "C" { # include "sentry_os.h" #endif #include "sentry_path.h" +#include "sentry_replay_clip.h" #include "sentry_screenshot.h" #include "sentry_string.h" #include "sentry_sync.h" @@ -219,6 +220,18 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, sentry__attachment_free(screenshot); } + if (options->attach_replay_clip) { + sentry_attachment_t *clip = sentry__attachment_from_path( + sentry__replay_clip_get_path(options)); + if (clip + && sentry__replay_clip_capture(clip->path, + options->replay_clip_duration_ms, 0)) { + sentry__envelope_add_attachment(envelope, clip); + } else { + sentry__attachment_free(clip); + } + } + if (!sentry__launch_external_crash_reporter(options, envelope)) { // capture the envelope with the disk transport sentry_transport_t *disk_transport diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 6b7d6b395..31c419f2c 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -15,6 +15,7 @@ # include "sentry_os.h" # include #endif +#include "sentry_replay_clip.h" #include "sentry_scope.h" #include "sentry_screenshot.h" #include "sentry_sync.h" @@ -1202,6 +1203,18 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, sentry__attachment_free(screenshot); } + if (options->attach_replay_clip) { + sentry_attachment_t *clip = sentry__attachment_from_path( + sentry__replay_clip_get_path(options)); + if (clip + && sentry__replay_clip_capture(clip->path, + options->replay_clip_duration_ms, 0)) { + sentry__envelope_add_attachment(envelope, clip); + } else { + sentry__attachment_free(clip); + } + } + if (!sentry__launch_external_crash_reporter(options, envelope)) { // capture the envelope with the disk transport sentry_transport_t *disk_transport diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index d9bbcf038..d5835ab3b 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -192,6 +192,8 @@ native_backend_startup( // Pass debug logging setting to daemon ctx->debug_enabled = options->debug; ctx->attach_screenshot = options->attach_screenshot; + ctx->attach_replay_clip = options->attach_replay_clip; + ctx->replay_clip_duration_ms = options->replay_clip_duration_ms; ctx->cache_keep = options->cache_keep; ctx->require_user_consent = options->require_user_consent; ctx->enable_large_attachments = options->enable_large_attachments; diff --git a/src/replay_clip/sentry_replay_clip.c b/src/replay_clip/sentry_replay_clip.c new file mode 100644 index 000000000..00a9c8208 --- /dev/null +++ b/src/replay_clip/sentry_replay_clip.c @@ -0,0 +1,7 @@ +#include "sentry_replay_clip.h" + +sentry_path_t * +sentry__replay_clip_get_path(const sentry_options_t *options) +{ + return sentry__path_join_str(options->run->run_path, "replay-clip.mp4"); +} diff --git a/src/replay_clip/sentry_replay_clip_none.c b/src/replay_clip/sentry_replay_clip_none.c new file mode 100644 index 000000000..b47919c9c --- /dev/null +++ b/src/replay_clip/sentry_replay_clip_none.c @@ -0,0 +1,10 @@ +#include "sentry_replay_clip.h" + +#include "sentry_core.h" + +bool +sentry__replay_clip_capture(const sentry_path_t *UNUSED(path), + uint32_t UNUSED(duration_ms), uint32_t UNUSED(pid)) +{ + return false; +} diff --git a/src/sentry_options.c b/src/sentry_options.c index 100015cfe..56af8a3c0 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -48,6 +48,8 @@ sentry_options_new(void) opts->auto_session_tracking = true; opts->system_crash_reporter_enabled = false; opts->attach_screenshot = false; + opts->attach_replay_clip = false; + opts->replay_clip_duration_ms = 5000; opts->crashpad_wait_for_upload = false; // On macOS, breakpad suspends all other threads of the process before // writing the minidump and invokes our backend callback from inside @@ -651,6 +653,19 @@ sentry_options_set_before_screenshot(sentry_options_t *opts, opts->before_screenshot_data = user_data; } +void +sentry_options_set_attach_replay_clip(sentry_options_t *opts, int val) +{ + opts->attach_replay_clip = !!val; +} + +void +sentry_options_set_replay_clip_duration_ms( + sentry_options_t *opts, uint32_t duration_ms) +{ + opts->replay_clip_duration_ms = duration_ms; +} + void sentry_options_set_handler_path(sentry_options_t *opts, const char *path) { diff --git a/src/sentry_options.h b/src/sentry_options.h index 0a063a3aa..3fff5d60a 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -43,6 +43,8 @@ struct sentry_options_s { bool attach_screenshot; sentry_before_screenshot_function_t before_screenshot_func; void *before_screenshot_data; + bool attach_replay_clip; + uint32_t replay_clip_duration_ms; bool crashpad_wait_for_upload; bool enable_logging_when_crashed; bool propagate_traceparent; diff --git a/src/sentry_replay_clip.h b/src/sentry_replay_clip.h new file mode 100644 index 000000000..8b0feb213 --- /dev/null +++ b/src/sentry_replay_clip.h @@ -0,0 +1,27 @@ +#ifndef SENTRY_REPLAY_CLIP_H_INCLUDED +#define SENTRY_REPLAY_CLIP_H_INCLUDED + +#include "sentry_boot.h" + +#include "sentry_options.h" +#include "sentry_path.h" + +/** + * Captures a short retroactive video clip and saves it to the specified path. + * + * @param path The path where the clip should be saved (typically MP4). + * @param duration_ms The requested duration in milliseconds. + * @param pid The process ID whose output should be captured (0 = current + * process). + * + * Returns true if the clip was successfully captured and saved. + */ +bool sentry__replay_clip_capture( + const sentry_path_t *path, uint32_t duration_ms, uint32_t pid); + +/** + * Returns the path where a replay clip should be saved. + */ +sentry_path_t *sentry__replay_clip_get_path(const sentry_options_t *options); + +#endif From 73ce86a3ce6cf6a96407fc2dbd905b4997539f51 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 8 May 2026 16:55:24 +0300 Subject: [PATCH 2/2] Fix lint errors --- src/backends/native/sentry_crash_daemon.c | 3 +-- src/backends/sentry_backend_breakpad.cpp | 4 ++-- src/backends/sentry_backend_inproc.c | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 269b361c5..b2a58757d 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -2954,8 +2954,7 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) = sentry__path_join_str(run_folder, "replay-clip.mp4"); if (clip_path) { if (sentry__replay_clip_capture(clip_path, - ctx->replay_clip_duration_ms, - (uint32_t)ctx->crashed_pid)) { + ctx->replay_clip_duration_ms, (uint32_t)ctx->crashed_pid)) { SENTRY_DEBUG("Replay clip captured successfully"); } else { SENTRY_DEBUG("Replay clip capture failed"); diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index 971771b10..244c09ad6 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -224,8 +224,8 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, sentry_attachment_t *clip = sentry__attachment_from_path( sentry__replay_clip_get_path(options)); if (clip - && sentry__replay_clip_capture(clip->path, - options->replay_clip_duration_ms, 0)) { + && sentry__replay_clip_capture( + clip->path, options->replay_clip_duration_ms, 0)) { sentry__envelope_add_attachment(envelope, clip); } else { sentry__attachment_free(clip); diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 31c419f2c..88a25eaae 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1207,8 +1207,8 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, sentry_attachment_t *clip = sentry__attachment_from_path( sentry__replay_clip_get_path(options)); if (clip - && sentry__replay_clip_capture(clip->path, - options->replay_clip_duration_ms, 0)) { + && sentry__replay_clip_capture( + clip->path, options->replay_clip_duration_ms, 0)) { sentry__envelope_add_attachment(envelope, clip); } else { sentry__attachment_free(clip);