Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4522,8 +4522,8 @@ const api = {
},

recordings: {
async list(params: RecordingsQuery): Promise<RecordingsQueryResponse> {
return await new ApiRequest().recordings().withQueryString(toParams(params)).get()
async list(params: RecordingsQuery, options?: ApiMethodOptions): Promise<RecordingsQueryResponse> {
return await new ApiRequest().recordings().withQueryString(toParams(params)).get(options)
},
async getMatchingEvents(params: string): Promise<MatchingEventsResponse> {
return await new ApiRequest().recordingMatchingEvents().withQueryString(params).get()
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/scenes/session-recordings/playlist/Playlist.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,26 @@ describe('Playlist', () => {
})
expect(screen.queryByText(/selected recording/)).not.toBeInTheDocument()
})

it('recovers from a failed load via the error banner', async () => {
useMocks({ get: { '/api/environments/:team_id/session_recordings': () => [500, { detail: 'boom' }] } })
logic.actions.loadSessionRecordings()

renderPlaylist()

expect((await screen.findAllByText("We couldn't load recordings.")).length).toBeGreaterThan(0)

useMocks({
get: {
'/api/environments/:team_id/session_recordings': { results: [], has_next: false },
},
})
userEvent.click(screen.getAllByText('Try again')[0])

// A successful reload has to clear the error, even when it legitimately finds nothing.
await waitFor(() => {
expect(screen.queryByText("We couldn't load recordings.")).not.toBeInTheDocument()
})
expect((await screen.findAllByTestId('mock-troubleshooting')).length).toBeGreaterThan(0)
})
})
23 changes: 21 additions & 2 deletions frontend/src/scenes/session-recordings/playlist/Playlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -398,13 +398,32 @@ const TitleWithCount = ({ title, count }: { title?: string; count: number }): JS
)
}

const LoadErrorBanner = (): JSX.Element => {
const { sessionRecordingsResponseLoading } = useValues(sessionRecordingsPlaylistLogic)
const { loadSessionRecordings } = useActions(sessionRecordingsPlaylistLogic)

return (
<LemonBanner
type="error"
action={{
children: 'Try again',
loading: sessionRecordingsResponseLoading,
disabledReason: sessionRecordingsResponseLoading ? 'Loading recordings' : undefined,
onClick: () => loadSessionRecordings(),
}}
>
We couldn't load recordings.
</LemonBanner>
)
}

const ListEmptyState = (): JSX.Element => {
const { sessionRecordingsAPIErrored, unusableEventsInFilter } = useValues(sessionRecordingsPlaylistLogic)

return (
<div className="p-3 text-sm text-secondary">
{sessionRecordingsAPIErrored ? (
<LemonBanner type="error">Error while trying to load recordings.</LemonBanner>
<LoadErrorBanner />
) : unusableEventsInFilter.length ? (
<UnusableEventsWarning unusableEventsInFilter={unusableEventsInFilter} />
) : (
Expand All @@ -428,7 +447,7 @@ const CollectionEmptyState = ({
return (
<div className="p-3 text-sm text-secondary">
{sessionRecordingsAPIErrored ? (
<LemonBanner type="error">Error while trying to load recordings.</LemonBanner>
<LoadErrorBanner />
) : unusableEventsInFilter.length ? (
<UnusableEventsWarning unusableEventsInFilter={unusableEventsInFilter} />
) : isSynthetic ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,8 @@ describe('sessionRecordingsPlaylistLogic', () => {
expect.objectContaining({ session_ids: ['s1', 's2'] })
)
expect(listSpy).toHaveBeenLastCalledWith(
expect.objectContaining({ session_ids: ['s1', 's2'], date_from: '-7d' })
expect.objectContaining({ session_ids: ['s1', 's2'], date_from: '-7d' }),
expect.anything()
)
})

Expand All @@ -817,7 +818,10 @@ describe('sessionRecordingsPlaylistLogic', () => {
}).toDispatchActions(['setFilters', 'loadSessionRecordings', 'loadSessionRecordingsSuccess'])

expect(logic.values.filters.session_ids).toBeUndefined()
expect(listSpy).toHaveBeenLastCalledWith(expect.objectContaining({ session_ids: undefined }))
expect(listSpy).toHaveBeenLastCalledWith(
expect.objectContaining({ session_ids: undefined }),
expect.anything()
)
})
})

Expand Down Expand Up @@ -923,7 +927,8 @@ describe('sessionRecordingsPlaylistLogic', () => {
await expectLogic(logic).toDispatchActions(['loadSessionRecordings', 'loadSessionRecordingsSuccess'])

expect(listSpy).toHaveBeenLastCalledWith(
expect.objectContaining({ hide_viewed_recordings: 'current-user' })
expect.objectContaining({ hide_viewed_recordings: 'current-user' }),
expect.anything()
)
})

Expand All @@ -934,7 +939,10 @@ describe('sessionRecordingsPlaylistLogic', () => {
logic.actions.loadSessionRecordings()
await expectLogic(logic).toDispatchActions(['loadSessionRecordingsSuccess'])

expect(listSpy).toHaveBeenLastCalledWith(expect.objectContaining({ hide_viewed_recordings: undefined }))
expect(listSpy).toHaveBeenLastCalledWith(
expect.objectContaining({ hide_viewed_recordings: undefined }),
expect.anything()
)
})

it('bulk delete only marks successfully deleted recordings', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ interface BackendEventsMatching {
export type MatchingEventsMatchType = NoEventsToMatch | EventNamesMatching | EventUUIDsMatching | BackendEventsMatching

export const RECORDINGS_LIMIT = 20
/** Well past the slowest healthy list query, so this only fires on a request that has stalled. */
const RECORDINGS_LIST_TIMEOUT_MS = 90_000
export const PINNED_RECORDINGS_LIMIT = 100 // NOTE: This is high but avoids the need for pagination for now...

export const defaultRecordingDurationFilter: RecordingDurationFilter = {
Expand Down Expand Up @@ -990,7 +992,22 @@ export const sessionRecordingsPlaylistLogic = kea<sessionRecordingsPlaylistLogic
await breakpoint(400) // Debounce for lots of quick filter changes

const startTime = performance.now()
const response = await api.recordings.list(params)
// A stalled connection never rejects on its own, and nothing else can clear the
// loading flag once it is set - that is the endless spinner, which also blocks
// "load more". Time the request out so it lands on the recoverable error state.
const abortController = new AbortController()
const timeout = setTimeout(() => {
abortController.abort()
// The abort travels as an AbortError, which is deliberately dropped by the global
// loader error handler, so report it here or the timeout leaves no trace at all.
posthog.captureException(new Error('Timed out loading session recordings'))
}, RECORDINGS_LIST_TIMEOUT_MS)
let response: RecordingsQueryResponse
try {
response = await api.recordings.list(params, { signal: abortController.signal })
} finally {
clearTimeout(timeout)
}
const loadTimeMs = performance.now() - startTime

actions.reportRecordingsListFetched(loadTimeMs, values.filters, defaultRecordingDurationFilter)
Expand Down Expand Up @@ -1167,7 +1184,7 @@ export const sessionRecordingsPlaylistLogic = kea<sessionRecordingsPlaylistLogic
false,
{
loadSessionRecordingsFailure: () => true,
loadSessionRecordingSuccess: () => false,
loadSessionRecordingsSuccess: () => false,
setFilters: () => false,
setAdvancedFilters: () => false,
loadNext: () => false,
Expand Down
Loading