diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 757097fa12025..5c90b368f203b 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -5236,6 +5236,9 @@ "experiment_id": { "type": "integer" }, + "funnels_query": { + "$ref": "#/definitions/FunnelsQuery" + }, "kind": { "const": "ExperimentFunnelsQuery", "type": "string" @@ -5246,12 +5249,9 @@ }, "response": { "$ref": "#/definitions/ExperimentFunnelsQueryResponse" - }, - "source": { - "$ref": "#/definitions/FunnelsQuery" } }, - "required": ["experiment_id", "kind", "source"], + "required": ["experiment_id", "funnels_query", "kind"], "type": "object" }, "ExperimentFunnelsQueryResponse": { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 8fb58464d3ce4..1f9ad49a66200 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -1651,7 +1651,7 @@ export type CachedExperimentFunnelsQueryResponse = CachedQueryResponse { kind: NodeKind.ExperimentFunnelsQuery - source: FunnelsQuery + funnels_query: FunnelsQuery experiment_id: integer } diff --git a/frontend/src/scenes/experiments/experimentLogic.tsx b/frontend/src/scenes/experiments/experimentLogic.tsx index 4db270269a634..3e365140b96d5 100644 --- a/frontend/src/scenes/experiments/experimentLogic.tsx +++ b/frontend/src/scenes/experiments/experimentLogic.tsx @@ -827,9 +827,42 @@ export const experimentLogic = kea([ }, ], secondaryMetricResults: [ - null as SecondaryMetricResults[] | null, + null as + | SecondaryMetricResults[] + | (CachedExperimentTrendsQueryResponse | CachedExperimentFunnelsQueryResponse)[] + | null, { - loadSecondaryMetricResults: async (refresh?: boolean) => { + loadSecondaryMetricResults: async ( + refresh?: boolean + ): Promise< + | SecondaryMetricResults[] + | (CachedExperimentTrendsQueryResponse | CachedExperimentFunnelsQueryResponse)[] + | null + > => { + if (values.featureFlags[FEATURE_FLAGS.EXPERIMENTS_HOGQL]) { + const secondaryMetrics = + values.experiment?.metrics?.filter((metric) => metric.type === 'secondary') || [] + + return (await Promise.all( + secondaryMetrics.map(async (metric) => { + try { + const response: ExperimentResults = await api.create( + `api/projects/${values.currentTeamId}/query`, + { query: metric.query } + ) + + return { + ...response, + fakeInsightId: Math.random().toString(36).substring(2, 15), + last_refresh: response.last_refresh || '', + } + } catch (error) { + return {} + } + }) + )) as unknown as (CachedExperimentTrendsQueryResponse | CachedExperimentFunnelsQueryResponse)[] + } + const refreshParam = refresh ? '&refresh=true' : '' return await Promise.all( @@ -846,6 +879,7 @@ export const experimentLogic = kea([ last_refresh: secResults.last_refresh, } } + return { ...secResults.result, fakeInsightId: Math.random().toString(36).substring(2, 15), @@ -1255,9 +1289,10 @@ export const experimentLogic = kea([ | CachedExperimentTrendsQueryResponse | CachedExperimentFunnelsQueryResponse | null, - variant: string + variant: string, + type: 'primary' | 'secondary' = 'primary' ): number | null => { - const usingMathAggregationType = experimentMathAggregationForTrends() + const usingMathAggregationType = type === 'primary' ? experimentMathAggregationForTrends() : false if (!experimentResults || !experimentResults.insight) { return null } @@ -1392,15 +1427,31 @@ export const experimentLogic = kea([ }, ], tabularSecondaryMetricResults: [ - (s) => [s.experiment, s.secondaryMetricResults], - (experiment, secondaryMetricResults): TabularSecondaryMetricResults[] => { + (s) => [s.experiment, s.secondaryMetricResults, s.conversionRateForVariant, s.countDataForVariant], + ( + experiment, + secondaryMetricResults, + conversionRateForVariant, + countDataForVariant + ): TabularSecondaryMetricResults[] => { + if (!secondaryMetricResults) { + return [] + } + const variantsWithResults: TabularSecondaryMetricResults[] = [] experiment?.parameters?.feature_flag_variants?.forEach((variant) => { const metricResults: SecondaryMetricResult[] = [] experiment?.secondary_metrics?.forEach((metric, idx) => { + let result + if (metric.filters.insight === InsightType.FUNNELS) { + result = conversionRateForVariant(secondaryMetricResults?.[idx], variant.key) + } else { + result = countDataForVariant(secondaryMetricResults?.[idx], variant.key, 'secondary') + } + metricResults.push({ insightType: metric.filters.insight || InsightType.TRENDS, - result: secondaryMetricResults?.[idx]?.result?.[variant.key], + result: result || undefined, }) }) diff --git a/posthog/hogql_queries/experiments/experiment_funnels_query_runner.py b/posthog/hogql_queries/experiments/experiment_funnels_query_runner.py index 23ba38cd742a3..acb648172d287 100644 --- a/posthog/hogql_queries/experiments/experiment_funnels_query_runner.py +++ b/posthog/hogql_queries/experiments/experiment_funnels_query_runner.py @@ -16,6 +16,7 @@ ExperimentFunnelsQueryResponse, ExperimentSignificanceCode, ExperimentVariantFunnelsBaseStats, + FunnelsFilter, FunnelsQuery, FunnelsQueryResponse, InsightDateRange, @@ -46,6 +47,11 @@ def calculate(self) -> ExperimentFunnelsQueryResponse: self._validate_event_variants(funnels_result) + # Filter results to only include valid variants in the first step + funnels_result.results = [ + result for result in funnels_result.results if result[0]["breakdown_value"][0] in self.variants + ] + # Statistical analysis control_variant, test_variants = self._get_variants_with_base_stats(funnels_result) probabilities = calculate_probabilities(control_variant, test_variants) @@ -76,8 +82,8 @@ def _prepare_funnel_query(self) -> FunnelsQuery: 2. Configure the breakdown to use the feature flag key, which allows us to separate results for different experiment variants. """ - # Clone the source query - prepared_funnels_query = FunnelsQuery(**self.query.source.model_dump()) + # Clone the funnels query + prepared_funnels_query = FunnelsQuery(**self.query.funnels_query.model_dump()) # Set the date range to match the experiment's duration, using the project's timezone if self.team.timezone: @@ -100,6 +106,10 @@ def _prepare_funnel_query(self) -> FunnelsQuery: breakdown_type="event", ) + prepared_funnels_query.funnelsFilter = FunnelsFilter( + funnelVizType="steps", + ) + return prepared_funnels_query def _get_variants_with_base_stats( @@ -180,4 +190,4 @@ def _validate_event_variants(self, funnels_result: FunnelsQueryResponse): raise ValidationError(detail=json.dumps(errors)) def to_query(self) -> ast.SelectQuery: - raise ValueError(f"Cannot convert source query of type {self.query.source.kind} to query") + raise ValueError(f"Cannot convert source query of type {self.query.funnels_query.kind} to query") diff --git a/posthog/hogql_queries/experiments/test/test_experiment_funnels_query_runner.py b/posthog/hogql_queries/experiments/test/test_experiment_funnels_query_runner.py index 005fe82e089ae..9d4963bb59824 100644 --- a/posthog/hogql_queries/experiments/test/test_experiment_funnels_query_runner.py +++ b/posthog/hogql_queries/experiments/test/test_experiment_funnels_query_runner.py @@ -69,7 +69,7 @@ def test_query_runner(self): experiment_query = ExperimentFunnelsQuery( experiment_id=experiment.id, kind="ExperimentFunnelsQuery", - source=funnels_query, + funnels_query=funnels_query, ) experiment.metrics = [{"type": "primary", "query": experiment_query.model_dump()}] @@ -130,7 +130,7 @@ def test_query_runner_standard_flow(self): experiment_query = ExperimentFunnelsQuery( experiment_id=experiment.id, kind="ExperimentFunnelsQuery", - source=funnels_query, + funnels_query=funnels_query, ) experiment.metrics = [{"type": "primary", "query": experiment_query.model_dump()}] @@ -213,7 +213,7 @@ def test_validate_event_variants_no_events(self): experiment_query = ExperimentFunnelsQuery( experiment_id=experiment.id, kind="ExperimentFunnelsQuery", - source=funnels_query, + funnels_query=funnels_query, ) query_runner = ExperimentFunnelsQueryRunner(query=experiment_query, team=self.team) @@ -255,7 +255,7 @@ def test_validate_event_variants_no_control(self): experiment_query = ExperimentFunnelsQuery( experiment_id=experiment.id, kind="ExperimentFunnelsQuery", - source=funnels_query, + funnels_query=funnels_query, ) query_runner = ExperimentFunnelsQueryRunner(query=experiment_query, team=self.team) @@ -297,7 +297,7 @@ def test_validate_event_variants_no_test(self): experiment_query = ExperimentFunnelsQuery( experiment_id=experiment.id, kind="ExperimentFunnelsQuery", - source=funnels_query, + funnels_query=funnels_query, ) query_runner = ExperimentFunnelsQueryRunner(query=experiment_query, team=self.team) @@ -341,7 +341,7 @@ def test_validate_event_variants_no_flag_info(self): experiment_query = ExperimentFunnelsQuery( experiment_id=experiment.id, kind="ExperimentFunnelsQuery", - source=funnels_query, + funnels_query=funnels_query, ) query_runner = ExperimentFunnelsQueryRunner(query=experiment_query, team=self.team) diff --git a/posthog/schema.py b/posthog/schema.py index 199a19f1fdfe3..c3fd4b9cda736 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -6183,12 +6183,12 @@ class ExperimentFunnelsQuery(BaseModel): extra="forbid", ) experiment_id: int + funnels_query: FunnelsQuery kind: Literal["ExperimentFunnelsQuery"] = "ExperimentFunnelsQuery" modifiers: Optional[HogQLQueryModifiers] = Field( default=None, description="Modifiers used when performing the query" ) response: Optional[ExperimentFunnelsQueryResponse] = None - source: FunnelsQuery class FunnelCorrelationQuery(BaseModel):