From b169fc127b003d9c41854d8bb9ca4a4e8f06e82c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Oberm=C3=BCller?= Date: Wed, 3 Apr 2024 16:26:54 +0200 Subject: [PATCH] feat(hogql): Add funnels to paths insight (#21133) * Use funnel in paths query Closes #21044 --------- Co-authored-by: Julian Bez --- .../utils/filtersToQueryNode.test.ts | 43 ++++- .../InsightQuery/utils/filtersToQueryNode.ts | 17 +- .../utils/queryNodeToFilter.test.ts | 101 +++++++++++- .../InsightQuery/utils/queryNodeToFilter.ts | 13 +- frontend/src/queries/schema.json | 26 ++- frontend/src/queries/schema.ts | 10 +- .../insights/EditorFilters/PathsTarget.tsx | 61 +++---- .../insights/InsightNav/insightNavLogic.tsx | 6 + .../scenes/insights/insightVizDataLogic.ts | 1 + frontend/src/scenes/paths/PathNodeCard.tsx | 12 +- frontend/src/scenes/paths/Paths.tsx | 16 +- frontend/src/scenes/paths/pathUtils.ts | 18 ++- frontend/src/scenes/paths/pathsDataLogic.ts | 3 +- frontend/src/scenes/paths/renderPaths.ts | 8 +- mypy-baseline.txt | 56 ------- .../hogql_queries/insights/funnels/utils.py | 2 +- .../insights/paths_query_runner.py | 150 ++++++++++++++++-- .../legacy_compatibility/filter_to_query.py | 25 ++- .../test/test_filter_to_query.py | 58 ++++--- posthog/schema.py | 128 ++++++++------- 20 files changed, 540 insertions(+), 214 deletions(-) diff --git a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts index fea11606ec4b6..630801daa6577 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts @@ -507,7 +507,25 @@ describe('filtersToQueryNode', () => { end_point: 'b', path_groupings: ['c', 'd'], funnel_paths: FunnelPathType.between, - funnel_filter: { a: 1 }, + funnel_filter: { + events: [ + { + id: '$pageview', + name: '$pageview', + order: 0, + type: 'events', + }, + { + id: null, + order: 1, + type: 'events', + }, + ], + exclusions: [], + funnel_step: 1, + funnel_viz_type: 'steps', + insight: 'FUNNELS', + }, exclude_events: ['e', 'f'], step_limit: 1, path_start_key: 'g', @@ -529,8 +547,6 @@ describe('filtersToQueryNode', () => { startPoint: 'a', endPoint: 'b', pathGroupings: ['c', 'd'], - funnelPaths: FunnelPathType.between, - funnelFilter: { a: 1 }, excludeEvents: ['e', 'f'], stepLimit: 1, pathReplacements: true, @@ -539,6 +555,27 @@ describe('filtersToQueryNode', () => { minEdgeWeight: 1, maxEdgeWeight: 1, }, + funnelPathsFilter: { + funnelPathType: FunnelPathType.between, + funnelStep: 1, + funnelSource: { + funnelsFilter: { + funnelVizType: FunnelVizType.Steps, + }, + kind: NodeKind.FunnelsQuery, + series: [ + { + event: '$pageview', + kind: NodeKind.EventsNode, + name: '$pageview', + }, + { + event: null, + kind: NodeKind.EventsNode, + }, + ], + }, + }, } expect(result).toEqual(query) }) diff --git a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts index b832a167813e2..3eee6a5984db4 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts @@ -18,7 +18,9 @@ import { EventsNode, FunnelExclusionActionsNode, FunnelExclusionEventsNode, + FunnelPathsFilter, FunnelsFilter, + FunnelsQuery, InsightNodeKind, InsightQueryNode, InsightsQueryBase, @@ -303,6 +305,7 @@ export const filtersToQueryNode = (filters: Partial): InsightQueryNo // paths filter if (isPathsFilter(filters) && isPathsQuery(query)) { query.pathsFilter = pathsFilterToQuery(filters) + query.funnelPathsFilter = filtersToFunnelPathsQuery(filters) } // stickiness filter @@ -378,8 +381,6 @@ export const pathsFilterToQuery = (filters: Partial): PathsFilt startPoint: filters.start_point, endPoint: filters.end_point, pathGroupings: filters.path_groupings, - funnelPaths: filters.funnel_paths, - funnelFilter: filters.funnel_filter, excludeEvents: filters.exclude_events, stepLimit: filters.step_limit, pathReplacements: filters.path_replacements, @@ -390,6 +391,18 @@ export const pathsFilterToQuery = (filters: Partial): PathsFilt }) } +export const filtersToFunnelPathsQuery = (filters: Partial): FunnelPathsFilter | undefined => { + if (filters.funnel_paths === undefined || filters.funnel_filter === undefined) { + return undefined + } + + return { + funnelPathType: filters.funnel_paths, + funnelSource: filtersToQueryNode(filters.funnel_filter) as FunnelsQuery, + funnelStep: filters.funnel_filter?.funnel_step, + } +} + export const stickinessFilterToQuery = (filters: Record): StickinessFilter => { return objectCleanWithEmpty({ display: filters.display, diff --git a/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.test.ts b/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.test.ts index 9b9dd04421962..94777bfa254c1 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.test.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.test.ts @@ -1,16 +1,19 @@ import { FunnelLayout } from 'lib/constants' import { hiddenLegendItemsToKeys, queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' -import { FunnelsQuery, LifecycleQuery, NodeKind, TrendsQuery } from '~/queries/schema' +import { FunnelsQuery, LifecycleQuery, NodeKind, PathsQuery, TrendsQuery } from '~/queries/schema' import { BreakdownAttributionType, ChartDisplayType, FunnelConversionWindowTimeUnit, + FunnelPathType, FunnelsFilterType, FunnelStepReference, FunnelVizType, InsightType, LifecycleFilterType, + PathsFilterType, + PathType, StepOrderValue, TrendsFilterType, } from '~/types' @@ -178,6 +181,102 @@ describe('queryNodeToFilter', () => { } expect(result).toEqual(filters) }) + + test('converts a pathsFilter and funnelPathsFilter into filter properties', () => { + const query: PathsQuery = { + kind: NodeKind.PathsQuery, + pathsFilter: { + includeEventTypes: [PathType.Screen, PathType.PageView], + startPoint: 'a', + endPoint: 'b', + pathGroupings: ['c', 'd'], + excludeEvents: ['e', 'f'], + stepLimit: 1, + pathReplacements: true, + localPathCleaningFilters: [{ alias: 'home' }], + edgeLimit: 1, + minEdgeWeight: 1, + maxEdgeWeight: 1, + }, + funnelPathsFilter: { + funnelPathType: FunnelPathType.between, + funnelStep: 1, + funnelSource: { + funnelsFilter: { + funnelVizType: FunnelVizType.Steps, + }, + kind: NodeKind.FunnelsQuery, + series: [ + { + event: '$pageview', + kind: NodeKind.EventsNode, + name: '$pageview', + }, + { + event: null, + kind: NodeKind.EventsNode, + }, + ], + }, + }, + } + + const result = queryNodeToFilter(query) + + const filters: Partial = { + insight: InsightType.PATHS, + include_event_types: [PathType.Screen, PathType.PageView], + start_point: 'a', + end_point: 'b', + path_groupings: ['c', 'd'], + funnel_paths: FunnelPathType.between, + entity_type: 'events', + funnel_filter: { + entity_type: 'events', + events: [ + { + id: '$pageview', + name: '$pageview', + order: 0, + type: 'events', + }, + { + id: null, + order: 1, + type: 'events', + }, + ], + exclusions: undefined, + funnel_step: 1, + funnel_viz_type: 'steps', + insight: 'FUNNELS', + bin_count: undefined, + breakdown_attribution_type: undefined, + breakdown_attribution_value: undefined, + funnel_aggregate_by_hogql: undefined, + funnel_from_step: undefined, + funnel_to_step: undefined, + funnel_order_type: undefined, + funnel_step_reference: undefined, + funnel_window_interval: undefined, + funnel_window_interval_unit: undefined, + hidden_legend_keys: undefined, + interval: undefined, + }, + exclude_events: ['e', 'f'], + step_limit: 1, + // path_start_key: 'g', + // path_end_key: 'h', + // path_dropoff_key: 'i', + path_replacements: true, + local_path_cleaning_filters: [{ alias: 'home' }], + edge_limit: 1, + min_edge_weight: 1, + max_edge_weight: 1, + paths_hogql_expression: undefined, + } + expect(result).toEqual(filters) + }) }) describe('hiddenLegendItemsToKeys', () => { diff --git a/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts b/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts index 8c03814ead320..125d138e8779d 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts @@ -266,8 +266,14 @@ export const queryNodeToFilter = (query: InsightQueryNode): Partial camelCasedPathsProps.local_path_cleaning_filters = queryCopy.pathsFilter?.localPathCleaningFilters camelCasedPathsProps.min_edge_weight = queryCopy.pathsFilter?.minEdgeWeight camelCasedPathsProps.max_edge_weight = queryCopy.pathsFilter?.maxEdgeWeight - camelCasedPathsProps.funnel_paths = queryCopy.pathsFilter?.funnelPaths - camelCasedPathsProps.funnel_filter = queryCopy.pathsFilter?.funnelFilter + camelCasedPathsProps.funnel_paths = queryCopy.funnelPathsFilter?.funnelPathType + camelCasedPathsProps.funnel_filter = + queryCopy.funnelPathsFilter !== undefined + ? { + ...queryNodeToFilter(queryCopy.funnelPathsFilter.funnelSource), + funnel_step: queryCopy.funnelPathsFilter.funnelStep, + } + : undefined delete queryCopy.pathsFilter?.edgeLimit delete queryCopy.pathsFilter?.pathsHogQLExpression delete queryCopy.pathsFilter?.includeEventTypes @@ -280,8 +286,7 @@ export const queryNodeToFilter = (query: InsightQueryNode): Partial delete queryCopy.pathsFilter?.localPathCleaningFilters delete queryCopy.pathsFilter?.minEdgeWeight delete queryCopy.pathsFilter?.maxEdgeWeight - delete queryCopy.pathsFilter?.funnelPaths - delete queryCopy.pathsFilter?.funnelFilter + delete queryCopy.funnelPathsFilter } else if (isStickinessQuery(queryCopy)) { camelCasedStickinessProps.show_legend = queryCopy.stickinessFilter?.showLegend camelCasedStickinessProps.show_values_on_series = queryCopy.stickinessFilter?.showValuesOnSeries diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index c846f43882f88..8f971367e02fd 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1989,6 +1989,22 @@ "enum": ["funnel_path_before_step", "funnel_path_between_steps", "funnel_path_after_step"], "type": "string" }, + "FunnelPathsFilter": { + "additionalProperties": false, + "properties": { + "funnelPathType": { + "$ref": "#/definitions/FunnelPathType" + }, + "funnelSource": { + "$ref": "#/definitions/FunnelsQuery" + }, + "funnelStep": { + "type": "integer" + } + }, + "required": ["funnelSource"], + "type": "object" + }, "FunnelStepReference": { "enum": ["total", "previous"], "type": "string" @@ -3273,12 +3289,6 @@ }, "type": "array" }, - "funnelFilter": { - "type": "object" - }, - "funnelPaths": { - "$ref": "#/definitions/FunnelPathType" - }, "includeEventTypes": { "items": { "$ref": "#/definitions/PathType" @@ -3409,6 +3419,10 @@ "description": "Exclude internal and test users by applying the respective filters", "type": "boolean" }, + "funnelPathsFilter": { + "$ref": "#/definitions/FunnelPathsFilter", + "description": "Used for displaying paths in relation to funnel steps." + }, "kind": { "const": "PathsQuery", "type": "string" diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 2e8f15368df6d..1401572d4f75b 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -788,8 +788,6 @@ export type PathsFilter = { localPathCleaningFilters?: PathsFilterLegacy['local_path_cleaning_filters'] minEdgeWeight?: PathsFilterLegacy['min_edge_weight'] maxEdgeWeight?: PathsFilterLegacy['max_edge_weight'] - funnelPaths?: PathsFilterLegacy['funnel_paths'] - funnelFilter?: PathsFilterLegacy['funnel_filter'] /** Relevant only within actors query */ pathStartKey?: string @@ -799,11 +797,19 @@ export type PathsFilter = { pathDropoffKey?: string } +export type FunnelPathsFilter = { + funnelPathType: PathsFilterLegacy['funnel_paths'] + funnelSource: FunnelsQuery + funnelStep?: integer +} + export interface PathsQuery extends InsightsQueryBase { kind: NodeKind.PathsQuery response?: PathsQueryResponse /** Properties specific to the paths insight */ pathsFilter: PathsFilter + /** Used for displaying paths in relation to funnel steps. */ + funnelPathsFilter?: FunnelPathsFilter } /** `StickinessFilterType` minus everything inherited from `FilterType` and persons modal related params diff --git a/frontend/src/scenes/insights/EditorFilters/PathsTarget.tsx b/frontend/src/scenes/insights/EditorFilters/PathsTarget.tsx index 73c5a243e180f..27c7c68ad0fc5 100644 --- a/frontend/src/scenes/insights/EditorFilters/PathsTarget.tsx +++ b/frontend/src/scenes/insights/EditorFilters/PathsTarget.tsx @@ -6,6 +6,8 @@ import { IconFunnelVertical } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { pathsDataLogic } from 'scenes/paths/pathsDataLogic' +import { queryNodeToFilter } from '~/queries/nodes/InsightQuery/utils/queryNodeToFilter' +import { FunnelsQuery, PathsQuery } from '~/queries/schema' import { EditorFilterProps, FunnelPathType } from '~/types' export function PathsTargetStart(props: EditorFilterProps): JSX.Element { @@ -21,13 +23,14 @@ type PathTargetProps = { } & EditorFilterProps function PathsTarget({ position, insightProps }: PathTargetProps): JSX.Element { - const { pathsFilter, taxonomicGroupTypes } = useValues(pathsDataLogic(insightProps)) - const { updateInsightFilter } = useActions(pathsDataLogic(insightProps)) + const { pathsFilter, funnelPathsFilter, taxonomicGroupTypes } = useValues(pathsDataLogic(insightProps)) + const { updateInsightFilter, updateQuerySource } = useActions(pathsDataLogic(insightProps)) - const { funnelPaths, funnelFilter, startPoint, endPoint, pathGroupings } = pathsFilter || {} + const { startPoint, endPoint, pathGroupings } = pathsFilter || {} + const { funnelPathType, funnelSource, funnelStep } = funnelPathsFilter || {} - const overrideStartInput = funnelPaths && [FunnelPathType.between, FunnelPathType.after].includes(funnelPaths) - const overrideEndInput = funnelPaths && [FunnelPathType.between, FunnelPathType.before].includes(funnelPaths) + const overrideStartInput = funnelPathType && [FunnelPathType.between, FunnelPathType.after].includes(funnelPathType) + const overrideEndInput = funnelPathType && [FunnelPathType.between, FunnelPathType.before].includes(funnelPathType) const overrideInputs = overrideStartInput || overrideEndInput const key = position === 'start' ? 'startPoint' : 'endPoint' @@ -35,29 +38,24 @@ function PathsTarget({ position, insightProps }: PathTargetProps): JSX.Element { updateInsightFilter({ [key]: item }) } const onReset = (): void => { - updateInsightFilter({ [key]: undefined, funnelFilter: undefined, funnelPaths: undefined }) + updateQuerySource({ + pathsFilter: { ...pathsFilter, [key]: undefined }, + funnelPathsFilter: undefined, + } as Partial) } - function _getStepNameAtIndex(filters: Record, index: number): string { - const targetEntity = - filters.events?.filter((event: Record) => { - return event.order === index - 1 - })?.[0] || - filters.actions?.filter((action: Record) => { - return action.order === index - 1 - })?.[0] - - return targetEntity?.name || '' + function _getStepNameAtIndex(filters: FunnelsQuery, index: number): string { + return filters.series[index - 1].name ?? '' } - function _getStepLabel(funnelFilters?: Record, index?: number, shift: number = 0): JSX.Element { - if (funnelFilters && index) { + function _getStepLabel(funnelSource?: FunnelsQuery, index?: number, shift: number = 0): JSX.Element { + if (funnelSource && index) { return (
{`${ index > 0 ? 'Funnel step ' + (index + shift) : 'Funnel dropoff ' + index * -1 - }: ${_getStepNameAtIndex(funnelFilters, index > 0 ? index + shift : index * -1)}`} + }: ${_getStepNameAtIndex(funnelSource, index > 0 ? index + shift : index * -1)}`}
) } else { @@ -66,12 +64,12 @@ function PathsTarget({ position, insightProps }: PathTargetProps): JSX.Element { } function getStartPointLabel(): JSX.Element { - if (funnelPaths) { - if (funnelPaths === FunnelPathType.after) { - return _getStepLabel(funnelFilter, funnelFilter?.funnel_step) - } else if (funnelPaths === FunnelPathType.between) { + if (funnelPathType) { + if (funnelPathType === FunnelPathType.after) { + return _getStepLabel(funnelSource, funnelStep) + } else if (funnelPathType === FunnelPathType.between) { // funnel_step targets the later of the 2 events when specifying between so the start point index is shifted back 1 - return _getStepLabel(funnelFilter, funnelFilter?.funnel_step, -1) + return _getStepLabel(funnelSource, funnelStep, -1) } else { return } @@ -85,9 +83,9 @@ function PathsTarget({ position, insightProps }: PathTargetProps): JSX.Element { } function getEndPointLabel(): JSX.Element { - if (funnelPaths) { - if (funnelPaths === FunnelPathType.before || funnelPaths === FunnelPathType.between) { - return _getStepLabel(funnelFilter, funnelFilter?.funnel_step) + if (funnelPathType) { + if (funnelPathType === FunnelPathType.before || funnelPathType === FunnelPathType.between) { + return _getStepLabel(funnelSource, funnelStep) } else { return } @@ -107,7 +105,7 @@ function PathsTarget({ position, insightProps }: PathTargetProps): JSX.Element { pathItem: startPoint, closeButtonEnabled: startPoint || overrideStartInput, disabled: overrideEndInput && !overrideStartInput, - funnelFilterLink: funnelFilter && overrideStartInput, + funnelFilterLink: funnelSource && overrideStartInput, }, end: { index: 1, @@ -115,7 +113,7 @@ function PathsTarget({ position, insightProps }: PathTargetProps): JSX.Element { pathItem: endPoint, closeButtonEnabled: endPoint || overrideEndInput, disabled: overrideStartInput && !overrideEndInput, - funnelFilterLink: funnelFilter && overrideEndInput, + funnelFilterLink: funnelSource && overrideEndInput, }, }[position] @@ -139,7 +137,10 @@ function PathsTarget({ position, insightProps }: PathTargetProps): JSX.Element { positionOptions.funnelFilterLink ? () => { router.actions.push( - combineUrl('/insights', encodeParams(funnelFilter as Record, '?')).url + combineUrl( + '/insights', + encodeParams(queryNodeToFilter(funnelSource as FunnelsQuery), '?') + ).url ) } : () => {} diff --git a/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx b/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx index 9dff8c87eeb8c..15a58a8c13e1d 100644 --- a/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx +++ b/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx @@ -38,6 +38,7 @@ import { isInsightQueryWithSeries, isInsightVizNode, isLifecycleQuery, + isPathsQuery, isRetentionQuery, isStickinessQuery, isTrendsQuery, @@ -385,6 +386,11 @@ const mergeCachedProperties = (query: InsightQueryNode, cache: QueryPropertyCach mergedQuery.breakdownFilter = cache.breakdownFilter } + // funnel paths filter + if (isPathsQuery(mergedQuery) && cache.funnelPathsFilter) { + mergedQuery.funnelPathsFilter = cache.funnelPathsFilter + } + // insight specific filter const filterKey = filterKeyForQuery(mergedQuery) if (cache[filterKey] || cache.commonFilter) { diff --git a/frontend/src/scenes/insights/insightVizDataLogic.ts b/frontend/src/scenes/insights/insightVizDataLogic.ts index a73ffdc89afa9..250ac40172add 100644 --- a/frontend/src/scenes/insights/insightVizDataLogic.ts +++ b/frontend/src/scenes/insights/insightVizDataLogic.ts @@ -178,6 +178,7 @@ export const insightVizDataLogic = kea([ pathsFilter: [(s) => [s.querySource], (q) => (isPathsQuery(q) ? q.pathsFilter : null)], stickinessFilter: [(s) => [s.querySource], (q) => (isStickinessQuery(q) ? q.stickinessFilter : null)], lifecycleFilter: [(s) => [s.querySource], (q) => (isLifecycleQuery(q) ? q.lifecycleFilter : null)], + funnelPathsFilter: [(s) => [s.querySource], (q) => (isPathsQuery(q) ? q.funnelPathsFilter : null)], isUsingSessionAnalysis: [ (s) => [s.series, s.breakdownFilter, s.properties], diff --git a/frontend/src/scenes/paths/PathNodeCard.tsx b/frontend/src/scenes/paths/PathNodeCard.tsx index b1b4dad0232a4..f52fc28af115c 100644 --- a/frontend/src/scenes/paths/PathNodeCard.tsx +++ b/frontend/src/scenes/paths/PathNodeCard.tsx @@ -1,6 +1,7 @@ import { LemonDropdown, Tooltip } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import { FunnelPathsFilter } from '~/queries/schema' import { InsightLogicProps } from '~/types' import { PATH_NODE_CARD_LEFT_OFFSET, PATH_NODE_CARD_TOP_OFFSET, PATH_NODE_CARD_WIDTH } from './constants' @@ -15,10 +16,11 @@ export type PathNodeCardProps = { } export function PathNodeCard({ insightProps, node }: PathNodeCardProps): JSX.Element | null { - const { pathsFilter } = useValues(pathsDataLogic(insightProps)) + const { pathsFilter: _pathsFilter, funnelPathsFilter: _funnelPathsFilter } = useValues(pathsDataLogic(insightProps)) const { updateInsightFilter, openPersonsModal, viewPathToFunnel } = useActions(pathsDataLogic(insightProps)) - const filter = pathsFilter || {} + const pathsFilter = _pathsFilter || {} + const funnelPathsFilter = _funnelPathsFilter || ({} as FunnelPathsFilter) if (!node.visible) { return null @@ -66,7 +68,9 @@ export function PathNodeCard({ insightProps, node }: PathNodeCardProps): JSX.Ele ? node.y0 + PATH_NODE_CARD_TOP_OFFSET : // use middle for end nodes node.y0 + (node.y1 - node.y0) / 2, - border: `1px solid ${isSelectedPathStartOrEnd(filter, node) ? 'purple' : 'var(--border)'}`, + border: `1px solid ${ + isSelectedPathStartOrEnd(pathsFilter, funnelPathsFilter, node) ? 'purple' : 'var(--border)' + }`, }} data-attr="path-node-card-button" > @@ -77,7 +81,7 @@ export function PathNodeCard({ insightProps, node }: PathNodeCardProps): JSX.Ele viewPathToFunnel={viewPathToFunnel} openPersonsModal={openPersonsModal} setFilter={updateInsightFilter} - filter={filter} + filter={pathsFilter} /> diff --git a/frontend/src/scenes/paths/Paths.tsx b/frontend/src/scenes/paths/Paths.tsx index e865923142e17..933f2825567f9 100644 --- a/frontend/src/scenes/paths/Paths.tsx +++ b/frontend/src/scenes/paths/Paths.tsx @@ -6,6 +6,8 @@ import { useEffect, useRef, useState } from 'react' import { InsightEmptyState, InsightErrorState } from 'scenes/insights/EmptyStates' import { insightLogic } from 'scenes/insights/insightLogic' +import { FunnelPathsFilter } from '~/queries/schema' + import { PathNodeCard } from './PathNodeCard' import { pathsDataLogic } from './pathsDataLogic' import type { PathNodeData } from './pathUtils' @@ -23,7 +25,9 @@ export function Paths(): JSX.Element { const [nodeCards, setNodeCards] = useState([]) const { insight, insightProps } = useValues(insightLogic) - const { paths, pathsFilter, insightDataLoading, insightDataError } = useValues(pathsDataLogic(insightProps)) + const { paths, pathsFilter, funnelPathsFilter, insightDataLoading, insightDataError } = useValues( + pathsDataLogic(insightProps) + ) const id = `'${insight?.short_id || DEFAULT_PATHS_ID}'` @@ -35,7 +39,15 @@ export function Paths(): JSX.Element { const elements = document?.getElementById(id)?.querySelectorAll(`.Paths__canvas`) elements?.forEach((node) => node?.parentNode?.removeChild(node)) - renderPaths(canvasRef, canvasWidth, canvasHeight, paths, pathsFilter || {}, setNodeCards) + renderPaths( + canvasRef, + canvasWidth, + canvasHeight, + paths, + pathsFilter || {}, + funnelPathsFilter || ({} as FunnelPathsFilter), + setNodeCards + ) }, [paths, !insightDataLoading, canvasWidth, canvasHeight]) if (insightDataError) { diff --git a/frontend/src/scenes/paths/pathUtils.ts b/frontend/src/scenes/paths/pathUtils.ts index fd0336483d3c7..16b82d61b2395 100644 --- a/frontend/src/scenes/paths/pathUtils.ts +++ b/frontend/src/scenes/paths/pathUtils.ts @@ -1,6 +1,6 @@ import { RGBColor } from 'd3' -import { PathsFilter } from '~/queries/schema' +import { FunnelPathsFilter, PathsFilter } from '~/queries/schema' import { FunnelPathType } from '~/types' export interface PathTargetLink { @@ -114,16 +114,22 @@ export function pageUrl(d: PathNodeData, display?: boolean): string { : name } -export const isSelectedPathStartOrEnd = (pathsFilter: PathsFilter, pathItemCard: PathNodeData): boolean => { +export const isSelectedPathStartOrEnd = ( + pathsFilter: PathsFilter, + funnelPathsFilter: FunnelPathsFilter, + pathItemCard: PathNodeData +): boolean => { const cardName = pageUrl(pathItemCard) const isPathStart = pathItemCard.targetLinks.length === 0 const isPathEnd = pathItemCard.sourceLinks.length === 0 - const { startPoint, endPoint, funnelPaths, funnelFilter } = pathsFilter + const { startPoint, endPoint } = pathsFilter + const { funnelPathType, funnelSource, funnelStep } = funnelPathsFilter || {} + return ( (startPoint === cardName && isPathStart) || (endPoint === cardName && isPathEnd) || - (funnelPaths === FunnelPathType.between && - ((cardName === funnelFilter?.events[funnelFilter.funnel_step - 1].name && isPathEnd) || - (cardName === funnelFilter?.events[funnelFilter.funnel_step - 2].name && isPathStart))) + (funnelPathType === FunnelPathType.between && + ((cardName === funnelSource?.series[funnelStep! - 1].name && isPathEnd) || + (cardName === funnelSource?.series[funnelStep! - 2].name && isPathStart))) ) } diff --git a/frontend/src/scenes/paths/pathsDataLogic.ts b/frontend/src/scenes/paths/pathsDataLogic.ts index 67a2c3b7e9dd9..66a2c931f2526 100644 --- a/frontend/src/scenes/paths/pathsDataLogic.ts +++ b/frontend/src/scenes/paths/pathsDataLogic.ts @@ -50,12 +50,13 @@ export const pathsDataLogic = kea([ 'insightDataLoading', 'insightDataError', 'pathsFilter', + 'funnelPathsFilter', 'dateRange', ], featureFlagLogic, ['featureFlags'], ], - actions: [insightVizDataLogic(props), ['updateInsightFilter']], + actions: [insightVizDataLogic(props), ['updateInsightFilter', 'updateQuerySource']], })), actions({ diff --git a/frontend/src/scenes/paths/renderPaths.ts b/frontend/src/scenes/paths/renderPaths.ts index f30a0c28346e1..2e8c6bbe6cafd 100644 --- a/frontend/src/scenes/paths/renderPaths.ts +++ b/frontend/src/scenes/paths/renderPaths.ts @@ -4,7 +4,7 @@ import { D3Selector } from 'lib/hooks/useD3' import { stripHTTP } from 'lib/utils' import { Dispatch, RefObject, SetStateAction } from 'react' -import { PathsFilter } from '~/queries/schema' +import { FunnelPathsFilter, PathsFilter } from '~/queries/schema' import { FALLBACK_CANVAS_WIDTH, HIDE_PATH_CARD_HEIGHT } from './Paths' import { PathNode } from './pathsDataLogic' @@ -34,6 +34,7 @@ const appendPathNodes = ( svg: any, nodes: PathNodeData[], pathsFilter: PathsFilter, + funnelPathsFilter: FunnelPathsFilter, setNodeCards: Dispatch> ): void => { svg.append('g') @@ -62,7 +63,7 @@ const appendPathNodes = ( } } } - if (isSelectedPathStartOrEnd(pathsFilter, d)) { + if (isSelectedPathStartOrEnd(pathsFilter, funnelPathsFilter, d)) { return d3.color('purple') } const startNodeColor = c && d3.color(c) ? d3.color(c) : d3.color('#5375ff') @@ -201,6 +202,7 @@ export function renderPaths( canvasHeight: number, paths: { links: PathNode[]; nodes: any[] }, pathsFilter: PathsFilter, + funnelPathsFilter: FunnelPathsFilter, setNodeCards: Dispatch> ): void { if (!paths || paths.nodes.length === 0) { @@ -227,7 +229,7 @@ export function renderPaths( setNodeCards(nodes.map((node: PathNodeData) => ({ ...node, visible: node.y1 - node.y0 > HIDE_PATH_CARD_HEIGHT }))) - appendPathNodes(svg, nodes, pathsFilter, setNodeCards) + appendPathNodes(svg, nodes, pathsFilter, funnelPathsFilter, setNodeCards) appendDropoffs(svg) appendPathLinks(svg, links, nodes, setNodeCards) addChartAxisLines(svg, height, nodes, maxLayer) diff --git a/mypy-baseline.txt b/mypy-baseline.txt index a0051274809e0..493a739a4fd12 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -463,63 +463,7 @@ posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Need type annotation for "properties_3" (hint: "properties_3: Dict[, ] = ...") [var-annotated] posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Need type annotation for "filter" (hint: "filter: Dict[, ] = ...") [var-annotated] posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Need type annotation for "filter" (hint: "filter: Dict[, ] = ...") [var-annotated] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "None" of "DateRange | None" has no attribute "date_from" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "None" of "DateRange | None" has no attribute "date_to" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "interval" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "interval" [union-attr] posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Need type annotation for "filter" (hint: "filter: Dict[, ] = ...") [var-annotated] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "series" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "series" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "series" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "series" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "series" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "series" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "series" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "series" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "series" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "series" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "breakdownFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "breakdownFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "StickinessQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "breakdownFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "LifecycleQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "breakdownFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "breakdownFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "breakdownFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "StickinessQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "breakdownFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "LifecycleQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "breakdownFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "breakdownFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "breakdownFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "StickinessQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "breakdownFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "LifecycleQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "breakdownFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "FunnelsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "trendsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "trendsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "trendsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "StickinessQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "trendsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "LifecycleQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "trendsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "TrendsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "funnelsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "funnelsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "funnelsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "StickinessQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "funnelsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "LifecycleQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "funnelsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "TrendsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "retentionFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "FunnelsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "retentionFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "retentionFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "StickinessQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "retentionFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "LifecycleQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "retentionFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "TrendsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "pathsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "FunnelsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "pathsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "pathsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "StickinessQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "pathsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "LifecycleQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "pathsFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "TrendsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "stickinessFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "FunnelsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "stickinessFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "stickinessFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "stickinessFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "LifecycleQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "stickinessFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "TrendsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "lifecycleFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "FunnelsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "lifecycleFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "RetentionQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "lifecycleFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "PathsQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "lifecycleFilter" [union-attr] -posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Item "StickinessQuery" of "TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery" has no attribute "lifecycleFilter" [union-attr] posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py:0: error: Item "SelectUnionQuery" of "SelectQuery | SelectUnionQuery" has no attribute "select" [union-attr] posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py:0: error: Item "SelectUnionQuery" of "SelectQuery | SelectUnionQuery" has no attribute "select" [union-attr] posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py:0: error: Item "SelectUnionQuery" of "SelectQuery | SelectUnionQuery" has no attribute "group_by" [union-attr] diff --git a/posthog/hogql_queries/insights/funnels/utils.py b/posthog/hogql_queries/insights/funnels/utils.py index cdccce0251a33..95374f179e1af 100644 --- a/posthog/hogql_queries/insights/funnels/utils.py +++ b/posthog/hogql_queries/insights/funnels/utils.py @@ -57,7 +57,7 @@ def funnel_window_interval_unit_to_sql( elif funnelWindowIntervalUnit == "day": return "DAY" else: - raise ValidationError("{funnelWindowIntervalUnit} not supported") + raise ValidationError(f"{funnelWindowIntervalUnit} not supported") def get_breakdown_expr( diff --git a/posthog/hogql_queries/insights/paths_query_runner.py b/posthog/hogql_queries/insights/paths_query_runner.py index c454feb8e56ac..34fff8e1d0cc8 100644 --- a/posthog/hogql_queries/insights/paths_query_runner.py +++ b/posthog/hogql_queries/insights/paths_query_runner.py @@ -16,17 +16,23 @@ from posthog.hogql.property import property_to_expr from posthog.hogql.query import execute_hogql_query from posthog.hogql.timings import HogQLTimings +from posthog.hogql_queries.insights.funnels.funnels_query_runner import FunnelsQueryRunner +from posthog.hogql_queries.insights.funnels.utils import funnel_window_interval_unit_to_sql from posthog.hogql_queries.query_runner import QueryRunner from posthog.hogql_queries.utils.query_date_range import QueryDateRange from posthog.models import Team from posthog.models.filters.mixins.utils import cached_property from posthog.queries.util import correct_result_for_sampling from posthog.schema import ( + FunnelsActorsQuery, + FunnelsFilter, HogQLQueryModifiers, PathsQueryResponse, PathCleaningFilter, PathsFilter, PathType, + FunnelPathType, + FunnelConversionWindowTimeUnit, ) from posthog.schema import PathsQuery @@ -126,6 +132,106 @@ def construct_event_hogql(self) -> ast.Expr: return event_hogql + def handle_funnel(self) -> tuple[list, Optional[ast.Expr]]: + if not self.query.funnelPathsFilter: + return [], None + + funnelPathType, funnelSource, funnelStep = ( + self.query.funnelPathsFilter.funnelPathType, + self.query.funnelPathsFilter.funnelSource, + self.query.funnelPathsFilter.funnelStep, + ) + funnelSourceFilter = funnelSource.funnelsFilter or FunnelsFilter() + + if funnelPathType in ( + FunnelPathType.funnel_path_after_step, + FunnelPathType.funnel_path_before_step, + ): + funnel_fields = [ + ast.Alias(alias="target_timestamp", expr=ast.Field(chain=["funnel_actors", "timestamp"])), + ] + interval = funnelSourceFilter.funnelWindowInterval or 14 + unit = funnelSourceFilter.funnelWindowIntervalUnit + interval_unit = funnel_window_interval_unit_to_sql(unit) + operator = ">=" if funnelPathType == FunnelPathType.funnel_path_after_step else "<=" + default_case = f"events.timestamp {operator} toTimeZone({{target_timestamp}}, 'UTC')" + if funnelPathType == FunnelPathType.funnel_path_after_step and funnelStep and funnelStep < 0: + default_case += f" + INTERVAL {interval} {interval_unit}" + event_filter = parse_expr( + default_case, {"target_timestamp": ast.Field(chain=["funnel_actors", "timestamp"])} + ) + return funnel_fields, event_filter + elif funnelPathType == FunnelPathType.funnel_path_between_steps: + funnel_fields = [ + ast.Alias(alias="min_timestamp", expr=ast.Field(chain=["funnel_actors", "min_timestamp"])), + ast.Alias(alias="max_timestamp", expr=ast.Field(chain=["funnel_actors", "max_timestamp"])), + ] + event_filter = ast.And( + exprs=[ + ast.CompareOperation( + op=ast.CompareOperationOp.GtEq, + left=ast.Field(chain=["events", "timestamp"]), + right=ast.Field(chain=["funnel_actors", "min_timestamp"]), + ), + ast.CompareOperation( + op=ast.CompareOperationOp.LtEq, + left=ast.Field(chain=["events", "timestamp"]), + right=ast.Field(chain=["funnel_actors", "max_timestamp"]), + ), + ] + ) + return funnel_fields, event_filter + else: + raise ValueError("Unexpected `funnelPathType` for funnel path filter.") + + def funnel_join(self) -> ast.JoinExpr: + if not self.query.funnelPathsFilter: + raise ValueError("Funnel paths filter is required for funnel paths.") + + from posthog.hogql_queries.insights.insight_actors_query_runner import InsightActorsQueryRunner + + funnelPathType, funnelSource, funnelStep = ( + self.query.funnelPathsFilter.funnelPathType, + self.query.funnelPathsFilter.funnelSource, + self.query.funnelPathsFilter.funnelStep, + ) + + actor_query = FunnelsActorsQuery(source=funnelSource, funnelStep=funnelStep) + actors_query_runner = InsightActorsQueryRunner( + query=actor_query, + team=self.team, + timings=self.timings, + modifiers=self.modifiers, + limit_context=self.limit_context, + ) + + assert isinstance(actors_query_runner.source_runner, FunnelsQueryRunner) + assert actors_query_runner.source_runner.context is not None + actors_query_runner.source_runner.context.includeTimestamp = funnelPathType in ( + FunnelPathType.funnel_path_after_step, + FunnelPathType.funnel_path_before_step, + ) + actors_query_runner.source_runner.context.includePrecedingTimestamp = ( + funnelPathType == FunnelPathType.funnel_path_between_steps + ) + actors_query = actors_query_runner.to_query() + + return ast.JoinExpr( + table=ast.Field(chain=["events"]), + next_join=ast.JoinExpr( + table=actors_query, + join_type="INNER JOIN", + alias="funnel_actors", + constraint=ast.JoinConstraint( + expr=ast.CompareOperation( + op=ast.CompareOperationOp.Eq, + left=ast.Field(chain=["events", "person_id"]), + right=ast.Field(chain=["funnel_actors", "actor_id"]), + ), + ), + ), + ) + def paths_events_query(self) -> ast.SelectQuery: event_filters = [] pathReplacements: list[PathCleaningFilter] = [] @@ -133,9 +239,14 @@ def paths_events_query(self) -> ast.SelectQuery: event_hogql = self.construct_event_hogql() event_conditional = parse_expr("ifNull({event_hogql}, '') AS path_item_ungrouped", {"event_hogql": event_hogql}) + funnel_fields, funnel_event_filter = self.handle_funnel() + if funnel_event_filter: + event_filters.append(funnel_event_filter) + fields = [ ast.Field(chain=["events", "timestamp"]), ast.Field(chain=["events", "person_id"]), + *funnel_fields, event_conditional, *[ast.Field(chain=["events", field]) for field in self.extra_event_fields], *[ @@ -226,10 +337,13 @@ def paths_events_query(self) -> ast.SelectQuery: where=ast.And(exprs=event_filters + self._get_event_query()), order_by=[ ast.OrderExpr(expr=ast.Field(chain=["person_id"])), - ast.OrderExpr(expr=ast.Field(chain=["timestamp"])), + ast.OrderExpr(expr=ast.Field(chain=["events", "timestamp"])), ], ) + if funnel_fields: + query.select_from = self.funnel_join() + if self.query.samplingFactor is not None and isinstance(self.query.samplingFactor, float) and query.select_from: query.select_from.sample = ast.SampleExpr( sample_value=ast.RatioExpr(left=ast.Constant(value=self.query.samplingFactor)) @@ -408,6 +522,27 @@ def get_target_clause(self) -> list[ast.Expr]: else: return self.get_filtered_path_ordering() + def get_session_threshold_clause(self) -> ast.Expr: + if self.query.funnelPathsFilter: + funnelSourceFilter = self.query.funnelPathsFilter.funnelSource.funnelsFilter or FunnelsFilter() + + interval = 14 + interval_unit = FunnelConversionWindowTimeUnit.day + + if funnelSourceFilter.funnelWindowInterval: + interval = funnelSourceFilter.funnelWindowInterval + unit = funnelSourceFilter.funnelWindowIntervalUnit + interval_unit = funnel_window_interval_unit_to_sql(unit) # type: ignore + + return parse_expr( + f"arraySplit(x -> if(toDateTime('2018-01-01') + toIntervalSecond(_toInt64(x.3)) < toDateTime('2018-01-01') + INTERVAL {interval} {interval_unit}, 0, 1), paths_tuple)" + ) + + return parse_expr( + "arraySplit(x -> if(x.3 < ({session_time_threshold}), 0, 1), paths_tuple)", + {"session_time_threshold": ast.Constant(value=SESSION_TIME_THRESHOLD_DEFAULT_SECONDS)}, + ) + def paths_per_person_query(self) -> ast.SelectQuery: target_point = self.query.pathsFilter.endPoint or self.query.pathsFilter.startPoint target_point = ( @@ -431,15 +566,8 @@ def paths_per_person_query(self) -> ast.SelectQuery: "path_event_query": self.paths_events_query(), "boundary_event_filter": ast.Constant(value=None), "target_point": ast.Constant(value=target_point), - "session_threshold_clause": ast.Constant(value=None), - "session_time_threshold": ast.Constant(value=SESSION_TIME_THRESHOLD_DEFAULT_SECONDS), + "session_threshold_clause": self.get_session_threshold_clause(), "path_tuples_expr": path_tuples_expr, - # TODO: "extra_final_select_statements": ast.Constant(value=None), - "extra_joined_path_tuple_select_statements": ast.Constant(value=None), - "extra_array_filter_select_statements": ast.Constant(value=None), - "extra_limited_path_tuple_elements": ast.Constant(value=None), - "extra_path_time_tuple_select_statements": ast.Constant(value=None), - "extra_group_array_select_statements": ast.Constant(value=None), } select = cast( ast.SelectQuery, @@ -475,7 +603,7 @@ def paths_per_person_query(self) -> ast.SelectQuery: /* path_time_tuple.x added below if required */ session_index, {path_tuples_expr} as paths_tuple, - arraySplit(x -> if(x.3 < ({session_time_threshold}), 0, 1), paths_tuple) as session_paths + {session_threshold_clause} as session_paths FROM ( SELECT person_id, @@ -781,8 +909,6 @@ def to_actors_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: else: conditions.append(parse_expr("1=1")) - # TODO: Funnel? - actors_query = parse_select( """ SELECT diff --git a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py index 199294a3a5969..2868bcfef43e6 100644 --- a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py +++ b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py @@ -1,7 +1,7 @@ import copy from enum import Enum import json -from typing import List, Dict, Literal +from typing import Any, List, Dict, Literal from posthog.hogql_queries.legacy_compatibility.clean_properties import clean_entity_properties, clean_global_properties from posthog.models.entity.entity import Entity as LegacyEntity from posthog.schema import ( @@ -13,6 +13,7 @@ EventsNode, FunnelExclusionActionsNode, FunnelExclusionEventsNode, + FunnelPathsFilter, FunnelsFilter, FunnelsQuery, LifecycleFilter, @@ -353,9 +354,8 @@ def _insight_filter(filter: Dict): edgeLimit=filter.get("edge_limit"), minEdgeWeight=filter.get("min_edge_weight"), maxEdgeWeight=filter.get("max_edge_weight"), - funnelPaths=filter.get("funnel_paths"), - funnelFilter=filter.get("funnel_filter"), - ) + ), + "funnelPathsFilter": filters_to_funnel_paths_query(filter), # type: ignore } elif _insight_type(filter) == "LIFECYCLE": insight_filter = { @@ -382,6 +382,23 @@ def _insight_filter(filter: Dict): return insight_filter +def filters_to_funnel_paths_query(filter: Dict[str, Any]) -> FunnelPathsFilter | None: + funnel_paths = filter.get("funnel_paths") + funnel_filter = filter.get("funnel_filter") + + if funnel_paths is None or funnel_filter is None: + return None + + funnel_query = filter_to_query(funnel_filter) + assert isinstance(funnel_query, FunnelsQuery) + + return FunnelPathsFilter( + funnelPathType=funnel_paths, + funnelSource=funnel_query, + funnelStep=funnel_filter["funnel_step"], + ) + + def _insight_type(filter: Dict) -> INSIGHT_TYPE: if filter.get("insight") == "SESSIONS": return "TRENDS" diff --git a/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py index 9abc0f3506b8c..eee9046fbfe48 100644 --- a/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py +++ b/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py @@ -10,6 +10,7 @@ ChartDisplayType, CohortPropertyFilter, CountPerActorMathType, + DateRange, ElementPropertyFilter, EventPropertyFilter, EventsNode, @@ -17,26 +18,34 @@ FunnelExclusionActionsNode, FunnelExclusionEventsNode, FunnelPathType, + FunnelPathsFilter, FunnelVizType, + FunnelsQuery, GroupPropertyFilter, HogQLPropertyFilter, Key, + LifecycleQuery, LifecycleToggle, + MathGroupTypeIndex, PathCleaningFilter, PathType, + PathsQuery, PersonPropertyFilter, PropertyMathType, PropertyOperator, RetentionPeriod, + RetentionQuery, RetentionType, SessionPropertyFilter, StepOrderValue, + StickinessQuery, TrendsFilter, FunnelsFilter, RetentionFilter, PathsFilter, StickinessFilter, LifecycleFilter, + TrendsQuery, ) from posthog.test.base import BaseTest @@ -954,6 +963,7 @@ def test_date_range(self): query = filter_to_query(filter) + assert isinstance(query.dateRange, DateRange) self.assertEqual(query.dateRange.date_from, "-14d") self.assertEqual(query.dateRange.date_to, "-7d") @@ -962,6 +972,7 @@ def test_interval(self): query = filter_to_query(filter) + assert isinstance(query, TrendsQuery) self.assertEqual(query.interval, "hour") def test_series_default(self): @@ -969,6 +980,7 @@ def test_series_default(self): query = filter_to_query(filter) + assert isinstance(query, TrendsQuery) self.assertEqual(query.series, []) def test_series_custom(self): @@ -979,6 +991,7 @@ def test_series_custom(self): query = filter_to_query(filter) + assert isinstance(query, TrendsQuery) self.assertEqual( query.series, [ @@ -1000,6 +1013,7 @@ def test_series_order(self): query = filter_to_query(filter) + assert isinstance(query, TrendsQuery) self.assertEqual( query.series, [ @@ -1038,6 +1052,7 @@ def test_series_math(self): query = filter_to_query(filter) + assert isinstance(query, TrendsQuery) self.assertEqual( query.series, [ @@ -1057,7 +1072,7 @@ def test_series_math(self): event="$pageview", name="$pageview", math="unique_group", - math_group_type_index=0, + math_group_type_index=MathGroupTypeIndex.number_0, ), EventsNode( event="$pageview", @@ -1164,6 +1179,7 @@ def test_series_properties(self): query = filter_to_query(filter) + assert isinstance(query, TrendsQuery) self.assertEqual( query.series, [ @@ -1252,6 +1268,7 @@ def test_breakdown(self): query = filter_to_query(filter) + assert isinstance(query, TrendsQuery) self.assertEqual( query.breakdownFilter, BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser"), @@ -1262,6 +1279,7 @@ def test_breakdown_converts_multi(self): query = filter_to_query(filter) + assert isinstance(query, TrendsQuery) self.assertEqual( query.breakdownFilter, BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser"), @@ -1272,6 +1290,7 @@ def test_breakdown_type_default(self): query = filter_to_query(filter) + assert isinstance(query, TrendsQuery) self.assertEqual( query.breakdownFilter, BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="some_prop"), @@ -1294,6 +1313,7 @@ def test_trends_filter(self): query = filter_to_query(filter) + assert isinstance(query, TrendsQuery) self.assertEqual( query.trendsFilter, TrendsFilter( @@ -1359,6 +1379,7 @@ def test_funnels_filter(self): query = filter_to_query(filter) + assert isinstance(query, FunnelsQuery) self.assertEqual( query.funnelsFilter, FunnelsFilter( @@ -1407,6 +1428,7 @@ def test_retention_filter(self): query = filter_to_query(filter) + assert isinstance(query, RetentionQuery) self.assertEqual( query.retentionFilter, RetentionFilter( @@ -1467,6 +1489,7 @@ def test_paths_filter(self): query = filter_to_query(filter) + assert isinstance(query, PathsQuery) self.assertEqual( query.pathsFilter, PathsFilter( @@ -1484,24 +1507,21 @@ def test_paths_filter(self): excludeEvents=["http://localhost:8000/events"], stepLimit=5, pathGroupings=["/merchant/*/payment"], - funnelPaths=FunnelPathType.funnel_path_between_steps, - funnelFilter={ - "insight": "FUNNELS", - "events": [ - { - "type": "events", - "id": "$pageview", - "order": 0, - "name": "$pageview", - "math": "total", - }, - {"type": "events", "id": None, "order": 1, "math": "total"}, + ), + ) + self.assertEqual( + query.funnelPathsFilter, + FunnelPathsFilter( + funnelPathType=FunnelPathType.funnel_path_between_steps, + funnelSource=FunnelsQuery( + series=[ + EventsNode(event="$pageview", name="$pageview"), + EventsNode(event=None, name="All events"), ], - "funnel_viz_type": "steps", - "exclusions": [], - "filter_test_accounts": True, - "funnel_step": 2, - }, + filterTestAccounts=True, + funnelsFilter=FunnelsFilter(funnelVizType=FunnelVizType.steps, exclusions=[]), + ), + funnelStep=2, ), ) @@ -1516,6 +1536,7 @@ def test_stickiness_filter(self): query = filter_to_query(filter) + assert isinstance(query, StickinessQuery) self.assertEqual( query.stickinessFilter, StickinessFilter(compare=True, showLegend=True, showValuesOnSeries=True), @@ -1531,6 +1552,7 @@ def test_lifecycle_filter(self): query = filter_to_query(filter) + assert isinstance(query, LifecycleQuery) self.assertEqual( query.lifecycleFilter, LifecycleFilter( diff --git a/posthog/schema.py b/posthog/schema.py index 9cd8c96a64506..4212f9c8f895f 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -602,8 +602,6 @@ class PathsFilter(BaseModel): edgeLimit: Optional[int] = None endPoint: Optional[str] = None excludeEvents: Optional[List[str]] = None - funnelFilter: Optional[Dict[str, Any]] = None - funnelPaths: Optional[FunnelPathType] = None includeEventTypes: Optional[List[PathType]] = None localPathCleaningFilters: Optional[List[PathCleaningFilter]] = None maxEdgeWeight: Optional[int] = None @@ -2741,6 +2739,42 @@ class NamedParametersTypeofDateRangeForFilter(BaseModel): source: Optional[FilterType] = None +class FunnelPathsFilter(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + funnelPathType: Optional[FunnelPathType] = None + funnelSource: FunnelsQuery + funnelStep: Optional[int] = None + + +class FunnelsActorsQuery(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + funnelCustomSteps: Optional[List[int]] = Field( + default=None, + description="Custom step numbers to get persons for. This overrides `funnelStep`. Primarily for correlation use.", + ) + funnelStep: Optional[int] = Field( + default=None, + description="Index of the step for which we want to get the timestamp for, per person. Positive for converted persons, negative for dropped of persons.", + ) + funnelStepBreakdown: Optional[Union[str, float, List[Union[str, float]]]] = Field( + default=None, + description="The breakdown value for which to get persons for. This is an array for person and event properties, a string for groups and an integer for cohorts.", + ) + funnelTrendsDropOff: Optional[bool] = None + funnelTrendsEntrancePeriodStart: Optional[str] = Field( + default=None, + description="Used together with `funnelTrendsDropOff` for funnels time conversion date for the persons modal.", + ) + includeRecordings: Optional[bool] = None + kind: Literal["FunnelsActorsQuery"] = "FunnelsActorsQuery" + response: Optional[ActorsQueryResponse] = None + source: FunnelsQuery + + class PathsQuery(BaseModel): model_config = ConfigDict( extra="forbid", @@ -2750,6 +2784,9 @@ class PathsQuery(BaseModel): filterTestAccounts: Optional[bool] = Field( default=None, description="Exclude internal and test users by applying the respective filters" ) + funnelPathsFilter: Optional[FunnelPathsFilter] = Field( + default=None, description="Used for displaying paths in relation to funnel steps." + ) kind: Literal["PathsQuery"] = "PathsQuery" pathsFilter: PathsFilter = Field(..., description="Properties specific to the paths insight") properties: Optional[ @@ -2777,31 +2814,19 @@ class PathsQuery(BaseModel): samplingFactor: Optional[float] = Field(default=None, description="Sampling rate") -class FunnelsActorsQuery(BaseModel): +class FunnelCorrelationQuery(BaseModel): model_config = ConfigDict( extra="forbid", ) - funnelCustomSteps: Optional[List[int]] = Field( - default=None, - description="Custom step numbers to get persons for. This overrides `funnelStep`. Primarily for correlation use.", - ) - funnelStep: Optional[int] = Field( - default=None, - description="Index of the step for which we want to get the timestamp for, per person. Positive for converted persons, negative for dropped of persons.", - ) - funnelStepBreakdown: Optional[Union[str, float, List[Union[str, float]]]] = Field( - default=None, - description="The breakdown value for which to get persons for. This is an array for person and event properties, a string for groups and an integer for cohorts.", - ) - funnelTrendsDropOff: Optional[bool] = None - funnelTrendsEntrancePeriodStart: Optional[str] = Field( - default=None, - description="Used together with `funnelTrendsDropOff` for funnels time conversion date for the persons modal.", - ) - includeRecordings: Optional[bool] = None - kind: Literal["FunnelsActorsQuery"] = "FunnelsActorsQuery" - response: Optional[ActorsQueryResponse] = None - source: FunnelsQuery + funnelCorrelationEventExcludePropertyNames: Optional[List[str]] = None + funnelCorrelationEventNames: Optional[List[str]] = None + funnelCorrelationExcludeEventNames: Optional[List[str]] = None + funnelCorrelationExcludeNames: Optional[List[str]] = None + funnelCorrelationNames: Optional[List[str]] = None + funnelCorrelationType: FunnelCorrelationResultsType + kind: Literal["FunnelCorrelationQuery"] = "FunnelCorrelationQuery" + response: Optional[FunnelCorrelationResponse] = None + source: FunnelsActorsQuery class InsightVizNode(BaseModel): @@ -2828,41 +2853,6 @@ class InsightVizNode(BaseModel): vizSpecificOptions: Optional[VizSpecificOptions] = None -class FunnelCorrelationQuery(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) - funnelCorrelationEventExcludePropertyNames: Optional[List[str]] = None - funnelCorrelationEventNames: Optional[List[str]] = None - funnelCorrelationExcludeEventNames: Optional[List[str]] = None - funnelCorrelationExcludeNames: Optional[List[str]] = None - funnelCorrelationNames: Optional[List[str]] = None - funnelCorrelationType: FunnelCorrelationResultsType - kind: Literal["FunnelCorrelationQuery"] = "FunnelCorrelationQuery" - response: Optional[FunnelCorrelationResponse] = None - source: FunnelsActorsQuery - - -class InsightActorsQuery(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) - breakdown: Optional[Union[str, int]] = None - compare: Optional[Compare] = None - day: Optional[Union[str, int]] = None - includeRecordings: Optional[bool] = None - interval: Optional[int] = Field( - default=None, description="An interval selected out of available intervals in source query." - ) - kind: Literal["InsightActorsQuery"] = "InsightActorsQuery" - response: Optional[ActorsQueryResponse] = None - series: Optional[int] = None - source: Union[TrendsQuery, FunnelsQuery, RetentionQuery, PathsQuery, StickinessQuery, LifecycleQuery] = Field( - ..., discriminator="kind" - ) - status: Optional[str] = None - - class FunnelCorrelationActorsQuery(BaseModel): model_config = ConfigDict( extra="forbid", @@ -2893,6 +2883,26 @@ class FunnelCorrelationActorsQuery(BaseModel): source: FunnelCorrelationQuery +class InsightActorsQuery(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + breakdown: Optional[Union[str, int]] = None + compare: Optional[Compare] = None + day: Optional[Union[str, int]] = None + includeRecordings: Optional[bool] = None + interval: Optional[int] = Field( + default=None, description="An interval selected out of available intervals in source query." + ) + kind: Literal["InsightActorsQuery"] = "InsightActorsQuery" + response: Optional[ActorsQueryResponse] = None + series: Optional[int] = None + source: Union[TrendsQuery, FunnelsQuery, RetentionQuery, PathsQuery, StickinessQuery, LifecycleQuery] = Field( + ..., discriminator="kind" + ) + status: Optional[str] = None + + class InsightActorsQueryOptions(BaseModel): model_config = ConfigDict( extra="forbid",