From 3641eeeedbf171ff8744d421038f17bf00a3d7cd Mon Sep 17 00:00:00 2001 From: Ryan Clark Date: Wed, 1 Apr 2026 16:12:13 +0200 Subject: [PATCH] Surface errors during recording playback/loading errors (#64847) * Return a nicer error when a session recording cannot be accessed * Surface loading and playback errors in the recording player * Remove test error * Add comment about hasReceivedData --- lib/web/recordingplayback.go | 5 ++ .../view/player/RecordingPlayer.tsx | 54 +++++++++++++++++++ .../view/stream/SessionStream.ts | 12 +++++ 3 files changed, 71 insertions(+) diff --git a/lib/web/recordingplayback.go b/lib/web/recordingplayback.go index 0c9db057206..064220adfa9 100644 --- a/lib/web/recordingplayback.go +++ b/lib/web/recordingplayback.go @@ -631,6 +631,11 @@ func (s *recordingPlayback) sendEventBatch(batch []sessionEvent, requestID int) // sendError sends an error event to the client. func (s *recordingPlayback) sendError(err error, requestID int) { + if trace.IsAccessDenied(err) { + s.sendEvent(eventTypeError, 0, []byte("Session recording not found"), requestID) + return + } + s.sendEvent(eventTypeError, 0, []byte(err.Error()), requestID) } diff --git a/web/packages/teleport/src/SessionRecordings/view/player/RecordingPlayer.tsx b/web/packages/teleport/src/SessionRecordings/view/player/RecordingPlayer.tsx index a4043be66b8..5d351baec3a 100644 --- a/web/packages/teleport/src/SessionRecordings/view/player/RecordingPlayer.tsx +++ b/web/packages/teleport/src/SessionRecordings/view/player/RecordingPlayer.tsx @@ -25,12 +25,16 @@ import { useState, type RefObject, } from 'react'; +import { Link } from 'react-router'; import styled, { keyframes } from 'styled-components'; import { useEventListener } from 'usehooks-ts'; import { Box, Flex } from 'design'; +import { ButtonSecondary } from 'design'; +import { Alert } from 'design/Alert'; import { Pause, Play } from 'design/Icon'; +import cfg from 'teleport/config'; import type { SessionRecordingEvent } from 'teleport/services/recordings'; import { CurrentEventInfo, @@ -47,6 +51,7 @@ import { SessionStream, } from 'teleport/SessionRecordings/view/stream/SessionStream'; import type { BaseEvent } from 'teleport/SessionRecordings/view/stream/types'; +import useStickyClusterId from 'teleport/useStickyClusterId'; export interface RecordingPlayerProps< TEvent extends BaseEvent, @@ -84,7 +89,11 @@ export function RecordingPlayer< ref, ws, }: RecordingPlayerProps) { + const { clusterId } = useStickyClusterId(); + const [playerState, setPlayerState] = useState(PlayerState.Loading); + const [errorText, setErrorText] = useState(''); + const [isLoadingError, setIsLoadingError] = useState(false); const [speed, setSpeed] = useState(1); const [showPlayButton, setShowPlayButton] = useState(true); @@ -103,6 +112,15 @@ export function RecordingPlayer< setPlayerState(next); }); + stream.on('loadingError', message => { + setErrorText(message); + setIsLoadingError(true); + }); + + stream.on('error', message => { + setErrorText(message); + }); + stream.on('time', time => { if (controlsRef.current) { controlsRef.current.setTime(time); @@ -165,6 +183,25 @@ export function RecordingPlayer< moveToTime: handleSeek, })); + if (isLoadingError) { + return ( + + {errorText} + + + Go back to Session Recordings + + + ); + } + return ( + {errorText && ( + + + {errorText} + + + )} + {events && ( p.theme.space[2]}px; +`; + const PlayerBox = styled.div` background: black; // black bars on the sides of the terminal flex: 1; diff --git a/web/packages/teleport/src/SessionRecordings/view/stream/SessionStream.ts b/web/packages/teleport/src/SessionRecordings/view/stream/SessionStream.ts index 5eb85556d3a..2d50c0904ea 100644 --- a/web/packages/teleport/src/SessionRecordings/view/stream/SessionStream.ts +++ b/web/packages/teleport/src/SessionRecordings/view/stream/SessionStream.ts @@ -38,6 +38,8 @@ export enum PlayerState { } interface SessionStreamEvents { + error: [string]; + loadingError: [string]; state: [PlayerState]; time: [number]; } @@ -80,6 +82,9 @@ export class SessionStream< private startTime = 0; private atEnd = false; + // hasReceivedData is used to determine an error happened during the initial load of the recording + // or during the stream. It does not get reset on seek, as seeking happens after the initial load. + private hasReceivedData = false; // we track loading separately from the loading state as we can still play and load at the same time private loading = true; private requestId = 0; @@ -362,6 +367,7 @@ export class SessionStream< if (isBatchEvent(parsed)) { // mark loading as false as soon as we get any batch event so we can start playing again this.loading = false; + this.hasReceivedData = true; this.pushBatchToBuffer(parsed.events); @@ -405,6 +411,12 @@ export class SessionStream< } if (isErrorEvent(parsed)) { + if (this.hasReceivedData) { + this.emit('error', parsed.error); + } else { + this.emit('loadingError', parsed.error); + } + return; }