From b24f91627d6e2e3ddcd3bbb224122c89c6187e9a 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 838c1b45d1efd..87b13a13b312e 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)); } }