diff --git a/lib/web/recordingplayback.go b/lib/web/recordingplayback.go index 6e0bc25b451..0c9db057206 100644 --- a/lib/web/recordingplayback.go +++ b/lib/web/recordingplayback.go @@ -536,7 +536,9 @@ func (s *recordingPlayback) streamEvents(ctx context.Context, req *fetchRequest, case evt, ok := <-eventsChan: if !ok { flushBatch() - if req.requestCurrentScreen && !screenSent && inTimeRange { + // Send screen if requested and not already sent + // This handles the case where the stream ends, but we haven't sent the screen yet + if req.requestCurrentScreen && !screenSent { s.sendCurrentScreen(req.requestID, req.startOffset) } sendStop() @@ -546,6 +548,11 @@ func (s *recordingPlayback) streamEvents(ctx context.Context, req *fetchRequest, if !processEvent(evt) { flushBatch() + // Send screen if requested and not already sent when we reach the end of the time range + // (i.e. there was no event in the time range) + if req.requestCurrentScreen && !screenSent { + s.sendCurrentScreen(req.requestID, req.startOffset) + } sendStop() return diff --git a/lib/web/recordingplayback_test.go b/lib/web/recordingplayback_test.go index 8eef65f18c3..d3350e9ec4f 100644 --- a/lib/web/recordingplayback_test.go +++ b/lib/web/recordingplayback_test.go @@ -439,6 +439,43 @@ func TestBufferedEvents_LargeGap(t *testing.T) { require.Equal(t, byte(eventTypeStop), responses[3][0], "Fourth message should be stop event") } +func TestScreenWithNoEventsInRange(t *testing.T) { + ws, _ := createWebSocket(t, func(mockClient *mockStreamClient) { + <-mockClient.eventRequested + + mockClient.sendEvent(&apievents.SessionStart{ + TerminalSize: "80:24", + }) + mockClient.sendEvent(&apievents.SessionPrint{ + DelayMilliseconds: 500, + Data: []byte("Early content"), + }) + mockClient.sendEvent(&apievents.SessionPrint{ + DelayMilliseconds: 1500, + Data: []byte(" that should appear in screen"), + }) + // No events in the 3000-4000ms range we'll request + mockClient.sendEvent(&apievents.SessionEnd{ + StartTime: time.Now(), + EndTime: time.Now().Add(5 * time.Second), + }) + }) + + // Request a time range with no events but ask for screen + responses := fetchAndCollectResponses(t, ws, 3000, 4000, true, 1) + + require.Len(t, responses, 3, "Should receive 3 messages: start, screen, stop") + + require.Equal(t, byte(eventTypeStart), responses[0][0], "First message should be start event") + require.Equal(t, byte(eventTypeScreen), responses[1][0], "Second message should be screen event") + + // The screen should contain the content from events before the requested range + require.Contains(t, string(responses[1][responseHeaderSize:]), "Early content that should appear in screen", + "Screen should contain terminal state even though no events in requested range") + + require.Equal(t, byte(eventTypeStop), responses[2][0], "Third message should be stop event") +} + func TestUnsupportedRequest(t *testing.T) { mockClient := newMockStreamClient() diff --git a/web/packages/teleport/src/SessionRecordings/view/stream/SessionStream.ts b/web/packages/teleport/src/SessionRecordings/view/stream/SessionStream.ts index 254f703d342..44c1161218a 100644 --- a/web/packages/teleport/src/SessionRecordings/view/stream/SessionStream.ts +++ b/web/packages/teleport/src/SessionRecordings/view/stream/SessionStream.ts @@ -332,6 +332,8 @@ export class SessionStream< // mark loading as false as soon as we get any batch event so we can start playing again this.loading = false; + this.pushBatchToBuffer(parsed.events); + if (this.state === PlayerState.Loading) { if (this.wasPlayingBeforeSeek) { this.play(); @@ -340,8 +342,6 @@ export class SessionStream< } } - this.pushBatchToBuffer(parsed.events); - const hasEnd = parsed.events[parsed.events.length - 1].type === this.endEventType; @@ -362,6 +362,14 @@ export class SessionStream< this.updateLoadedTimes(parsed.startTime, parsed.endTime); + if (this.state === PlayerState.Loading) { + if (this.wasPlayingBeforeSeek) { + this.play(); + } else { + this.setState(PlayerState.Paused); + } + } + return; }