diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx new file mode 100644 index 0000000000000..8db5ac1043162 --- /dev/null +++ b/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx @@ -0,0 +1,109 @@ +import { LemonSelect } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { DateFilter } from 'lib/components/DateFilter/DateFilter' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import UniversalFilters from 'lib/components/UniversalFilters/UniversalFilters' +import { universalFiltersLogic } from 'lib/components/UniversalFilters/universalFiltersLogic' +import { TestAccountFilter } from 'scenes/insights/filters/TestAccountFilter' + +import { AnyPropertyFilter } from '~/types' + +import { errorTrackingLogic } from './errorTrackingLogic' +import { errorTrackingSceneLogic } from './errorTrackingSceneLogic' + +export const ErrorTrackingFilters = ({ showOrder = true }: { showOrder?: boolean }): JSX.Element => { + const { dateRange, filterGroup, filterTestAccounts } = useValues(errorTrackingLogic) + const { setDateRange, setFilterGroup, setFilterTestAccounts } = useActions(errorTrackingLogic) + const { order } = useValues(errorTrackingSceneLogic) + const { setOrder } = useActions(errorTrackingSceneLogic) + + return ( + { + setFilterGroup(filterGroup) + }} + > +
+
+ + +
+
+
+ { + setDateRange({ date_from: changedDateFrom, date_to: changedDateTo }) + }} + size="small" + /> + {showOrder && ( + + )} +
+
+ { + setFilterTestAccounts(filter_test_accounts || false) + }} + size="small" + /> +
+
+
+
+ ) +} + +const RecordingsUniversalFilterGroup = (): JSX.Element => { + const { filterGroup } = useValues(universalFiltersLogic) + const { replaceGroupValue, removeGroupValue } = useActions(universalFiltersLogic) + + const values = filterGroup.values as AnyPropertyFilter[] + + return ( + <> + {values.map((filter, index) => ( + removeGroupValue(index)} + onChange={(value) => replaceGroupValue(index, value)} + /> + ))} + + ) +} diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx index 80e910be9983a..10015b17d9ad4 100644 --- a/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx @@ -1,3 +1,4 @@ +import { PersonDisplay } from '@posthog/apps-common' import { LemonButton, LemonTabs, Spinner } from '@posthog/lemon-ui' import { useValues } from 'kea' import { ErrorDisplay } from 'lib/components/Errors/ErrorDisplay' @@ -7,9 +8,8 @@ import { useState } from 'react' import { SceneExport } from 'scenes/sceneTypes' import { SessionRecordingsPlaylist } from 'scenes/session-recordings/playlist/SessionRecordingsPlaylist' -import { EventType } from '~/types' - -import { errorTrackingGroupSceneLogic } from './errorTrackingGroupSceneLogic' +import { ErrorTrackingFilters } from './ErrorTrackingFilters' +import { errorTrackingGroupSceneLogic, ExceptionEventType } from './errorTrackingGroupSceneLogic' export const scene: SceneExport = { component: ErrorTrackingGroupScene, @@ -20,59 +20,72 @@ export const scene: SceneExport = { } export function ErrorTrackingGroupScene(): JSX.Element { - const { eventProperties, eventPropertiesLoading } = useValues(errorTrackingGroupSceneLogic) + const { events, eventsLoading } = useValues(errorTrackingGroupSceneLogic) const [activeTab, setActiveTab] = useState<'details' | 'recordings'>('details') - return eventPropertiesLoading ? ( - - ) : eventProperties && eventProperties.length > 0 ? ( - , - }, - { - key: 'recordings', - label: 'Recordings', - content: ( - p.$session_id).filter(Boolean)} /> - ), - }, - ]} - activeKey={activeTab} - onChange={setActiveTab} - /> + return eventsLoading ? ( + + ) : events && events.length > 0 ? ( +
+ + , + }, + { + key: 'recordings', + label: 'Recordings', + content: ( + e.properties.$session_id).filter(Boolean)} + /> + ), + }, + ]} + activeKey={activeTab} + onChange={setActiveTab} + /> +
) : ( ) } -const ExceptionDetails = ({ eventProperties }: { eventProperties: EventType['properties'] }): JSX.Element => { - const [activeEventId, setActiveEventId] = useState(eventProperties.length - 1) +const ExceptionDetails = ({ events }: { events: ExceptionEventType[] }): JSX.Element => { + const [activeEventId, setActiveEventId] = useState(events.length - 1) + + const event = events[activeEventId] return ( -
- {eventProperties.length > 1 && ( -
+
+ {events.length > 1 && ( +
} onClick={() => setActiveEventId(activeEventId - 1)} disabledReason={activeEventId <= 0 && 'No earlier examples'} /> } onClick={() => setActiveEventId(activeEventId + 1)} - disabledReason={activeEventId >= eventProperties.length - 1 && 'No newer examples'} + disabledReason={activeEventId >= events.length - 1 && 'No newer examples'} /> + + {activeEventId + 1} of {events.length} +
)} - +
+ +
+
) } diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx index 80fdfbe760a8d..e19be29f0d213 100644 --- a/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx @@ -1,20 +1,16 @@ -import { LemonSelect } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { DateFilter } from 'lib/components/DateFilter/DateFilter' -import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' -import UniversalFilters from 'lib/components/UniversalFilters/UniversalFilters' -import { universalFiltersLogic } from 'lib/components/UniversalFilters/universalFiltersLogic' +import { useValues } from 'kea' import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink' -import { TestAccountFilter } from 'scenes/insights/filters/TestAccountFilter' +import { useMemo } from 'react' import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' import { Query } from '~/queries/Query/Query' -import { NodeKind } from '~/queries/schema' import { QueryContext, QueryContextColumnComponent } from '~/queries/types' -import { AnyPropertyFilter } from '~/types' +import { ErrorTrackingFilters } from './ErrorTrackingFilters' +import { errorTrackingLogic } from './errorTrackingLogic' import { errorTrackingSceneLogic } from './errorTrackingSceneLogic' +import { errorTrackingQuery } from './queries' export const scene: SceneExport = { component: ErrorTrackingScene, @@ -22,7 +18,13 @@ export const scene: SceneExport = { } export function ErrorTrackingScene(): JSX.Element { - const { dateRange, order, filterTestAccounts, filterGroup } = useValues(errorTrackingSceneLogic) + const { order } = useValues(errorTrackingSceneLogic) + const { dateRange, filterTestAccounts, filterGroup } = useValues(errorTrackingLogic) + + const query = useMemo( + () => errorTrackingQuery({ order, dateRange, filterTestAccounts, filterGroup }), + [order, dateRange, filterTestAccounts, filterGroup] + ) const context: QueryContext = { columns: { @@ -36,39 +38,8 @@ export function ErrorTrackingScene(): JSX.Element { return (
- - + +
) } @@ -85,96 +56,3 @@ const CustomGroupTitleColumn: QueryContextColumnComponent = (props) => { /> ) } - -const Filters = (): JSX.Element => { - const { dateRange, order, filterGroup, filterTestAccounts } = useValues(errorTrackingSceneLogic) - const { setDateRange, setOrder, setFilterGroup, setFilterTestAccounts } = useActions(errorTrackingSceneLogic) - - return ( - { - setFilterGroup(filterGroup) - }} - > -
-
-
- { - setDateRange({ date_from: changedDateFrom, date_to: changedDateTo }) - }} - size="small" - /> - -
-
- { - setFilterTestAccounts(filter_test_accounts || false) - }} - size="small" - /> -
-
-
- - -
-
-
- ) -} - -const RecordingsUniversalFilterGroup = (): JSX.Element => { - const { filterGroup } = useValues(universalFiltersLogic) - const { replaceGroupValue, removeGroupValue } = useActions(universalFiltersLogic) - - const values = filterGroup.values as AnyPropertyFilter[] - - return ( - <> - {values.map((filter, index) => ( - removeGroupValue(index)} - onChange={(value) => replaceGroupValue(index, value)} - /> - ))} - - ) -} diff --git a/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts b/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts index c1ba3d9d88ff7..95490466861ea 100644 --- a/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts +++ b/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts @@ -1,53 +1,48 @@ -import { afterMount, kea, path, props, selectors } from 'kea' +import { afterMount, connect, kea, path, props, selectors } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' -import { uuid } from 'lib/utils' import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { HogQLQuery, NodeKind } from '~/queries/schema' -import { hogql } from '~/queries/utils' -import { Breadcrumb, ErrorTrackingGroup, EventType } from '~/types' +import { Breadcrumb, EventType } from '~/types' import type { errorTrackingGroupSceneLogicType } from './errorTrackingGroupSceneLogicType' +import { errorTrackingLogic } from './errorTrackingLogic' +import { errorTrackingGroupQuery } from './queries' export interface ErrorTrackingGroupSceneLogicProps { - id: ErrorTrackingGroup['id'] + id: string } +export type ExceptionEventType = Pick + export const errorTrackingGroupSceneLogic = kea([ path((key) => ['scenes', 'error-tracking', 'errorTrackingGroupSceneLogic', key]), props({} as ErrorTrackingGroupSceneLogicProps), - loaders(({ props }) => ({ - group: [ - null as ErrorTrackingGroup | null, - { - loadGroup: async () => { - // TODO: properly flesh out this page - return { - id: uuid(), - title: 'Placeholder title', - description: 'This is an error message', - occurrences: 0, - uniqueSessions: 0, - uniqueUsers: 0, - } - }, - }, - ], - eventProperties: [ - [] as EventType['properties'][], + connect({ + values: [errorTrackingLogic, ['dateRange', 'filterTestAccounts', 'filterGroup']], + }), + + loaders(({ props, values }) => ({ + events: [ + [] as ExceptionEventType[], { - loadGroupEvents: async () => { - const query: HogQLQuery = { - kind: NodeKind.HogQLQuery, - query: hogql`SELECT properties - FROM events e - WHERE event = '$exception' AND properties.$exception_type = ${props.id}`, - } - const res = await api.query(query) - return res.results.map((r) => JSON.parse(r[0])) + loadEvents: async () => { + const response = await api.query( + errorTrackingGroupQuery({ + group: props.id, + dateRange: values.dateRange, + filterTestAccounts: values.filterTestAccounts, + filterGroup: values.filterGroup, + }) + ) + + return response.results.map((r) => ({ + properties: JSON.parse(r[0]), + timestamp: r[1], + person: r[2], + })) }, }, ], @@ -55,8 +50,8 @@ export const errorTrackingGroupSceneLogic = kea [s.group], - (group): Breadcrumb[] => { + (_, p) => [p.id], + (id): Breadcrumb[] => { return [ { key: Scene.ErrorTracking, @@ -64,8 +59,8 @@ export const errorTrackingGroupSceneLogic = kea { - actions.loadGroup() - actions.loadGroupEvents() + actions.loadEvents() }), ]) diff --git a/frontend/src/scenes/error-tracking/errorTrackingLogic.ts b/frontend/src/scenes/error-tracking/errorTrackingLogic.ts new file mode 100644 index 0000000000000..6d0a7149cec16 --- /dev/null +++ b/frontend/src/scenes/error-tracking/errorTrackingLogic.ts @@ -0,0 +1,48 @@ +import { actions, kea, path, reducers } from 'kea' +import { UniversalFiltersGroup } from 'lib/components/UniversalFilters/UniversalFilters' + +import { DateRange, ErrorTrackingOrder } from '~/queries/schema' +import { FilterLogicalOperator } from '~/types' + +import type { errorTrackingLogicType } from './errorTrackingLogicType' + +export const errorTrackingLogic = kea([ + path(['scenes', 'error-tracking', 'errorTrackingLogic']), + + actions({ + setDateRange: (dateRange: DateRange) => ({ dateRange }), + setOrder: (order: ErrorTrackingOrder) => ({ order }), + setFilterGroup: (filterGroup: UniversalFiltersGroup) => ({ filterGroup }), + setFilterTestAccounts: (filterTestAccounts: boolean) => ({ filterTestAccounts }), + }), + reducers({ + dateRange: [ + { date_from: '-7d', date_to: null } as DateRange, + { persist: true }, + { + setDateRange: (_, { dateRange }) => dateRange, + }, + ], + order: [ + 'last_seen' as ErrorTrackingOrder, + { persist: true }, + { + setOrder: (_, { order }) => order, + }, + ], + filterGroup: [ + { type: FilterLogicalOperator.And, values: [] } as UniversalFiltersGroup, + { persist: true }, + { + setFilterGroup: (_, { filterGroup }) => filterGroup, + }, + ], + filterTestAccounts: [ + false as boolean, + { persist: true }, + { + setFilterTestAccounts: (_, { filterTestAccounts }) => filterTestAccounts, + }, + ], + }), +]) diff --git a/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts b/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts index 9e1eefa9d0390..85e867002fd9f 100644 --- a/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts +++ b/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts @@ -1,8 +1,6 @@ import { actions, kea, path, reducers } from 'kea' -import { UniversalFiltersGroup } from 'lib/components/UniversalFilters/UniversalFilters' -import { DateRange, ErrorTrackingOrder } from '~/queries/schema' -import { FilterLogicalOperator } from '~/types' +import { ErrorTrackingOrder } from '~/queries/schema' import type { errorTrackingSceneLogicType } from './errorTrackingSceneLogicType' @@ -10,35 +8,15 @@ export const errorTrackingSceneLogic = kea([ path(['scenes', 'error-tracking', 'errorTrackingSceneLogic']), actions({ - setDateRange: (dateRange: DateRange) => ({ dateRange }), setOrder: (order: ErrorTrackingOrder) => ({ order }), - setFilterGroup: (filterGroup: UniversalFiltersGroup) => ({ filterGroup }), - setFilterTestAccounts: (filterTestAccounts: boolean) => ({ filterTestAccounts }), }), reducers({ - dateRange: [ - { date_from: '-7d', date_to: null } as DateRange, - { - setDateRange: (_, { dateRange }) => dateRange, - }, - ], order: [ 'last_seen' as ErrorTrackingOrder, + { persist: true }, { setOrder: (_, { order }) => order, }, ], - filterGroup: [ - { type: FilterLogicalOperator.And, values: [] } as UniversalFiltersGroup, - { - setFilterGroup: (_, { filterGroup }) => filterGroup, - }, - ], - filterTestAccounts: [ - false as boolean, - { - setFilterTestAccounts: (_, { filterTestAccounts }) => filterTestAccounts, - }, - ], }), ]) diff --git a/frontend/src/scenes/error-tracking/queries.ts b/frontend/src/scenes/error-tracking/queries.ts new file mode 100644 index 0000000000000..5729c81a991d3 --- /dev/null +++ b/frontend/src/scenes/error-tracking/queries.ts @@ -0,0 +1,81 @@ +import { UniversalFiltersGroup } from 'lib/components/UniversalFilters/UniversalFilters' + +import { DataTableNode, DateRange, ErrorTrackingOrder, EventsQuery, NodeKind } from '~/queries/schema' +import { AnyPropertyFilter } from '~/types' + +export const errorTrackingQuery = ({ + order, + dateRange, + filterTestAccounts, + filterGroup, +}: { + order: ErrorTrackingOrder + dateRange: DateRange + filterTestAccounts: boolean + filterGroup: UniversalFiltersGroup +}): DataTableNode => { + return { + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.EventsQuery, + select: [ + 'any(properties) -- Error', + 'properties.$exception_type', + 'count() as unique_occurrences -- Occurrences', + 'count(distinct $session_id) as unique_sessions -- Sessions', + 'count(distinct distinct_id) as unique_users -- Users', + 'max(timestamp) as last_seen', + 'min(timestamp) as first_seen', + ], + orderBy: [order], + ...defaultProperties({ dateRange, filterTestAccounts, filterGroup }), + }, + hiddenColumns: [ + 'properties.$exception_type', + 'first_value(properties)', + 'max(timestamp) as last_seen', + 'min(timestamp) as first_seen', + ], + showActions: false, + showTimings: false, + } +} + +export const errorTrackingGroupQuery = ({ + group, + dateRange, + filterTestAccounts, + filterGroup, +}: { + group: string + dateRange: DateRange + filterTestAccounts: boolean + filterGroup: UniversalFiltersGroup +}): EventsQuery => { + return { + kind: NodeKind.EventsQuery, + select: ['properties', 'timestamp', 'person'], + where: [`properties.$exception_type = '${group}'`], + ...defaultProperties({ dateRange, filterTestAccounts, filterGroup }), + } +} + +const defaultProperties = ({ + dateRange, + filterTestAccounts, + filterGroup, +}: { + dateRange: DateRange + filterTestAccounts: boolean + filterGroup: UniversalFiltersGroup +}): Pick => { + const properties = filterGroup.values as AnyPropertyFilter[] + + return { + event: '$exception', + after: dateRange.date_from || undefined, + before: dateRange.date_to || undefined, + filterTestAccounts, + properties, + } +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index be05ba1d1fb0a..5624a8841ccbc 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1007,15 +1007,6 @@ export type ErrorCluster = { } export type ErrorClusterResponse = ErrorCluster[] | null -export type ErrorTrackingGroup = { - id: string - title: string - description: string - occurrences: number - uniqueSessions: number - uniqueUsers: number -} - export type EntityType = 'actions' | 'events' | 'data_warehouse' | 'new_entity' export interface Entity {