chore: make replay and error tracking the same style (#41421)

This commit is contained in:
Alex V
2025-11-13 11:49:33 +01:00
committed by GitHub
parent 3201af2c5d
commit 2e23ac7afa
5 changed files with 90 additions and 17 deletions

View File

@@ -12,7 +12,8 @@ export function LemonTableLink({
title,
description,
...props
}: Pick<LinkProps, 'to' | 'onClick' | 'target' | 'className'> & LemonTableLinkContentProps): JSX.Element {
}: Pick<LinkProps, 'to' | 'onClick' | 'target' | 'className' | 'targetBlankIcon'> &
LemonTableLinkContentProps): JSX.Element {
if (!props.to) {
return <LemonTableLinkContent title={title} description={description} />
}

View File

@@ -19,7 +19,10 @@ export interface RecordingRowProps {
type ACTIVITY_DESCRIPTIONS = 'very low' | 'low' | 'medium' | 'high' | 'very high'
function ActivityScoreLabel({ score }: { score: number | undefined }): JSX.Element {
function getActivityScoreDescription({ score }: { score: number | undefined }): {
description: ACTIVITY_DESCRIPTIONS
backgroundColor: string
} {
const n = score ?? 0
let backgroundColor = 'bg-primary-alt-highlight'
let description: ACTIVITY_DESCRIPTIONS = 'very low'
@@ -37,6 +40,22 @@ function ActivityScoreLabel({ score }: { score: number | undefined }): JSX.Eleme
description = 'low'
}
return { description, backgroundColor }
}
export function ActivityScoreLabel({
score,
clean = false,
}: {
score: number | undefined
clean?: boolean
}): JSX.Element {
const { description, backgroundColor } = getActivityScoreDescription({ score })
if (clean) {
return <>{description}</>
}
return <LemonSnack className={clsx(backgroundColor, 'text-xs')}>activity: {description}</LemonSnack>
}

View File

@@ -24,26 +24,25 @@ export const CustomGroupTitleColumn: QueryContextColumnComponent = (props) => {
return (
<div className="flex items-start gap-x-1.5 group">
<LemonTableLink
target="_blank"
title={record.name || 'Unknown Type'}
description={
<div className="deprecated-space-y-1">
<div className="line-clamp-1">{record.description}</div>
<div className="deprecated-space-x-1">
<TZLabel time={record.last_seen} className="border-dotted border-b" />
</div>
</div>
}
className="flex-1"
description={<div className="line-clamp-1">{record.description}</div>}
className="flex"
to={urls.errorTrackingIssue(record.id)}
/>
</div>
)
}
const LastSeenColumn = ({ record }: { record: unknown }): JSX.Element => {
const last_seen = (record as ErrorTrackingIssue).last_seen
return <TZLabel time={last_seen} className="border-dotted border-b" />
}
const CountColumn = ({ record, columnName }: { record: unknown; columnName: string }): JSX.Element => {
const aggregations = (record as ErrorTrackingIssue).aggregations!
const count = aggregations[columnName as 'occurrences' | 'users']
return <span className="text-lg font-medium">{humanFriendlyLargeNumber(count)}</span>
return <>{humanFriendlyLargeNumber(count)}</>
}
const context: QueryContext = {
@@ -52,7 +51,7 @@ const context: QueryContext = {
showQueryEditor: false,
columns: {
error: {
width: '50%',
align: 'left',
render: CustomGroupTitleColumn,
},
users: {
@@ -63,6 +62,11 @@ const context: QueryContext = {
align: 'right',
render: CountColumn,
},
last_seen: {
title: 'Last seen',
align: 'right',
render: LastSeenColumn,
},
},
}

View File

@@ -1,19 +1,25 @@
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { IconRewindPlay } from '@posthog/icons'
import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton'
import { LemonTable } from 'lib/lemon-ui/LemonTable'
import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture'
import { IconOpenInNew } from 'lib/lemon-ui/icons'
import { humanFriendlyDuration } from 'lib/utils'
import { ProductIntentContext } from 'lib/utils/product-intents'
import { RecordingRow } from 'scenes/session-recordings/components/RecordingRow'
import { asDisplay } from 'scenes/persons/person-utils'
import { ActivityScoreLabel } from 'scenes/session-recordings/components/RecordingRow'
import { sessionRecordingsPlaylistLogic } from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'
import { ReplayTile } from 'scenes/web-analytics/common'
import { webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic'
import { ProductKey, ReplayTabs } from '~/types'
import { ProductKey, ReplayTabs, SessionRecordingType } from '~/types'
export function WebAnalyticsRecordingsTile({ tile }: { tile: ReplayTile }): JSX.Element {
const { layout } = tile
@@ -72,7 +78,50 @@ export function WebAnalyticsRecordingsTile({ tile }: { tile: ReplayTile }): JSX.
) : items.length === 0 && emptyMessage ? (
<EmptyMessage {...emptyMessage} />
) : (
items.map((item, index) => <RecordingRow key={index} recording={item} />)
<LemonTable
className="mt-4"
columns={[
{
title: 'Person',
render: (_, recording: SessionRecordingType) => (
<>
<ProfilePicture
size="sm"
name={asDisplay(recording.person)}
className="mr-2"
/>
{asDisplay(recording.person)}{' '}
</>
),
},
{
title: 'Activity',
render: (_, recording: SessionRecordingType) => (
<>
<ActivityScoreLabel score={recording.activity_score} clean={true} />
</>
),
},
{
title: 'Duration',
render: (_, recording: SessionRecordingType) => (
<>{humanFriendlyDuration(recording.recording_duration)}</>
),
},
{
title: '',
render: (_, recording: SessionRecordingType) => (
<LemonButton
size="xsmall"
targetBlank
to={urls.replaySingle(recording.id ?? '')}
icon={<IconRewindPlay />}
/>
),
},
]}
dataSource={items}
/>
)}
</div>
<div className="flex flex-row-reverse my-2">

View File

@@ -1158,7 +1158,7 @@ export const webAnalyticsLogic = kea<webAnalyticsLogicType>([
dateRange: dateRange,
filterTestAccounts: filterTestAccounts,
filterGroup: replayFilters.filter_group,
columns: ['error', 'users', 'occurrences'],
columns: ['error', 'users', 'occurrences', 'last_seen'],
limit: 4,
})
} catch (e) {