From a7352b41056f14e638dbac1a63f2efb7e7c49e5b Mon Sep 17 00:00:00 2001 From: Paolo D'Amico Date: Fri, 23 Apr 2021 17:13:50 -0700 Subject: [PATCH] Revert "Revert "Continuation - Frontend - Refactor event & properties for async server-side loading (#4078)" (#4092)" (#4093) This reverts commit d67ab843f176e5ed96ae912e9308c160c2333de3. --- .ts-strict-blacklist | 10 +- .../PropertyFilters/PropertySelect.tsx | 6 +- .../PropertyFilters/propertyFilterLogic.js | 12 +- ...PropertyKeyInfo.js => PropertyKeyInfo.tsx} | 31 +++-- frontend/src/lib/components/SelectBox.tsx | 8 +- frontend/src/lib/utils.tsx | 5 +- frontend/src/scenes/PreflightCheck/logic.ts | 1 - .../actions/{EventName.js => EventName.tsx} | 12 +- .../src/scenes/events/EventsVolumeTable.tsx | 108 ++++++++++-------- .../scenes/events/PropertiesVolumeTable.tsx | 21 ++-- .../scenes/events/eventDefinitionsLogic.ts | 89 +++++++++++++++ .../scenes/events/propertyDefinitionsLogic.ts | 82 +++++++++++++ .../ActionFilter/ActionFilterDropdown.tsx | 53 ++++----- .../insights/ActionFilter/ActionFilterRow.js | 24 ++-- .../ActionFilter/entityFilterLogic.js | 6 +- .../src/scenes/insights/BreakdownFilter.js | 10 +- .../scenes/insights/InsightTabs/PathTab.tsx | 4 +- .../sessions/filters/EventPropertyFilter.tsx | 6 +- .../sessions/filters/SessionsFilterBox.tsx | 37 +++--- frontend/src/scenes/teamLogic.tsx | 55 +-------- frontend/src/scenes/trends/trendsLogic.ts | 25 ++-- frontend/src/types.ts | 32 +++++- posthog/api/team.py | 27 +---- posthog/api/test/test_team.py | 14 ++- .../sync_event_and_properties_definitions.py | 8 +- posthog/test/test_team.py | 8 +- 26 files changed, 417 insertions(+), 277 deletions(-) rename frontend/src/lib/components/{PropertyKeyInfo.js => PropertyKeyInfo.tsx} (96%) rename frontend/src/scenes/actions/{EventName.js => EventName.tsx} (86%) create mode 100644 frontend/src/scenes/events/eventDefinitionsLogic.ts create mode 100644 frontend/src/scenes/events/propertyDefinitionsLogic.ts diff --git a/.ts-strict-blacklist b/.ts-strict-blacklist index f47d11e5a40..a323c8b3cb1 100644 --- a/.ts-strict-blacklist +++ b/.ts-strict-blacklist @@ -1,5 +1,6 @@ # This file contains a list of 'ignored' typescript errors. If any new ones are added they will cause ci breakage -# Add lines via bin/check-typescript-strict | cut -d":" -f1 +# You can get an updated list of blacklisted errors by running bin/check-typescript-strict | cut -d":" -f1 +# You will need to manually update this file if applicable. Type.ts frontend/src/initKea.ts(2,32) frontend/src/initKea.ts(4,31) @@ -60,13 +61,6 @@ frontend/src/scenes/sessions/SessionDetails.tsx(55,51) frontend/src/scenes/sessions/SessionsView.tsx(199,29) frontend/src/scenes/sessions/filters/DurationFilter.tsx(27,34) frontend/src/scenes/sessions/filters/EventPropertyFilter.tsx(21,41) -frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(43,13) -frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(44,13) -frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(78,13) -frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(79,13) -frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(87,13) -frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(116,13) -frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(173,9) frontend/src/scenes/sessions/sessionsPlayLogic.ts(155,61) frontend/src/toolbar/ToolbarApp.tsx(35,74) frontend/src/toolbar/actions/SelectorCount.tsx(2,38) diff --git a/frontend/src/lib/components/PropertyFilters/PropertySelect.tsx b/frontend/src/lib/components/PropertyFilters/PropertySelect.tsx index cc7c3cd36a6..6498a083450 100644 --- a/frontend/src/lib/components/PropertyFilters/PropertySelect.tsx +++ b/frontend/src/lib/components/PropertyFilters/PropertySelect.tsx @@ -3,13 +3,11 @@ import Fuse from 'fuse.js' import { Select } from 'antd' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { SelectGradientOverflow } from 'lib/components/SelectGradientOverflow' -import { EventProperty } from 'scenes/teamLogic' - -type PropertyOption = EventProperty +import { SelectOption } from '~/types' interface Props { optionGroups: Array - value: Partial | null + value: Partial | null onChange: (type: PropertyOptionGroup['type'], value: string) => void placeholder: string autoOpenIfEmpty?: boolean diff --git a/frontend/src/lib/components/PropertyFilters/propertyFilterLogic.js b/frontend/src/lib/components/PropertyFilters/propertyFilterLogic.js index d3f3e77c298..847e766387a 100644 --- a/frontend/src/lib/components/PropertyFilters/propertyFilterLogic.js +++ b/frontend/src/lib/components/PropertyFilters/propertyFilterLogic.js @@ -2,7 +2,7 @@ import { kea } from 'kea' import api from 'lib/api' import { objectsEqual } from 'lib/utils' import { router } from 'kea-router' -import { teamLogic } from 'scenes/teamLogic' +import { propertyDefinitionsLogic } from 'scenes/events/propertyDefinitionsLogic' export function parseProperties(input) { if (Array.isArray(input) || !input) { @@ -103,11 +103,11 @@ export const propertyFilterLogic = kea({ } } }, - [teamLogic.actionTypes.loadCurrentTeamSuccess]: async () => { - /* Set the event properties in case the `currentTeam` request came later, or the event + [propertyDefinitionsLogic.actionTypes.loadPropertyDefinitionsSuccess]: async () => { + /* Set the event properties in case the `loadPropertyDefinitions` request came later, or the event properties were updated. */ if (props.endpoint !== 'person' && props.endpoint !== 'sessions') { - actions.setProperties(teamLogic.values.eventProperties) + actions.setProperties(propertyDefinitionsLogic.values.transformedPropertyDefinitions) } }, }), @@ -136,7 +136,7 @@ export const propertyFilterLogic = kea({ }), selectors: { - filtersLoading: [() => [teamLogic.selectors.currentTeamLoading], (currentTeamLoading) => currentTeamLoading], + filtersLoading: [() => [propertyDefinitionsLogic.selectors.loaded], (loaded) => !loaded], }, events: ({ actions, props }) => ({ @@ -145,7 +145,7 @@ export const propertyFilterLogic = kea({ actions.loadPersonProperties() // TODO: Event properties in sessions is temporarily unsupported (context https://github.com/PostHog/posthog/issues/2735) if (props.endpoint !== 'person' && props.endpoint !== 'sessions') { - actions.setProperties(teamLogic.values.eventProperties) + actions.setProperties(propertyDefinitionsLogic.values.transformedPropertyDefinitions) } }, }), diff --git a/frontend/src/lib/components/PropertyKeyInfo.js b/frontend/src/lib/components/PropertyKeyInfo.tsx similarity index 96% rename from frontend/src/lib/components/PropertyKeyInfo.js rename to frontend/src/lib/components/PropertyKeyInfo.tsx index 82db966de85..93f3adb0863 100644 --- a/frontend/src/lib/components/PropertyKeyInfo.js +++ b/frontend/src/lib/components/PropertyKeyInfo.tsx @@ -1,7 +1,13 @@ import React from 'react' import { Popover } from 'antd' +import { KeyMapping } from '~/types' -export const keyMapping = { +export interface KeyMappingInterface { + event: Record + element: Record +} + +export const keyMapping: KeyMappingInterface = { event: { $timestamp: { label: 'Timestamp', @@ -357,12 +363,12 @@ export const keyMapping = { $geoip_latitude: { label: 'Latitude', description: `Approximated latitude matched to this event's IP address.`, - examples: [-33.8591, 13.1337], + examples: ['-33.8591', '13.1337'], }, $geoip_longitude: { label: 'Longitude', description: `Approximated longitude matched to this event's IP address.`, - examples: [151.2, 80.8008], + examples: ['151.2', '80.8008'], }, $geoip_time_zone: { label: 'Timezone', @@ -427,16 +433,17 @@ export const keyMapping = { }, } -export function PropertyKeyInfo({ value, type = 'event' }) { - let data - if (type === 'cohort') { - return value.name - } else { - data = keyMapping[type][value] - } +interface PropertyKeyInfoInterface { + value: string + type?: 'event' | 'element' +} - if (!data) { - return value +export function PropertyKeyInfo({ value, type = 'event' }: PropertyKeyInfoInterface): JSX.Element { + let data = null + if (value in keyMapping[type]) { + data = keyMapping[type][value] + } else { + return <>{value} } return ( diff --git a/frontend/src/lib/components/SelectBox.tsx b/frontend/src/lib/components/SelectBox.tsx index b363716bfe3..1b7a83c0aae 100644 --- a/frontend/src/lib/components/SelectBox.tsx +++ b/frontend/src/lib/components/SelectBox.tsx @@ -19,14 +19,14 @@ export interface SelectBoxItem { } export interface SelectedItem { - id?: number // Populated for actions + id?: number | string // Populated for actions (string is used for UUIDs) name: string key: string value?: string action?: ActionType - event?: string - volume?: number - usage_count?: number + volume_30_day?: number | null // Only for properties or events + query_usage_30_day?: number | null // Only for properties or events + is_numerical?: boolean // Only for properties category?: string cohort?: CohortType } diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index 092ca37f88c..c7e604d36d6 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -11,6 +11,7 @@ import { featureFlagLogic } from './logic/featureFlagLogic' import { open } from '@papercups-io/chat-widget' import posthog from 'posthog-js' import { WEBHOOK_SERVICES } from 'lib/constants' +import { KeyMappingInterface } from 'lib/components/PropertyKeyInfo' const SI_PREFIXES: { value: number; symbol: string }[] = [ { value: 1e18, symbol: 'E' }, @@ -269,7 +270,7 @@ export function isValidRegex(value: string): boolean { export function formatPropertyLabel( item: Record, cohorts: Record[], - keyMapping: Record> + keyMapping: KeyMappingInterface ): string { const { value, key, operator, type } = item return type === 'cohort' @@ -535,7 +536,7 @@ export function dateFilterToText( return name } -export function humanizeNumber(number: number, digits: number = 1): string { +export function humanizeNumber(number: number | null, digits: number = 1): string { if (number === null) { return '-' } diff --git a/frontend/src/scenes/PreflightCheck/logic.ts b/frontend/src/scenes/PreflightCheck/logic.ts index d1b06fa8b7d..bd162c2a41d 100644 --- a/frontend/src/scenes/PreflightCheck/logic.ts +++ b/frontend/src/scenes/PreflightCheck/logic.ts @@ -76,7 +76,6 @@ export const preflightLogic = kea ({ '/preflight': (_: any, { mode }: { mode: PreflightMode | null }) => { - console.log(mode) if (mode) { actions.setPreflightMode(mode, true) } diff --git a/frontend/src/scenes/actions/EventName.js b/frontend/src/scenes/actions/EventName.tsx similarity index 86% rename from frontend/src/scenes/actions/EventName.js rename to frontend/src/scenes/actions/EventName.tsx index 19690c33b52..559146db976 100644 --- a/frontend/src/scenes/actions/EventName.js +++ b/frontend/src/scenes/actions/EventName.tsx @@ -1,11 +1,17 @@ import React from 'react' import { Select } from 'antd' import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic' -export function EventName({ value, onChange, isActionStep = false }) { - const { eventNamesGrouped } = useValues(teamLogic) +interface EventNameInterface { + value: string + onChange: (value: string) => void + isActionStep?: boolean +} + +export function EventName({ value, onChange, isActionStep = false }: EventNameInterface): JSX.Element { + const { eventNamesGrouped } = useValues(eventDefinitionsLogic) return ( diff --git a/frontend/src/scenes/events/EventsVolumeTable.tsx b/frontend/src/scenes/events/EventsVolumeTable.tsx index 32c245a6862..18ebeb5dcb2 100644 --- a/frontend/src/scenes/events/EventsVolumeTable.tsx +++ b/frontend/src/scenes/events/EventsVolumeTable.tsx @@ -1,49 +1,53 @@ import React, { useEffect, useState } from 'react' import { useValues } from 'kea' -import { Alert, Input, Table, Tooltip } from 'antd' +import { Alert, Input, Skeleton, Table, Tooltip } from 'antd' import Fuse from 'fuse.js' import { InfoCircleOutlined, WarningOutlined } from '@ant-design/icons' -import { humanizeNumber } from 'lib/utils' -import { teamLogic } from 'scenes/teamLogic' +import { capitalizeFirstLetter, humanizeNumber } from 'lib/utils' import { preflightLogic } from 'scenes/PreflightCheck/logic' import { ColumnsType } from 'antd/lib/table' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' - -export interface EventOrPropType { - event?: string - key?: string - usage_count: number - volume: number - warnings: string[] -} +import { eventDefinitionsLogic } from './eventDefinitionsLogic' +import { EventDefinition, PropertyDefinition } from '~/types' type EventTableType = 'event' | 'property' -const searchEvents = (sources: EventOrPropType[], search: string, key: EventTableType): EventOrPropType[] => { +type EventOrPropType = EventDefinition & PropertyDefinition + +interface VolumeTableRecord { + eventOrProp: EventOrPropType + warnings: string[] +} + +const search = (sources: VolumeTableRecord[], searchQuery: string): VolumeTableRecord[] => { return new Fuse(sources, { - keys: [key], + keys: ['eventOrProp.name'], threshold: 0.3, }) - .search(search) + .search(searchQuery) .map((result) => result.item) } -export function VolumeTable({ type, data }: { type: EventTableType; data: EventOrPropType[] }): JSX.Element { +export function VolumeTable({ + type, + data, +}: { + type: EventTableType + data: Array +}): JSX.Element { const [searchTerm, setSearchTerm] = useState(false as string | false) - const [dataWithWarnings, setDataWithWarnings] = useState([] as EventOrPropType[]) + const [dataWithWarnings, setDataWithWarnings] = useState([] as VolumeTableRecord[]) - const key = type === 'property' ? 'key' : type // Properties are stored under `key` - - const columns: ColumnsType = [ + const columns: ColumnsType = [ { - title: type, - render: function RenderEvent(item: EventOrPropType): JSX.Element { + title: `${capitalizeFirstLetter(type)} name`, + render: function Render(_, record): JSX.Element { return ( - + - {item.warnings?.map((warning) => ( + {record.warnings?.map((warning) => ( ) }, - sorter: (a: EventOrPropType, b: EventOrPropType) => ('' + a[key]).localeCompare(b[key] || ''), + sorter: (a, b) => ('' + a.eventOrProp.name).localeCompare(b.eventOrProp.name || ''), filters: [ { text: 'Has warnings', value: 'warnings' }, { text: 'No warnings', value: 'noWarnings' }, @@ -78,11 +82,13 @@ export function VolumeTable({ type, data }: { type: EventTableType; data: EventO ) }, - render: function RenderVolume(item: EventOrPropType) { - return {humanizeNumber(item.volume)} + render: function RenderVolume(_, record) { + return {humanizeNumber(record.eventOrProp.volume_30_day)} }, - sorter: (a: EventOrPropType, b: EventOrPropType) => - a.volume == b.volume ? a.usage_count - b.usage_count : a.volume - b.volume, + sorter: (a, b) => + a.eventOrProp.volume_30_day == b.eventOrProp.volume_30_day + ? (a.eventOrProp.volume_30_day || -1) - (b.eventOrProp.volume_30_day || -1) + : (a.eventOrProp.volume_30_day || -1) - (b.eventOrProp.volume_30_day || -1), }, { title: function QueriesTitle() { @@ -96,30 +102,34 @@ export function VolumeTable({ type, data }: { type: EventTableType; data: EventO ) }, - // eslint-disable-next-line react/display-name - render: (item: EventOrPropType) => ( - {humanizeNumber(item.usage_count)} - ), - sorter: (a: EventOrPropType, b: EventOrPropType) => - a.usage_count == b.usage_count ? a.volume - b.volume : a.usage_count - b.usage_count, + render: function Render(_, item) { + return {humanizeNumber(item.eventOrProp.query_usage_30_day)} + }, + sorter: (a, b) => + a.eventOrProp.query_usage_30_day == b.eventOrProp.query_usage_30_day + ? (a.eventOrProp.query_usage_30_day || -1) - (b.eventOrProp.query_usage_30_day || -1) + : (a.eventOrProp.query_usage_30_day || -1) - (b.eventOrProp.query_usage_30_day || -1), }, ] + useEffect(() => { setDataWithWarnings( data.map( - (item): EventOrPropType => { - item.warnings = [] - if (item[key]?.endsWith(' ')) { - item.warnings.push(`This ${type} ends with a space.`) + (eventOrProp: EventOrPropType): VolumeTableRecord => { + const record = { eventOrProp } as VolumeTableRecord + record.warnings = [] + if (eventOrProp.name?.endsWith(' ')) { + record.warnings.push(`This ${type} ends with a space.`) } - if (item[key]?.startsWith(' ')) { - item.warnings.push(`This ${type} starts with a space.`) + if (eventOrProp.name?.startsWith(' ')) { + record.warnings.push(`This ${type} starts with a space.`) } - return item + return record } ) || [] ) }, []) + return ( <>
item.eventOrProp.name} size="small" style={{ marginBottom: '4rem' }} pagination={{ pageSize: 99999, hideOnSinglePage: true }} @@ -165,15 +175,15 @@ export function UsageDisabledWarning({ tab }: { tab: string }): JSX.Element { } export function EventsVolumeTable(): JSX.Element | null { - const { currentTeam } = useValues(teamLogic) const { preflight } = useValues(preflightLogic) + const { eventDefinitions, loaded } = useValues(eventDefinitionsLogic) - return currentTeam?.event_names_with_usage ? ( + return loaded ? ( <> {preflight && !preflight?.is_event_property_usage_enabled ? ( ) : ( - currentTeam?.event_names_with_usage[0]?.volume === null && ( + eventDefinitions[0].volume_30_day === null && ( <> ) )} - + - ) : null + ) : ( + + ) } diff --git a/frontend/src/scenes/events/PropertiesVolumeTable.tsx b/frontend/src/scenes/events/PropertiesVolumeTable.tsx index d29c40a41bb..58de7d3f9aa 100644 --- a/frontend/src/scenes/events/PropertiesVolumeTable.tsx +++ b/frontend/src/scenes/events/PropertiesVolumeTable.tsx @@ -1,28 +1,31 @@ import React from 'react' import { useValues } from 'kea' -import { VolumeTable, UsageDisabledWarning, EventOrPropType } from './EventsVolumeTable' -import { Alert } from 'antd' +import { VolumeTable, UsageDisabledWarning } from './EventsVolumeTable' +import { Alert, Skeleton } from 'antd' import { preflightLogic } from 'scenes/PreflightCheck/logic' -import { teamLogic } from 'scenes/teamLogic' +import { propertyDefinitionsLogic } from './propertyDefinitionsLogic' export function PropertiesVolumeTable(): JSX.Element | null { - const { currentTeam } = useValues(teamLogic) const { preflight } = useValues(preflightLogic) - return currentTeam?.event_properties_with_usage ? ( + const { propertyDefinitions, loaded } = useValues(propertyDefinitionsLogic) + + return loaded ? ( <> {preflight && !preflight?.is_event_property_usage_enabled ? ( ) : ( - currentTeam?.event_properties_with_usage[0]?.volume === null && ( + propertyDefinitions[0].volume_30_day === null && ( <> ) )} - + - ) : null + ) : ( + + ) } diff --git a/frontend/src/scenes/events/eventDefinitionsLogic.ts b/frontend/src/scenes/events/eventDefinitionsLogic.ts new file mode 100644 index 00000000000..5413ac0732e --- /dev/null +++ b/frontend/src/scenes/events/eventDefinitionsLogic.ts @@ -0,0 +1,89 @@ +import { kea } from 'kea' +import api from 'lib/api' +import { posthogEvents } from 'lib/utils' +import { EventDefinition, SelectOption } from '~/types' +import { eventDefinitionsLogicType } from './eventDefinitionsLogicType' + +interface EventDefinitionStorage { + count: number + next: null | string + results: EventDefinition[] +} + +interface EventsGroupedInterface { + label: string + options: SelectOption[] +} + +export const eventDefinitionsLogic = kea< + eventDefinitionsLogicType +>({ + loaders: ({ values }) => ({ + eventStorage: [ + { results: [], next: null, count: 0 } as EventDefinitionStorage, + { + loadEventDefinitions: async (initial?: boolean) => { + const url = initial + ? 'api/projects/@current/event_definitions/?limit=500' + : values.eventStorage.next + if (!url) { + throw new Error('Incorrect call to eventDefinitionsLogic.loadEventDefinitions') + } + const eventStorage = await api.get(url) + return { + count: eventStorage.count, + results: [...values.eventStorage.results, ...eventStorage.results], + next: eventStorage.next, + } + }, + }, + ], + }), + listeners: ({ actions }) => ({ + loadEventDefinitionsSuccess: ({ eventStorage }) => { + if (eventStorage.next) { + actions.loadEventDefinitions() + } + }, + }), + events: ({ actions }) => ({ + afterMount: () => { + actions.loadEventDefinitions(true) + }, + }), + selectors: { + loaded: [ + // Whether *all* the event definitions are fully loaded + (s) => [s.eventStorage, s.eventStorageLoading], + (eventStorage, eventStorageLoading): boolean => !eventStorageLoading && !eventStorage.next, + ], + eventDefinitions: [(s) => [s.eventStorage], (eventStorage): EventDefinition[] => eventStorage.results], + eventNames: [ + (s) => [s.eventDefinitions], + (eventDefinitions): string[] => eventDefinitions.map((definition) => definition.name), + ], + customEventNames: [ + (s) => [s.eventNames], + (eventNames): string[] => eventNames.filter((event) => !event.startsWith('!')), + ], + eventNamesGrouped: [ + (s) => [s.eventNames], + (eventNames): EventsGroupedInterface[] => { + const data: EventsGroupedInterface[] = [ + { label: 'Custom events', options: [] }, + { label: 'PostHog events', options: [] }, + ] + + eventNames.forEach((name: string) => { + const format = { label: name, value: name } + if (posthogEvents.includes(name)) { + return data[1].options.push(format) + } + data[0].options.push(format) + }) + + return data + }, + ], + }, +}) diff --git a/frontend/src/scenes/events/propertyDefinitionsLogic.ts b/frontend/src/scenes/events/propertyDefinitionsLogic.ts new file mode 100644 index 00000000000..09b77c69244 --- /dev/null +++ b/frontend/src/scenes/events/propertyDefinitionsLogic.ts @@ -0,0 +1,82 @@ +import { kea } from 'kea' +import api from 'lib/api' +import { PropertyDefinition, SelectOption } from '~/types' +import { propertyDefinitionsLogicType } from './propertyDefinitionsLogicType' + +interface PropertySelectOption extends SelectOption { + is_numerical?: boolean +} + +interface PropertyDefinitionStorage { + count: number + next: null | string + results: PropertyDefinition[] +} + +export const propertyDefinitionsLogic = kea< + propertyDefinitionsLogicType +>({ + loaders: ({ values }) => ({ + propertyStorage: [ + { results: [], next: null, count: 0 } as PropertyDefinitionStorage, + { + loadPropertyDefinitions: async (initial?: boolean) => { + const url = initial + ? 'api/projects/@current/property_definitions/?limit=500' + : values.propertyStorage.next + if (!url) { + throw new Error('Incorrect call to propertyDefinitionsLogic.loadPropertyDefinitions') + } + const propertyStorage = await api.get(url) + return { + count: propertyStorage.count, + results: [...values.propertyStorage.results, ...propertyStorage.results], + next: propertyStorage.next, + } + }, + }, + ], + }), + listeners: ({ actions }) => ({ + loadPropertyDefinitionsSuccess: ({ propertyStorage }) => { + if (propertyStorage.next) { + actions.loadPropertyDefinitions() + } + }, + }), + events: ({ actions }) => ({ + afterMount: () => { + actions.loadPropertyDefinitions(true) + }, + }), + selectors: { + loaded: [ + // Whether *all* the property definitions are fully loaded + (s) => [s.propertyStorage, s.propertyStorageLoading], + (propertyStorage, propertyStorageLoading): boolean => !propertyStorageLoading && !propertyStorage.next, + ], + propertyDefinitions: [ + (s) => [s.propertyStorage], + (propertyStorage): PropertyDefinition[] => propertyStorage.results || [], + ], + transformedPropertyDefinitions: [ + // Transformed propertyDefinitions to use in `Select` components + (s) => [s.propertyDefinitions], + (propertyDefinitions): PropertySelectOption[] => + propertyDefinitions.map((property) => ({ + value: property.name, + label: property.name, + is_numerical: property.is_numerical, + })), + ], + propertyNames: [ + (s) => [s.propertyDefinitions], + (propertyDefinitions): string[] => propertyDefinitions.map((definition) => definition.name), + ], + numericalPropertyNames: [ + (s) => [s.transformedPropertyDefinitions], + (transformedPropertyDefinitions): PropertySelectOption[] => + transformedPropertyDefinitions.filter((definition) => definition.is_numerical), + ], + }, +}) diff --git a/frontend/src/scenes/insights/ActionFilter/ActionFilterDropdown.tsx b/frontend/src/scenes/insights/ActionFilter/ActionFilterDropdown.tsx index b7c152ca2b5..ff27754434b 100644 --- a/frontend/src/scenes/insights/ActionFilter/ActionFilterDropdown.tsx +++ b/frontend/src/scenes/insights/ActionFilter/ActionFilterDropdown.tsx @@ -1,7 +1,6 @@ import React, { RefObject } from 'react' import { BuiltLogic, useActions, useValues } from 'kea' -import { ActionType } from '~/types' -import { EventUsageType } from '~/types' +import { ActionType, EventDefinition } from '~/types' import { EntityTypes } from '../../trends/trendsLogic' import { actionsModel } from '~/models/actionsModel' import { FireOutlined, InfoCircleOutlined, AimOutlined, ContainerOutlined } from '@ant-design/icons' @@ -9,8 +8,8 @@ import { Tooltip } from 'antd' import { ActionSelectInfo } from '../ActionSelectInfo' import { SelectBox, SelectedItem } from '../../../lib/components/SelectBox' import { Link } from 'lib/components/Link' -import { teamLogic } from 'scenes/teamLogic' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic' interface FilterType { filter: { @@ -26,10 +25,10 @@ interface FilterType { index: number } -const getSuggestions = (events: EventUsageType[]): EventUsageType[] => { +const getSuggestions = (events: EventDefinition[]): EventDefinition[] => { return events - .filter((event) => event.usage_count > 0) - .sort((a, b) => b.usage_count - a.usage_count) + .filter((event) => (event.query_usage_30_day || -1) > 0) + .sort((a, b) => (b.query_usage_30_day || -1) - (a.query_usage_30_day || -1)) .slice(0, 3) } @@ -52,7 +51,7 @@ export function ActionFilterDropdown({ const { updateFilter, setEntityFilterVisibility } = useActions(logic) const { actions } = useValues(actionsModel) - const { currentTeam } = useValues(teamLogic) + const { eventDefinitions } = useValues(eventDefinitionsLogic) const handleDismiss = (event: MouseEvent): void => { if (openButtonRef?.current?.contains(event.target as Node)) { @@ -68,7 +67,7 @@ export function ActionFilterDropdown({ setEntityFilterVisibility(selectedFilter.index, true) } } - const suggestions = getSuggestions(currentTeam?.event_names_with_usage || []) + const suggestions = getSuggestions(eventDefinitions || []) return ( ), - dataSource: suggestions.map((event) => ({ - key: 'suggestions' + event.event, - name: event.event, - ...event, + dataSource: suggestions.map((definition) => ({ + ...definition, + key: 'suggestions' + definition.id, })), renderInfo: function renderSuggestions({ item }) { return ( @@ -96,22 +94,22 @@ export function ActionFilterDropdown({ Suggestions

{item.name}

- {(item?.volume ?? 0 > 0) && ( + {(item?.volume_30_day ?? 0 > 0) && ( <> - Seen {item.volume} times.{' '} + Seen {item.volume_30_day} times.{' '} )} - {(item?.usage_count ?? 0 > 0) && ( + {(item?.query_usage_30_day ?? 0 > 0) && ( <> - Used in {item.usage_count} queries. + Used in {item.query_usage_30_day} queries. )} ) }, type: EntityTypes.EVENTS, - getValue: (item: SelectedItem) => item.event || '', - getLabel: (item: SelectedItem) => item.event || '', + getValue: (item: SelectedItem) => item.name || '', + getLabel: (item: SelectedItem) => item.name || '', }, { name: ( @@ -138,10 +136,9 @@ export function ActionFilterDropdown({ ), dataSource: - currentTeam?.event_names_with_usage.map((event) => ({ - key: EntityTypes.EVENTS + event.event, - name: event.event, - ...event, + eventDefinitions.map((definition) => ({ + ...definition, + key: EntityTypes.EVENTS + definition.id, })) || [], renderInfo: function events({ item }) { return ( @@ -149,22 +146,22 @@ export function ActionFilterDropdown({ Events

{item.name}

- {(item?.volume ?? 0 > 0) && ( + {(item?.volume_30_day ?? 0 > 0) && ( <> - Seen {item.volume} times.{' '} + Seen {item.volume_30_day} times.{' '} )} - {(item?.usage_count ?? 0 > 0) && ( + {(item?.query_usage_30_day ?? 0 > 0) && ( <> - Used in {item.usage_count} queries. + Used in {item.query_usage_30_day} queries. )} ) }, type: EntityTypes.EVENTS, - getValue: (item: SelectedItem) => item.event || '', - getLabel: (item: SelectedItem) => item.event || '', + getValue: (item: SelectedItem) => item.name || '', + getLabel: (item: SelectedItem) => item.name || '', }, ]} /> diff --git a/frontend/src/scenes/insights/ActionFilter/ActionFilterRow.js b/frontend/src/scenes/insights/ActionFilter/ActionFilterRow.js index cf9249f2ada..de0783dc595 100644 --- a/frontend/src/scenes/insights/ActionFilter/ActionFilterRow.js +++ b/frontend/src/scenes/insights/ActionFilter/ActionFilterRow.js @@ -8,10 +8,10 @@ import { PROPERTY_MATH_TYPE, EVENT_MATH_TYPE, MATHS } from 'lib/constants' import { DownOutlined, DeleteOutlined } from '@ant-design/icons' import { SelectGradientOverflow } from 'lib/components/SelectGradientOverflow' import './ActionFilterRow.scss' -import { teamLogic } from 'scenes/teamLogic' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { preflightLogic } from 'scenes/PreflightCheck/logic' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { propertyDefinitionsLogic } from 'scenes/events/propertyDefinitionsLogic' const EVENT_MATH_ENTRIES = Object.entries(MATHS).filter(([, item]) => item.type == EVENT_MATH_TYPE) const PROPERTY_MATH_ENTRIES = Object.entries(MATHS).filter(([, item]) => item.type == PROPERTY_MATH_TYPE) @@ -47,7 +47,7 @@ export function ActionFilterRow({ updateFilterProperty, setEntityFilterVisibility, } = useActions(logic) - const { eventProperties, eventPropertiesNumerical } = useValues(teamLogic) + const { propertyNames, numericalPropertyNames } = useValues(propertyDefinitionsLogic) const visible = entityFilterVisible[filter.order] @@ -58,20 +58,20 @@ export function ActionFilterRow({ const onClose = () => { removeLocalFilter({ value: filter.id, type: filter.type, index }) } - const onMathSelect = (_, math) => { + const onMathSelect = (_, selectedMath) => { updateFilterMath({ - math, - math_property: MATHS[math]?.onProperty ? mathProperty : undefined, - onProperty: MATHS[math]?.onProperty, + math: selectedMath, + math_property: MATHS[selectedMath]?.onProperty ? mathProperty : undefined, + onProperty: MATHS[selectedMath]?.onProperty, value: filter.id, type: filter.type, index: index, }) } - const onMathPropertySelect = (_, mathProperty) => { + const onMathPropertySelect = (_, selectedMathProperty) => { updateFilterMath({ math: filter.math, - math_property: mathProperty, + math_property: selectedMathProperty, value: filter.id, type: filter.type, index: index, @@ -139,9 +139,7 @@ export function ActionFilterRow({ math={math} index={index} onMathSelect={onMathSelect} - areEventPropertiesNumericalAvailable={ - eventPropertiesNumerical && eventPropertiesNumerical.length > 0 - } + areEventPropertiesNumericalAvailable={!!numericalPropertyNames.length} style={{ maxWidth: '100%', width: 'initial' }} /> )} @@ -168,7 +166,7 @@ export function ActionFilterRow({ mathProperty={mathProperty} index={index} onMathPropertySelect={onMathPropertySelect} - properties={eventPropertiesNumerical} + properties={numericalPropertyNames} /> )} {(!hidePropertySelector || (filter.properties && filter.properties.length > 0)) && ( @@ -188,7 +186,7 @@ export function ActionFilterRow({
updateFilterProperty({ properties, index })} style={{ marginBottom: 0 }} diff --git a/frontend/src/scenes/insights/ActionFilter/entityFilterLogic.js b/frontend/src/scenes/insights/ActionFilter/entityFilterLogic.js index 78f652eb4fd..a8b9c7750cb 100644 --- a/frontend/src/scenes/insights/ActionFilter/entityFilterLogic.js +++ b/frontend/src/scenes/insights/ActionFilter/entityFilterLogic.js @@ -1,7 +1,7 @@ import { kea } from 'kea' import { actionsModel } from '~/models/actionsModel' import { EntityTypes } from '../../trends/trendsLogic' -import { teamLogic } from 'scenes/teamLogic' +import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic' export function toLocalFilters(filters) { return [ @@ -33,7 +33,7 @@ export function toFilters(localFilters) { export const entityFilterLogic = kea({ key: (props) => props.typeKey, connect: { - values: [teamLogic, ['eventNames'], actionsModel, ['actions']], + values: [actionsModel, ['actions']], }, actions: () => ({ selectFilter: (filter) => ({ filter }), @@ -76,7 +76,7 @@ export const entityFilterLogic = kea({ selectors: ({ selectors }) => ({ entities: [ - () => [selectors.eventNames, selectors.actions], + () => [eventDefinitionsLogic.selectors.eventNames, selectors.actions], (events, actions) => { return { [EntityTypes.ACTIONS]: actions, diff --git a/frontend/src/scenes/insights/BreakdownFilter.js b/frontend/src/scenes/insights/BreakdownFilter.js index 20c7db18a4d..914d3285d46 100644 --- a/frontend/src/scenes/insights/BreakdownFilter.js +++ b/frontend/src/scenes/insights/BreakdownFilter.js @@ -1,17 +1,17 @@ import React, { useState } from 'react' import { Tooltip, Select, Tabs, Popover, Button } from 'antd' import { useValues } from 'kea' -import { teamLogic } from 'scenes/teamLogic' import { propertyFilterLogic } from 'lib/components/PropertyFilters/propertyFilterLogic' import { cohortsModel } from '../../models/cohortsModel' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { SelectGradientOverflow } from 'lib/components/SelectGradientOverflow' import { ShownAsValue } from 'lib/constants' +import { propertyDefinitionsLogic } from 'scenes/events/propertyDefinitionsLogic' const { TabPane } = Tabs function PropertyFilter({ breakdown, onChange }) { - const { eventProperties } = useValues(teamLogic) + const { transformedPropertyDefinitions } = useValues(propertyDefinitionsLogic) const { personProperties } = useValues(propertyFilterLogic({ pageKey: 'breakdown' })) return ( option.value?.toLowerCase().indexOf(input.toLowerCase()) >= 0} data-attr="prop-breakdown-select" > - {eventProperties.length > 0 && ( + {transformedPropertyDefinitions.length > 0 && ( - {Object.entries(eventProperties).map(([key, item], index) => ( + {Object.entries(transformedPropertyDefinitions).map(([key, item], index) => ( diff --git a/frontend/src/scenes/insights/InsightTabs/PathTab.tsx b/frontend/src/scenes/insights/InsightTabs/PathTab.tsx index 747b0ad94c4..b6d3d408eba 100644 --- a/frontend/src/scenes/insights/InsightTabs/PathTab.tsx +++ b/frontend/src/scenes/insights/InsightTabs/PathTab.tsx @@ -12,10 +12,10 @@ import { import { Select } from 'antd' import { PropertyValue } from 'lib/components/PropertyFilters' import { TestAccountFilter } from '../TestAccountFilter' -import { teamLogic } from 'scenes/teamLogic' +import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic' export function PathTab(): JSX.Element { - const { customEventNames } = useValues(teamLogic) + const { customEventNames } = useValues(eventDefinitionsLogic) const { filter } = useValues(pathsLogic({ dashboardItemId: null })) const { setFilter } = useActions(pathsLogic({ dashboardItemId: null })) diff --git a/frontend/src/scenes/sessions/filters/EventPropertyFilter.tsx b/frontend/src/scenes/sessions/filters/EventPropertyFilter.tsx index 2a487385f90..eded63db412 100644 --- a/frontend/src/scenes/sessions/filters/EventPropertyFilter.tsx +++ b/frontend/src/scenes/sessions/filters/EventPropertyFilter.tsx @@ -5,7 +5,7 @@ import { EventTypePropertyFilter } from '~/types' import { keyMapping } from 'lib/components/PropertyKeyInfo' import { OperatorValueSelect } from 'lib/components/PropertyFilters/OperatorValueSelect' import { sessionsFiltersLogic } from 'scenes/sessions/filters/sessionsFiltersLogic' -import { teamLogic } from 'scenes/teamLogic' +import { propertyDefinitionsLogic } from 'scenes/events/propertyDefinitionsLogic' interface Props { filter: EventTypePropertyFilter @@ -13,7 +13,7 @@ interface Props { } export function EventPropertyFilter({ filter, selector }: Props): JSX.Element { - const { eventProperties } = useValues(teamLogic) + const { transformedPropertyDefinitions } = useValues(propertyDefinitionsLogic) const { updateFilter } = useActions(sessionsFiltersLogic) const property = filter.properties && filter.properties.length > 0 ? filter.properties[0] : null @@ -29,7 +29,7 @@ export function EventPropertyFilter({ filter, selector }: Props): JSX.Element { { type: 'event', label: 'Event properties', - options: eventProperties, + options: transformedPropertyDefinitions, }, ]} onChange={(_, key) => { diff --git a/frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx b/frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx index ac8b505e729..81a4a033ba4 100644 --- a/frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx +++ b/frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx @@ -9,14 +9,14 @@ import { ActionInfo } from 'scenes/insights/ActionFilter/ActionFilterDropdown' import { FilterSelector, sessionsFiltersLogic } from 'scenes/sessions/filters/sessionsFiltersLogic' import { Link } from 'lib/components/Link' import { cohortsModel } from '~/models/cohortsModel' -import { teamLogic } from 'scenes/teamLogic' +import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic' export function SessionsFilterBox({ selector }: { selector: FilterSelector }): JSX.Element | null { const { openFilter, personProperties } = useValues(sessionsFiltersLogic) const { closeFilterSelect, dropdownSelected } = useActions(sessionsFiltersLogic) - const { currentTeam } = useValues(teamLogic) + const { eventDefinitions } = useValues(eventDefinitionsLogic) const { actions } = useValues(actionsModel) const { cohorts } = useValues(cohortsModel) @@ -40,8 +40,8 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J })), renderInfo: ActionInfo, type: 'action_type', - getValue: (item: SelectedItem) => item.action?.id, - getLabel: (item: SelectedItem) => item.action?.name, + getValue: (item: SelectedItem) => item.action?.id || '', + getLabel: (item: SelectedItem) => item.action?.name || '', }, { name: ( @@ -50,10 +50,9 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J ), dataSource: - (currentTeam?.event_names_with_usage ?? []).map((event) => ({ - key: EntityTypes.EVENTS + event.event, - name: event.event, - ...event, + eventDefinitions.map((definition) => ({ + key: EntityTypes.EVENTS + definition.name, + ...definition, })) || [], renderInfo: function events({ item }) { return ( @@ -61,22 +60,22 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J Events

{item.name}

- {item?.volume && ( + {item?.volume_30_day && ( <> - Seen {item.volume} times.{' '} + Seen {item.volume_30_day} times.{' '} )} - {item?.usage_count && ( + {item?.query_usage_30_day && ( <> - Used in {item.usage_count} queries. + Used in {item.query_usage_30_day} queries. )} ) }, type: 'event_type', - getValue: (item: SelectedItem) => item.event, - getLabel: (item: SelectedItem) => item.event, + getValue: (item: SelectedItem) => item.name, + getLabel: (item: SelectedItem) => item.name, }, { name: ( @@ -86,7 +85,7 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J ), dataSource: cohorts.map((cohort: CohortType) => ({ key: 'cohorts' + cohort.id, - name: cohort.name, + name: cohort.name || '', id: cohort.id, cohort, })), @@ -113,7 +112,7 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J ) }, type: 'cohort', - getValue: (item: SelectedItem) => item.id, + getValue: (item: SelectedItem) => item.id || '', getLabel: (item: SelectedItem) => item.name, }, ] @@ -136,9 +135,9 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J User property

{item.name}

- {item?.usage_count && ( + {item?.query_usage_30_day && ( <> - {item.usage_count} users have this property. + {item.query_usage_30_day} users have this property. )} @@ -170,7 +169,7 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J ) }, type: 'recording', - getValue: (item: SelectedItem) => item.value, + getValue: (item: SelectedItem) => item.value || '', getLabel: (item: SelectedItem) => item.name, }) diff --git a/frontend/src/scenes/teamLogic.tsx b/frontend/src/scenes/teamLogic.tsx index 031c03ee4a9..56172bf3ae4 100644 --- a/frontend/src/scenes/teamLogic.tsx +++ b/frontend/src/scenes/teamLogic.tsx @@ -5,14 +5,9 @@ import { TeamType } from '~/types' import { userLogic } from './userLogic' import { toast } from 'react-toastify' import React from 'react' -import { posthogEvents, identifierToHuman, resolveWebhookService } from 'lib/utils' +import { identifierToHuman, resolveWebhookService } from 'lib/utils' -export interface EventProperty { - value: string - label: string -} - -export const teamLogic = kea>({ +export const teamLogic = kea>({ actions: { deleteTeam: (team: TeamType) => ({ team }), deleteTeamSuccess: true, @@ -97,52 +92,6 @@ export const teamLogic = kea>({ window.location.href = '/ingestion' }, }), - selectors: { - eventProperties: [ - (s) => [s.currentTeam], - (team): EventProperty[] => - team - ? team.event_properties.map( - (property: string) => ({ value: property, label: property } as EventProperty) - ) - : [], - ], - eventPropertiesNumerical: [ - (s) => [s.currentTeam], - (team): EventProperty[] => - team - ? team.event_properties_numerical.map( - (property: string) => ({ value: property, label: property } as EventProperty) - ) - : [], - ], - eventNames: [(s) => [s.currentTeam], (team): string[] => team?.event_names ?? []], - customEventNames: [ - (s) => [s.eventNames], - (eventNames): string[] => { - return eventNames.filter((event) => !event.startsWith('!')) - }, - ], - eventNamesGrouped: [ - (s) => [s.currentTeam], - (team) => { - const data = [ - { label: 'Custom events', options: [] as EventProperty[] }, - { label: 'PostHog events', options: [] as EventProperty[] }, - ] - if (team) { - team.event_names.forEach((name: string) => { - const format = { label: name, value: name } as EventProperty - if (posthogEvents.includes(name)) { - return data[1].options.push(format) - } - data[0].options.push(format) - }) - } - return data - }, - ], - }, events: ({ actions }) => ({ afterMount: [actions.loadCurrentTeam], }), diff --git a/frontend/src/scenes/trends/trendsLogic.ts b/frontend/src/scenes/trends/trendsLogic.ts index df02d44604a..6279c343e52 100644 --- a/frontend/src/scenes/trends/trendsLogic.ts +++ b/frontend/src/scenes/trends/trendsLogic.ts @@ -21,7 +21,8 @@ import { ActionType, EntityType, FilterType, PersonType, PropertyFilter, TrendRe import { cohortLogic } from 'scenes/persons/cohortLogic' import { trendsLogicType } from './trendsLogicType' import { dashboardItemsModel } from '~/models/dashboardItemsModel' -import { teamLogic } from 'scenes/teamLogic' +import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic' +import { propertyDefinitionsLogic } from 'scenes/events/propertyDefinitionsLogic' interface TrendResponse { result: TrendResult[] @@ -167,7 +168,7 @@ export const trendsLogic = kea< }, connect: { - values: [teamLogic, ['eventNames'], actionsModel, ['actions']], + values: [actionsModel, ['actions']], }, loaders: ({ values, props }) => ({ @@ -248,12 +249,11 @@ export const trendsLogic = kea< FilterType >, { - setFilters: (state, { filters, mergeFilters }) => { - return cleanFilters({ + setFilters: (state, { filters, mergeFilters }) => + cleanFilters({ ...(mergeFilters ? state : {}), ...filters, - }) - }, + }), }, ], people: [ @@ -317,8 +317,8 @@ export const trendsLogic = kea< selectors: () => ({ filtersLoading: [ - () => [teamLogic.selectors.currentTeamLoading], - (currentTeamLoading: any) => currentTeamLoading, + () => [eventDefinitionsLogic.selectors.loaded, propertyDefinitionsLogic.selectors.loaded], + (eventsLoaded, propertiesLoaded) => !eventsLoaded || !propertiesLoaded, ], results: [(selectors) => [selectors._results], (response) => response.result], resultsLoading: [(selectors) => [selectors._resultsLoading], (_resultsLoading) => _resultsLoading], @@ -511,8 +511,8 @@ export const trendsLogic = kea< }) actions.setBreakdownValuesLoading(false) }, - [teamLogic.actionTypes.loadCurrentTeamSuccess]: async () => { - actions.setFilters(getDefaultFilters(values.filters, values.eventNames), true) + [eventDefinitionsLogic.actionTypes.loadEventDefinitionsSuccess]: async () => { + actions.setFilters(getDefaultFilters(values.filters, eventDefinitionsLogic.values.eventNames), true) }, }), @@ -569,7 +569,10 @@ export const trendsLogic = kea< cleanSearchParams['compare'] = false } - Object.assign(cleanSearchParams, getDefaultFilters(cleanSearchParams, values.eventNames)) + Object.assign( + cleanSearchParams, + getDefaultFilters(cleanSearchParams, eventDefinitionsLogic.values.eventNames) + ) if (!objectsEqual(cleanSearchParams, values.filters)) { actions.setFilters(cleanSearchParams, false) diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 0880684f94e..88f9d8f58ce 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -120,11 +120,6 @@ export interface TeamBasicType { export interface TeamType extends TeamBasicType { anonymize_ips: boolean app_urls: string[] - event_names: string[] - event_properties: string[] - event_properties_numerical: string[] - event_names_with_usage: EventUsageType[] - event_properties_with_usage: PropertyUsageType[] slack_incoming_webhook: string session_recording_opt_in: boolean session_recording_retention_period_days: number | null @@ -675,3 +670,30 @@ export interface LicenseType { max_users: string | null created_at: string } + +export interface EventDefinition { + id: string + name: string + volume_30_day: number | null + query_usage_30_day: number | null +} + +export interface PropertyDefinition { + id: string + name: string + volume_30_day: number | null + query_usage_30_day: number | null + is_numerical?: boolean // Marked as optional to allow merge of EventDefinition & PropertyDefinition +} + +export interface SelectOption { + value: string + label: string +} + +export interface KeyMapping { + label: string + description: string | JSX.Element + examples?: string[] + hide?: boolean +} diff --git a/posthog/api/team.py b/posthog/api/team.py index e3dcbc5f346..159c03423b6 100644 --- a/posthog/api/team.py +++ b/posthog/api/team.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Type, cast +from typing import Any, Dict, Optional, Type, cast from django.db import transaction from django.shortcuts import get_object_or_404 @@ -10,12 +10,7 @@ from posthog.mixins import AnalyticsDestroyModelMixin from posthog.models import Organization, Team from posthog.models.user import User from posthog.models.utils import generate_random_token -from posthog.permissions import ( - CREATE_METHODS, - OrganizationAdminWritePermissions, - OrganizationMemberPermissions, - ProjectMembershipNecessaryPermissions, -) +from posthog.permissions import CREATE_METHODS, OrganizationAdminWritePermissions, ProjectMembershipNecessaryPermissions class PremiumMultiprojectPermissions(permissions.BasePermission): @@ -37,10 +32,6 @@ class PremiumMultiprojectPermissions(permissions.BasePermission): class TeamSerializer(serializers.ModelSerializer): - - event_names_with_usage = serializers.SerializerMethodField() - event_properties_with_usage = serializers.SerializerMethodField() - class Meta: model = Team fields = ( @@ -62,11 +53,6 @@ class TeamSerializer(serializers.ModelSerializer): "data_attributes", "session_recording_opt_in", "session_recording_retention_period_days", - "event_names", - "event_properties", - "event_properties_numerical", - "event_names_with_usage", - "event_properties_with_usage", ) read_only_fields = ( "id", @@ -77,9 +63,6 @@ class TeamSerializer(serializers.ModelSerializer): "created_at", "updated_at", "ingested_event", - "event_names", - "event_properties", - "event_properties_numerical", ) def create(self, validated_data: Dict[str, Any], **kwargs) -> Team: @@ -92,12 +75,6 @@ class TeamSerializer(serializers.ModelSerializer): request.user.save() return team - def get_event_names_with_usage(self, instance: Team) -> List: - return instance.get_latest_event_names_with_usage() - - def get_event_properties_with_usage(self, instance: Team) -> List: - return instance.get_latest_event_properties_with_usage() - class TeamViewSet(AnalyticsDestroyModelMixin, viewsets.ModelViewSet): serializer_class = TeamSerializer diff --git a/posthog/api/test/test_team.py b/posthog/api/test/test_team.py index 288b3a23baf..3cded968b70 100644 --- a/posthog/api/test/test_team.py +++ b/posthog/api/test/test_team.py @@ -17,6 +17,8 @@ class TestTeamAPI(APIBaseTest): self.assertEqual(response_data["results"][0]["name"], self.team.name) self.assertNotIn("test_account_filters", response_data["results"][0]) self.assertNotIn("data_attributes", response_data["results"][0]) + + # TODO: #4070 These assertions will no longer make sense when we fully remove these attributes from the model self.assertNotIn("event_names", response_data["results"][0]) self.assertNotIn("event_properties", response_data["results"][0]) self.assertNotIn("event_properties_numerical", response_data["results"][0]) @@ -31,11 +33,13 @@ class TestTeamAPI(APIBaseTest): self.assertEqual(response_data["timezone"], "UTC") self.assertEqual(response_data["is_demo"], False) self.assertEqual(response_data["slack_incoming_webhook"], self.team.slack_incoming_webhook) - self.assertIn("event_names", response_data) - self.assertIn("event_properties", response_data) - self.assertIn("event_properties_numerical", response_data) - self.assertIn("event_names_with_usage", response_data) - self.assertIn("event_properties_with_usage", response_data) + # The properties below are no longer included as part of the request + # TODO: #4070 These assertions will no longer make sense when we fully remove these attributes from the model + self.assertNotIn("event_names", response_data) + self.assertNotIn("event_properties", response_data) + self.assertNotIn("event_properties_numerical", response_data) + self.assertNotIn("event_names_with_usage", response_data) + self.assertNotIn("event_properties_with_usage", response_data) def test_cant_retrieve_project_from_another_org(self): org = Organization.objects.create(name="New Org") diff --git a/posthog/tasks/sync_event_and_properties_definitions.py b/posthog/tasks/sync_event_and_properties_definitions.py index 90079a3defa..f860c539fc4 100644 --- a/posthog/tasks/sync_event_and_properties_definitions.py +++ b/posthog/tasks/sync_event_and_properties_definitions.py @@ -32,8 +32,8 @@ def sync_event_and_properties_definitions(team_uuid: str) -> None: # Add or update any existing events for event in team.event_names: instance, _ = EventDefinition.objects.get_or_create(team=team, name=event) - instance.volume_30_day = transformed_event_usage.get(event, {}).get("volume") or 0 - instance.query_usage_30_day = transformed_event_usage.get(event, {}).get("usage_count") or 0 + instance.volume_30_day = transformed_event_usage.get(event, {}).get("volume") + instance.query_usage_30_day = transformed_event_usage.get(event, {}).get("usage_count") instance.save() # Remove any deleted events @@ -42,8 +42,8 @@ def sync_event_and_properties_definitions(team_uuid: str) -> None: # Add or update any existing properties for property in team.event_properties: property_instance, _ = PropertyDefinition.objects.get_or_create(team=team, name=property) - property_instance.volume_30_day = transformed_property_usage.get(property, {}).get("volume") or 0 - property_instance.query_usage_30_day = transformed_property_usage.get(property, {}).get("usage_count") or 0 + property_instance.volume_30_day = transformed_property_usage.get(property, {}).get("volume") + property_instance.query_usage_30_day = transformed_property_usage.get(property, {}).get("usage_count") property_instance.is_numerical = property in team.event_properties_numerical property_instance.save() diff --git a/posthog/test/test_team.py b/posthog/test/test_team.py index 2d09d26a260..53f70728985 100644 --- a/posthog/test/test_team.py +++ b/posthog/test/test_team.py @@ -56,8 +56,8 @@ class TestTeam(BaseTest): for obj in EventDefinition.objects.filter(team=team): self.assertIn(obj.name, expected_events) - self.assertEqual(obj.volume_30_day, 0) - self.assertEqual(obj.query_usage_30_day, 0) + self.assertEqual(obj.volume_30_day, None) + self.assertEqual(obj.query_usage_30_day, None) # Test adding and removing one event team.event_names.pop(0) @@ -111,8 +111,8 @@ class TestTeam(BaseTest): for obj in PropertyDefinition.objects.filter(team=team): self.assertIn(obj.name, expected_properties) - self.assertEqual(obj.volume_30_day, 0) - self.assertEqual(obj.query_usage_30_day, 0) + self.assertEqual(obj.volume_30_day, None) + self.assertEqual(obj.query_usage_30_day, None) self.assertEqual(obj.is_numerical, obj.name in numerical_properties) # Test adding and removing one event