-
-
Notifications
You must be signed in to change notification settings - Fork 209
feat(session-replay): upload replay envelope from the native crash daemon #1809
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
b437853
Add session replay capturing
tustanivsky 205b288
Handle replay envelope upload internally without public interface
tustanivsky f9a6b08
Fix replay dir path
tustanivsky 0e6246a
Fix json property names
tustanivsky da1371b
Limit replay envelope to native daemon
tustanivsky 8895c83
Clean up
tustanivsky cbab847
Fix lint
tustanivsky 8e27cb0
Fix tests
tustanivsky f461222
Inline helper functions
tustanivsky cb3fc58
Clean up
tustanivsky 772dcbb
Fix lint
tustanivsky 148c0a1
Add replay files cleanup upon successfull envelope creation
tustanivsky be68d1b
Classify `replay_video` under the replay data/rate-limit category
tustanivsky 998e92d
Remove redundant videoFilename check
tustanivsky 0106e1f
Fix comment
tustanivsky 52b905b
Fix issue with uploading orphan replays if crash wasn't captured
tustanivsky 85a3cec
Clean up
tustanivsky 50526b0
Guard replay_recording assembly against string-builder failure
tustanivsky acecbb8
Add helper allowing to check if there's staged replay
tustanivsky 3299782
Reuse crash envelope path var and check if replay is staged during da…
tustanivsky d4dce27
Check replay dir content
tustanivsky 706d491
Check if replay dir hold mp4 clip
tustanivsky b1ab19c
Add integration tests
tustanivsky 6cabda1
Guard buffer add
tustanivsky File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
|
jpnurmi marked this conversation as resolved.
|
||
|
|
||
| #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 <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"); | ||
| } | ||
|
|
||
| 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; | ||
|
tustanivsky marked this conversation as resolved.
|
||
| 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); | ||
|
sentry[bot] marked this conversation as resolved.
Outdated
|
||
| 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)); | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| 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")); | ||
|
tustanivsky marked this conversation as resolved.
|
||
| 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")); | ||
|
Check notice on line 325 in src/session_replay/sentry_session_replay.c
|
||
| sentry_path_t *mp4_path = (video_filename && video_filename[0]) | ||
| ? sentry__path_join_str(dir, video_filename) | ||
| : NULL; | ||
|
sentry-warden[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.