Skip to content

Commit

Permalink
feat: add event breakdowns to exceptions (#23124)
Browse files Browse the repository at this point in the history
  • Loading branch information
daibhin authored Jun 20, 2024
1 parent a7b678b commit 8c9fa40
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 78 deletions.
98 changes: 23 additions & 75 deletions frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
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 { Playlist } from 'lib/components/Playlist/Playlist'
import { LemonTabs } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { SceneExport } from 'scenes/sceneTypes'
import { PropertyIcons } from 'scenes/session-recordings/playlist/SessionRecordingPreview'

import { ErrorTrackingFilters } from './ErrorTrackingFilters'
import { errorTrackingGroupSceneLogic, ExceptionEventType } from './errorTrackingGroupSceneLogic'
import { ErrorGroupTab, errorTrackingGroupSceneLogic } from './errorTrackingGroupSceneLogic'
import { BreakdownsTab } from './groups/BreakdownsTab'
import { OverviewTab } from './groups/OverviewTab'

export const scene: SceneExport = {
component: ErrorTrackingGroupScene,
Expand All @@ -23,75 +18,28 @@ export const scene: SceneExport = {
}

export function ErrorTrackingGroupScene(): JSX.Element {
const { events, eventsLoading } = useValues(errorTrackingGroupSceneLogic)
const { errorGroupTab } = useValues(errorTrackingGroupSceneLogic)
const { setErrorGroupTab } = useActions(errorTrackingGroupSceneLogic)

return eventsLoading ? (
<Spinner className="self-align-center justify-self-center" />
) : events && events.length > 0 ? (
return (
<div className="space-y-4">
<ErrorTrackingFilters showOrder={false} />

<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 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']
}

return { property, value, label }
})
.filter((property) => !!property.value)

return (
<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>
{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}
<LemonTabs
activeKey={errorGroupTab}
onChange={setErrorGroupTab}
tabs={[
{
key: ErrorGroupTab.Overview,
label: 'Overview',
content: <OverviewTab />,
},
{
key: ErrorGroupTab.Breakdowns,
label: 'Breakdowns',
content: <BreakdownsTab />,
},
]}
/>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterMount, connect, kea, path, props, selectors } from 'kea'
import { actions, afterMount, connect, kea, path, props, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
import api from 'lib/api'
import { Scene } from 'scenes/sceneTypes'
Expand All @@ -16,6 +16,11 @@ export interface ErrorTrackingGroupSceneLogicProps {

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

export enum ErrorGroupTab {
Overview = 'overview',
Breakdowns = 'breakdowns',
}

export const errorTrackingGroupSceneLogic = kea<errorTrackingGroupSceneLogicType>([
path((key) => ['scenes', 'error-tracking', 'errorTrackingGroupSceneLogic', key]),
props({} as ErrorTrackingGroupSceneLogicProps),
Expand All @@ -24,6 +29,19 @@ export const errorTrackingGroupSceneLogic = kea<errorTrackingGroupSceneLogicType
values: [errorTrackingLogic, ['dateRange', 'filterTestAccounts', 'filterGroup']],
}),

actions({
setErrorGroupTab: (tab: ErrorGroupTab) => ({ tab }),
}),

reducers(() => ({
errorGroupTab: [
ErrorGroupTab.Overview as ErrorGroupTab,
{
setErrorGroupTab: (_, { tab }) => tab,
},
],
})),

loaders(({ props, values }) => ({
events: [
[] as ExceptionEventType[],
Expand Down
82 changes: 82 additions & 0 deletions frontend/src/scenes/error-tracking/groups/BreakdownsTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { LemonSegmentedButton, LemonSegmentedButtonOption } from '@posthog/lemon-ui'
import clsx from 'clsx'
import { useValues } from 'kea'
import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver'
import { useState } from 'react'

import { Query } from '~/queries/Query/Query'

import { errorTrackingLogic } from '../errorTrackingLogic'
import { errorTrackingGroupBreakdownQuery } from '../queries'

const gridColumnsMap = {
small: 'grid-cols-1',
medium: 'grid-cols-2',
large: 'grid-cols-3',
}

type BreakdownGroup = { title: string; options: LemonSegmentedButtonOption<string>[] }

export const BreakdownsTab = (): JSX.Element => {
const breakdownGroups: BreakdownGroup[] = [
{
title: 'Device',
options: [
{ value: '$browser', label: 'Browser' },
{ value: '$device_type', label: 'Device type' },
{ value: '$os', label: 'Operating system' },
],
},
{
title: 'User',
options: [
{ value: '$user_id', label: 'User ID' },
{ value: '$ip', label: 'IP address' },
],
},
{ title: 'URL', options: [{ value: '$pathname', label: 'Path' }] },
]

const { ref, size } = useResizeBreakpoints({
0: 'small',
750: 'medium',
1200: 'large',
})

return (
<div className={clsx('grid gap-5', gridColumnsMap[size])} ref={ref}>
{breakdownGroups.map((group, index) => (
<BreakdownGroup key={index} group={group} />
))}
</div>
)
}

const BreakdownGroup = ({ group }: { group: BreakdownGroup }): JSX.Element => {
const { dateRange, filterTestAccounts, filterGroup } = useValues(errorTrackingLogic)
const [selectedProperty, setSelectedProperty] = useState<string>(group.options[0].value)

return (
<div>
<div className="flex justify-between">
<h2>{group.title}</h2>
{group.options.length > 1 && (
<LemonSegmentedButton
size="small"
value={selectedProperty}
options={group.options}
onChange={setSelectedProperty}
/>
)}
</div>
<Query
query={errorTrackingGroupBreakdownQuery({
breakdownProperty: selectedProperty,
dateRange: dateRange,
filterTestAccounts: filterTestAccounts,
filterGroup: filterGroup,
})}
/>
</div>
)
}
79 changes: 79 additions & 0 deletions frontend/src/scenes/error-tracking/groups/OverviewTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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 { Playlist } from 'lib/components/Playlist/Playlist'
import { PropertyIcons } from 'scenes/session-recordings/playlist/SessionRecordingPreview'

import { errorTrackingGroupSceneLogic, ExceptionEventType } from '../errorTrackingGroupSceneLogic'

export const OverviewTab = (): JSX.Element => {
const { events, eventsLoading } = useValues(errorTrackingGroupSceneLogic)

return eventsLoading ? (
<Spinner className="self-align-center justify-self-center" />
) : (
<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>
)
}

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']
}

return { property, value, label }
})
.filter((property) => !!property.value)

return (
<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>
{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>
)
}
43 changes: 41 additions & 2 deletions frontend/src/scenes/error-tracking/queries.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UniversalFiltersGroup } from 'lib/components/UniversalFilters/UniversalFilters'

import { DataTableNode, DateRange, ErrorTrackingOrder, EventsQuery, NodeKind } from '~/queries/schema'
import { AnyPropertyFilter } from '~/types'
import { DataTableNode, DateRange, ErrorTrackingOrder, EventsQuery, InsightVizNode, NodeKind } from '~/queries/schema'
import { AnyPropertyFilter, BaseMathType, ChartDisplayType } from '~/types'

export const errorTrackingQuery = ({
order,
Expand Down Expand Up @@ -60,6 +60,45 @@ export const errorTrackingGroupQuery = ({
}
}

export const errorTrackingGroupBreakdownQuery = ({
breakdownProperty,
dateRange,
filterTestAccounts,
filterGroup,
}: {
breakdownProperty: string
dateRange: DateRange
filterTestAccounts: boolean
filterGroup: UniversalFiltersGroup
}): InsightVizNode => {
return {
kind: NodeKind.InsightVizNode,
source: {
kind: NodeKind.TrendsQuery,
trendsFilter: {
display: ChartDisplayType.ActionsBarValue,
},
breakdownFilter: {
breakdown_type: 'event',
breakdown: breakdownProperty,
breakdown_limit: 10,
},
series: [
{
kind: NodeKind.EventsNode,
event: '$exception',
math: BaseMathType.TotalCount,
name: 'This is the series name',
custom_name: 'Boomer',
},
],
dateRange: dateRange,
properties: filterGroup.values as AnyPropertyFilter[],
filterTestAccounts,
},
}
}

const defaultProperties = ({
dateRange,
filterTestAccounts,
Expand Down

0 comments on commit 8c9fa40

Please sign in to comment.