mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-21 14:35:22 +08:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user