diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index 33f0a612b31e8..4548e8ab49a9e 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -204,7 +204,7 @@ const InsightFunnelsQuery: FunnelsQuery = { }, series, funnelsFilter: { - funnel_order_type: StepOrderValue.ORDERED, + funnelOrderType: StepOrderValue.ORDERED, }, breakdownFilter: { breakdown: '$geoip_country_code', diff --git a/frontend/src/queries/nodes/InsightQuery/defaults.ts b/frontend/src/queries/nodes/InsightQuery/defaults.ts index c8742c3f35b58..3580fe50f7821 100644 --- a/frontend/src/queries/nodes/InsightQuery/defaults.ts +++ b/frontend/src/queries/nodes/InsightQuery/defaults.ts @@ -34,7 +34,7 @@ export const funnelsQueryDefault: FunnelsQuery = { }, ], funnelsFilter: { - funnel_viz_type: FunnelVizType.Steps, + funnelVizType: FunnelVizType.Steps, }, } diff --git a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts index e96f51797b85a..e7a85f6d1394b 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts @@ -381,20 +381,20 @@ describe('filtersToQueryNode', () => { const query: FunnelsQuery = { kind: NodeKind.FunnelsQuery, funnelsFilter: { - funnel_viz_type: FunnelVizType.Steps, - funnel_from_step: 1, - funnel_to_step: 2, - funnel_step_reference: FunnelStepReference.total, - breakdown_attribution_type: BreakdownAttributionType.AllSteps, - breakdown_attribution_value: 1, - bin_count: 'auto', - funnel_window_interval_unit: FunnelConversionWindowTimeUnit.Day, - funnel_window_interval: 7, - funnel_order_type: StepOrderValue.ORDERED, + funnelVizType: FunnelVizType.Steps, + funnelFromStep: 1, + funnelToStep: 2, + funnelStepReference: FunnelStepReference.total, + breakdownAttributionType: BreakdownAttributionType.AllSteps, + breakdownAttributionValue: 1, + binCount: 'auto', + funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit.Day, + funnelWindowInterval: 7, + funnelOrderType: StepOrderValue.ORDERED, exclusions: [ { - funnel_from_step: 0, - funnel_to_step: 1, + funnelFromStep: 0, + funnelToStep: 1, }, ], layout: FunnelLayout.horizontal, @@ -1101,7 +1101,7 @@ describe('filtersToQueryNode', () => { ], filterTestAccounts: true, funnelsFilter: { - funnel_viz_type: FunnelVizType.Steps, + funnelVizType: FunnelVizType.Steps, }, } expect(result).toEqual(query) @@ -1171,7 +1171,7 @@ describe('filtersToQueryNode', () => { ], filterTestAccounts: true, funnelsFilter: { - funnel_viz_type: FunnelVizType.Steps, + funnelVizType: FunnelVizType.Steps, }, } 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 f826849125c51..065c07c63a15b 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts @@ -277,20 +277,27 @@ export const filtersToQueryNode = (filters: Partial): InsightQueryNo // funnels filter if (isFunnelsFilter(filters) && isFunnelsQuery(query)) { query.funnelsFilter = objectCleanWithEmpty({ - funnel_viz_type: filters.funnel_viz_type, - funnel_from_step: filters.funnel_from_step, - funnel_to_step: filters.funnel_to_step, - funnel_step_reference: filters.funnel_step_reference, - breakdown_attribution_type: filters.breakdown_attribution_type, - breakdown_attribution_value: filters.breakdown_attribution_value, - bin_count: filters.bin_count, - funnel_window_interval_unit: filters.funnel_window_interval_unit, - funnel_window_interval: filters.funnel_window_interval, - funnel_order_type: filters.funnel_order_type, - exclusions: filters.exclusions, + funnelVizType: filters.funnel_viz_type, + funnelFromStep: filters.funnel_from_step, + funnelToStep: filters.funnel_to_step, + funnelStepReference: filters.funnel_step_reference, + breakdownAttributionType: filters.breakdown_attribution_type, + breakdownAttributionValue: filters.breakdown_attribution_value, + binCount: filters.bin_count, + funnelWindowIntervalUnit: filters.funnel_window_interval_unit, + funnelWindowInterval: filters.funnel_window_interval, + funnelOrderType: filters.funnel_order_type, + exclusions: + filters.exclusions !== undefined + ? filters.exclusions.map(({ funnel_from_step, funnel_to_step, ...rest }) => ({ + funnelFromStep: funnel_from_step, + funnelToStep: funnel_to_step, + ...rest, + })) + : undefined, layout: filters.layout, hidden_legend_breakdowns: cleanHiddenLegendSeries(filters.hidden_legend_keys), - funnel_aggregate_by_hogql: filters.funnel_aggregate_by_hogql, + funnelAggregateByHogQL: filters.funnel_aggregate_by_hogql, }) } diff --git a/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts b/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts index fab44bffaf3c9..0da74e81849bc 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/queryNodeToFilter.ts @@ -5,6 +5,7 @@ import { ActionsNode, BreakdownFilter, EventsNode, + FunnelsFilterLegacy, InsightNodeKind, InsightQueryNode, LifecycleFilterLegacy, @@ -155,6 +156,7 @@ export const queryNodeToFilter = (query: InsightQueryNode): Partial // replace camel cased props with the snake cased variant const camelCasedTrendsProps: TrendsFilterLegacy = {} + const camelCasedFunnelsProps: FunnelsFilterLegacy = {} const camelCasedRetentionProps: RetentionFilterLegacy = {} const camelCasedPathsProps: PathsFilterLegacy = {} const camelCasedStickinessProps: StickinessFilterLegacy = {} @@ -178,6 +180,39 @@ export const queryNodeToFilter = (query: InsightQueryNode): Partial delete queryCopy.trendsFilter?.showPercentStackView delete queryCopy.trendsFilter?.showLegend delete queryCopy.trendsFilter?.showValuesOnSeries + } else if (isFunnelsQuery(queryCopy)) { + camelCasedFunnelsProps.exclusions = queryCopy.funnelsFilter?.exclusions + ? queryCopy.funnelsFilter.exclusions.map(({ funnelFromStep, funnelToStep, ...rest }) => ({ + funnel_from_step: funnelFromStep, + funnel_to_step: funnelToStep, + ...rest, + })) + : undefined + camelCasedFunnelsProps.bin_count = queryCopy.funnelsFilter?.binCount + camelCasedFunnelsProps.breakdown_attribution_type = queryCopy.funnelsFilter?.breakdownAttributionType + camelCasedFunnelsProps.breakdown_attribution_value = queryCopy.funnelsFilter?.breakdownAttributionValue + camelCasedFunnelsProps.funnel_aggregate_by_hogql = queryCopy.funnelsFilter?.funnelAggregateByHogQL + camelCasedFunnelsProps.funnel_to_step = queryCopy.funnelsFilter?.funnelToStep + camelCasedFunnelsProps.funnel_from_step = queryCopy.funnelsFilter?.funnelFromStep + camelCasedFunnelsProps.funnel_order_type = queryCopy.funnelsFilter?.funnelOrderType + camelCasedFunnelsProps.funnel_viz_type = queryCopy.funnelsFilter?.funnelVizType + camelCasedFunnelsProps.funnel_window_interval = queryCopy.funnelsFilter?.funnelWindowInterval + camelCasedFunnelsProps.funnel_window_interval_unit = queryCopy.funnelsFilter?.funnelWindowIntervalUnit + // camelCasedFunnelsProps.hidden_legend_breakdowns = queryCopy.funnelsFilter?.hiddenLegendBreakdowns + camelCasedFunnelsProps.funnel_step_reference = queryCopy.funnelsFilter?.funnelStepReference + delete queryCopy.funnelsFilter?.exclusions + delete queryCopy.funnelsFilter?.binCount + delete queryCopy.funnelsFilter?.breakdownAttributionType + delete queryCopy.funnelsFilter?.breakdownAttributionValue + delete queryCopy.funnelsFilter?.funnelAggregateByHogQL + delete queryCopy.funnelsFilter?.funnelToStep + delete queryCopy.funnelsFilter?.funnelFromStep + delete queryCopy.funnelsFilter?.funnelOrderType + delete queryCopy.funnelsFilter?.funnelVizType + delete queryCopy.funnelsFilter?.funnelWindowInterval + delete queryCopy.funnelsFilter?.funnelWindowIntervalUnit + // delete queryCopy.funnelsFilter?.hiddenLegendBreakdowns + delete queryCopy.funnelsFilter?.funnelStepReference } else if (isRetentionQuery(queryCopy)) { camelCasedRetentionProps.retention_reference = queryCopy.retentionFilter?.retentionReference camelCasedRetentionProps.retention_type = queryCopy.retentionFilter?.retentionType @@ -228,6 +263,7 @@ export const queryNodeToFilter = (query: InsightQueryNode): Partial delete queryCopy.lifecycleFilter?.showValuesOnSeries } Object.assign(filters, camelCasedTrendsProps) + Object.assign(filters, camelCasedFunnelsProps) Object.assign(filters, camelCasedRetentionProps) Object.assign(filters, camelCasedPathsProps) Object.assign(filters, camelCasedStickinessProps) diff --git a/frontend/src/queries/nodes/InsightViz/InsightVizDisplay.tsx b/frontend/src/queries/nodes/InsightViz/InsightVizDisplay.tsx index b13c17d32a005..a48ef44487011 100644 --- a/frontend/src/queries/nodes/InsightViz/InsightVizDisplay.tsx +++ b/frontend/src/queries/nodes/InsightViz/InsightVizDisplay.tsx @@ -151,7 +151,7 @@ export function InsightVizDisplay({ timedOutQueryId === null && isFunnelWithEnoughSteps && hasFunnelResults && - funnelsFilter?.funnel_viz_type === FunnelVizType.Steps && + funnelsFilter?.funnelVizType === FunnelVizType.Steps && !disableTable ) { return ( diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index dac8ddbda7c38..346807cfd3e66 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1317,6 +1317,36 @@ "type": "string" }, "FunnelExclusion": { + "additionalProperties": false, + "properties": { + "custom_name": { + "type": ["string", "null"] + }, + "funnelFromStep": { + "type": "number" + }, + "funnelToStep": { + "type": "number" + }, + "id": { + "type": ["string", "number", "null"] + }, + "index": { + "type": "number" + }, + "name": { + "type": ["string", "null"] + }, + "order": { + "type": "number" + }, + "type": { + "$ref": "#/definitions/EntityType" + } + }, + "type": "object" + }, + "FunnelExclusionLegacy": { "additionalProperties": false, "properties": { "custom_name": { @@ -1363,6 +1393,60 @@ "type": "string" }, "FunnelsFilter": { + "additionalProperties": false, + "properties": { + "binCount": { + "$ref": "#/definitions/BinCountValue" + }, + "breakdownAttributionType": { + "$ref": "#/definitions/BreakdownAttributionType" + }, + "breakdownAttributionValue": { + "type": "number" + }, + "exclusions": { + "items": { + "$ref": "#/definitions/FunnelExclusion" + }, + "type": "array" + }, + "funnelAggregateByHogQL": { + "type": "string" + }, + "funnelFromStep": { + "type": "number" + }, + "funnelOrderType": { + "$ref": "#/definitions/StepOrderValue" + }, + "funnelStepReference": { + "$ref": "#/definitions/FunnelStepReference" + }, + "funnelToStep": { + "type": "number" + }, + "funnelVizType": { + "$ref": "#/definitions/FunnelVizType" + }, + "funnelWindowInterval": { + "type": "number" + }, + "funnelWindowIntervalUnit": { + "$ref": "#/definitions/FunnelConversionWindowTimeUnit" + }, + "hidden_legend_breakdowns": { + "items": { + "type": "string" + }, + "type": "array" + }, + "layout": { + "$ref": "#/definitions/FunnelLayout" + } + }, + "type": "object" + }, + "FunnelsFilterLegacy": { "additionalProperties": false, "description": "`FunnelsFilterType` minus everything inherited from `FilterType` and persons modal related params and `hidden_legend_keys` replaced by `hidden_legend_breakdowns`", "properties": { @@ -1377,7 +1461,7 @@ }, "exclusions": { "items": { - "$ref": "#/definitions/FunnelExclusion" + "$ref": "#/definitions/FunnelExclusionLegacy" }, "type": "array" }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 1f891d0909b1f..33f8ff5e1073d 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -9,6 +9,7 @@ import { EventPropertyFilter, EventType, FilterType, + FunnelExclusion, FunnelsFilterType, GroupMathType, HogQLMathType, @@ -549,7 +550,7 @@ export interface TrendsQuery extends InsightsQueryBase { /** `FunnelsFilterType` minus everything inherited from `FilterType` and persons modal related params * and `hidden_legend_keys` replaced by `hidden_legend_breakdowns` */ -export type FunnelsFilter = Omit< +export type FunnelsFilterLegacy = Omit< FunnelsFilterType & { hidden_legend_breakdowns?: string[] }, | keyof FilterType | 'hidden_legend_keys' @@ -561,6 +562,24 @@ export type FunnelsFilter = Omit< | 'funnel_step' | 'funnel_custom_steps' > + +export type FunnelsFilter = { + exclusions?: FunnelExclusion[] + layout?: FunnelsFilterLegacy['layout'] + binCount?: FunnelsFilterLegacy['bin_count'] + breakdownAttributionType?: FunnelsFilterLegacy['breakdown_attribution_type'] + breakdownAttributionValue?: FunnelsFilterLegacy['breakdown_attribution_value'] + funnelAggregateByHogQL?: FunnelsFilterLegacy['funnel_aggregate_by_hogql'] + funnelToStep?: FunnelsFilterLegacy['funnel_to_step'] + funnelFromStep?: FunnelsFilterLegacy['funnel_from_step'] + funnelOrderType?: FunnelsFilterLegacy['funnel_order_type'] + funnelVizType?: FunnelsFilterLegacy['funnel_viz_type'] + funnelWindowInterval?: FunnelsFilterLegacy['funnel_window_interval'] + funnelWindowIntervalUnit?: FunnelsFilterLegacy['funnel_window_interval_unit'] + hidden_legend_breakdowns?: FunnelsFilterLegacy['hidden_legend_breakdowns'] + funnelStepReference?: FunnelsFilterLegacy['funnel_step_reference'] +} + export interface FunnelsQuery extends InsightsQueryBase { kind: NodeKind.FunnelsQuery /** Granularity of the response. Can be one of `hour`, `day`, `week` or `month` */ diff --git a/frontend/src/scenes/funnels/Funnel.tsx b/frontend/src/scenes/funnels/Funnel.tsx index 167841f982f08..a9636f0fee9e6 100644 --- a/frontend/src/scenes/funnels/Funnel.tsx +++ b/frontend/src/scenes/funnels/Funnel.tsx @@ -15,12 +15,12 @@ import { FunnelHistogram } from './FunnelHistogram' export function Funnel(props: ChartParams): JSX.Element { const { insightProps } = useValues(insightLogic) const { funnelsFilter } = useValues(funnelDataLogic(insightProps)) - const { funnel_viz_type, layout } = funnelsFilter || {} + const { funnelVizType, layout } = funnelsFilter || {} let viz: JSX.Element | null = null - if (funnel_viz_type == FunnelVizType.Trends) { + if (funnelVizType == FunnelVizType.Trends) { viz = - } else if (funnel_viz_type == FunnelVizType.TimeToConvert) { + } else if (funnelVizType == FunnelVizType.TimeToConvert) { viz = } else if ((layout || FunnelLayout.vertical) === FunnelLayout.vertical) { viz = @@ -30,8 +30,8 @@ export function Funnel(props: ChartParams): JSX.Element { return (
{viz} diff --git a/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.tsx b/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.tsx index 77f589785120a..ccbbe4a2be704 100644 --- a/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.tsx +++ b/frontend/src/scenes/funnels/FunnelBarGraph/FunnelBarGraph.tsx @@ -36,7 +36,7 @@ export function FunnelBarGraph({ const { ref: graphRef, width } = useResizeObserver() const steps = visibleStepsWithConversionMetrics - const stepReference = funnelsFilter?.funnel_step_reference || FunnelStepReference.total + const stepReference = funnelsFilter?.funnelStepReference || FunnelStepReference.total const showPersonsModal = canOpenPersonModal && showPersonsModalProp @@ -67,7 +67,7 @@ export function FunnelBarGraph({
- {funnelsFilter?.funnel_order_type === StepOrderValue.UNORDERED ? ( + {funnelsFilter?.funnelOrderType === StepOrderValue.UNORDERED ? ( @@ -79,13 +79,13 @@ export function FunnelBarGraph({
- {funnelsFilter?.funnel_order_type === StepOrderValue.UNORDERED ? ( + {funnelsFilter?.funnelOrderType === StepOrderValue.UNORDERED ? ( Completed {step.order + 1} steps ) : ( )}
- {funnelsFilter?.funnel_order_type !== StepOrderValue.UNORDERED && + {funnelsFilter?.funnelOrderType !== StepOrderValue.UNORDERED && stepIndex > 0 && step.action_id === steps[stepIndex - 1].action_id && } diff --git a/frontend/src/scenes/funnels/FunnelCanvasLabel.tsx b/frontend/src/scenes/funnels/FunnelCanvasLabel.tsx index ebc6b55ad2896..fb8ba57bccb8d 100644 --- a/frontend/src/scenes/funnels/FunnelCanvasLabel.tsx +++ b/frontend/src/scenes/funnels/FunnelCanvasLabel.tsx @@ -17,7 +17,7 @@ export function FunnelCanvasLabel(): JSX.Element | null { const { updateInsightFilter } = useActions(funnelDataLogic(insightProps)) const labels = [ - ...(funnelsFilter?.funnel_viz_type === FunnelVizType.Steps + ...(funnelsFilter?.funnelVizType === FunnelVizType.Steps ? [ <> @@ -32,7 +32,7 @@ export function FunnelCanvasLabel(): JSX.Element | null { , ] : []), - ...(funnelsFilter?.funnel_viz_type !== FunnelVizType.Trends + ...(funnelsFilter?.funnelVizType !== FunnelVizType.Trends ? [ <> @@ -43,14 +43,14 @@ export function FunnelCanvasLabel(): JSX.Element | null { Average time to convert - {funnelsFilter?.funnel_viz_type === FunnelVizType.TimeToConvert && } + {funnelsFilter?.funnelVizType === FunnelVizType.TimeToConvert && } : - {funnelsFilter?.funnel_viz_type === FunnelVizType.TimeToConvert ? ( + {funnelsFilter?.funnelVizType === FunnelVizType.TimeToConvert ? ( {humanFriendlyDuration(conversionMetrics.averageTime)} ) : ( updateInsightFilter({ funnel_viz_type: FunnelVizType.TimeToConvert })} + onClick={() => updateInsightFilter({ funnelVizType: FunnelVizType.TimeToConvert })} > {humanFriendlyDuration(conversionMetrics.averageTime)} @@ -58,7 +58,7 @@ export function FunnelCanvasLabel(): JSX.Element | null { , ] : []), - ...(funnelsFilter?.funnel_viz_type === FunnelVizType.Trends + ...(funnelsFilter?.funnelVizType === FunnelVizType.Trends ? [ <> Conversion rate diff --git a/frontend/src/scenes/funnels/funnelDataLogic.test.ts b/frontend/src/scenes/funnels/funnelDataLogic.test.ts index a2d90bfc81ee2..b46cb5d60f5e9 100644 --- a/frontend/src/scenes/funnels/funnelDataLogic.test.ts +++ b/frontend/src/scenes/funnels/funnelDataLogic.test.ts @@ -74,7 +74,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.Steps, + funnelVizType: FunnelVizType.Steps, }, } @@ -93,7 +93,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.TimeToConvert, + funnelVizType: FunnelVizType.TimeToConvert, }, } @@ -112,7 +112,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.Trends, + funnelVizType: FunnelVizType.Trends, }, } @@ -242,7 +242,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.TimeToConvert, + funnelVizType: FunnelVizType.TimeToConvert, }, } const insight: Partial = { @@ -759,7 +759,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.TimeToConvert, + funnelVizType: FunnelVizType.TimeToConvert, }, } const insight: Partial = { @@ -799,7 +799,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.TimeToConvert, + funnelVizType: FunnelVizType.TimeToConvert, }, } const insight: Partial = { @@ -825,7 +825,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.TimeToConvert, + funnelVizType: FunnelVizType.TimeToConvert, }, } const insight: Partial = { @@ -848,7 +848,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.TimeToConvert, + funnelVizType: FunnelVizType.TimeToConvert, }, } const insight: Partial = { @@ -882,7 +882,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.Steps, + funnelVizType: FunnelVizType.Steps, }, } @@ -906,7 +906,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.TimeToConvert, + funnelVizType: FunnelVizType.TimeToConvert, }, } @@ -930,7 +930,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.Trends, + funnelVizType: FunnelVizType.Trends, }, } @@ -956,7 +956,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.Steps, + funnelVizType: FunnelVizType.Steps, }, } @@ -984,7 +984,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.TimeToConvert, + funnelVizType: FunnelVizType.TimeToConvert, }, } const insight: Partial = { @@ -1011,7 +1011,7 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_viz_type: FunnelVizType.Trends, + funnelVizType: FunnelVizType.Trends, }, } @@ -1039,8 +1039,8 @@ describe('funnelDataLogic', () => { it('with defaults', async () => { await expectLogic(logic).toMatchValues({ conversionWindow: { - funnel_window_interval: 14, - funnel_window_interval_unit: 'day', + funnelWindowInterval: 14, + funnelWindowIntervalUnit: 'day', }, }) }) @@ -1050,8 +1050,8 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_window_interval: 3, - funnel_window_interval_unit: FunnelConversionWindowTimeUnit.Week, + funnelWindowInterval: 3, + funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit.Week, }, } @@ -1059,8 +1059,8 @@ describe('funnelDataLogic', () => { logic.actions.updateQuerySource(query) }).toMatchValues({ conversionWindow: { - funnel_window_interval: 3, - funnel_window_interval_unit: 'week', + funnelWindowInterval: 3, + funnelWindowIntervalUnit: 'week', }, }) }) @@ -1096,8 +1096,8 @@ describe('funnelDataLogic', () => { kind: NodeKind.FunnelsQuery, series: [], funnelsFilter: { - funnel_window_interval: 2, - funnel_window_interval_unit: FunnelConversionWindowTimeUnit.Day, + funnelWindowInterval: 2, + funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit.Day, }, } diff --git a/frontend/src/scenes/funnels/funnelDataLogic.ts b/frontend/src/scenes/funnels/funnelDataLogic.ts index 946feafccfc9d..52dc7e90e74e6 100644 --- a/frontend/src/scenes/funnels/funnelDataLogic.ts +++ b/frontend/src/scenes/funnels/funnelDataLogic.ts @@ -91,19 +91,19 @@ export const funnelDataLogic = kea([ ? null : funnelsFilter === undefined ? true - : funnelsFilter.funnel_viz_type === FunnelVizType.Steps + : funnelsFilter.funnelVizType === FunnelVizType.Steps }, ], isTimeToConvertFunnel: [ (s) => [s.funnelsFilter], (funnelsFilter): boolean | null => { - return funnelsFilter === null ? null : funnelsFilter?.funnel_viz_type === FunnelVizType.TimeToConvert + return funnelsFilter === null ? null : funnelsFilter?.funnelVizType === FunnelVizType.TimeToConvert }, ], isTrendsFunnel: [ (s) => [s.funnelsFilter], (funnelsFilter): boolean | null => { - return funnelsFilter === null ? null : funnelsFilter?.funnel_viz_type === FunnelVizType.Trends + return funnelsFilter === null ? null : funnelsFilter?.funnelVizType === FunnelVizType.Trends }, ], @@ -124,8 +124,8 @@ export const funnelDataLogic = kea([ return { singular: '', plural: '' } } - return querySource.funnelsFilter?.funnel_aggregate_by_hogql - ? aggregationLabelForHogQL(querySource.funnelsFilter.funnel_aggregate_by_hogql) + return querySource.funnelsFilter?.funnelAggregateByHogQL + ? aggregationLabelForHogQL(querySource.funnelsFilter.funnelAggregateByHogQL) : aggregationLabel(querySource.aggregation_group_type_index) }, ], @@ -173,7 +173,7 @@ export const funnelDataLogic = kea([ stepsWithConversionMetrics: [ (s) => [s.steps, s.funnelsFilter], (steps, funnelsFilter): FunnelStepWithConversionMetrics[] => { - const stepReference = funnelsFilter?.funnel_step_reference || FunnelStepReference.total + const stepReference = funnelsFilter?.funnelStepReference || FunnelStepReference.total return stepsWithConversionMetrics(steps, stepReference) }, ], @@ -219,7 +219,7 @@ export const funnelDataLogic = kea([ timeConversionResults: [ (s) => [s.results, s.funnelsFilter], (results, funnelsFilter): FunnelsTimeConversionBins | null => { - return funnelsFilter?.funnel_viz_type === FunnelVizType.TimeToConvert + return funnelsFilter?.funnelVizType === FunnelVizType.TimeToConvert ? (results as FunnelsTimeConversionBins) : null }, @@ -253,11 +253,11 @@ export const funnelDataLogic = kea([ hasFunnelResults: [ (s) => [s.funnelsFilter, s.steps, s.histogramGraphData], (funnelsFilter, steps, histogramGraphData) => { - if (funnelsFilter?.funnel_viz_type === FunnelVizType.Steps || !funnelsFilter?.funnel_viz_type) { + if (funnelsFilter?.funnelVizType === FunnelVizType.Steps || !funnelsFilter?.funnelVizType) { return !!(steps && steps[0] && steps[0].count > -1) - } else if (funnelsFilter.funnel_viz_type === FunnelVizType.TimeToConvert) { + } else if (funnelsFilter.funnelVizType === FunnelVizType.TimeToConvert) { return (histogramGraphData?.length ?? 0) > 0 - } else if (funnelsFilter.funnel_viz_type === FunnelVizType.Trends) { + } else if (funnelsFilter.funnelVizType === FunnelVizType.Trends) { return (steps?.length ?? 0) > 0 && !!steps?.[0]?.labels } else { return false @@ -267,10 +267,10 @@ export const funnelDataLogic = kea([ numericBinCount: [ (s) => [s.funnelsFilter, s.timeConversionResults], (funnelsFilter, timeConversionResults): number => { - if (funnelsFilter?.bin_count === BIN_COUNT_AUTO) { + if (funnelsFilter?.binCount === BIN_COUNT_AUTO) { return timeConversionResults?.bins?.length ?? 0 } - return funnelsFilter?.bin_count ?? 0 + return funnelsFilter?.binCount ?? 0 }, ], @@ -278,7 +278,7 @@ export const funnelDataLogic = kea([ (s) => [s.steps, s.funnelsFilter, s.timeConversionResults], (steps, funnelsFilter, timeConversionResults): FunnelTimeConversionMetrics => { // steps should be empty in time conversion view. Return metrics precalculated on backend - if (funnelsFilter?.funnel_viz_type === FunnelVizType.TimeToConvert) { + if (funnelsFilter?.funnelVizType === FunnelVizType.TimeToConvert) { return { averageTime: timeConversionResults?.average_conversion_time ?? 0, stepRate: 0, @@ -287,7 +287,7 @@ export const funnelDataLogic = kea([ } // Handle metrics for trends - if (funnelsFilter?.funnel_viz_type === FunnelVizType.Trends) { + if (funnelsFilter?.funnelVizType === FunnelVizType.Trends) { return { averageTime: 0, stepRate: 0, @@ -321,10 +321,10 @@ export const funnelDataLogic = kea([ conversionWindow: [ (s) => [s.funnelsFilter], (funnelsFilter): FunnelConversionWindow => { - const { funnel_window_interval, funnel_window_interval_unit } = funnelsFilter || {} + const { funnelWindowInterval, funnelWindowIntervalUnit } = funnelsFilter || {} return { - funnel_window_interval: funnel_window_interval || 14, - funnel_window_interval_unit: funnel_window_interval_unit || FunnelConversionWindowTimeUnit.Day, + funnelWindowInterval: funnelWindowInterval || 14, + funnelWindowIntervalUnit: funnelWindowIntervalUnit || FunnelConversionWindowTimeUnit.Day, } }, ], @@ -348,18 +348,18 @@ export const funnelDataLogic = kea([ ], /* - * Advanced options: funnel_order_type, funnel_step_reference, exclusions + * Advanced options: funnelOrderType, funnelStepReference, exclusions */ advancedOptionsUsedCount: [ (s) => [s.funnelsFilter], (funnelsFilter): number => { let count = 0 - if (funnelsFilter?.funnel_order_type && funnelsFilter?.funnel_order_type !== StepOrderValue.ORDERED) { + if (funnelsFilter?.funnelOrderType && funnelsFilter?.funnelOrderType !== StepOrderValue.ORDERED) { count = count + 1 } if ( - funnelsFilter?.funnel_step_reference && - funnelsFilter?.funnel_step_reference !== FunnelStepReference.total + funnelsFilter?.funnelStepReference && + funnelsFilter?.funnelStepReference !== FunnelStepReference.total ) { count = count + 1 } diff --git a/frontend/src/scenes/funnels/funnelPersonsModalLogic.ts b/frontend/src/scenes/funnels/funnelPersonsModalLogic.ts index b92357c68f81e..afc63a1cc8065 100644 --- a/frontend/src/scenes/funnels/funnelPersonsModalLogic.ts +++ b/frontend/src/scenes/funnels/funnelPersonsModalLogic.ts @@ -74,7 +74,7 @@ export const funnelPersonsModalLogic = kea([ canOpenPersonModal: [ (s) => [s.funnelsFilter, s.isInDashboardContext], (funnelsFilter, isInDashboardContext): boolean => { - return !isInDashboardContext && !funnelsFilter?.funnel_aggregate_by_hogql + return !isInDashboardContext && !funnelsFilter?.funnelAggregateByHogQL }, ], }), @@ -94,7 +94,7 @@ export const funnelPersonsModalLogic = kea([ step: typeof stepIndex === 'number' ? stepIndex + 1 : step.order + 1, label: step.name, seriesId: step.order, - order_type: values.funnelsFilter?.funnel_order_type, + order_type: values.funnelsFilter?.funnelOrderType, }), }) }, @@ -112,7 +112,7 @@ export const funnelPersonsModalLogic = kea([ breakdown_value: breakdownValues.isEmpty ? undefined : breakdownValues.breakdown_value.join(', '), label: step.name, seriesId: step.order, - order_type: values.funnelsFilter?.funnel_order_type, + order_type: values.funnelsFilter?.funnelOrderType, }), }) }, diff --git a/frontend/src/scenes/funnels/funnelUtils.test.ts b/frontend/src/scenes/funnels/funnelUtils.test.ts index d47b7e171a39d..0c83d4f0a826a 100644 --- a/frontend/src/scenes/funnels/funnelUtils.test.ts +++ b/frontend/src/scenes/funnels/funnelUtils.test.ts @@ -1,7 +1,7 @@ import { dayjs } from 'lib/dayjs' +import { EventsNode, FunnelsQuery, NodeKind } from '~/queries/schema' import { - FilterType, FunnelConversionWindowTimeUnit, FunnelCorrelation, FunnelCorrelationResultsType, @@ -12,7 +12,7 @@ import { import { EMPTY_BREAKDOWN_VALUES, getBreakdownStepValues, - getClampedStepRangeFilter, + getClampedStepRange, getIncompleteConversionWindowStartDate, getMeanAndStandardDeviation, getVisibilityKey, @@ -133,100 +133,107 @@ describe('getVisibilityKey()', () => { describe('getIncompleteConversionWindowStartDate()', () => { const windows = [ { - funnel_window_interval: 10, - funnel_window_interval_unit: FunnelConversionWindowTimeUnit.Second, + funnelWindowInterval: 10, + funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit.Second, expected: '2018-04-04T15:59:50.000Z', }, { - funnel_window_interval: 60, - funnel_window_interval_unit: FunnelConversionWindowTimeUnit.Minute, + funnelWindowInterval: 60, + funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit.Minute, expected: '2018-04-04T15:00:00.000Z', }, { - funnel_window_interval: 24, - funnel_window_interval_unit: FunnelConversionWindowTimeUnit.Hour, + funnelWindowInterval: 24, + funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit.Hour, expected: '2018-04-03T16:00:00.000Z', }, { - funnel_window_interval: 7, - funnel_window_interval_unit: FunnelConversionWindowTimeUnit.Day, + funnelWindowInterval: 7, + funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit.Day, expected: '2018-03-28T16:00:00.000Z', }, { - funnel_window_interval: 53, - funnel_window_interval_unit: FunnelConversionWindowTimeUnit.Week, + funnelWindowInterval: 53, + funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit.Week, expected: '2017-03-29T16:00:00.000Z', }, { - funnel_window_interval: 12, - funnel_window_interval_unit: FunnelConversionWindowTimeUnit.Month, + funnelWindowInterval: 12, + funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit.Month, expected: '2017-04-04T16:00:00.000Z', }, ] const frozenStartDate = dayjs('2018-04-04T16:00:00.000Z') windows.forEach(({ expected, ...w }) => { - it(`get start date of conversion window ${w.funnel_window_interval} ${w.funnel_window_interval_unit}s`, () => { + it(`get start date of conversion window ${w.funnelWindowInterval} ${w.funnelWindowIntervalUnit}s`, () => { expect(getIncompleteConversionWindowStartDate(w, frozenStartDate).toISOString()).toEqual(expected) }) }) }) -describe('getClampedStepRangeFilter', () => { +describe('getClampedStepRange', () => { it('prefers step range to existing filters', () => { - const stepRange = { - funnel_from_step: 0, - funnel_to_step: 1, - } as FunnelExclusion - const filters = { - funnel_from_step: 1, - funnel_to_step: 2, - actions: [{}, {}], - events: [{}, {}], - } as FilterType - const clampedStepRange = getClampedStepRangeFilter({ + const stepRange: FunnelExclusion = { + funnelFromStep: 0, + funnelToStep: 1, + } + const query: FunnelsQuery = { + kind: NodeKind.FunnelsQuery, + funnelsFilter: { + funnelFromStep: 1, + funnelToStep: 2, + }, + series: [{}, {}] as EventsNode[], + } + const clampedStepRange = getClampedStepRange({ stepRange, - filters, + query, }) expect(clampedStepRange).toEqual({ - funnel_from_step: 0, - funnel_to_step: 1, + funnelFromStep: 0, + funnelToStep: 1, }) }) it('ensures step range is clamped to step range', () => { - const stepRange = {} as FunnelExclusion - const filters = { - funnel_from_step: -1, - funnel_to_step: 12, - actions: [{}, {}], - events: [{}, {}], - } as FilterType - const clampedStepRange = getClampedStepRangeFilter({ + const stepRange: FunnelExclusion = {} + const query: FunnelsQuery = { + kind: NodeKind.FunnelsQuery, + funnelsFilter: { + funnelFromStep: -1, + + funnelToStep: 12, + }, + series: [{}, {}, {}] as EventsNode[], + } + const clampedStepRange = getClampedStepRange({ stepRange, - filters, + query, }) expect(clampedStepRange).toEqual({ - funnel_from_step: 0, - funnel_to_step: 3, + funnelFromStep: 0, + funnelToStep: 2, }) }) it('returns undefined if the incoming filters are undefined', () => { - const stepRange = {} as FunnelExclusion - const filters = { - funnel_from_step: undefined, - funnel_to_step: undefined, - actions: [{}, {}], - events: [{}, {}], - } as FilterType - const clampedStepRange = getClampedStepRangeFilter({ + const stepRange: FunnelExclusion = {} + const query: FunnelsQuery = { + kind: NodeKind.FunnelsQuery, + funnelsFilter: { + funnelFromStep: undefined, + funnelToStep: undefined, + }, + series: [{}, {}] as EventsNode[], + } + const clampedStepRange = getClampedStepRange({ stepRange, - filters, + query, }) expect(clampedStepRange).toEqual({ - funnel_from_step: undefined, - funnel_to_step: undefined, + funnelFromStep: undefined, + funnelToStep: undefined, }) }) }) diff --git a/frontend/src/scenes/funnels/funnelUtils.ts b/frontend/src/scenes/funnels/funnelUtils.ts index b19dcd865fed2..1623a0d87f1e6 100644 --- a/frontend/src/scenes/funnels/funnelUtils.ts +++ b/frontend/src/scenes/funnels/funnelUtils.ts @@ -19,7 +19,6 @@ import { FunnelCorrelationResultsType, FunnelExclusion, FunnelResultType, - FunnelsFilterType, FunnelStep, FunnelStepReference, FunnelStepWithConversionMetrics, @@ -220,87 +219,33 @@ export const getBreakdownStepValues = ( return EMPTY_BREAKDOWN_VALUES } -export const isStepsEmpty = (filters: FunnelsFilterType): boolean => - [...(filters.actions || []), ...(filters.events || [])].length === 0 - -export const isStepsUndefined = (filters: FunnelsFilterType): boolean => - typeof filters.events === 'undefined' && (typeof filters.actions === 'undefined' || filters.actions.length === 0) - -export const deepCleanFunnelExclusionEvents = (filters: FunnelsFilterType): FunnelExclusion[] | undefined => { - if (!filters.exclusions) { - return undefined - } - - const lastIndex = Math.max((filters.events?.length || 0) + (filters.actions?.length || 0) - 1, 1) - const exclusions = filters.exclusions.map((event) => { - const funnel_from_step = event.funnel_from_step ? clamp(event.funnel_from_step, 0, lastIndex - 1) : 0 - return { - ...event, - ...{ funnel_from_step }, - ...{ - funnel_to_step: event.funnel_to_step - ? clamp(event.funnel_to_step, funnel_from_step + 1, lastIndex) - : lastIndex, - }, - } - }) - return exclusions.length > 0 ? exclusions : undefined -} - const findFirstNumber = (candidates: (number | undefined)[]): number | undefined => candidates.find((s) => typeof s === 'number') -export const getClampedStepRangeFilter = ({ - stepRange, - filters, -}: { - stepRange?: FunnelExclusion - filters: FunnelsFilterType -}): FunnelExclusion => { - const maxStepIndex = Math.max((filters.events?.length || 0) + (filters.actions?.length || 0) - 1, 1) - - let funnel_from_step = findFirstNumber([stepRange?.funnel_from_step, filters.funnel_from_step]) - let funnel_to_step = findFirstNumber([stepRange?.funnel_to_step, filters.funnel_to_step]) - - const funnelFromStepIsSet = typeof funnel_from_step === 'number' - const funnelToStepIsSet = typeof funnel_to_step === 'number' - - if (funnelFromStepIsSet && funnelToStepIsSet) { - funnel_from_step = clamp(funnel_from_step ?? 0, 0, maxStepIndex) - funnel_to_step = clamp(funnel_to_step ?? maxStepIndex, funnel_from_step + 1, maxStepIndex) - } - - return { - ...(stepRange || {}), - funnel_from_step, - funnel_to_step, - } -} - -export const getClampedStepRangeFilterDataExploration = ({ +export const getClampedStepRange = ({ stepRange, query, }: { stepRange?: FunnelExclusion query: FunnelsQuery }): FunnelExclusion => { - const maxStepIndex = Math.max(query.series.length || 0 - 1, 1) + const maxStepIndex = Math.max((query.series.length || 0) - 1, 1) - let funnel_from_step = findFirstNumber([stepRange?.funnel_from_step, query.funnelsFilter?.funnel_from_step]) - let funnel_to_step = findFirstNumber([stepRange?.funnel_to_step, query.funnelsFilter?.funnel_to_step]) + let funnelFromStep = findFirstNumber([stepRange?.funnelFromStep, query.funnelsFilter?.funnelFromStep]) + let funnelToStep = findFirstNumber([stepRange?.funnelToStep, query.funnelsFilter?.funnelToStep]) - const funnelFromStepIsSet = typeof funnel_from_step === 'number' - const funnelToStepIsSet = typeof funnel_to_step === 'number' + const funnelFromStepIsSet = typeof funnelFromStep === 'number' + const funnelToStepIsSet = typeof funnelToStep === 'number' if (funnelFromStepIsSet && funnelToStepIsSet) { - funnel_from_step = clamp(funnel_from_step ?? 0, 0, maxStepIndex) - funnel_to_step = clamp(funnel_to_step ?? maxStepIndex, funnel_from_step + 1, maxStepIndex) + funnelFromStep = clamp(funnelFromStep ?? 0, 0, maxStepIndex) + funnelToStep = clamp(funnelToStep ?? maxStepIndex, funnelFromStep + 1, maxStepIndex) } return { ...(stepRange || {}), - funnel_from_step, - funnel_to_step, + funnelFromStep, + funnelToStep, } } @@ -323,8 +268,8 @@ export function getIncompleteConversionWindowStartDate( window: FunnelConversionWindow, startDate: dayjs.Dayjs = dayjs() ): dayjs.Dayjs { - const { funnel_window_interval, funnel_window_interval_unit } = window - return startDate.subtract(funnel_window_interval, funnel_window_interval_unit) + const { funnelWindowInterval, funnelWindowIntervalUnit } = window + return startDate.subtract(funnelWindowInterval, funnelWindowIntervalUnit) } export function generateBaselineConversionUrl(url?: string | null): string { @@ -643,11 +588,11 @@ export const appendToCorrelationConfig = ( }) } -export function aggregationLabelForHogQL(funnel_aggregate_by_hogql: string): Noun { - if (funnel_aggregate_by_hogql === 'person_id') { +export function aggregationLabelForHogQL(funnelAggregateByHogQL: string): Noun { + if (funnelAggregateByHogQL === 'person_id') { return { singular: 'person', plural: 'persons' } } - if (funnel_aggregate_by_hogql === 'properties.$session_id') { + if (funnelAggregateByHogQL === 'properties.$session_id') { return { singular: 'session', plural: 'sessions' } } return { singular: 'result', plural: 'results' } diff --git a/frontend/src/scenes/insights/EditorFilters/AttributionFilter.tsx b/frontend/src/scenes/insights/EditorFilters/AttributionFilter.tsx index 013ca006ca8d4..2cd3628b89e8a 100644 --- a/frontend/src/scenes/insights/EditorFilters/AttributionFilter.tsx +++ b/frontend/src/scenes/insights/EditorFilters/AttributionFilter.tsx @@ -11,15 +11,15 @@ export function Attribution({ insightProps }: EditorFilterProps): JSX.Element { const { insightFilter, steps } = useValues(funnelDataLogic(insightProps)) const { updateInsightFilter } = useActions(funnelDataLogic(insightProps)) - const { breakdown_attribution_type, breakdown_attribution_value, funnel_order_type } = (insightFilter || + const { breakdownAttributionType, breakdownAttributionValue, funnelOrderType } = (insightFilter || {}) as FunnelsFilter const currentValue: BreakdownAttributionType | `${BreakdownAttributionType.Step}/${number}` = - !breakdown_attribution_type + !breakdownAttributionType ? BreakdownAttributionType.FirstTouch - : breakdown_attribution_type === BreakdownAttributionType.Step - ? `${breakdown_attribution_type}/${breakdown_attribution_value || 0}` - : breakdown_attribution_type + : breakdownAttributionType === BreakdownAttributionType.Step + ? `${breakdownAttributionType}/${breakdownAttributionValue || 0}` + : breakdownAttributionType return ( = steps.length, })), - hidden: funnel_order_type === StepOrderValue.UNORDERED, + hidden: funnelOrderType === StepOrderValue.UNORDERED, }, ]} onChange={(value) => { const [breakdownAttributionType, breakdownAttributionValue] = (value || '').split('/') if (value) { updateInsightFilter({ - breakdown_attribution_type: breakdownAttributionType as BreakdownAttributionType, - breakdown_attribution_value: breakdownAttributionValue - ? parseInt(breakdownAttributionValue) - : 0, + breakdownAttributionType: breakdownAttributionType as BreakdownAttributionType, + breakdownAttributionValue: breakdownAttributionValue ? parseInt(breakdownAttributionValue) : 0, }) } }} diff --git a/frontend/src/scenes/insights/EditorFilters/FunnelsAdvanced.tsx b/frontend/src/scenes/insights/EditorFilters/FunnelsAdvanced.tsx index b7ad15cc28546..4eb2ebd8c4c99 100644 --- a/frontend/src/scenes/insights/EditorFilters/FunnelsAdvanced.tsx +++ b/frontend/src/scenes/insights/EditorFilters/FunnelsAdvanced.tsx @@ -41,8 +41,8 @@ export function FunnelsAdvanced({ insightProps }: EditorFilterProps): JSX.Elemen status="danger" onClick={() => { updateInsightFilter({ - funnel_order_type: undefined, - funnel_step_reference: undefined, + funnelOrderType: undefined, + funnelStepReference: undefined, exclusions: undefined, }) }} diff --git a/frontend/src/scenes/insights/InsightNav/insightNavLogic.test.ts b/frontend/src/scenes/insights/InsightNav/insightNavLogic.test.ts index b9e4b0cd990fd..3babb46e20b34 100644 --- a/frontend/src/scenes/insights/InsightNav/insightNavLogic.test.ts +++ b/frontend/src/scenes/insights/InsightNav/insightNavLogic.test.ts @@ -155,8 +155,8 @@ describe('insightNavLogic', () => { }, ], funnelsFilter: { - funnel_order_type: StepOrderValue.STRICT, - funnel_viz_type: FunnelVizType.Steps, + funnelOrderType: StepOrderValue.STRICT, + funnelVizType: FunnelVizType.Steps, }, }, } @@ -239,8 +239,8 @@ describe('insightNavLogic', () => { queryPropertyCache: expect.objectContaining({ commonFilter: { showValuesOnSeries: true, - funnel_order_type: 'strict', - funnel_viz_type: 'steps', + funnelOrderType: 'strict', + funnelVizType: 'steps', }, }), }) diff --git a/frontend/src/scenes/insights/filters/AggregationSelect.tsx b/frontend/src/scenes/insights/filters/AggregationSelect.tsx index 830a0f17a48d0..5d4d3ea6acc3c 100644 --- a/frontend/src/scenes/insights/filters/AggregationSelect.tsx +++ b/frontend/src/scenes/insights/filters/AggregationSelect.tsx @@ -55,14 +55,14 @@ export function AggregationSelect({ isLifecycleQuery(querySource) || isStickinessQuery(querySource) ? undefined : querySource.aggregation_group_type_index, - isFunnelsQuery(querySource) ? querySource.funnelsFilter?.funnel_aggregate_by_hogql : undefined + isFunnelsQuery(querySource) ? querySource.funnelsFilter?.funnelAggregateByHogQL : undefined ) const onChange = (value: string): void => { const { aggregationQuery, groupIndex } = hogQLToFilterValue(value) if (isFunnelsQuery(querySource)) { updateQuerySource({ aggregation_group_type_index: groupIndex, - funnelsFilter: { ...querySource.funnelsFilter, funnel_aggregate_by_hogql: aggregationQuery }, + funnelsFilter: { ...querySource.funnelsFilter, funnelAggregateByHogQL: aggregationQuery }, } as FunnelsQuery) } else { updateQuerySource({ aggregation_group_type_index: groupIndex } as FunnelsQuery) diff --git a/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/ExclusionRowSuffix.tsx b/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/ExclusionRowSuffix.tsx index e44eea401dfa4..99189d392ad95 100644 --- a/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/ExclusionRowSuffix.tsx +++ b/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/ExclusionRowSuffix.tsx @@ -2,12 +2,12 @@ import { LemonButton, LemonSelect } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' import { IconDelete } from 'lib/lemon-ui/icons' -import { getClampedStepRangeFilterDataExploration } from 'scenes/funnels/funnelUtils' +import { getClampedStepRange } from 'scenes/funnels/funnelUtils' import { insightLogic } from 'scenes/insights/insightLogic' import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' -import { FunnelsQuery } from '~/queries/schema' -import { ActionFilter as ActionFilterType, FunnelExclusion, FunnelsFilterType } from '~/types' +import { FunnelsFilter, FunnelsQuery } from '~/queries/schema' +import { ActionFilter as ActionFilterType, FunnelExclusion } from '~/types' type ExclusionRowSuffixComponentBaseProps = { filter: ActionFilterType | FunnelExclusion @@ -29,9 +29,9 @@ export function ExclusionRowSuffix({ const { updateInsightFilter } = useActions(insightVizDataLogic(insightProps)) const setOneEventExclusionFilter = (eventFilter: FunnelExclusion, index: number): void => { - const exclusions = ((insightFilter as FunnelsFilterType)?.exclusions || []).map((e, e_i) => + const exclusions = ((insightFilter as FunnelsFilter)?.exclusions || []).map((e, e_i) => e_i === index - ? getClampedStepRangeFilterDataExploration({ + ? getClampedStepRange({ stepRange: eventFilter, query: querySource as FunnelsQuery, }) @@ -43,23 +43,23 @@ export function ExclusionRowSuffix({ }) } - const exclusions = (insightFilter as FunnelsFilterType)?.exclusions + const exclusions = (insightFilter as FunnelsFilter)?.exclusions const numberOfSeries = series?.length || 0 const stepRange = { - funnel_from_step: exclusions?.[index]?.funnel_from_step ?? exclusionDefaultStepRange.funnel_from_step, - funnel_to_step: exclusions?.[index]?.funnel_to_step ?? exclusionDefaultStepRange.funnel_to_step, + funnelFromStep: exclusions?.[index]?.funnelFromStep ?? exclusionDefaultStepRange.funnelFromStep, + funnelToStep: exclusions?.[index]?.funnelToStep ?? exclusionDefaultStepRange.funnelToStep, } const onChange = ( - funnel_from_step: number | undefined = stepRange.funnel_from_step, - funnel_to_step: number | undefined = stepRange.funnel_to_step + funnelFromStep: number | undefined = stepRange.funnelFromStep, + funnelToStep: number | undefined = stepRange.funnelToStep ): void => { setOneEventExclusionFilter( { ...filter, - funnel_from_step, - funnel_to_step, + funnelFromStep, + funnelToStep, }, index ) @@ -71,7 +71,7 @@ export function ExclusionRowSuffix({ onChange(stepRange.funnel_from_step, toStep)} + value={stepRange.funnelToStep || (stepRange.funnelFromStep ?? 0) + 1} + onChange={(toStep: number) => onChange(stepRange.funnelFromStep, toStep)} options={Array.from(Array(numberOfSeries).keys()) - .slice((stepRange.funnel_from_step ?? 0) + 1) + .slice((stepRange.funnelFromStep ?? 0) + 1) .map((stepIndex) => ({ value: stepIndex, label: `Step ${stepIndex + 1}` }))} disabled={!isFunnelWithEnoughSteps} /> diff --git a/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/FunnelExclusionsFilter.tsx b/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/FunnelExclusionsFilter.tsx index a9ce4de510ff4..5641541a242be 100644 --- a/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/FunnelExclusionsFilter.tsx +++ b/frontend/src/scenes/insights/filters/FunnelExclusionsFilter/FunnelExclusionsFilter.tsx @@ -27,8 +27,8 @@ export function FunnelExclusionsFilter(): JSX.Element { const setFilters = (filters: Partial): void => { const exclusions = (filters.events as FunnelExclusion[]).map((e) => ({ ...e, - funnel_from_step: e.funnel_from_step || exclusionDefaultStepRange.funnel_from_step, - funnel_to_step: e.funnel_to_step || exclusionDefaultStepRange.funnel_to_step, + funnelFromStep: e.funnelFromStep || exclusionDefaultStepRange.funnelFromStep, + funnelToStep: e.funnelToStep || exclusionDefaultStepRange.funnelToStep, })) updateInsightFilter({ exclusions }) } diff --git a/frontend/src/scenes/insights/filters/FunnelStepReferencePicker.tsx b/frontend/src/scenes/insights/filters/FunnelStepReferencePicker.tsx index b8b9cedf1f362..ce7e173f73530 100644 --- a/frontend/src/scenes/insights/filters/FunnelStepReferencePicker.tsx +++ b/frontend/src/scenes/insights/filters/FunnelStepReferencePicker.tsx @@ -11,7 +11,7 @@ export function FunnelStepReferencePicker(): JSX.Element | null { const { insightFilter } = useValues(funnelDataLogic(insightProps)) const { updateInsightFilter } = useActions(funnelDataLogic(insightProps)) - const { funnel_step_reference } = (insightFilter || {}) as FunnelsFilter + const { funnelStepReference } = (insightFilter || {}) as FunnelsFilter const options = [ { @@ -26,8 +26,8 @@ export function FunnelStepReferencePicker(): JSX.Element | null { return ( stepRef && updateInsightFilter({ funnel_step_reference: stepRef })} + value={funnelStepReference || FunnelStepReference.total} + onChange={(stepRef) => stepRef && updateInsightFilter({ funnelStepReference: stepRef })} dropdownMatchSelectWidth={false} data-attr="funnel-step-reference-selector" options={options} diff --git a/frontend/src/scenes/insights/insightVizDataLogic.ts b/frontend/src/scenes/insights/insightVizDataLogic.ts index 2f6d1755fd36e..c655ac7b434fb 100644 --- a/frontend/src/scenes/insights/insightVizDataLogic.ts +++ b/frontend/src/scenes/insights/insightVizDataLogic.ts @@ -289,8 +289,8 @@ export const insightVizDataLogic = kea([ exclusionDefaultStepRange: [ (s) => [s.querySource], (querySource: FunnelsQuery): Omit => ({ - funnel_from_step: 0, - funnel_to_step: (querySource.series || []).length > 1 ? querySource.series.length - 1 : 1, + funnelFromStep: 0, + funnelToStep: (querySource.series || []).length > 1 ? querySource.series.length - 1 : 1, }), ], exclusionFilters: [ diff --git a/frontend/src/scenes/insights/summarizeInsight.ts b/frontend/src/scenes/insights/summarizeInsight.ts index d99cbcf763ba6..be21357cd7e71 100644 --- a/frontend/src/scenes/insights/summarizeInsight.ts +++ b/frontend/src/scenes/insights/summarizeInsight.ts @@ -228,17 +228,17 @@ export function summarizeInsightQuery(query: InsightQueryNode, context: SummaryC } else if (isFunnelsQuery(query)) { let summary const linkSymbol = - query.funnelsFilter?.funnel_order_type === StepOrderValue.STRICT + query.funnelsFilter?.funnelOrderType === StepOrderValue.STRICT ? '⇉' - : query.funnelsFilter?.funnel_order_type === StepOrderValue.UNORDERED + : query.funnelsFilter?.funnelOrderType === StepOrderValue.UNORDERED ? '&' : '→' summary = `${query.series.map((s) => getDisplayNameFromEntityNode(s)).join(` ${linkSymbol} `)} ${ context.aggregationLabel(query.aggregation_group_type_index, true).singular } conversion` - if (query.funnelsFilter?.funnel_viz_type === FunnelVizType.TimeToConvert) { + if (query.funnelsFilter?.funnelVizType === FunnelVizType.TimeToConvert) { summary += ' time' - } else if (query.funnelsFilter?.funnel_viz_type === FunnelVizType.Trends) { + } else if (query.funnelsFilter?.funnelVizType === FunnelVizType.Trends) { summary += ' trend' } else { // Steps are the default viz type diff --git a/frontend/src/scenes/insights/utils/cleanFilters.ts b/frontend/src/scenes/insights/utils/cleanFilters.ts index 67d934f66676f..6ee20604fa484 100644 --- a/frontend/src/scenes/insights/utils/cleanFilters.ts +++ b/frontend/src/scenes/insights/utils/cleanFilters.ts @@ -7,8 +7,8 @@ import { RETENTION_FIRST_TIME, ShownAsValue, } from 'lib/constants' +import { clamp } from 'lib/utils' import { getDefaultEventName } from 'lib/utils/getAppContext' -import { deepCleanFunnelExclusionEvents, getClampedStepRangeFilter, isStepsUndefined } from 'scenes/funnels/funnelUtils' import { isURLNormalizeable } from 'scenes/insights/filters/BreakdownFilter/taxonomicBreakdownFilterUtils' import { isFunnelsFilter, @@ -26,6 +26,7 @@ import { Entity, EntityTypes, FilterType, + FunnelExclusionLegacy, FunnelsFilterType, FunnelVizType, InsightType, @@ -51,6 +52,60 @@ export function getDefaultEvent(): Entity { } } +export const isStepsUndefined = (filters: FunnelsFilterType): boolean => + typeof filters.events === 'undefined' && (typeof filters.actions === 'undefined' || filters.actions.length === 0) + +const findFirstNumber = (candidates: (number | undefined)[]): number | undefined => + candidates.find((s) => typeof s === 'number') + +export const getClampedStepRangeFilter = ({ + stepRange, + filters, +}: { + stepRange?: FunnelExclusionLegacy + filters: FunnelsFilterType +}): FunnelExclusionLegacy => { + const maxStepIndex = Math.max((filters.events?.length || 0) + (filters.actions?.length || 0) - 1, 1) + + let funnel_from_step = findFirstNumber([stepRange?.funnel_from_step, filters.funnel_from_step]) + let funnel_to_step = findFirstNumber([stepRange?.funnel_to_step, filters.funnel_to_step]) + + const funnelFromStepIsSet = typeof funnel_from_step === 'number' + const funnelToStepIsSet = typeof funnel_to_step === 'number' + + if (funnelFromStepIsSet && funnelToStepIsSet) { + funnel_from_step = clamp(funnel_from_step ?? 0, 0, maxStepIndex) + funnel_to_step = clamp(funnel_to_step ?? maxStepIndex, funnel_from_step + 1, maxStepIndex) + } + + return { + ...(stepRange || {}), + funnel_from_step, + funnel_to_step, + } +} + +export const deepCleanFunnelExclusionEvents = (filters: FunnelsFilterType): FunnelExclusionLegacy[] | undefined => { + if (!filters.exclusions) { + return undefined + } + + const lastIndex = Math.max((filters.events?.length || 0) + (filters.actions?.length || 0) - 1, 1) + const exclusions = filters.exclusions.map((event) => { + const funnel_from_step = event.funnel_from_step ? clamp(event.funnel_from_step, 0, lastIndex - 1) : 0 + return { + ...event, + ...{ funnel_from_step }, + ...{ + funnel_to_step: event.funnel_to_step + ? clamp(event.funnel_to_step, funnel_from_step + 1, lastIndex) + : lastIndex, + }, + } + }) + return exclusions.length > 0 ? exclusions : undefined +} + /** Take the first series from filters and, based on it, apply the most relevant breakdown type to cleanedParams. */ const useMostRelevantBreakdownType = (cleanedParams: Partial, filters: Partial): void => { const series: LocalFilter | undefined = toLocalFilters(filters)[0] diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelBinsPicker.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelBinsPicker.tsx index cfa19f26624dd..9ee22482b10ef 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelBinsPicker.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelBinsPicker.tsx @@ -48,7 +48,7 @@ export function FunnelBinsPicker({ disabled }: FunnelBinsPickerProps): JSX.Eleme const { updateInsightFilter } = useActions(funnelDataLogic(insightProps)) const setBinCount = (binCount: BinCountValue): void => { - updateInsightFilter({ bin_count: binCount && binCount !== BIN_COUNT_AUTO ? binCount : undefined }) + updateInsightFilter({ binCount: binCount && binCount !== BIN_COUNT_AUTO ? binCount : undefined }) } return ( @@ -57,7 +57,7 @@ export function FunnelBinsPicker({ disabled }: FunnelBinsPickerProps): JSX.Eleme dropdownClassName="funnel-bin-filter-dropdown" data-attr="funnel-bin-filter" defaultValue={BIN_COUNT_AUTO} - value={funnelsFilter?.bin_count || BIN_COUNT_AUTO} + value={funnelsFilter?.binCount || BIN_COUNT_AUTO} onSelect={(count) => setBinCount(count)} dropdownRender={(menu) => { return ( diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelConversionWindowFilter.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelConversionWindowFilter.tsx index e938e03c69eb6..3b37189d3cfdc 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelConversionWindowFilter.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelConversionWindowFilter.tsx @@ -24,26 +24,26 @@ export function FunnelConversionWindowFilter({ insightProps }: Pick({ - funnel_window_interval, - funnel_window_interval_unit, + funnelWindowInterval, + funnelWindowIntervalUnit, }) const options: LemonSelectOption[] = Object.keys(TIME_INTERVAL_BOUNDS).map( (unit) => ({ - label: capitalizeFirstLetter(pluralize(funnel_window_interval ?? 7, unit, `${unit}s`, false)), + label: capitalizeFirstLetter(pluralize(funnelWindowInterval ?? 7, unit, `${unit}s`, false)), value: unit as FunnelConversionWindowTimeUnit, }) ) - const intervalBounds = TIME_INTERVAL_BOUNDS[funnel_window_interval_unit ?? FunnelConversionWindowTimeUnit.Day] + const intervalBounds = TIME_INTERVAL_BOUNDS[funnelWindowIntervalUnit ?? FunnelConversionWindowTimeUnit.Day] const setConversionWindow = useDebouncedCallback((): void => { if ( - localConversionWindow.funnel_window_interval !== funnel_window_interval || - localConversionWindow.funnel_window_interval_unit !== funnel_window_interval_unit + localConversionWindow.funnelWindowInterval !== funnelWindowInterval || + localConversionWindow.funnelWindowIntervalUnit !== funnelWindowIntervalUnit ) { updateInsightFilter(localConversionWindow) } @@ -74,12 +74,12 @@ export function FunnelConversionWindowFilter({ insightProps }: Pick { + defaultValue={funnelWindowInterval} + value={localConversionWindow.funnelWindowInterval} + onChange={(funnelWindowInterval) => { setLocalConversionWindow((state) => ({ ...state, - funnel_window_interval: Number(funnel_window_interval), + funnelWindowInterval: Number(funnelWindowInterval), })) setConversionWindow() }} @@ -88,10 +88,10 @@ export function FunnelConversionWindowFilter({ insightProps }: Pick { - if (funnel_window_interval_unit) { - setLocalConversionWindow((state) => ({ ...state, funnel_window_interval_unit })) + value={localConversionWindow.funnelWindowIntervalUnit} + onChange={(funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit | null) => { + if (funnelWindowIntervalUnit) { + setLocalConversionWindow((state) => ({ ...state, funnelWindowIntervalUnit })) setConversionWindow() } }} diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelStepOrderPicker.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelStepOrderPicker.tsx index 615fc8bfa1a77..5db5e0c16e860 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelStepOrderPicker.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelStepOrderPicker.tsx @@ -32,14 +32,14 @@ export function FunnelStepOrderPicker(): JSX.Element { const { insightFilter } = useValues(funnelDataLogic(insightProps)) const { updateInsightFilter } = useActions(funnelDataLogic(insightProps)) - const { funnel_order_type } = (insightFilter || {}) as FunnelsFilter + const { funnelOrderType } = (insightFilter || {}) as FunnelsFilter return ( stepOrder && updateInsightFilter({ funnel_order_type: stepOrder })} + value={funnelOrderType || StepOrderValue.ORDERED} + onChange={(stepOrder) => stepOrder && updateInsightFilter({ funnelOrderType: stepOrder })} dropdownMatchSelectWidth={false} options={options} /> diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelStepsPicker.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelStepsPicker.tsx index c875b1cec5355..a91a2fb43eeff 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelStepsPicker.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelStepsPicker.tsx @@ -11,15 +11,15 @@ export function FunnelStepsPicker(): JSX.Element | null { const { series, isFunnelWithEnoughSteps, funnelsFilter } = useValues(insightVizDataLogic(insightProps)) const { updateInsightFilter } = useActions(insightVizDataLogic(insightProps)) - const onChange = (funnel_from_step?: number, funnel_to_step?: number): void => { - updateInsightFilter({ funnel_from_step, funnel_to_step }) + const onChange = (funnelFromStep?: number, funnelToStep?: number): void => { + updateInsightFilter({ funnelFromStep, funnelToStep }) } const filterSteps = series || [] const numberOfSeries = series?.length || 0 const fromRange = isFunnelWithEnoughSteps ? Array.from(Array(Math.max(numberOfSeries)).keys()).slice(0, -1) : [0] const toRange = isFunnelWithEnoughSteps - ? Array.from(Array(Math.max(numberOfSeries)).keys()).slice((funnelsFilter?.funnel_from_step ?? 0) + 1) + ? Array.from(Array(Math.max(numberOfSeries)).keys()).slice((funnelsFilter?.funnelFromStep ?? 0) + 1) : [1] const optionsForRange = (range: number[]): LemonSelectOptions => { @@ -51,9 +51,9 @@ export function FunnelStepsPicker(): JSX.Element | null { optionTooltipPlacement="bottomLeft" disabled={!isFunnelWithEnoughSteps} options={optionsForRange(fromRange)} - value={funnelsFilter?.funnel_from_step || 0} + value={funnelsFilter?.funnelFromStep || 0} onChange={(fromStep: number | null) => - fromStep != null && onChange(fromStep, funnelsFilter?.funnel_to_step) + fromStep != null && onChange(fromStep, funnelsFilter?.funnelToStep) } /> to @@ -64,10 +64,8 @@ export function FunnelStepsPicker(): JSX.Element | null { optionTooltipPlacement="bottomLeft" disabled={!isFunnelWithEnoughSteps} options={optionsForRange(toRange)} - value={funnelsFilter?.funnel_to_step || Math.max(numberOfSeries - 1, 1)} - onChange={(toStep: number | null) => - toStep != null && onChange(funnelsFilter?.funnel_from_step, toStep) - } + value={funnelsFilter?.funnelToStep || Math.max(numberOfSeries - 1, 1)} + onChange={(toStep: number | null) => toStep != null && onChange(funnelsFilter?.funnelFromStep, toStep)} />
) diff --git a/frontend/src/scenes/insights/views/Funnels/FunnelVizType.tsx b/frontend/src/scenes/insights/views/Funnels/FunnelVizType.tsx index 9850c935468c6..eda478037df5c 100644 --- a/frontend/src/scenes/insights/views/Funnels/FunnelVizType.tsx +++ b/frontend/src/scenes/insights/views/Funnels/FunnelVizType.tsx @@ -35,7 +35,7 @@ export function FunnelVizType({ insightProps }: Pick { - if (funnel_viz_type !== value) { - updateInsightFilter({ funnel_viz_type: value }) + if (funnelVizType !== value) { + updateInsightFilter({ funnelVizType: value }) } }} options={options} diff --git a/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx b/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx index 3791046e658d5..11c9bc9170a06 100644 --- a/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx +++ b/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx @@ -144,7 +144,7 @@ const SLASH_COMMANDS: SlashCommandsItem[] = [ }, ], funnelsFilter: { - funnel_viz_type: FunnelVizType.Steps, + funnelVizType: FunnelVizType.Steps, }, }) ), diff --git a/frontend/src/types.ts b/frontend/src/types.ts index a532c5e5f39ce..763870ec321e2 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -825,11 +825,16 @@ export type EntityFilter = { order?: number } -export interface FunnelExclusion extends Partial { +export interface FunnelExclusionLegacy extends Partial { funnel_from_step?: number funnel_to_step?: number } +export interface FunnelExclusion extends Partial { + funnelFromStep?: number + funnelToStep?: number +} + export type EntityFilterTypes = EntityFilter | ActionFilter | null export interface PersonType { @@ -1847,7 +1852,7 @@ export interface FunnelsFilterType extends FilterType { funnel_window_interval_unit?: FunnelConversionWindowTimeUnit // minutes, days, weeks, etc. for conversion window funnel_window_interval?: number | undefined // length of conversion window funnel_order_type?: StepOrderValue - exclusions?: FunnelExclusion[] // used in funnel exclusion filters + exclusions?: FunnelExclusionLegacy[] // used in funnel exclusion filters funnel_aggregate_by_hogql?: string // frontend only @@ -2117,8 +2122,8 @@ export interface FunnelTimeConversionMetrics { } export interface FunnelConversionWindow { - funnel_window_interval_unit: FunnelConversionWindowTimeUnit - funnel_window_interval: number + funnelWindowIntervalUnit: FunnelConversionWindowTimeUnit + funnelWindowInterval: number } // https://github.com/PostHog/posthog/blob/master/posthog/models/filters/mixins/funnel.py#L100 diff --git a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py index 25d4c7afb1b77..b5ec1c7bb648d 100644 --- a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py +++ b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py @@ -332,27 +332,27 @@ def _insight_filter(filter: Dict): elif _insight_type(filter) == "FUNNELS": insight_filter = { "funnelsFilter": FunnelsFilter( - funnel_viz_type=filter.get("funnel_viz_type"), - funnel_order_type=filter.get("funnel_order_type"), - funnel_from_step=filter.get("funnel_from_step"), - funnel_to_step=filter.get("funnel_to_step"), - funnel_window_interval_unit=filter.get("funnel_window_interval_unit"), - funnel_window_interval=filter.get("funnel_window_interval"), - funnel_step_reference=filter.get("funnel_step_reference"), - breakdown_attribution_type=filter.get("breakdown_attribution_type"), - breakdown_attribution_value=filter.get("breakdown_attribution_value"), - bin_count=filter.get("bin_count"), + funnelVizType=filter.get("funnel_viz_type"), + funnelOrderType=filter.get("funnel_order_type"), + funnelFromStep=filter.get("funnel_from_step"), + funnelToStep=filter.get("funnel_to_step"), + funnelWindowIntervalUnit=filter.get("funnel_window_interval_unit"), + funnelWindowInterval=filter.get("funnel_window_interval"), + funnelStepReference=filter.get("funnel_step_reference"), + breakdownAttributionType=filter.get("breakdown_attribution_type"), + breakdownAttributionValue=filter.get("breakdown_attribution_value"), + binCount=filter.get("bin_count"), exclusions=[ FunnelExclusion( **to_base_entity_dict(entity), - funnel_from_step=entity.get("funnel_from_step"), - funnel_to_step=entity.get("funnel_to_step"), + funnelFromStep=entity.get("funnel_from_step"), + funnelToStep=entity.get("funnel_to_step"), ) for entity in filter.get("exclusions", []) ], layout=filter.get("layout"), # hidden_legend_breakdowns: cleanHiddenLegendSeries(filter.get('hidden_legend_keys')), - funnel_aggregate_by_hogql=filter.get("funnel_aggregate_by_hogql"), + funnelAggregateByHogQL=filter.get("funnel_aggregate_by_hogql"), ), } elif _insight_type(filter) == "RETENTION": 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 a617886e5193c..ca72ea039c6fd 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 @@ -1354,26 +1354,26 @@ def test_funnels_filter(self): self.assertEqual( query.funnelsFilter, FunnelsFilter( - funnel_viz_type=FunnelVizType.steps, - funnel_from_step=1, - funnel_to_step=2, - funnel_window_interval_unit=FunnelConversionWindowTimeUnit.hour, - funnel_window_interval=13, - breakdown_attribution_type=BreakdownAttributionType.step, - breakdown_attribution_value=2, - funnel_order_type=StepOrderValue.strict, + funnelVizType=FunnelVizType.steps, + funnelFromStep=1, + funnelToStep=2, + funnelWindowIntervalUnit=FunnelConversionWindowTimeUnit.hour, + funnelWindowInterval=13, + breakdownAttributionType=BreakdownAttributionType.step, + breakdownAttributionValue=2, + funnelOrderType=StepOrderValue.strict, exclusions=[ FunnelExclusion( id="$pageview", type=EntityType.events, order=0, name="$pageview", - funnel_from_step=1, - funnel_to_step=2, + funnelFromStep=1, + funnelToStep=2, ) ], - bin_count=15, - funnel_aggregate_by_hogql="person_id", + binCount=15, + funnelAggregateByHogQL="person_id", # funnel_step_reference=FunnelStepReference.previous, ), ) diff --git a/posthog/schema.py b/posthog/schema.py index 000ca36fe8ac3..65dc7cd0d3dcf 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -211,6 +211,20 @@ class FunnelConversionWindowTimeUnit(str, Enum): class FunnelExclusion(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + custom_name: Optional[str] = None + funnelFromStep: Optional[float] = None + funnelToStep: Optional[float] = None + id: Optional[Union[str, float]] = None + index: Optional[float] = None + name: Optional[str] = None + order: Optional[float] = None + type: Optional[EntityType] = None + + +class FunnelExclusionLegacy(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -956,13 +970,33 @@ class FeaturePropertyFilter(BaseModel): class FunnelsFilter(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + binCount: Optional[Union[float, str]] = None + breakdownAttributionType: Optional[BreakdownAttributionType] = None + breakdownAttributionValue: Optional[float] = None + exclusions: Optional[List[FunnelExclusion]] = None + funnelAggregateByHogQL: Optional[str] = None + funnelFromStep: Optional[float] = None + funnelOrderType: Optional[StepOrderValue] = None + funnelStepReference: Optional[FunnelStepReference] = None + funnelToStep: Optional[float] = None + funnelVizType: Optional[FunnelVizType] = None + funnelWindowInterval: Optional[float] = None + funnelWindowIntervalUnit: Optional[FunnelConversionWindowTimeUnit] = None + hidden_legend_breakdowns: Optional[List[str]] = None + layout: Optional[FunnelLayout] = None + + +class FunnelsFilterLegacy(BaseModel): model_config = ConfigDict( extra="forbid", ) bin_count: Optional[Union[float, str]] = None breakdown_attribution_type: Optional[BreakdownAttributionType] = None breakdown_attribution_value: Optional[float] = None - exclusions: Optional[List[FunnelExclusion]] = None + exclusions: Optional[List[FunnelExclusionLegacy]] = None funnel_aggregate_by_hogql: Optional[str] = None funnel_from_step: Optional[float] = None funnel_order_type: Optional[StepOrderValue] = None