0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-21 13:39:22 +01:00

feat: Remove flag for infinite scroller and fix issue with reloading (#16148)

This commit is contained in:
Ben White 2023-06-20 11:45:44 +02:00 committed by GitHub
parent 2a6f2a98a4
commit 687190fa89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 134 additions and 251 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

After

Width:  |  Height:  |  Size: 268 KiB

View File

@ -145,7 +145,6 @@ export const FEATURE_FLAGS = {
RECORDINGS_DOM_EXPLORER: 'recordings-dom-explorer', // owner: #team-session-recordings
AUTO_REDIRECT: 'auto-redirect', // owner: @lharries
SESSION_RECORDING_BLOB_REPLAY: 'session-recording-blob-replay', // owner: #team-monitoring
SESSION_RECORDING_INFINITE_LIST: 'session-recording-infinite-list', // owner: #team-monitoring
SESSION_RECORDING_SUMMARY_LISTING: 'session-recording-summary-listing', // owner: #team-monitoring
SURVEYS: 'surveys', // owner: @liyiy
NEW_EMPTY_STATES: 'new-empty-states', // experiment, owner: @raquelmsmith

View File

@ -55,6 +55,7 @@ export const personsLogic = kea<personsLogicType>({
deleteProperty: (key: string) => ({ key }),
navigateToCohort: (cohort: CohortType) => ({ cohort }),
navigateToTab: (tab: PersonsTabType) => ({ tab }),
setActiveTab: (tab: PersonsTabType) => ({ tab }),
setSplitMergeModalShown: (shown: boolean) => ({ shown }),
setDistinctId: (distinctId: string) => ({ distinctId }),
},
@ -94,6 +95,7 @@ export const personsLogic = kea<personsLogicType>({
null as PersonsTabType | null,
{
navigateToTab: (_, { tab }) => tab,
setActiveTab: (_, { tab }) => tab,
},
],
splitMergeModalShown: [
@ -305,11 +307,7 @@ export const personsLogic = kea<personsLogicType>({
},
navigateToTab: () => {
if (props.syncWithUrl && router.values.location.pathname.indexOf('/person') > -1) {
const searchParams = { ...router.values.searchParams }
if (values.activeTab !== PersonsTabType.HISTORY) {
delete searchParams['page']
}
const searchParams = {}
return [
router.values.location.pathname,
@ -333,16 +331,14 @@ export const personsLogic = kea<personsLogicType>({
}
}
},
'/person/*': ({ _: rawPersonDistinctId }, { sessionRecordingId }, { activeTab }) => {
'/person/*': ({ _: rawPersonDistinctId }, {}, { activeTab }) => {
if (props.syncWithUrl) {
if (sessionRecordingId) {
actions.navigateToTab(PersonsTabType.SESSION_RECORDINGS)
} else if (activeTab && values.activeTab !== activeTab) {
actions.navigateToTab(activeTab as PersonsTabType)
if (activeTab && values.activeTab !== activeTab) {
actions.setActiveTab(activeTab as PersonsTabType)
}
if (!activeTab && values.activeTab && values.activeTab !== PersonsTabType.PROPERTIES) {
actions.navigateToTab(PersonsTabType.PROPERTIES)
if (!activeTab) {
actions.setActiveTab(PersonsTabType.PROPERTIES)
}
if (rawPersonDistinctId) {

View File

@ -211,19 +211,24 @@ export function SessionRecordingsFilters({
}}
/>
<LemonLabel info="Show recordings by persons who match the set criteria">
Filter by persons and cohorts
</LemonLabel>
{showPropertyFilters && (
<PropertyFilters
pageKey={'session-recordings'}
taxonomicGroupTypes={[TaxonomicFilterGroupType.PersonProperties, TaxonomicFilterGroupType.Cohorts]}
propertyFilters={filters.properties}
onChange={(properties) => {
setFilters({ properties })
}}
/>
<>
<LemonLabel info="Show recordings by persons who match the set criteria">
Filter by persons and cohorts
</LemonLabel>
<PropertyFilters
pageKey={'session-recordings'}
taxonomicGroupTypes={[
TaxonomicFilterGroupType.PersonProperties,
TaxonomicFilterGroupType.Cohorts,
]}
propertyFilters={filters.properties}
onChange={(properties) => {
setFilters({ properties })
}}
/>
</>
)}
<FlaggedFeature flag={FEATURE_FLAGS.SESSION_RECORDING_SHOW_CONSOLE_LOGS_FILTER} match={true}>

View File

@ -29,7 +29,7 @@ describe('sessionPlayerModalLogic', () => {
})
it('is set by openSessionPlayer and cleared by closeSessionPlayer', async () => {
expectLogic(logic, () => logic.actions.openSessionPlayer({ id: 'abc' }))
.toDispatchActions(['getSessionRecordingsSuccess'])
.toDispatchActions(['loadSessionRecordingsSuccess'])
.toMatchValues({
selectedSessionRecording: { id: 'abc' },
activeSessionRecording: listOfSessionRecordings[0],

View File

@ -12,12 +12,10 @@ import './SessionRecordingsPlaylist.scss'
import { SessionRecordingPlayer } from '../player/SessionRecordingPlayer'
import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage'
import { LemonButton, LemonDivider, LemonSwitch } from '@posthog/lemon-ui'
import { IconChevronLeft, IconChevronRight, IconFilter, IconPause, IconPlay, IconWithCount } from 'lib/lemon-ui/icons'
import { IconFilter, IconPause, IconPlay, IconWithCount } from 'lib/lemon-ui/icons'
import { SessionRecordingsList } from './SessionRecordingsList'
import clsx from 'clsx'
import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { FEATURE_FLAGS } from 'lib/constants'
import { Spinner } from 'lib/lemon-ui/Spinner'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { SessionRecordingsFilters } from '../filters/SessionRecordingsFilters'
@ -45,7 +43,6 @@ export function RecordingsLists({
const {
filters,
hasNext,
hasPrev,
sessionRecordings,
sessionRecordingsResponseLoading,
activeSessionRecording,
@ -55,23 +52,11 @@ export function RecordingsLists({
totalFiltersCount,
listingVersion,
} = useValues(logic)
const {
setSelectedRecordingId,
loadNext,
loadPrev,
setFilters,
maybeLoadSessionRecordings,
setShowFilters,
resetFilters,
} = useActions(logic)
const { setSelectedRecordingId, setFilters, maybeLoadSessionRecordings, setShowFilters, resetFilters } =
useActions(logic)
const { autoplayDirection } = useValues(playerSettingsLogic)
const { toggleAutoplayDirection } = useActions(playerSettingsLogic)
const { featureFlags } = useValues(featureFlagLogic)
const infiniteScrollerEnabled = featureFlags[FEATURE_FLAGS.SESSION_RECORDING_INFINITE_LIST]
const [collapsed, setCollapsed] = useState({ pinned: false, other: false })
const offset = filters.offset ?? 0
const nextLength = offset + (sessionRecordingsResponseLoading ? RECORDINGS_LIMIT : sessionRecordings.length)
const onRecordingClick = (recording: SessionRecordingType): void => {
setSelectedRecordingId(recording.id)
@ -81,26 +66,6 @@ export function RecordingsLists({
setFilters(defaultPageviewPropertyEntityFilter(filters, property, value))
}
const paginationControls = nextLength ? (
<div className="flex items-center gap-1 mx-2">
<span>{`${offset + 1} - ${nextLength}`}</span>
<LemonButton
icon={<IconChevronLeft />}
status="stealth"
size="small"
disabled={!hasPrev}
onClick={() => loadPrev()}
/>
<LemonButton
icon={<IconChevronRight />}
status="stealth"
disabled={!hasNext}
size="small"
onClick={() => loadNext()}
/>
</div>
) : null
return (
<>
<div className="SessionRecordingsPlaylist__lists">
@ -144,27 +109,23 @@ export function RecordingsLists({
title={!playlistShortId ? 'Recordings' : 'Other recordings'}
titleRight={
<>
{infiniteScrollerEnabled ? (
sessionRecordings.length ? (
<Tooltip
placement="bottom"
title={
<>
Showing {sessionRecordings.length} results.
<br />
Scrolling to the bottom or the top of the list will load older or newer
recordings respectively.
</>
}
>
<span>
<CounterBadge>{Math.min(999, sessionRecordings.length)}+</CounterBadge>
</span>
</Tooltip>
) : null
) : (
paginationControls
)}
{sessionRecordings.length ? (
<Tooltip
placement="bottom"
title={
<>
Showing {sessionRecordings.length} results.
<br />
Scrolling to the bottom or the top of the list will load older or newer
recordings respectively.
</>
}
>
<span>
<CounterBadge>{Math.min(999, sessionRecordings.length)}+</CounterBadge>
</span>
</Tooltip>
) : null}
<LemonButton
noPadding
@ -249,30 +210,25 @@ export function RecordingsLists({
</div>
}
activeRecordingId={activeSessionRecording?.id}
onScrollToEnd={infiniteScrollerEnabled ? () => maybeLoadSessionRecordings('older') : undefined}
onScrollToStart={infiniteScrollerEnabled ? () => maybeLoadSessionRecordings('newer') : undefined}
onScrollToEnd={() => maybeLoadSessionRecordings('older')}
onScrollToStart={() => maybeLoadSessionRecordings('newer')}
footer={
infiniteScrollerEnabled ? (
<>
<LemonDivider />
<div className="m-4 h-10 flex items-center justify-center gap-2 text-muted-alt">
{sessionRecordingsResponseLoading ? (
<>
<Spinner monocolor /> Loading older recordings
</>
) : hasNext ? (
<LemonButton
status="primary"
onClick={() => maybeLoadSessionRecordings('older')}
>
Load more
</LemonButton>
) : (
'No more results'
)}
</div>
</>
) : null
<>
<LemonDivider />
<div className="m-4 h-10 flex items-center justify-center gap-2 text-muted-alt">
{sessionRecordingsResponseLoading ? (
<>
<Spinner monocolor /> Loading older recordings
</>
) : hasNext ? (
<LemonButton status="primary" onClick={() => maybeLoadSessionRecordings('older')}>
Load more
</LemonButton>
) : (
'No more results'
)}
</div>
</>
}
draggableHref={urls.replay(ReplayTabs.Recent, filters)}
/>

View File

@ -15,6 +15,7 @@ describe('sessionRecordingsListLogic', () => {
let logic: ReturnType<typeof sessionRecordingsListLogic.build>
const aRecording = { id: 'abc', viewed: false, recording_duration: 10 }
const listOfSessionRecordings = [aRecording]
jest.setTimeout(500)
beforeEach(() => {
useMocks({
@ -103,7 +104,7 @@ describe('sessionRecordingsListLogic', () => {
describe('core assumptions', () => {
it('loads recent recordings and pinned recordings after mounting', async () => {
await expectLogic(logic)
.toDispatchActionsInAnyOrder(['getSessionRecordingsSuccess', 'loadPinnedRecordingsSuccess'])
.toDispatchActionsInAnyOrder(['loadSessionRecordingsSuccess', 'loadPinnedRecordingsSuccess'])
.toMatchValues({
sessionRecordings: listOfSessionRecordings,
pinnedRecordingsResponse: {
@ -119,30 +120,30 @@ describe('sessionRecordingsListLogic', () => {
})
it('is set by setSessionRecordingId', async () => {
expectLogic(logic, () => logic.actions.setSelectedRecordingId('abc'))
.toDispatchActions(['getSessionRecordingsSuccess'])
.toDispatchActions(['loadSessionRecordingsSuccess'])
.toMatchValues({
selectedRecordingId: 'abc',
activeSessionRecording: listOfSessionRecordings[0],
})
expect(router.values.hashParams).toHaveProperty('sessionRecordingId', 'abc')
expect(router.values.searchParams).toHaveProperty('sessionRecordingId', 'abc')
})
it('is partial if sessionRecordingId not in list', async () => {
expectLogic(logic, () => logic.actions.setSelectedRecordingId('not-in-list'))
.toDispatchActions(['getSessionRecordingsSuccess'])
.toDispatchActions(['loadSessionRecordingsSuccess'])
.toMatchValues({
selectedRecordingId: 'not-in-list',
activeSessionRecording: { id: 'not-in-list' },
})
expect(router.values.hashParams).toHaveProperty('sessionRecordingId', 'not-in-list')
expect(router.values.searchParams).toHaveProperty('sessionRecordingId', 'not-in-list')
})
it('is read from the URL on the session recording page', async () => {
router.actions.push('/replay', {}, { sessionRecordingId: 'abc' })
expect(router.values.hashParams).toHaveProperty('sessionRecordingId', 'abc')
expect(router.values.searchParams).toHaveProperty('sessionRecordingId', 'abc')
await expectLogic(logic)
.toDispatchActionsInAnyOrder(['setSelectedRecordingId', 'getSessionRecordingsSuccess'])
.toDispatchActionsInAnyOrder(['setSelectedRecordingId', 'loadSessionRecordingsSuccess'])
.toMatchValues({
selectedRecordingId: 'abc',
activeSessionRecording: listOfSessionRecordings[0],
@ -156,11 +157,11 @@ describe('sessionRecordingsListLogic', () => {
})
it('returns the first session recording if none selected', () => {
expectLogic(logic).toDispatchActions(['getSessionRecordingsSuccess']).toMatchValues({
expectLogic(logic).toDispatchActions(['loadSessionRecordingsSuccess']).toMatchValues({
selectedRecordingId: undefined,
activeSessionRecording: listOfSessionRecordings[0],
})
expect(router.values.hashParams).not.toHaveProperty('sessionRecordingId', 'not-in-list')
expect(router.values.searchParams).not.toHaveProperty('sessionRecordingId', 'not-in-list')
})
})
@ -175,7 +176,7 @@ describe('sessionRecordingsListLogic', () => {
events: [{ id: '$autocapture', type: 'events', order: 0, name: '$autocapture' }],
})
})
.toDispatchActions(['setFilters', 'getSessionRecordings', 'getSessionRecordingsSuccess'])
.toDispatchActions(['setFilters', 'loadSessionRecordings', 'loadSessionRecordingsSuccess'])
.toMatchValues({
sessionRecordings: ['List of recordings filtered by events'],
})
@ -185,26 +186,6 @@ describe('sessionRecordingsListLogic', () => {
})
})
describe('limit and offset', () => {
it('is set by loadNext and loadPrev and gets the right results and sets the url', async () => {
await expectLogic(logic, () => {
logic.actions.loadNext()
})
.toMatchValues({ filters: expect.objectContaining({ offset: RECORDINGS_LIMIT }) })
.toDispatchActions(['loadNext', 'getSessionRecordingsSuccess'])
.toMatchValues({ sessionRecordings: [`List of recordings offset by ${RECORDINGS_LIMIT}`] })
expect(router.values.searchParams.filters).toHaveProperty('offset', RECORDINGS_LIMIT)
await expectLogic(logic, () => {
logic.actions.loadPrev()
})
.toMatchValues({ filters: expect.objectContaining({ offset: 0 }) })
.toDispatchActions(['loadPrev', 'getSessionRecordingsSuccess'])
.toMatchValues({ sessionRecordings: listOfSessionRecordings })
expect(router.values.searchParams.filters).toHaveProperty('offset', 0)
})
})
describe('date range', () => {
it('is set by setFilters and fetches results from server and sets the url', async () => {
await expectLogic(logic, () => {
@ -219,7 +200,7 @@ describe('sessionRecordingsListLogic', () => {
date_to: '2021-10-20',
}),
})
.toDispatchActions(['setFilters', 'getSessionRecordingsSuccess'])
.toDispatchActions(['setFilters', 'loadSessionRecordingsSuccess'])
.toMatchValues({ sessionRecordings: ['Recordings filtered by date'] })
expect(router.values.searchParams.filters).toHaveProperty('date_from', '2021-10-05')
@ -248,7 +229,7 @@ describe('sessionRecordingsListLogic', () => {
},
}),
})
.toDispatchActions(['setFilters', 'getSessionRecordingsSuccess'])
.toDispatchActions(['setFilters', 'loadSessionRecordingsSuccess'])
.toMatchValues({ sessionRecordings: ['Recordings filtered by duration'] })
expect(router.values.searchParams.filters).toHaveProperty('session_recording_duration', {
@ -289,7 +270,7 @@ describe('sessionRecordingsListLogic', () => {
})
logic.mount()
await expectLogic(logic).toDispatchActions(['getSessionRecordingsSuccess']).toMatchValues({
await expectLogic(logic).toDispatchActions(['loadSessionRecordingsSuccess']).toMatchValues({
selectedRecordingId: 'abc',
})
@ -340,7 +321,7 @@ describe('sessionRecordingsListLogic', () => {
events: [{ id: '$autocapture', type: 'events', order: 0, name: '$autocapture' }],
})
})
.toDispatchActions(['setFilters', 'getSessionRecordings', 'getSessionRecordingsSuccess'])
.toDispatchActions(['setFilters', 'loadSessionRecordings', 'loadSessionRecordingsSuccess'])
.toMatchValues({
sessionRecordings: ['List of recordings filtered by events'],
})
@ -365,7 +346,7 @@ describe('sessionRecordingsListLogic', () => {
})
await expectLogic(logic)
.toDispatchActions(['replaceFilters'])
.toDispatchActions(['setFilters'])
.toMatchValues({
filters: {
events: [{ id: '$autocapture', type: 'events', order: 0, name: '$autocapture' }],
@ -373,6 +354,8 @@ describe('sessionRecordingsListLogic', () => {
date_from: '2021-10-01',
date_to: '2021-10-10',
offset: 50,
console_logs: [],
properties: [],
session_recording_duration: {
type: PropertyFilterType.Recording,
key: 'duration',
@ -391,11 +374,19 @@ describe('sessionRecordingsListLogic', () => {
})
await expectLogic(logic)
.toDispatchActions(['replaceFilters'])
.toDispatchActions(['setFilters'])
.toMatchValues({
customFilters: {
actions: [{ id: '1', type: 'actions', order: 0, name: 'View Recording' }],
},
filters: {
actions: [{ id: '1', type: 'actions', order: 0, name: 'View Recording' }],
session_recording_duration: defaultRecordingDurationFilter,
console_logs: [],
date_from: '-7d',
date_to: null,
events: [],
properties: [],
},
})
})
@ -413,13 +404,13 @@ describe('sessionRecordingsListLogic', () => {
it('loads session recordings for a specific user', async () => {
await expectLogic(logic)
.toDispatchActions(['getSessionRecordingsSuccess'])
.toDispatchActions(['loadSessionRecordingsSuccess'])
.toMatchValues({ sessionRecordings: ["List of specific user's recordings from server"] })
})
it('reads sessionRecordingId from the URL on the person page', async () => {
router.actions.push('/person/123', {}, { sessionRecordingId: 'abc' })
expect(router.values.hashParams).toHaveProperty('sessionRecordingId', 'abc')
expect(router.values.searchParams).toHaveProperty('sessionRecordingId', 'abc')
await expectLogic(logic).toDispatchActions([logic.actionCreators.setSelectedRecordingId('abc')])
})

View File

@ -1,6 +1,6 @@
import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea'
import api from 'lib/api'
import { toParams } from 'lib/utils'
import { objectClean, toParams } from 'lib/utils'
import {
AnyPropertyFilter,
PropertyFilterType,
@ -22,11 +22,9 @@ import { sessionRecordingsListPropertiesLogic } from './sessionRecordingsListPro
import { playerSettingsLogic } from '../player/playerSettingsLogic'
export type PersonUUID = string
interface Params {
filters?: RecordingFilters
}
interface HashParams {
sessionRecordingId?: SessionRecordingId
}
@ -146,7 +144,6 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
}),
actions({
setFilters: (filters: Partial<RecordingFilters>) => ({ filters }),
replaceFilters: (filters: RecordingFilters) => ({ filters }),
setShowFilters: (showFilters: boolean) => ({ showFilters }),
resetFilters: true,
setSelectedRecordingId: (id: SessionRecordingType['id'] | null) => ({
@ -154,7 +151,6 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
}),
loadAllRecordings: true,
loadPinnedRecordings: true,
getSessionRecordings: true,
loadSessionRecordings: (direction?: 'newer' | 'older') => ({ direction }),
maybeLoadSessionRecordings: (direction?: 'newer' | 'older') => ({ direction }),
loadNext: true,
@ -167,27 +163,6 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
has_next: false,
} as SessionRecordingsResponse,
{
getSessionRecordings: async (_, breakpoint) => {
const paramsDict = {
...values.filters,
person_uuid: props.personUUID ?? '',
limit: RECORDINGS_LIMIT,
version: values.listingVersion,
}
const params = toParams(paramsDict)
await breakpoint(100) // Debounce for lots of quick filter changes
const startTime = performance.now()
const response = await api.recordings.list(params)
const loadTimeMs = performance.now() - startTime
actions.reportRecordingsListFetched(loadTimeMs, values.listingVersion)
breakpoint()
return response
},
loadSessionRecordings: async ({ direction }, breakpoint) => {
const currentResults = direction ? values.sessionRecordingsResponse?.results ?? [] : []
@ -278,20 +253,14 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
],
})),
reducers(({ props }) => ({
filters: [
props.filters || getDefaultFilters(props.personUUID),
customFilters: [
(props.filters ?? null) as RecordingFilters | null,
{
replaceFilters: (_, { filters }) => {
return {
...filters,
session_recording_duration:
filters.session_recording_duration || defaultRecordingDurationFilter,
}
},
setFilters: (state, { filters }) => ({
...state,
...filters,
}),
resetFilters: () => null,
},
],
showFilters: [
@ -303,9 +272,6 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
sessionRecordings: [
[] as SessionRecordingType[],
{
getSessionRecordingsSuccess: (_, { sessionRecordingsResponse }) => {
return [...(sessionRecordingsResponse?.results ?? [])]
},
loadSessionRecordingsSuccess: (_, { sessionRecordingsResponse }) => {
return [...(sessionRecordingsResponse?.results ?? [])]
},
@ -331,43 +297,13 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
})),
listeners(({ props, actions, values }) => ({
loadAllRecordings: () => {
if (values.featureFlags[FEATURE_FLAGS.SESSION_RECORDING_INFINITE_LIST]) {
actions.loadSessionRecordings()
} else {
actions.getSessionRecordings()
}
actions.loadSessionRecordings()
actions.loadPinnedRecordings()
},
setFilters: () => {
if (values.featureFlags[FEATURE_FLAGS.SESSION_RECORDING_INFINITE_LIST]) {
actions.loadSessionRecordings()
} else {
actions.getSessionRecordings()
}
actions.loadSessionRecordings()
props.onFiltersChange?.(values.filters)
},
replaceFilters: () => {
if (values.featureFlags[FEATURE_FLAGS.SESSION_RECORDING_INFINITE_LIST]) {
actions.loadSessionRecordings()
} else {
actions.getSessionRecordings()
}
},
resetFilters: () => {
actions.setFilters(getDefaultFilters(props.personUUID))
},
loadNext: () => {
actions.setFilters({
offset: (values.filters?.offset || 0) + RECORDINGS_LIMIT,
})
},
loadPrev: () => {
actions.setFilters({
offset: Math.max((values.filters?.offset || 0) - RECORDINGS_LIMIT, 0),
})
},
maybeLoadSessionRecordings: ({ direction }) => {
if (direction === 'older' && !values.hasNext) {
@ -383,20 +319,26 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
actions.maybeLoadPropertiesForSessions(values.sessionRecordings.map((s) => s.id))
},
getSessionRecordingsSuccess: () => {
actions.maybeLoadPropertiesForSessions(values.sessionRecordings.map((s) => s.id))
},
setSelectedRecordingId: () => {
if (values.featureFlags[FEATURE_FLAGS.SESSION_RECORDING_INFINITE_LIST]) {
// If we are at the end of the list then try to load more
const recordingIndex = values.sessionRecordings.findIndex((s) => s.id === values.selectedRecordingId)
if (recordingIndex === values.sessionRecordings.length - 1) {
actions.maybeLoadSessionRecordings('older')
}
// If we are at the end of the list then try to load more
const recordingIndex = values.sessionRecordings.findIndex((s) => s.id === values.selectedRecordingId)
if (recordingIndex === values.sessionRecordings.length - 1) {
actions.maybeLoadSessionRecordings('older')
}
},
})),
selectors({
filters: [
(s) => [s.customFilters, (_, props) => props.personUUID],
(customFilters, personUUID): RecordingFilters => {
const defaultFilters = getDefaultFilters(personUUID)
return {
...defaultFilters,
...customFilters,
}
},
],
listingVersion: [
(s) => [s.featureFlags],
(featureFlags): '1' | '2' | '3' => {
@ -437,8 +379,6 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
: sessionRecordings[activeSessionRecordingIndex - 1]
},
],
hasPrev: [(s) => [s.filters], (filters) => (filters.offset || 0) > 0],
hasNext: [
(s) => [s.sessionRecordingsResponse],
(sessionRecordingsResponse) => sessionRecordingsResponse.has_next,
@ -447,6 +387,7 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
(s) => [s.filters, (_, props) => props.personUUID],
(filters, personUUID) => {
const defaultFilters = getDefaultFilters(personUUID)
return (
(filters?.actions?.length || 0) +
(filters?.events?.length || 0) +
@ -475,42 +416,41 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
replace: boolean
}
] => {
const params: Params = {
filters: values.filters,
}
const hashParams: HashParams = {
...router.values.hashParams,
}
if (!values.selectedRecordingId) {
delete hashParams.sessionRecordingId
} else {
hashParams.sessionRecordingId = values.selectedRecordingId
const params: Params = objectClean({
filters: values.customFilters ?? undefined,
sessionRecordingId: values.selectedRecordingId ?? undefined,
})
// We used to have sessionRecordingId in the hash, so we keep it there for backwards compatibility
if (router.values.hashParams.sessionRecordingId) {
delete router.values.hashParams.sessionRecordingId
}
return [router.values.location.pathname, params, hashParams, { replace }]
return [router.values.location.pathname, params, router.values.hashParams, { replace }]
}
return {
getSessionRecordings: () => buildURL(true),
setSelectedRecordingId: () => buildURL(false),
setFilters: () => buildURL(true),
resetFilters: () => buildURL(true),
}
}),
urlToAction(({ actions, values, props }) => {
const urlToAction = (_: any, params: Params, hashParams: HashParams): void => {
const urlToAction = (_: any, params: Params, hashParams: Params): void => {
if (!props.updateSearchParams) {
return
}
const nulledSessionRecordingId = hashParams.sessionRecordingId ?? null
// We changed to have the sessionRecordingId in the query params, but it used to be in the hash so backwards compatibility
const nulledSessionRecordingId = params.sessionRecordingId ?? hashParams.sessionRecordingId ?? null
if (nulledSessionRecordingId !== values.selectedRecordingId) {
actions.setSelectedRecordingId(nulledSessionRecordingId)
}
if (params.filters) {
if (!equal(params.filters, values.filters)) {
actions.replaceFilters(params.filters)
if (!equal(params.filters, values.customFilters)) {
actions.setFilters(params.filters)
}
}
}
@ -520,12 +460,8 @@ export const sessionRecordingsListLogic = kea<sessionRecordingsListLogicType>([
}),
// NOTE: It is important this comes after urlToAction, as it will override the default behavior
afterMount(({ actions, values }) => {
if (values.featureFlags[FEATURE_FLAGS.SESSION_RECORDING_INFINITE_LIST]) {
actions.loadSessionRecordings()
} else {
actions.getSessionRecordings()
}
afterMount(({ actions }) => {
actions.loadSessionRecordings()
actions.loadPinnedRecordings()
}),
])

View File

@ -694,7 +694,6 @@ export interface RecordingFilters {
events?: FilterType['events']
actions?: FilterType['actions']
properties?: AnyPropertyFilter[]
offset?: number
session_recording_duration?: RecordingDurationFilter
duration_type_filter?: DurationTypeFilter
console_logs?: FilterableLogLevel[]

View File

@ -20,6 +20,7 @@
"copy-scripts": "mkdir -p frontend/dist/ && ./bin/copy-posthog-js",
"test": "pnpm test:unit && pnpm test:visual-regression",
"test:unit": "jest --testPathPattern=frontend/",
"jest": "jest",
"test:visual-regression": "docker compose -f docker-compose.playwright.yml run --rm -it --build playwright pnpm test:visual-regression:legacy:docker && pnpm test:visual-regression:stories:docker",
"test:visual-regression:legacy": "docker compose -f docker-compose.playwright.yml run --rm -it --build playwright pnpm test:visual-regression:legacy:docker",
"test:visual-regression:legacy:docker": "STORYBOOK_URL=http://host.docker.internal:6006 playwright test -u",