diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 155235d624..e2de2e3c6d 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -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 (`/__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"); diff --git a/src/sentry_session_replay.h b/src/sentry_session_replay.h index e2b21a657e..e92a3f21e1 100644 --- a/src/sentry_session_replay.h +++ b/src/sentry_session_replay.h @@ -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 + * `/replays/` (a `replay-.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 (`/__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 diff --git a/src/session_replay/sentry_session_replay.c b/src/session_replay/sentry_session_replay.c index 3799d56f58..953c7cf6ea 100644 --- a/src/session_replay/sentry_session_replay.c +++ b/src/session_replay/sentry_session_replay.c @@ -1,7 +1,341 @@ #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" + +#if defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4127) // conditional expression is constant +# if defined(__clang__) // clang-cl +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdocumentation" +# pragma clang diagnostic ignored "-Wpre-c11-compat" +# endif +#elif defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wstatic-in-inline" +#endif + +#include "../vendor/mpack.h" + +#if defined(_MSC_VER) +# pragma warning(pop) +# ifdef __clang__ // clang-cl +# pragma clang diagnostic pop +# endif +#elif defined(__clang__) +# pragma clang diagnostic pop +#endif + +#include + 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"); } + +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; +} + +// 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)) { + 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); + } + } + + 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 + = sentry_value_as_int32(sentry_value_get_by_key(meta, "width")); + const int32_t height + = sentry_value_as_int32(sentry_value_get_by_key(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(sentry_value_as_int32( + sentry_value_get_by_key(meta, "frameCount")))); + sentry_value_set_by_key(payload, "frameRate", + sentry_value_new_int32( + sentry_value_as_int32(sentry_value_get_by_key(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 + = sentry_value_as_double(sentry_value_get_by_key(meta, "durationMs")); + if (end_sec <= 0.0) { + end_sec = sentry_value_as_double( + sentry_value_get_by_key(meta, "endTimestampSec")); + } + const double start_sec = end_sec - duration_ms / 1000.0; + const int32_t segment_id + = sentry_value_as_int32(sentry_value_get_by_key(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) { + 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); + } + + sentry__path_free(mp4_path); + sentry_value_decref(meta); + } + sentry__pathiter_free(iter); + sentry__path_free(dir); +}