diff --git a/frontend/src/scenes/paths/PathNodeCard.tsx b/frontend/src/scenes/paths/PathNodeCard.tsx index 2f999f94bef03..0e1f2e27055ce 100644 --- a/frontend/src/scenes/paths/PathNodeCard.tsx +++ b/frontend/src/scenes/paths/PathNodeCard.tsx @@ -25,11 +25,15 @@ export function PathNodeCard({ insightProps, node }: PathNodeCardProps): JSX.Ele return null } + // Attention: targetLinks are the incoming links, sourceLinks are the outgoing links const isPathStart = node.targetLinks.length === 0 const isPathEnd = node.sourceLinks.length === 0 const continuingCount = node.sourceLinks.reduce((prev, curr) => prev + curr.value, 0) const dropOffCount = node.value - continuingCount - const averageConversionTime = !isPathStart ? node.targetLinks[0].average_conversion_time / 1000 : null + const averageConversionTime = !isPathStart + ? node.targetLinks.reduce((prev, curr) => prev + curr.average_conversion_time / 1000, 0) / + node.targetLinks.length + : null return ( diff --git a/frontend/src/scenes/paths/pathsDataLogic.ts b/frontend/src/scenes/paths/pathsDataLogic.ts index 5f54718e1900a..6493fd9f91dae 100644 --- a/frontend/src/scenes/paths/pathsDataLogic.ts +++ b/frontend/src/scenes/paths/pathsDataLogic.ts @@ -147,6 +147,9 @@ export const pathsDataLogic = kea([ label: path_dropoff_key || path_start_key || path_end_key || 'Pageview', isDropOff: Boolean(path_dropoff_key), }), + additionalFields: { + value_at_data_point: 'event_count', + }, }) } else if (personsUrl) { openPersonsModal({ diff --git a/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx b/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx index dc0ec85ec8b4a..067ec40f10388 100644 --- a/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx +++ b/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx @@ -26,7 +26,6 @@ import { sessionPlayerModalLogic } from 'scenes/session-recordings/player/modal/ import { teamLogic } from 'scenes/teamLogic' import { Noun } from '~/models/groupsModel' -import { InsightActorsQuery } from '~/queries/schema' import { ActorType, ExporterFormat, @@ -35,13 +34,11 @@ import { SessionRecordingType, } from '~/types' -import { personsModalLogic } from './personsModalLogic' +import { PersonModalLogicProps, personsModalLogic } from './personsModalLogic' import { SaveCohortModal } from './SaveCohortModal' -export interface PersonsModalProps extends Pick { +export interface PersonsModalProps extends PersonModalLogicProps, Pick { onAfterClose?: () => void - query?: InsightActorsQuery | null - url?: string | null urlsIndex?: number urls?: { label: string | JSX.Element @@ -58,6 +55,7 @@ export function PersonsModal({ title, onAfterClose, inline, + additionalFields, }: PersonsModalProps): JSX.Element { const [selectedUrlIndex, setSelectedUrlIndex] = useState(urlsIndex || 0) const originalUrl = (urls || [])[selectedUrlIndex]?.value || _url || '' @@ -65,6 +63,7 @@ export function PersonsModal({ const logic = personsModalLogic({ url: originalUrl, query: _query, + additionalFields, }) const { @@ -80,7 +79,7 @@ export function PersonsModal({ missingActorsCount, propertiesTimelineFilterFromUrl, exploreUrl, - ActorsQuery, + actorsQuery, } = useValues(logic) const { updateActorsQuery, setSearchTerm, saveAsCohort, setIsCohortModalOpen, closeModal, loadNextActors } = useActions(logic) @@ -221,7 +220,7 @@ export function PersonsModal({ void triggerExport({ export_format: ExporterFormat.CSV, export_context: query - ? { source: ActorsQuery as Record } + ? { source: actorsQuery as Record } : { path: originalUrl }, }) }} diff --git a/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts b/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts index 314863f85ecde..49c8e66ab7adf 100644 --- a/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts +++ b/frontend/src/scenes/trends/persons-modal/personsModalLogic.ts @@ -23,6 +23,7 @@ import { ActorType, BreakdownType, ChartDisplayType, + CommonActorType, IntervalType, PersonActorType, PropertiesTimelineFilterType, @@ -35,6 +36,7 @@ const RESULTS_PER_PAGE = 100 export interface PersonModalLogicProps { query?: InsightActorsQuery | null url?: string | null + additionalFields?: Partial> } export interface ListActorsResponse { @@ -61,16 +63,19 @@ export const personsModalLogic = kea([ query, clear, offset, + additionalFields, }: { url?: string | null query?: InsightActorsQuery | null clear?: boolean offset?: number + additionalFields?: PersonModalLogicProps['additionalFields'] }) => ({ url, query, clear, offset, + additionalFields, }), loadNextActors: true, updateActorsQuery: (query: Partial) => ({ query }), @@ -85,7 +90,7 @@ export const personsModalLogic = kea([ actorsResponse: [ null as ListActorsResponse | null, { - loadActors: async ({ url, query, clear, offset }, breakpoint) => { + loadActors: async ({ url, query, clear, offset, additionalFields }, breakpoint) => { if (url) { url += '&include_recordings=true' @@ -102,28 +107,41 @@ export const personsModalLogic = kea([ return res } else if (query) { const response = await performQuery({ - ...values.ActorsQuery, + ...values.actorsQuery, limit: RESULTS_PER_PAGE + 1, offset: offset || 0, } as ActorsQuery) breakpoint() + + const assembledSelectFields = values.selectFields + const additionalFieldIndices = Object.values(additionalFields || {}).map((field) => + assembledSelectFields.indexOf(field) + ) const newResponse: ListActorsResponse = { results: [ { count: response.results.length, - people: response.results.slice(0, RESULTS_PER_PAGE).map( - (result): PersonActorType => ({ - type: 'person', - id: result[0].id, - uuid: result[0].id, - distinct_ids: result[0].distinct_ids, - is_identified: result[0].is_identified, - properties: result[0].properties, - created_at: result[0].created_at, - matched_recordings: [], - value_at_data_point: null, - }) - ), + people: response.results + .slice(0, RESULTS_PER_PAGE) + .map((result): PersonActorType => { + const person: PersonActorType = { + type: 'person', + id: result[0].id, + uuid: result[0].id, + distinct_ids: result[0].distinct_ids, + is_identified: result[0].is_identified, + properties: result[0].properties, + created_at: result[0].created_at, + matched_recordings: [], + value_at_data_point: null, + } + + Object.keys(additionalFields || {}).forEach((field, index) => { + person[field] = result[additionalFieldIndices[index]] + }) + + return person + }), }, ], } @@ -213,8 +231,8 @@ export const personsModalLogic = kea([ is_static: true, name: cohortName, } - if (values.ActorsQuery) { - const cohort = await api.create('api/cohort', { ...cohortParams, query: values.ActorsQuery }) + if (values.actorsQuery) { + const cohort = await api.create('api/cohort', { ...cohortParams, query: values.actorsQuery }) cohortsModel.actions.cohortCreated(cohort) lemonToast.success('Cohort saved', { toastId: `cohort-saved-${cohort.id}`, @@ -299,28 +317,35 @@ export const personsModalLogic = kea([ return cleanFilters(filter) }, ], - ActorsQuery: [ - (s) => [s.query, s.searchTerm], - (query, searchTerm): ActorsQuery | null => { + selectFields: [ + () => [(_, p) => p.additionalFields], + (additionalFields: PersonModalLogicProps['additionalFields']): string[] => { + const extra = Object.values(additionalFields || {}) + return ['person', 'created_at', ...extra] + }, + ], + actorsQuery: [ + (s) => [(_, p) => p.query, s.searchTerm, s.selectFields], + (query, searchTerm, selectFields): ActorsQuery | null => { if (!query) { return null } return { kind: NodeKind.ActorsQuery, source: query, - select: ['person', 'created_at'], + select: selectFields, orderBy: ['created_at DESC'], search: searchTerm, } }, ], exploreUrl: [ - (s) => [s.ActorsQuery], - (ActorsQuery): string | null => { - if (!ActorsQuery) { + (s) => [s.actorsQuery], + (actorsQuery): string | null => { + if (!actorsQuery) { return null } - const { select: _select, ...source } = ActorsQuery + const { select: _select, ...source } = actorsQuery const query: DataTableNode = { kind: NodeKind.DataTableNode, source, @@ -332,7 +357,7 @@ export const personsModalLogic = kea([ }), afterMount(({ actions, props }) => { - actions.loadActors({ query: props.query, url: props.url }) + actions.loadActors({ query: props.query, url: props.url, additionalFields: props.additionalFields }) actions.reportPersonsModalViewed({ url: props.url, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 778c0f815413e..820c7acab4bed 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -891,7 +891,7 @@ export interface MatchedRecording { events: MatchedRecordingEvent[] } -interface CommonActorType { +export interface CommonActorType { id: string | number properties: Record /** @format date-time */ diff --git a/posthog/hogql_queries/insights/paths_query_runner.py b/posthog/hogql_queries/insights/paths_query_runner.py index 154649a8a6b29..3932d315908e9 100644 --- a/posthog/hogql_queries/insights/paths_query_runner.py +++ b/posthog/hogql_queries/insights/paths_query_runner.py @@ -504,23 +504,23 @@ def to_actors_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: if self.query.pathsFilter.pathDropoffKey: conditions.append( parse_expr( - "path_dropoff_key = {path_dropoff_key} AND path_dropoff_key = path_key", - {"path_dropoff_key": ast.Constant(value=self.query.pathsFilter.pathDropoffKey)}, + "path_dropoff_key = {key} AND path_dropoff_key = path_key", + {"key": ast.Constant(value=self.query.pathsFilter.pathDropoffKey)}, ) ) else: if self.query.pathsFilter.pathStartKey: conditions.append( parse_expr( - "last_path_key = {path_start_key}", - {"path_start_key": ast.Constant(value=self.query.pathsFilter.pathStartKey)}, + "last_path_key = {key}", + {"key": ast.Constant(value=self.query.pathsFilter.pathStartKey)}, ) ) if self.query.pathsFilter.pathEndKey: conditions.append( parse_expr( - "path_key = {path_end_key}", - {"path_end_key": ast.Constant(value=self.query.pathsFilter.pathEndKey)}, + "path_key = {key}", + {"key": ast.Constant(value=self.query.pathsFilter.pathEndKey)}, ) ) else: @@ -531,10 +531,12 @@ def to_actors_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: actors_query = parse_select( """ - SELECT DISTINCT - person_id as actor_id + SELECT + person_id as actor_id, + COUNT(*) as event_count FROM {paths_per_person_query} WHERE {conditions} + GROUP BY person_id ORDER BY actor_id """, placeholders={