diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx index d7509e87aa99e..668565b97495c 100644 --- a/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx @@ -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, @@ -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 ? ( - - ) : events && events.length > 0 ? ( + return (
-
-
- Empty
} - content={({ activeItem: event }) => - event ? ( -
- -
- ) : ( - - ) - } - /> -
-
- - ) : ( - - ) -} - -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 ( -
-
- - -
- {event.properties.$current_url && ( -
{event.properties.$current_url}
- )} - , + }, + { + key: ErrorGroupTab.Breakdowns, + label: 'Breakdowns', + content: , + }, + ]} />
) diff --git a/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts b/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts index 1a7685e2ed1b9..d3f3f8f346af8 100644 --- a/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts +++ b/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts @@ -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' @@ -16,6 +16,11 @@ export interface ErrorTrackingGroupSceneLogicProps { export type ExceptionEventType = Pick +export enum ErrorGroupTab { + Overview = 'overview', + Breakdowns = 'breakdowns', +} + export const errorTrackingGroupSceneLogic = kea([ path((key) => ['scenes', 'error-tracking', 'errorTrackingGroupSceneLogic', key]), props({} as ErrorTrackingGroupSceneLogicProps), @@ -24,6 +29,19 @@ export const errorTrackingGroupSceneLogic = kea ({ tab }), + }), + + reducers(() => ({ + errorGroupTab: [ + ErrorGroupTab.Overview as ErrorGroupTab, + { + setErrorGroupTab: (_, { tab }) => tab, + }, + ], + })), + loaders(({ props, values }) => ({ events: [ [] as ExceptionEventType[], diff --git a/frontend/src/scenes/error-tracking/groups/BreakdownsTab.tsx b/frontend/src/scenes/error-tracking/groups/BreakdownsTab.tsx new file mode 100644 index 0000000000000..c9696694b1c2a --- /dev/null +++ b/frontend/src/scenes/error-tracking/groups/BreakdownsTab.tsx @@ -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[] } + +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 ( +
+ {breakdownGroups.map((group, index) => ( + + ))} +
+ ) +} + +const BreakdownGroup = ({ group }: { group: BreakdownGroup }): JSX.Element => { + const { dateRange, filterTestAccounts, filterGroup } = useValues(errorTrackingLogic) + const [selectedProperty, setSelectedProperty] = useState(group.options[0].value) + + return ( +
+
+

{group.title}

+ {group.options.length > 1 && ( + + )} +
+ +
+ ) +} diff --git a/frontend/src/scenes/error-tracking/groups/OverviewTab.tsx b/frontend/src/scenes/error-tracking/groups/OverviewTab.tsx new file mode 100644 index 0000000000000..bcd1e32f31c36 --- /dev/null +++ b/frontend/src/scenes/error-tracking/groups/OverviewTab.tsx @@ -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 ? ( + + ) : ( +
+
+ Empty
} + content={({ activeItem: event }) => + event ? ( +
+ +
+ ) : ( + + ) + } + /> +
+ + ) +} + +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 ( +
+
+ + +
+ {event.properties.$current_url && ( +
{event.properties.$current_url}
+ )} + +
+ ) +} diff --git a/frontend/src/scenes/error-tracking/queries.ts b/frontend/src/scenes/error-tracking/queries.ts index 3502dabe8f901..e4c993917ce12 100644 --- a/frontend/src/scenes/error-tracking/queries.ts +++ b/frontend/src/scenes/error-tracking/queries.ts @@ -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, @@ -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,