Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof api.recordings.list>)

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<typeof api.recordings.list>)
.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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,11 @@ export const sessionRecordingsPlaylistLogic = kea<sessionRecordingsPlaylistLogic
},
{
loadSessionRecordings: async ({ direction, userModifiedFilters }, breakpoint) => {
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 ?? '',
Expand Down Expand Up @@ -1005,11 +1009,14 @@ export const sessionRecordingsPlaylistLogic = kea<sessionRecordingsPlaylistLogic

actions.reportRecordingsListFetched(
loadTimeMs,
values.filters,
filters,
defaultRecordingDurationFilter,
props.analyticsSource
)

// Must run after the fetch report (superseded and abandoned fetches still
// count toward load-time metrics) and before the `values` reads below
// (they throw once the logic is unmounted).
breakpoint()

return {
Expand Down
Loading