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
This commit is contained in:
Ryan Clark
2026-04-01 14:12:13 +00:00
committed by GitHub
parent 3a09c34131
commit 3641eeeedb
3 changed files with 71 additions and 0 deletions
+5
View File
@@ -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)
}
@@ -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<TEventType>,
@@ -84,7 +89,11 @@ export function RecordingPlayer<
ref,
ws,
}: RecordingPlayerProps<TEvent>) {
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 (
<Flex
height="100%"
flex={1}
p={3}
flexDirection="column"
alignItems="center"
justifyContent="center"
>
<Alert kind="danger">{errorText}</Alert>
<ButtonSecondary as={Link} to={cfg.getRecordingsRoute(clusterId)}>
Go back to Session Recordings
</ButtonSecondary>
</Flex>
);
}
return (
<Box height="100%" flex={1} p={3}>
<Flex
@@ -177,6 +214,14 @@ export function RecordingPlayer<
overflow="hidden"
position="relative"
>
{errorText && (
<ErrorOverlay>
<Alert kind="danger" bg="levels.sunken">
{errorText}
</Alert>
</ErrorOverlay>
)}
{events && (
<CurrentEventInfo
events={events}
@@ -342,6 +387,15 @@ const AnimatedState = styled.div`
animation: ${appear} 0.8s linear forwards;
`;
const ErrorOverlay = styled.div`
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
padding: ${p => p.theme.space[2]}px;
`;
const PlayerBox = styled.div`
background: black; // black bars on the sides of the terminal
flex: 1;
@@ -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;
}