Skip to content

Commit

Permalink
feat: exceptions in a playlist (#23111)
Browse files Browse the repository at this point in the history
  • Loading branch information
daibhin authored Jun 20, 2024
1 parent 1073e2e commit fcc1884
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 71 deletions.
10 changes: 6 additions & 4 deletions frontend/src/lib/components/EmptyMessage/EmptyMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { LemonButton } from 'lib/lemon-ui/LemonButton'
export interface EmptyMessageProps {
title: string
description: string
buttonText: string
buttonText?: string
buttonTo?: string
}

Expand All @@ -16,9 +16,11 @@ export function EmptyMessage({ title, description, buttonText, buttonTo }: Empty
<h3 className="title">{title}</h3>

<p className="text-muted description">{description}</p>
<LemonButton type="secondary" to={buttonTo}>
{buttonText}
</LemonButton>
{buttonText && (
<LemonButton type="secondary" to={buttonTo}>
{buttonText}
</LemonButton>
)}
</div>
</div>
)
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/scenes/error-tracking/ErrorTracking.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.ErrorTracking__group {
height: calc(100vh - 13rem);
min-height: 41rem;
}
123 changes: 61 additions & 62 deletions frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { PersonDisplay } from '@posthog/apps-common'
import { LemonButton, LemonTabs, Spinner } from '@posthog/lemon-ui'
import './ErrorTracking.scss'

import { PersonDisplay, TZLabel } from '@posthog/apps-common'
import { Spinner } from '@posthog/lemon-ui'
import clsx from 'clsx'
import { useValues } from 'kea'
import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage'
import { ErrorDisplay } from 'lib/components/Errors/ErrorDisplay'
import { NotFound } from 'lib/components/NotFound'
import { IconChevronLeft, IconChevronRight } from 'lib/lemon-ui/icons'
import { useState } from 'react'
import { Playlist } from 'lib/components/Playlist/Playlist'
import { SceneExport } from 'scenes/sceneTypes'
import { SessionRecordingsPlaylist } from 'scenes/session-recordings/playlist/SessionRecordingsPlaylist'
import { PropertyIcons } from 'scenes/session-recordings/playlist/SessionRecordingPreview'

import { ErrorTrackingFilters } from './ErrorTrackingFilters'
import { errorTrackingGroupSceneLogic, ExceptionEventType } from './errorTrackingGroupSceneLogic'
Expand All @@ -21,79 +24,75 @@ export const scene: SceneExport = {

export function ErrorTrackingGroupScene(): JSX.Element {
const { events, eventsLoading } = useValues(errorTrackingGroupSceneLogic)
const [activeTab, setActiveTab] = useState<'details' | 'recordings'>('details')

return eventsLoading ? (
<Spinner className="self-align-center justify-self-center" />
) : events && events.length > 0 ? (
<div>
<div className="space-y-4">
<ErrorTrackingFilters showOrder={false} />
<LemonTabs
tabs={[
{
key: 'details',
label: 'Details',
content: <ExceptionDetails events={events} />,
},
{
key: 'recordings',
label: 'Recordings',
content: (
<ExceptionRecordings
sessionIds={events.map((e) => e.properties.$session_id).filter(Boolean)}
/>
),
},
]}
activeKey={activeTab}
onChange={setActiveTab}
/>

<div className="ErrorTracking__group">
<div className="h-full space-y-2">
<Playlist
title="Exceptions"
sections={[
{
key: 'exceptions',
title: 'Exceptions',
items: events,
render: ListItemException,
},
]}
listEmptyState={<div>Empty</div>}
content={({ activeItem: event }) =>
event ? (
<div className="pl-2">
<ErrorDisplay eventProperties={event.properties} />
</div>
) : (
<EmptyMessage
title="No exception selected"
description="Please select an exception from the list on the left"
/>
)
}
/>
</div>
</div>
</div>
) : (
<NotFound object="exception" />
)
}

const ExceptionDetails = ({ events }: { events: ExceptionEventType[] }): JSX.Element => {
const [activeEventId, setActiveEventId] = useState<number>(events.length - 1)
const ListItemException = ({ item: event, isActive }: { item: ExceptionEventType; isActive: boolean }): JSX.Element => {
const properties = ['$browser', '$device_type', '$os']
.flatMap((property) => {
let value = event.properties[property]
const label = value
if (property === '$device_type') {
value = event.properties['$device_type'] || event.properties['$initial_device_type']
}

const event = events[activeEventId]
return { property, value, label }
})
.filter((property) => !!property.value)

return (
<div className="space-y-4">
{events.length > 1 && (
<div className="flex space-x-1 items-center">
<LemonButton
size="xsmall"
type="secondary"
icon={<IconChevronLeft />}
onClick={() => setActiveEventId(activeEventId - 1)}
disabledReason={activeEventId <= 0 && 'No earlier examples'}
/>
<LemonButton
size="xsmall"
type="secondary"
icon={<IconChevronRight />}
onClick={() => setActiveEventId(activeEventId + 1)}
disabledReason={activeEventId >= events.length - 1 && 'No newer examples'}
/>
<span>
{activeEventId + 1} of {events.length}
</span>
</div>
)}
<div className="bg-bg-light border rounded p-2">
<div className={clsx('cursor-pointer p-2 space-y-1', isActive && 'border-l-4 border-primary-3000')}>
<div className="flex justify-between items-center">
<PersonDisplay person={event.person} withIcon />
<PropertyIcons recordingProperties={properties} iconClassNames="text-muted" />
</div>
<ErrorDisplay eventProperties={event.properties} />
</div>
)
}

const ExceptionRecordings = ({ sessionIds }: { sessionIds: string[] }): JSX.Element => {
return (
<div className="SessionRecordingPlaylistHeightWrapper">
<SessionRecordingsPlaylist pinnedRecordings={sessionIds} />
{event.properties.$current_url && (
<div className="text-xs text-muted truncate">{event.properties.$current_url}</div>
)}
<TZLabel
className="overflow-hidden text-ellipsis text-xs text-muted shrink-0"
time={event.timestamp}
placement="right"
showPopover={false}
/>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface ErrorTrackingGroupSceneLogicProps {
id: string
}

export type ExceptionEventType = Pick<EventType, 'properties' | 'timestamp' | 'person'>
export type ExceptionEventType = Pick<EventType, 'id' | 'properties' | 'timestamp' | 'person'>

export const errorTrackingGroupSceneLogic = kea<errorTrackingGroupSceneLogicType>([
path((key) => ['scenes', 'error-tracking', 'errorTrackingGroupSceneLogic', key]),
Expand All @@ -39,9 +39,10 @@ export const errorTrackingGroupSceneLogic = kea<errorTrackingGroupSceneLogicType
)

return response.results.map((r) => ({
properties: JSON.parse(r[0]),
timestamp: r[1],
person: r[2],
id: r[0],
properties: JSON.parse(r[1]),
timestamp: r[2],
person: r[3],
}))
},
},
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/error-tracking/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const errorTrackingGroupQuery = ({
}): EventsQuery => {
return {
kind: NodeKind.EventsQuery,
select: ['properties', 'timestamp', 'person'],
select: ['uuid', 'properties', 'timestamp', 'person'],
where: [`properties.$exception_type = '${group}'`],
...defaultProperties({ dateRange, filterTestAccounts, filterGroup }),
}
Expand Down

0 comments on commit fcc1884

Please sign in to comment.