Skip to content

Commit

Permalink
fix: playback scene snags (#27662)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Alex V <[email protected]>
  • Loading branch information
3 people authored Jan 21, 2025
1 parent c32ebcf commit ca6f7b3
Show file tree
Hide file tree
Showing 12 changed files with 386 additions and 143 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function Header(): JSX.Element {
<LemonMenu
items={[
{
label: 'Playback from file',
label: 'Playback from PostHog JSON file',
to: urls.replayFilePlayback(),
},
]}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import clsx from 'clsx'
import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton'
import {
LemonButton,
LemonButtonWithoutSideActionProps,
LemonButtonWithSideActionProps,
} from 'lib/lemon-ui/LemonButton'
import { LemonMenu, LemonMenuItem, LemonMenuProps } from 'lib/lemon-ui/LemonMenu/LemonMenu'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { PropsWithChildren } from 'react'
Expand Down Expand Up @@ -73,7 +77,10 @@ export function SettingsMenu({
)
}

type SettingsButtonProps = Omit<LemonButtonProps, 'status' | 'sideAction' | 'className'> & {
type SettingsButtonProps = (
| Omit<LemonButtonWithoutSideActionProps, 'status' | 'className'>
| Omit<LemonButtonWithSideActionProps, 'status' | 'className'>
) & {
title?: string
icon?: JSX.Element | null
label: JSX.Element | string
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import clsx from 'clsx'
import { Dayjs, dayjs } from 'lib/dayjs'
import { shortTimeZone } from 'lib/utils'
import { memo } from 'react'
import { TimestampFormat } from 'scenes/session-recordings/player/playerSettingsLogic'

function formattedReplayTime(
time: string | number | Dayjs | null | undefined,
timestampFormat: TimestampFormat
): string {
if (time == null) {
return '--/--/----, 00:00:00'
}

let d = dayjs(time)
const isUTC = timestampFormat === TimestampFormat.UTC
if (isUTC) {
d = d.tz('UTC')
}
const formatted = d.format(formatStringFor(d))
const timezone = isUTC ? 'UTC' : shortTimeZone(undefined, d.toDate())
return `${formatted} ${timezone}`
}

function formatStringFor(d: Dayjs): string {
const today = dayjs()
Expand All @@ -10,22 +30,28 @@ function formatStringFor(d: Dayjs): string {
return 'DD/MM/YYYY HH:mm:ss'
}

export function SimpleTimeLabel({
const truncateToSeconds = (time: string | number | Dayjs): number => {
switch (typeof time) {
case 'number':
return Math.floor(time / 1000) * 1000
case 'string':
return Math.floor(new Date(time).getTime() / 1000) * 1000
default:
return time.startOf('second').valueOf()
}
}

export function _SimpleTimeLabel({
startTime,
isUTC,
timestampFormat,
muted = true,
size = 'xsmall',
}: {
startTime: string | number | Dayjs
isUTC: boolean
startTime: string | number | Dayjs | undefined
timestampFormat: TimestampFormat
muted?: boolean
size?: 'small' | 'xsmall'
}): JSX.Element {
let d = dayjs(startTime)
if (isUTC) {
d = d.tz('UTC')
}

return (
<div
className={clsx(
Expand All @@ -35,7 +61,26 @@ export function SimpleTimeLabel({
size === 'small' && 'text-sm'
)}
>
{d.format(formatStringFor(d))} {isUTC ? 'UTC' : shortTimeZone(undefined, dayjs(d).toDate())}
{formattedReplayTime(startTime, timestampFormat)}
</div>
)
}

export const SimpleTimeLabel = memo(
_SimpleTimeLabel,
// we can truncate time when considering whether to re-render the component as we only go down to seconds in dispay,
// but it will be called with multiple millisecond values between each second
// in local tests this rendered at least 4x less (400 vs 1600 renders)
// this gets better for recordings with more activity
(prevProps, nextProps) => {
const prevStartTimeTruncated = prevProps.startTime ? truncateToSeconds(prevProps.startTime) : null
const nextStartTimeTruncated = nextProps.startTime ? truncateToSeconds(nextProps.startTime) : null

return (
prevStartTimeTruncated === nextStartTimeTruncated &&
prevProps.timestampFormat === nextProps.timestampFormat &&
prevProps.muted === nextProps.muted &&
prevProps.size === nextProps.size
)
}
)
86 changes: 49 additions & 37 deletions frontend/src/scenes/session-recordings/player/PlayerMetaLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FEATURE_FLAGS } from 'lib/constants'
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
import { IconComment } from 'lib/lemon-ui/icons'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { Fragment } from 'react'
import { Fragment, useMemo } from 'react'
import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext'
import { NotebookSelectButton } from 'scenes/notebooks/NotebookSelectButton/NotebookSelectButton'
import {
Expand Down Expand Up @@ -163,48 +163,60 @@ export function PlayerMetaLinks({ iconsOnly }: { iconsOnly: boolean }): JSX.Elem

const MenuActions = (): JSX.Element => {
const { logicProps } = useValues(sessionRecordingPlayerLogic)
const { exportRecordingToFile, deleteRecording, setIsFullScreen } = useActions(sessionRecordingPlayerLogic)
const { deleteRecording, setIsFullScreen, exportRecordingToFile } = useActions(sessionRecordingPlayerLogic)

const hasMobileExportFlag = useFeatureFlag('SESSION_REPLAY_EXPORT_MOBILE_DATA')
const hasMobileExport = window.IMPERSONATED_SESSION || hasMobileExportFlag

const onDelete = (): void => {
setIsFullScreen(false)
LemonDialog.open({
title: 'Delete recording',
description: 'Are you sure you want to delete this recording? This cannot be undone.',
secondaryButton: {
children: 'Cancel',
const onDelete = useMemo(
() => () => {
setIsFullScreen(false)
LemonDialog.open({
title: 'Delete recording',
description: 'Are you sure you want to delete this recording? This cannot be undone.',
secondaryButton: {
children: 'Cancel',
},
primaryButton: {
children: 'Delete',
status: 'danger',
onClick: deleteRecording,
},
})
},
[deleteRecording, setIsFullScreen]
)

const items: LemonMenuItems = useMemo(() => {
const itemsArray: LemonMenuItems = [
{
label: '.json',
status: 'default',
icon: <IconDownload />,
onClick: () => exportRecordingToFile(false),
tooltip: 'Export recording to a JSON file. This can be loaded later into PostHog for playback.',
},
primaryButton: {
children: 'Delete',
]
if (hasMobileExport) {
itemsArray.push({
label: 'DEBUG - mobile.json',
status: 'default',
icon: <IconDownload />,
onClick: () => exportRecordingToFile(true),
tooltip:
'DEBUG - ONLY VISIBLE TO POSTHOG STAFF - Export untransformed recording to a file. This can be loaded later into PostHog for playback.',
})
}
if (logicProps.playerKey !== 'modal') {
itemsArray.push({
label: 'Delete recording',
status: 'danger',
onClick: deleteRecording,
},
})
}

const items: LemonMenuItems = [
{
label: 'Export to file',
onClick: () => exportRecordingToFile(false),
icon: <IconDownload />,
tooltip: 'Export recording to a file. This can be loaded later into PostHog for playback.',
},
hasMobileExport && {
label: 'Export mobile replay to file',
onClick: () => exportRecordingToFile(true),
tooltip:
'DEBUG ONLY - Export untransformed recording to a file. This can be loaded later into PostHog for playback.',
icon: <IconDownload />,
},
logicProps.playerKey !== 'modal' && {
label: 'Delete recording',
status: 'danger',
onClick: onDelete,
icon: <IconTrash />,
},
]
onClick: onDelete,
icon: <IconTrash />,
})
}
return itemsArray
}, [logicProps.playerKey, onDelete, exportRecordingToFile, hasMobileExport])

return (
<LemonMenu items={items} buttonSize="xsmall">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,42 @@ import { HotKeyOrModifier } from '~/types'
import { playerSettingsLogic, TimestampFormat } from '../playerSettingsLogic'
import { seekbarLogic } from './seekbarLogic'

export function Timestamp(): JSX.Element {
const { logicProps, currentPlayerTime, currentTimestamp, sessionPlayerData } =
useValues(sessionRecordingPlayerLogic)
function RelativeTimestampLabel(): JSX.Element {
const { logicProps, currentPlayerTime, sessionPlayerData } = useValues(sessionRecordingPlayerLogic)
const { isScrubbing, scrubbingTime } = useValues(seekbarLogic(logicProps))
const { timestampFormat } = useValues(playerSettingsLogic)

const startTimeSeconds = ((isScrubbing ? scrubbingTime : currentPlayerTime) ?? 0) / 1000
const endTimeSeconds = Math.floor(sessionPlayerData.durationMs / 1000)

const fixedUnits = endTimeSeconds > 3600 ? 3 : 2

return (
<div className="flex gap-0.5">
<span>{colonDelimitedDuration(startTimeSeconds, fixedUnits)}</span>
<span>/</span>
<span>{colonDelimitedDuration(endTimeSeconds, fixedUnits)}</span>
</div>
)
}

export function Timestamp(): JSX.Element {
const { logicProps, currentTimestamp, sessionPlayerData } = useValues(sessionRecordingPlayerLogic)
const { isScrubbing, scrubbingTime } = useValues(seekbarLogic(logicProps))
const { timestampFormat } = useValues(playerSettingsLogic)

const scrubbingTimestamp = sessionPlayerData.start?.valueOf()
? scrubbingTime + sessionPlayerData.start?.valueOf()
: undefined

return (
<div data-attr="recording-timestamp" className="text-center whitespace-nowrap font-mono text-xs">
{timestampFormat === TimestampFormat.Relative ? (
<div className="flex gap-0.5">
<span>{colonDelimitedDuration(startTimeSeconds, fixedUnits)}</span>
<span>/</span>
<span>{colonDelimitedDuration(endTimeSeconds, fixedUnits)}</span>
</div>
) : currentTimestamp ? (
<SimpleTimeLabel startTime={currentTimestamp} isUTC={timestampFormat === TimestampFormat.UTC} />
<RelativeTimestampLabel />
) : (
'--/--/----, 00:00:00'
<SimpleTimeLabel
startTime={isScrubbing ? scrubbingTimestamp : currentTimestamp}
timestampFormat={timestampFormat}
/>
)}
</div>
)
Expand Down
Loading

0 comments on commit ca6f7b3

Please sign in to comment.