From c5b1c04a9df24039d38a0ac34e13ade92b72328c 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 0aebaed643de3..36359a3d77934 100644 --- a/lib/web/tty_playback.go +++ b/lib/web/tty_playback.go @@ -74,6 +74,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 { @@ -83,13 +88,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)); } }