Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions src/backends/native/sentry_crash_daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -3571,6 +3571,30 @@ 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.
if (options && options->transport) {
sentry_value_t crash_event = sentry_value_new_null();
if (run_folder) {
sentry_path_t *sentry_event_path
= sentry__path_join_str(run_folder, "__sentry-event");
if (sentry_event_path) {
size_t ev_len = 0;
char *ev_json
= sentry__path_read_to_buffer(sentry_event_path, &ev_len);
if (ev_json) {
crash_event = sentry__value_from_json(ev_json, ev_len);
sentry_free(ev_json);
}
sentry__path_free(sentry_event_path);
}
}
sentry__session_replay_flush_pending(
options, options->transport, crash_event);
sentry_value_decref(crash_event);
}

// 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
14 changes: 14 additions & 0 deletions src/sentry_session_replay.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,18 @@ bool sentry__session_replay_capture(
*/
sentry_path_t *sentry__session_replay_get_path(const sentry_options_t *options);

/**
* Build and send the session-replay envelope(s) the embedder staged in
* `<database>/replays/` (a `replay-<id>.json` sidecar next to its mp4). Native-
* daemon-only: called out-of-process by the crash daemon, so it runs only on a
* crash and delivers same-session. Sources are left on disk for the embedder to
* clear.
*
* `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. Null skips enrichment.
*/
void sentry__session_replay_flush_pending(const sentry_options_t *options,
sentry_transport_t *transport, sentry_value_t scope_source);

#endif
324 changes: 324 additions & 0 deletions src/session_replay/sentry_session_replay.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,331 @@
#include "sentry_session_replay.h"

#include "sentry_core.h"
#include "sentry_database.h"
#include "sentry_envelope.h"
#include "sentry_json.h"
#include "sentry_string.h"
#include "sentry_utils.h"
#include "sentry_value.h"

#include "../vendor/mpack.h"

#include <stdio.h>

sentry_path_t *
sentry__session_replay_get_path(const sentry_options_t *options)
{
return sentry__path_join_str(options->run->run_path, "session-replay.mp4");
}

// Resolve `<database>/replays` from the run path's parent (the daemon's
// `options->database_path` is an unusable relative default). Caller owns it.
static sentry_path_t *
session_replay_dir(const sentry_options_t *options)
{
if (!options->run || !options->run->run_path) {
return NULL;
}
sentry_path_t *db_dir = sentry__path_dir(options->run->run_path);
if (!db_dir) {
return NULL;
}
sentry_path_t *replays = sentry__path_join_str(db_dir, "replays");
sentry__path_free(db_dir);
return replays;
}

static int32_t
meta_int32(sentry_value_t meta, const char *key)
{
return sentry_value_as_int32(sentry_value_get_by_key(meta, key));
}

static double
meta_double(sentry_value_t meta, const char *key)
{
return sentry_value_as_double(sentry_value_get_by_key(meta, key));
}

// Build the replay_event from the recorder's metadata. When `scope_source` (the
// crash event) is non-null, its scope fields and trace id are copied onto the
// replay so it shares the crash's context. `error_ids` is omitted (deprecated).
static sentry_value_t
build_replay_event(sentry_value_t meta, const char *replay_id, double start_sec,
double end_sec, int32_t segment_id, sentry_value_t scope_source)
{
const char *replay_type
= sentry_value_as_string(sentry_value_get_by_key(meta, "replayType"));

sentry_value_t event = sentry_value_new_object();
sentry_value_set_by_key(
event, "type", sentry_value_new_string("replay_event"));
sentry_value_set_by_key(event, "replay_type",
sentry_value_new_string(
replay_type && replay_type[0] ? replay_type : "buffer"));
sentry_value_set_by_key(
event, "segment_id", sentry_value_new_int32(segment_id));
sentry_value_set_by_key(
event, "replay_id", sentry_value_new_string(replay_id));
sentry_value_set_by_key(
event, "event_id", sentry_value_new_string(replay_id));
sentry_value_set_by_key(
event, "platform", sentry_value_new_string("native"));
sentry_value_set_by_key(
event, "timestamp", sentry_value_new_double(end_sec));
sentry_value_set_by_key(
event, "replay_start_timestamp", sentry_value_new_double(start_sec));
sentry_value_set_by_key(event, "urls", sentry_value_new_list());

if (!sentry_value_is_null(scope_source)) {
// Scope fields only, not the crash's exception/threads.
static const char *const scope_keys[] = { "tags", "contexts", "release",
"environment", "dist", "user", "sdk" };
for (size_t i = 0; i < sizeof(scope_keys) / sizeof(scope_keys[0]);
i++) {
sentry_value_t v
= sentry_value_get_by_key(scope_source, scope_keys[i]);
if (!sentry_value_is_null(v)) {
sentry_value_incref(v);
sentry_value_set_by_key(event, scope_keys[i], v);
}
}

// trace_ids <- contexts.trace.trace_id
sentry_value_t trace_id = sentry_value_get_by_key(
sentry_value_get_by_key(
sentry_value_get_by_key(scope_source, "contexts"), "trace"),
"trace_id");
sentry_value_t trace_ids = sentry_value_new_list();
if (!sentry_value_is_null(trace_id)) {
sentry_value_incref(trace_id);
sentry_value_append(trace_ids, trace_id);
}
sentry_value_set_by_key(event, "trace_ids", trace_ids);
}
return event;
}

// Build the rrweb recording list (meta event + video event) describing the
// clip.
static sentry_value_t
build_replay_recording(sentry_value_t meta, double start_sec,
int32_t segment_id, double size_bytes, double duration_ms)
{
const int32_t width = meta_int32(meta, "width");
const int32_t height = meta_int32(meta, "height");
const double ts_ms = start_sec * 1000.0;

sentry_value_t meta_data = sentry_value_new_object();
sentry_value_set_by_key(meta_data, "href", sentry_value_new_string(""));
sentry_value_set_by_key(meta_data, "width", sentry_value_new_int32(width));
sentry_value_set_by_key(
meta_data, "height", sentry_value_new_int32(height));
sentry_value_t meta_event = sentry_value_new_object();
sentry_value_set_by_key(meta_event, "type", sentry_value_new_int32(4));
sentry_value_set_by_key(
meta_event, "timestamp", sentry_value_new_double(ts_ms));
sentry_value_set_by_key(meta_event, "data", meta_data);

sentry_value_t payload = sentry_value_new_object();
sentry_value_set_by_key(
payload, "segmentId", sentry_value_new_int32(segment_id));
sentry_value_set_by_key(
payload, "size", sentry_value_new_double(size_bytes));
sentry_value_set_by_key(
payload, "duration", sentry_value_new_double(duration_ms));
sentry_value_set_by_key(
payload, "encoding", sentry_value_new_string("h264"));
sentry_value_set_by_key(
payload, "container", sentry_value_new_string("mp4"));
sentry_value_set_by_key(payload, "height", sentry_value_new_int32(height));
sentry_value_set_by_key(payload, "width", sentry_value_new_int32(width));
sentry_value_set_by_key(payload, "left", sentry_value_new_int32(0));
sentry_value_set_by_key(payload, "top", sentry_value_new_int32(0));
sentry_value_set_by_key(payload, "frameCount",
sentry_value_new_int32(meta_int32(meta, "frameCount")));
sentry_value_set_by_key(payload, "frameRate",
sentry_value_new_int32(meta_int32(meta, "frameRate")));
sentry_value_set_by_key(
payload, "frameRateType", sentry_value_new_string("variable"));
sentry_value_t video_data = sentry_value_new_object();
sentry_value_set_by_key(
video_data, "tag", sentry_value_new_string("video"));
sentry_value_set_by_key(video_data, "payload", payload);
sentry_value_t video_event = sentry_value_new_object();
sentry_value_set_by_key(video_event, "type", sentry_value_new_int32(5));
sentry_value_set_by_key(
video_event, "timestamp", sentry_value_new_double(ts_ms));
sentry_value_set_by_key(video_event, "data", video_data);

sentry_value_t recording = sentry_value_new_list();
sentry_value_append(recording, meta_event);
sentry_value_append(recording, video_event);
return recording;
}

// Build the replay_video envelope from the parsed sidecar + its mp4. `end_sec`
// is the window end (crash time); <= 0 falls back to the sidecar's. NULL on
// failure.
static sentry_envelope_t *
build_replay_envelope(const sentry_options_t *options, sentry_value_t meta,
const sentry_path_t *mp4_path, double end_sec, sentry_value_t scope_source)
{
if (sentry_value_is_null(meta) || !mp4_path) {
return NULL;
}

const char *replay_id
= sentry_value_as_string(sentry_value_get_by_key(meta, "replayId"));
if (!replay_id || !replay_id[0]) {
return NULL;
}

size_t video_len = 0;
char *video = sentry__path_read_to_buffer(mp4_path, &video_len);
if (!video || video_len == 0) {
sentry_free(video);
return NULL;
}

const double duration_ms = meta_double(meta, "durationMs");
if (end_sec <= 0.0) {
end_sec = meta_double(meta, "endTimestampSec");
}
const double start_sec = end_sec - duration_ms / 1000.0;
const int32_t segment_id = meta_int32(meta, "segmentId");

sentry_value_t event = build_replay_event(
meta, replay_id, start_sec, end_sec, segment_id, scope_source);
sentry_value_t recording = build_replay_recording(
meta, start_sec, segment_id, (double)video_len, duration_ms);

sentry_envelope_t *envelope = NULL;

size_t event_len = 0;
char *event_json = sentry__value_to_json(event, &event_len);
size_t rrweb_len = 0;
char *rrweb_json = sentry__value_to_json(recording, &rrweb_len);

if (event_json && rrweb_json) {
// replay_recording = `{"segment_id":N}\n` + the rrweb array.
char hdr[48];
int hdr_len
= snprintf(hdr, sizeof(hdr), "{\"segment_id\":%d}\n", segment_id);

sentry_stringbuilder_t rb;
sentry__stringbuilder_init(&rb);
sentry__stringbuilder_append_buf(&rb, hdr, (size_t)hdr_len);
sentry__stringbuilder_append_buf(&rb, rrweb_json, rrweb_len);
size_t recording_len = sentry__stringbuilder_len(&rb);
char *recording_buf = sentry__stringbuilder_into_string(&rb);

// replay_video item body: msgpack map of three raw blobs.
mpack_writer_t writer;
char *body = NULL;
size_t body_len = 0;
mpack_writer_init_growable(&writer, &body, &body_len);
mpack_start_map(&writer, 3);
mpack_write_cstr(&writer, "replay_event");
mpack_write_bin(&writer, event_json, (uint32_t)event_len);
mpack_write_cstr(&writer, "replay_recording");
mpack_write_bin(&writer, recording_buf, (uint32_t)recording_len);
mpack_write_cstr(&writer, "replay_video");
mpack_write_bin(&writer, video, (uint32_t)video_len);
mpack_finish_map(&writer);
bool body_ok = mpack_writer_destroy(&writer) == mpack_ok;

sentry_free(recording_buf);

if (body_ok && body) {
envelope = sentry__envelope_new();
if (envelope) {
// Relay keys the replay on the envelope event_id.
sentry__envelope_set_header(
envelope, "event_id", sentry_value_new_string(replay_id));
const char *dsn = sentry_options_get_dsn(options);
if (dsn && dsn[0]) {
sentry__envelope_set_header(
envelope, "dsn", sentry_value_new_string(dsn));
}
sentry__envelope_add_from_buffer(
envelope, body, body_len, "replay_video");
}
}
sentry_free(body);
}

sentry_free(rrweb_json);
sentry_free(event_json);
sentry_value_decref(event);
sentry_value_decref(recording);
sentry_free(video);
return envelope;
}

void
sentry__session_replay_flush_pending(const sentry_options_t *options,
sentry_transport_t *transport, sentry_value_t scope_source)
{
if (!options || !transport) {
return;
}

sentry_path_t *dir = session_replay_dir(options);
if (!dir) {
return;
}
if (!sentry__path_is_dir(dir)) {
sentry__path_free(dir);
return;
}

// End the replay window at the crash time (from the crash event); falls back
// to the sidecar's own end timestamp in build_replay_envelope.
double end_sec = 0.0;
if (!sentry_value_is_null(scope_source)) {
const char *ts = sentry_value_as_string(
sentry_value_get_by_key(scope_source, "timestamp"));
if (ts && ts[0]) {
uint64_t usec = sentry__iso8601_to_usec(ts);
if (usec) {
end_sec = (double)usec / 1000000.0;
}
}
}

sentry_pathiter_t *iter = sentry__path_iter_directory(dir);
const sentry_path_t *file;
while (iter && (file = sentry__pathiter_next(iter)) != NULL) {
if (!sentry__path_ends_with(file, ".json")) {
continue;
}

size_t json_len = 0;
char *json = sentry__path_read_to_buffer(file, &json_len);
if (!json) {
continue;
}
sentry_value_t meta = sentry__value_from_json(json, json_len);
sentry_free(json);

const char *video_filename = sentry_value_as_string(
sentry_value_get_by_key(meta, "videoFilename"));
sentry_path_t *mp4_path = (video_filename && video_filename[0])
? sentry__path_join_str(dir, video_filename)
: NULL;

sentry_envelope_t *envelope = build_replay_envelope(
options, meta, mp4_path, end_sec, scope_source);
if (envelope) {
sentry__capture_envelope(transport, envelope, options);
}

// Leave the sources on disk: the embedder owns `<database>/replays` and
// clears it next launch, and the mp4 may still back a crash attachment.
sentry__path_free(mp4_path);
sentry_value_decref(meta);
}
sentry__pathiter_free(iter);
sentry__path_free(dir);
}
Loading