Fix session recording seeking in long inactive periods (#59637)

* Fix the playback API not returning the screen state when there are no events in requested time range

* Continue playing if in loading state when receiving the stop event

* Push the batch to the buffer before resuming playback on batch events
This commit is contained in:
Ryan Clark
2025-09-29 15:09:27 +00:00
committed by GitHub
parent ee3a3ffbe1
commit fff3946d85
3 changed files with 55 additions and 3 deletions
+8 -1
View File
@@ -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
+37
View File
@@ -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()
@@ -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;
}