Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b437853
Add session replay capturing
tustanivsky Jun 18, 2026
205b288
Handle replay envelope upload internally without public interface
tustanivsky Jun 29, 2026
f9a6b08
Fix replay dir path
tustanivsky Jun 29, 2026
0e6246a
Fix json property names
tustanivsky Jun 29, 2026
da1371b
Limit replay envelope to native daemon
tustanivsky Jun 29, 2026
8895c83
Clean up
tustanivsky Jun 29, 2026
cbab847
Fix lint
tustanivsky Jun 29, 2026
8e27cb0
Fix tests
tustanivsky Jun 29, 2026
f461222
Inline helper functions
tustanivsky Jun 29, 2026
cb3fc58
Clean up
tustanivsky Jun 29, 2026
772dcbb
Fix lint
tustanivsky Jun 29, 2026
148c0a1
Add replay files cleanup upon successfull envelope creation
tustanivsky Jun 30, 2026
be68d1b
Classify `replay_video` under the replay data/rate-limit category
tustanivsky Jun 30, 2026
998e92d
Remove redundant videoFilename check
tustanivsky Jul 1, 2026
0106e1f
Fix comment
tustanivsky Jul 1, 2026
52b905b
Fix issue with uploading orphan replays if crash wasn't captured
tustanivsky Jul 1, 2026
85a3cec
Clean up
tustanivsky Jul 1, 2026
50526b0
Guard replay_recording assembly against string-builder failure
tustanivsky Jul 1, 2026
acecbb8
Add helper allowing to check if there's staged replay
tustanivsky Jul 2, 2026
3299782
Reuse crash envelope path var and check if replay is staged during da…
tustanivsky Jul 2, 2026
d4dce27
Check replay dir content
tustanivsky Jul 2, 2026
706d491
Check if replay dir hold mp4 clip
tustanivsky Jul 2, 2026
b1ab19c
Add integration tests
tustanivsky Jul 2, 2026
6cabda1
Guard buffer add
tustanivsky Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,15 @@ main(int argc, char **argv)
sentry_set_trace(direct_trace_id, direct_parent_span_id);
}

if (has_arg(argc, argv, "replay-context")) {
// mimics an embedder (e.g. sentry-unreal) that stages replay clips in
// `<database>/replays/` and announces the active replay on the scope
sentry_value_t replay = sentry_value_new_object();
sentry_value_set_by_key(replay, "replay_id",
sentry_value_new_string("deadbeefdeadbeefdeadbeefdeadbeef"));
sentry_set_context("replay", replay);
}

if (has_arg(argc, argv, "attach-after-init")) {
// assuming the example / test is run directly from the cmake build
// directory
Expand Down
27 changes: 25 additions & 2 deletions src/backends/native/sentry_crash_daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -3340,8 +3340,6 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)
SENTRY_DEBUG("Extracting run folder from event path");
sentry_path_t *ev_path = sentry__path_from_str(event_path);
sentry_path_t *run_folder = ev_path ? sentry__path_dir(ev_path) : NULL;
if (ev_path)
sentry__path_free(ev_path);

// Acquire the run directory lock file so that process_old_runs() in a
// new SDK run will skip this directory while the daemon is still
Expand Down Expand Up @@ -3369,6 +3367,7 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)

if (path_len < 0 || path_len >= (int)sizeof(envelope_path)) {
SENTRY_WARN("Envelope path truncated or invalid");
sentry__path_free(ev_path);
if (run_folder) {
sentry__path_free(run_folder);
}
Expand Down Expand Up @@ -3472,6 +3471,7 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)

if (!envelope_written) {
SENTRY_WARN("Failed to write envelope");
sentry__path_free(ev_path);
if (run_folder) {
sentry__path_free(run_folder);
}
Expand Down Expand Up @@ -3571,6 +3571,28 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)
#endif

cleanup:
// Send the staged session-replay envelope same-session, enriched from the
// crash event (`<run>/__sentry-event`) so it shares the crash's
// tags/contexts/trace. Only flush when the crash itself was delivered:
// `cleanup` is also reached via `goto` on error paths where the crash was
// never captured, and flushing there would consume (and delete) the staged
// replay for a crash that never arrived.
if (crash_captured && options && options->transport
&& sentry__session_replay_has_pending(options)) {
sentry_value_t crash_event = sentry_value_new_null();
if (ev_path) {
size_t ev_len = 0;
char *ev_json = sentry__path_read_to_buffer(ev_path, &ev_len);
if (ev_json) {
crash_event = sentry__value_from_json(ev_json, ev_len);
sentry_free(ev_json);
}
}
sentry__session_replay_flush_pending(
options, options->transport, crash_event);
sentry_value_decref(crash_event);
}
Comment thread
tustanivsky marked this conversation as resolved.

// Send all other envelopes from run folder (logs, etc.) before cleanup
if (run_folder && options && options->transport && options->run) {
SENTRY_DEBUG("Checking for additional envelopes in run folder");
Expand Down Expand Up @@ -3613,6 +3635,7 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)
sentry__path_remove_all(run_folder);
sentry__path_free(run_folder);
}
sentry__path_free(ev_path);

// Release and clean up the lock file
if (run_lock) {
Expand Down
1 change: 1 addition & 0 deletions src/sentry_client_report.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ typedef enum {
SENTRY_DATA_CATEGORY_LOG_ITEM,
SENTRY_DATA_CATEGORY_FEEDBACK,
SENTRY_DATA_CATEGORY_TRACE_METRIC,
SENTRY_DATA_CATEGORY_REPLAY,
SENTRY_DATA_CATEGORY_MAX
} sentry_data_category_t;

Expand Down
6 changes: 6 additions & 0 deletions src/sentry_envelope.c
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ envelope_item_get_ratelimiter_category(const sentry_envelope_item_t *item)
return SENTRY_RL_CATEGORY_SESSION;
} else if (sentry__string_eq(ty, "transaction")) {
return SENTRY_RL_CATEGORY_TRANSACTION;
} else if (sentry__string_eq(ty, "replay_video")) {
return SENTRY_RL_CATEGORY_REPLAY;
} else if (sentry__string_eq(ty, "client_report")) {
// internal telemetry, bypass rate limiting
return -1;
Expand All @@ -160,6 +162,8 @@ item_type_to_data_category(const char *ty)
return SENTRY_DATA_CATEGORY_FEEDBACK;
} else if (sentry__string_eq(ty, "trace_metric")) {
return SENTRY_DATA_CATEGORY_TRACE_METRIC;
} else if (sentry__string_eq(ty, "replay_video")) {
return SENTRY_DATA_CATEGORY_REPLAY;
}
return SENTRY_DATA_CATEGORY_ERROR;
}
Expand Down Expand Up @@ -1347,6 +1351,8 @@ data_category_to_string(sentry_data_category_t category)
return "feedback";
case SENTRY_DATA_CATEGORY_TRACE_METRIC:
return "trace_metric";
case SENTRY_DATA_CATEGORY_REPLAY:
return "replay";
case SENTRY_DATA_CATEGORY_MAX:
default:
return "unknown";
Expand Down
5 changes: 4 additions & 1 deletion src/sentry_ratelimiter.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#include "sentry_slice.h"
#include "sentry_utils.h"

#define MAX_RATE_LIMITS 4
#define MAX_RATE_LIMITS 5
#define MAX_RETRY_AFTER (24 * 60 * 60) // 24h

struct sentry_rate_limiter_s {
Expand All @@ -28,6 +28,7 @@ sentry__rate_limiter_new(void)
rl->disabled_until[SENTRY_RL_CATEGORY_ERROR] = 0;
rl->disabled_until[SENTRY_RL_CATEGORY_SESSION] = 0;
rl->disabled_until[SENTRY_RL_CATEGORY_TRANSACTION] = 0;
rl->disabled_until[SENTRY_RL_CATEGORY_REPLAY] = 0;
}
return rl;
}
Expand Down Expand Up @@ -64,6 +65,8 @@ sentry__rate_limiter_update_from_header(
} else if (sentry__slice_eqs(category, "transaction")) {
rl->disabled_until[SENTRY_RL_CATEGORY_TRANSACTION]
= retry_after;
} else if (sentry__slice_eqs(category, "replay")) {
rl->disabled_until[SENTRY_RL_CATEGORY_REPLAY] = retry_after;
}

categories = sentry__slice_advance(categories, category.len);
Expand Down
1 change: 1 addition & 0 deletions src/sentry_ratelimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#define SENTRY_RL_CATEGORY_ERROR 1
#define SENTRY_RL_CATEGORY_SESSION 2
#define SENTRY_RL_CATEGORY_TRANSACTION 3
#define SENTRY_RL_CATEGORY_REPLAY 4

typedef struct sentry_rate_limiter_s sentry_rate_limiter_t;

Expand Down
25 changes: 25 additions & 0 deletions src/sentry_session_replay.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,29 @@ bool sentry__session_replay_capture(
*/
sentry_path_t *sentry__session_replay_get_path(const sentry_options_t *options);

/**
* Returns whether the embedder's `<database>/replays/` staging directory holds
* a replay clip, so callers can skip crash-event work when nothing is staged.
*/
bool sentry__session_replay_has_pending(const sentry_options_t *options);

/**
* Build and send the session-replay envelope for the crash described by
* `scope_source`. The replay is identified by the crash event's
* `contexts.replay.replay_id` and staged by the embedder as
* `<database>/replays/replay-<id>.{json,mp4}`.
*
* Native-daemon-only: called out-of-process by the crash
* daemon, so it runs only on a crash and delivers same-session. The sidecar and
* mp4 are consumed once and removed after the flush attempt, regardless of
* whether the envelope was built or sent, so a failed flush is not retried.
*
* `scope_source` is the crash event (`<run>/__sentry-event`); its scope fields
* and trace id are copied onto the replay, and its timestamp ends the replay
* window. If it is NULL or carries no `contexts.replay.replay_id`, nothing is
* flushed.
*/
void sentry__session_replay_flush_pending(const sentry_options_t *options,
sentry_transport_t *transport, sentry_value_t scope_source);

#endif
Loading
Loading