Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: exceptions in a playlist #23111

Merged
merged 12 commits into from
Jun 20, 2024
Merged
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
Loading