diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts index a3f121d85928..a50be6aa3eae 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts @@ -1247,6 +1247,75 @@ describe('sessionRecordingsPlaylistLogic', () => { }) }) + describe('superseding or unmounting an in-flight load', () => { + afterEach(() => { + jest.restoreAllMocks() + }) + + it('abandons the load instead of failing on unmounted values reads', async () => { + let resolveList: (value: unknown) => void = () => {} + const pendingList = new Promise((resolve) => { + resolveList = resolve + }) + const listSpy = jest + .spyOn(api.recordings, 'list') + .mockImplementation(() => pendingList as ReturnType) + + const embeddedLogic = sessionRecordingsPlaylistLogic({ logicKey: 'unmount-mid-load' }) + embeddedLogic.mount() + + // afterMount kicks off a load; wait for it to get past the debounce and issue the request + while (listSpy.mock.calls.length === 0) { + await new Promise((resolve) => setTimeout(resolve, 25)) + } + + embeddedLogic.unmount() + resolveList({ results: [], has_next: false }) + + await expectLogic(embeddedLogic) + .toFinishAllListeners() + .toNotHaveDispatchedActions(['loadSessionRecordingsFailure']) + }) + + it('still reports a superseded fetch, with the filters the request was built from', async () => { + let resolveList: (value: unknown) => void = () => {} + const pendingList = new Promise((resolve) => { + resolveList = resolve + }) + const listSpy = jest + .spyOn(api.recordings, 'list') + .mockImplementationOnce(() => pendingList as ReturnType) + .mockImplementation( + () => + Promise.resolve({ results: [], has_next: false } as unknown) as ReturnType< + typeof api.recordings.list + > + ) + + const supersededLogic = sessionRecordingsPlaylistLogic({ logicKey: 'superseded-mid-load' }) + supersededLogic.mount() + + // afterMount kicks off a load; wait for it to get past the debounce and issue the request + while (listSpy.mock.calls.length === 0) { + await new Promise((resolve) => setTimeout(resolve, 25)) + } + + // supersede the in-flight load, then let its stale response land + supersededLogic.actions.setFilters({ filter_test_accounts: true }) + resolveList({ results: [], has_next: false }) + + await expectLogic(supersededLogic) + .toDispatchActions([ + (action) => + action.type === supersededLogic.actionTypes.reportRecordingsListFetched && + action.payload.filters.filter_test_accounts !== true, + ]) + .toFinishAllListeners() + + supersededLogic.unmount() + }) + }) + describe('convertUniversalFiltersToRecordingsQuery', () => { it('passes the visited_page filter as a recording property', () => { const result = convertUniversalFiltersToRecordingsQuery({ diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts index 263938657132..fc0c9ba95ed3 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts @@ -953,7 +953,11 @@ export const sessionRecordingsPlaylistLogic = kea { - const convertedQuery = convertUniversalFiltersToRecordingsQuery(values.filters) + // Captured before the awaits: `values` reads throw if this logic unmounts + // mid-flight, and the fetch report must carry the filters the request was + // built from, not whatever they are once the response lands. + const filters = values.filters + const convertedQuery = convertUniversalFiltersToRecordingsQuery(filters) const params: RecordingsQuery & { add_events_to_property_queries?: '1' } = { ...convertedQuery, person_uuid: props.personUUID ?? '', @@ -1005,11 +1009,14 @@ export const sessionRecordingsPlaylistLogic = kea