From 3ea265f6b0ec3886115abe909eae97b532192d1d Mon Sep 17 00:00:00 2001 From: Zac Bergquist Date: Wed, 25 Jun 2025 13:48:09 -0600 Subject: [PATCH] web player: make query parameters optional In #50262, we made it so that the "durationMs" query paramater in the session player URL is optional (at the expense of an extra API call to determine the recording length prior to playback). We did not, however, do the same for the "recordingType" parameter. This commit adds that information to the existing API call. As a result, users who want to build playback URLs directly instead of clicking the play button in the UI have a stable URL format that will play the session. Closes #55780 Closes gravitational/customer-sensitive-requests#472 --- lib/web/tty_playback.go | 12 +++++-- web/packages/teleport/src/Player/Player.tsx | 36 ++++++++++--------- .../src/services/recordings/recordings.ts | 2 +- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/lib/web/tty_playback.go b/lib/web/tty_playback.go index 76c456a4a7010..d3ec7a08b1174 100644 --- a/lib/web/tty_playback.go +++ b/lib/web/tty_playback.go @@ -73,6 +73,11 @@ func (h *Handler) sessionLengthHandle( return nil, trace.Wrap(err) } + type response struct { + Duration int64 `json:"durationMs"` + RecordingType string `json:"recordingType"` + } + evts, errs := clt.StreamSessionEvents(ctx, session.ID(sID), 0) for { select { @@ -82,13 +87,14 @@ func (h *Handler) sessionLengthHandle( if !ok { return nil, trace.NotFound("could not find end event for session %v", sID) } + switch evt := evt.(type) { case *events.SessionEnd: - return map[string]any{"durationMs": evt.EndTime.Sub(evt.StartTime).Milliseconds()}, nil + return response{evt.EndTime.Sub(evt.StartTime).Milliseconds(), "ssh"}, nil case *events.WindowsDesktopSessionEnd: - return map[string]any{"durationMs": evt.EndTime.Sub(evt.StartTime).Milliseconds()}, nil + return response{evt.EndTime.Sub(evt.StartTime).Milliseconds(), "desktop"}, nil case *events.DatabaseSessionEnd: - return map[string]any{"durationMs": evt.EndTime.Sub(evt.StartTime).Milliseconds()}, nil + return response{evt.EndTime.Sub(evt.StartTime).Milliseconds(), "database"}, nil } } } diff --git a/web/packages/teleport/src/Player/Player.tsx b/web/packages/teleport/src/Player/Player.tsx index 608abef2d33cf..57b223d99224f 100644 --- a/web/packages/teleport/src/Player/Player.tsx +++ b/web/packages/teleport/src/Player/Player.tsx @@ -64,8 +64,9 @@ export function Player() { const validRecordingType = validRecordingTypes.includes(recordingType); const durationMs = Number(getUrlParameter('durationMs', search)); - const shouldFetchSessionDuration = - validRecordingType && (!Number.isInteger(durationMs) || durationMs <= 0); + const validDuration = Number.isInteger(durationMs) && durationMs > 0; + + const shouldFetchSessionDuration = !validRecordingType || !validDuration; useEffect(() => { if (shouldFetchSessionDuration) { @@ -75,25 +76,12 @@ export function Player() { const combinedAttempt = shouldFetchSessionDuration ? fetchDurationAttempt - : makeSuccessAttempt({ durationMs }); + : makeSuccessAttempt({ durationMs, recordingType }); function onLogout() { session.logout(); } - if (!validRecordingType) { - return ( - - - - Invalid query parameter recordingType: {recordingType}, should be - one of {validRecordingTypes.join(', ')}. - - - - ); - } - if ( combinedAttempt.status === '' || combinedAttempt.status === 'processing' @@ -119,6 +107,20 @@ export function Player() { ); } + if (!validRecordingTypes.includes(combinedAttempt.data.recordingType)) { + return ( + + + + Invalid query parameter recordingType:{' '} + {combinedAttempt.data.recordingType}, should be one of{' '} + {validRecordingTypes.join(', ')}. + + + + ); + } + return ( @@ -134,7 +136,7 @@ export function Player() { position: 'relative', }} > - {recordingType === 'desktop' ? ( + {combinedAttempt.data.recordingType === 'desktop' ? ( { + ): Promise<{ durationMs: number; recordingType: string }> { return api.get(cfg.getSessionDurationUrl(clusterId, sessionId)); } }