diff --git a/ee/api/billing.py b/ee/api/billing.py index 8b7691be5e50a..7fe7e4942b586 100644 --- a/ee/api/billing.py +++ b/ee/api/billing.py @@ -275,6 +275,20 @@ def purchase_credits(self, request: Request, *args: Any, **kwargs: Any) -> HttpR res = billing_manager.purchase_credits(organization, request.data) return Response(res, status=status.HTTP_200_OK) + @action(methods=["POST"], detail=False, url_path="trials/activate") + def activate_trial(self, request: Request, *args: Any, **kwargs: Any) -> HttpResponse: + organization = self._get_org_required() + billing_manager = self.get_billing_manager() + res = billing_manager.activate_trial(organization, request.data) + return Response(res, status=status.HTTP_200_OK) + + @action(methods=["POST"], detail=False, url_path="trials/cancel") + def cancel_trial(self, request: Request, *args: Any, **kwargs: Any) -> HttpResponse: + organization = self._get_org_required() + billing_manager = self.get_billing_manager() + res = billing_manager.cancel_trial(organization, request.data) + return Response(res, status=status.HTTP_200_OK) + @action(methods=["POST"], detail=False, url_path="activate/authorize") def authorize(self, request: Request, *args: Any, **kwargs: Any) -> HttpResponse: license = get_cached_instance_license() diff --git a/ee/billing/billing_manager.py b/ee/billing/billing_manager.py index f28029bcaac97..18d3d3fbbeed4 100644 --- a/ee/billing/billing_manager.py +++ b/ee/billing/billing_manager.py @@ -378,6 +378,26 @@ def purchase_credits(self, organization: Organization, data: dict[str, Any]): return res.json() + def activate_trial(self, organization: Organization, data: dict[str, Any]): + res = requests.post( + f"{BILLING_SERVICE_URL}/api/trials/activate", + headers=self.get_auth_headers(organization), + json=data, + ) + + handle_billing_service_error(res) + + return res.json() + + def cancel_trial(self, organization: Organization, data: dict[str, Any]): + res = requests.post( + f"{BILLING_SERVICE_URL}/api/trials/cancel", + headers=self.get_auth_headers(organization), + json=data, + ) + + handle_billing_service_error(res) + def authorize(self, organization: Organization): res = requests.post( f"{BILLING_SERVICE_URL}/api/activate/authorize", diff --git a/ee/clickhouse/views/experiment_saved_metrics.py b/ee/clickhouse/views/experiment_saved_metrics.py index 0e8d40c0e9243..9dc2fcd94e073 100644 --- a/ee/clickhouse/views/experiment_saved_metrics.py +++ b/ee/clickhouse/views/experiment_saved_metrics.py @@ -1,3 +1,4 @@ +import pydantic from rest_framework import serializers, viewsets from rest_framework.exceptions import ValidationError @@ -5,6 +6,7 @@ from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.models.experiment import ExperimentSavedMetric, ExperimentToSavedMetric +from posthog.schema import FunnelsQuery, TrendsQuery class ExperimentToSavedMetricSerializer(serializers.ModelSerializer): @@ -47,6 +49,21 @@ class Meta: def validate_query(self, value): if not value: raise ValidationError("Query is required to create a saved metric") + + metric_query = value + + if metric_query.get("kind") not in ["TrendsQuery", "FunnelsQuery"]: + raise ValidationError("Metric query kind must be 'TrendsQuery' or 'FunnelsQuery'") + + # pydantic models are used to validate the query + try: + if metric_query["kind"] == "TrendsQuery": + TrendsQuery(**metric_query) + else: + FunnelsQuery(**metric_query) + except pydantic.ValidationError as e: + raise ValidationError(str(e.errors())) from e + return value def create(self, validated_data): diff --git a/ee/clickhouse/views/experiments.py b/ee/clickhouse/views/experiments.py index f043e98e8071c..98771897e1f14 100644 --- a/ee/clickhouse/views/experiments.py +++ b/ee/clickhouse/views/experiments.py @@ -31,6 +31,7 @@ from posthog.constants import INSIGHT_TRENDS from posthog.models.experiment import Experiment, ExperimentHoldout, ExperimentSavedMetric from posthog.models.filters.filter import Filter +from posthog.schema import ExperimentFunnelsQuery, ExperimentTrendsQuery from posthog.utils import generate_cache_key, get_safe_cache EXPERIMENT_RESULTS_CACHE_DEFAULT_TTL = 60 * 60 # 1 hour @@ -232,6 +233,40 @@ def validate_saved_metrics_ids(self, value): return value + def validate_metrics(self, value): + # TODO: This isn't correct most probably, we wouldn't have experiment_id inside ExperimentTrendsQuery + # on creation. Not sure how this is supposed to work yet. + if not value: + return value + + if not isinstance(value, list): + raise ValidationError("Metrics must be a list") + + if len(value) > 10: + raise ValidationError("Experiments can have a maximum of 10 metrics") + + for metric in value: + if not isinstance(metric, dict): + raise ValidationError("Metrics must be objects") + if not metric.get("query"): + raise ValidationError("Metric query is required") + + if metric.get("type") not in ["primary", "secondary"]: + raise ValidationError("Metric type must be 'primary' or 'secondary'") + + metric_query = metric["query"] + + if metric_query.get("kind") not in ["ExperimentTrendsQuery", "ExperimentFunnelsQuery"]: + raise ValidationError("Metric query kind must be 'ExperimentTrendsQuery' or 'ExperimentFunnelsQuery'") + + # pydantic models are used to validate the query + if metric_query["kind"] == "ExperimentTrendsQuery": + ExperimentTrendsQuery(**metric_query) + else: + ExperimentFunnelsQuery(**metric_query) + + return value + def validate_parameters(self, value): if not value: return value diff --git a/ee/clickhouse/views/test/test_clickhouse_experiments.py b/ee/clickhouse/views/test/test_clickhouse_experiments.py index e804af4a36d85..38a15cdbd87a7 100644 --- a/ee/clickhouse/views/test/test_clickhouse_experiments.py +++ b/ee/clickhouse/views/test/test_clickhouse_experiments.py @@ -315,7 +315,7 @@ def test_saved_metrics(self): { "name": "Test Experiment saved metric", "description": "Test description", - "query": {"events": [{"id": "$pageview"}]}, + "query": {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageview"}]}, }, ) @@ -325,9 +325,7 @@ def test_saved_metrics(self): self.assertEqual(response.json()["description"], "Test description") self.assertEqual( response.json()["query"], - { - "events": [{"id": "$pageview"}], - }, + {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageview"}]}, ) self.assertEqual(response.json()["created_by"]["id"], self.user.pk) @@ -364,7 +362,9 @@ def test_saved_metrics(self): self.assertEqual(experiment_to_saved_metric.metadata, {"type": "secondary"}) saved_metric = Experiment.objects.get(pk=exp_id).saved_metrics.first() self.assertEqual(saved_metric.id, saved_metric_id) - self.assertEqual(saved_metric.query, {"events": [{"id": "$pageview"}]}) + self.assertEqual( + saved_metric.query, {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageview"}]} + ) # Now try updating experiment with new saved metric response = self.client.post( @@ -372,9 +372,7 @@ def test_saved_metrics(self): { "name": "Test Experiment saved metric 2", "description": "Test description 2", - "query": { - "events": [{"id": "$pageleave"}], - }, + "query": {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageleave"}]}, }, ) @@ -460,7 +458,7 @@ def test_validate_saved_metrics_payload(self): { "name": "Test Experiment saved metric", "description": "Test description", - "query": {"events": [{"id": "$pageview"}]}, + "query": {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageview"}]}, }, ) diff --git a/ee/clickhouse/views/test/test_experiment_saved_metrics.py b/ee/clickhouse/views/test/test_experiment_saved_metrics.py index 79750d75745d3..51ef8242614ac 100644 --- a/ee/clickhouse/views/test/test_experiment_saved_metrics.py +++ b/ee/clickhouse/views/test/test_experiment_saved_metrics.py @@ -9,13 +9,91 @@ def test_can_list_experiment_saved_metrics(self): response = self.client.get(f"/api/projects/{self.team.id}/experiment_saved_metrics/") self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_validation_of_query_metric(self): + response = self.client.post( + f"/api/projects/{self.team.id}/experiment_saved_metrics/", + data={ + "name": "Test Experiment saved metric", + "description": "Test description", + "query": {}, + }, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json()["detail"], "Query is required to create a saved metric") + + response = self.client.post( + f"/api/projects/{self.team.id}/experiment_saved_metrics/", + data={ + "name": "Test Experiment saved metric", + "description": "Test description", + "query": {"not-kind": "ExperimentTrendsQuery"}, + }, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json()["detail"], "Metric query kind must be 'TrendsQuery' or 'FunnelsQuery'") + + response = self.client.post( + f"/api/projects/{self.team.id}/experiment_saved_metrics/", + data={ + "name": "Test Experiment saved metric", + "description": "Test description", + "query": {"kind": "not-ExperimentTrendsQuery"}, + }, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json()["detail"], "Metric query kind must be 'TrendsQuery' or 'FunnelsQuery'") + + response = self.client.post( + f"/api/projects/{self.team.id}/experiment_saved_metrics/", + data={ + "name": "Test Experiment saved metric", + "description": "Test description", + "query": {"kind": "ExperimentTrendsQuery"}, + }, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json()["detail"], "Metric query kind must be 'TrendsQuery' or 'FunnelsQuery'") + + response = self.client.post( + f"/api/projects/{self.team.id}/experiment_saved_metrics/", + data={ + "name": "Test Experiment saved metric", + "description": "Test description", + "query": {"kind": "TrendsQuery"}, + }, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertTrue("'loc': ('series',), 'msg': 'Field required'" in response.json()["detail"]) + + response = self.client.post( + f"/api/projects/{self.team.id}/experiment_saved_metrics/", + data={ + "name": "Test Experiment saved metric", + "description": "Test description", + "query": {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageview"}]}, + }, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def test_create_update_experiment_saved_metrics(self) -> None: response = self.client.post( f"/api/projects/{self.team.id}/experiment_saved_metrics/", data={ "name": "Test Experiment saved metric", "description": "Test description", - "query": {"events": [{"id": "$pageview"}]}, + "query": {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageview"}]}, }, format="json", ) @@ -26,7 +104,7 @@ def test_create_update_experiment_saved_metrics(self) -> None: self.assertEqual(response.json()["description"], "Test description") self.assertEqual( response.json()["query"], - {"events": [{"id": "$pageview"}]}, + {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageview"}]}, ) self.assertEqual(response.json()["created_by"]["id"], self.user.pk) @@ -63,7 +141,9 @@ def test_create_update_experiment_saved_metrics(self) -> None: self.assertEqual(experiment_to_saved_metric.metadata, {"type": "secondary"}) saved_metric = Experiment.objects.get(pk=exp_id).saved_metrics.first() self.assertEqual(saved_metric.id, saved_metric_id) - self.assertEqual(saved_metric.query, {"events": [{"id": "$pageview"}]}) + self.assertEqual( + saved_metric.query, {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageview"}]} + ) # Now try updating saved metric response = self.client.patch( @@ -71,7 +151,7 @@ def test_create_update_experiment_saved_metrics(self) -> None: { "name": "Test Experiment saved metric 2", "description": "Test description 2", - "query": {"events": [{"id": "$pageleave"}]}, + "query": {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageleave"}]}, }, ) @@ -79,7 +159,7 @@ def test_create_update_experiment_saved_metrics(self) -> None: self.assertEqual(response.json()["name"], "Test Experiment saved metric 2") self.assertEqual( response.json()["query"], - {"events": [{"id": "$pageleave"}]}, + {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageleave"}]}, ) # make sure experiment in question was updated as well @@ -88,11 +168,7 @@ def test_create_update_experiment_saved_metrics(self) -> None: self.assertEqual(saved_metric.id, saved_metric_id) self.assertEqual( saved_metric.query, - { - "events": [ - {"id": "$pageleave"}, - ] - }, + {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageleave"}]}, ) self.assertEqual(saved_metric.name, "Test Experiment saved metric 2") self.assertEqual(saved_metric.description, "Test description 2") @@ -110,7 +186,7 @@ def test_invalid_create(self): f"/api/projects/{self.team.id}/experiment_saved_metrics/", data={ "name": None, # invalid - "query": {"events": [{"id": "$pageview"}]}, + "query": {"kind": "TrendsQuery", "series": [{"kind": "EventsNode", "event": "$pageview"}]}, }, format="json", ) diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--dark.png index 8857278ca2573..039473eb307dd 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light.png index 87356fa9f2e2b..aa8524427af0c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-other-billing--billing--dark.png b/frontend/__snapshots__/scenes-other-billing--billing--dark.png index 001bea8f2daab..1f8a330ec0c16 100644 Binary files a/frontend/__snapshots__/scenes-other-billing--billing--dark.png and b/frontend/__snapshots__/scenes-other-billing--billing--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-billing--billing--light.png b/frontend/__snapshots__/scenes-other-billing--billing--light.png index 71fc9742561df..f2ab57943a816 100644 Binary files a/frontend/__snapshots__/scenes-other-billing--billing--light.png and b/frontend/__snapshots__/scenes-other-billing--billing--light.png differ diff --git a/frontend/__snapshots__/scenes-other-billing--billing-with-credit-cta--dark.png b/frontend/__snapshots__/scenes-other-billing--billing-with-credit-cta--dark.png index 957b54b65e78a..505db4406be40 100644 Binary files a/frontend/__snapshots__/scenes-other-billing--billing-with-credit-cta--dark.png and b/frontend/__snapshots__/scenes-other-billing--billing-with-credit-cta--dark.png differ diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index f61fc539c2fe2..b971755535e1c 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -225,6 +225,7 @@ export const FEATURE_FLAGS = { EXPERIMENTS_HOLDOUTS: 'experiments-holdouts', // owner: @jurajmajerik #team-experiments MESSAGING: 'messaging', // owner @mariusandra #team-cdp SESSION_REPLAY_URL_BLOCKLIST: 'session-replay-url-blocklist', // owner: @richard-better #team-replay + BILLING_TRIAL_FLOW: 'billing-trial-flow', // owner: @zach DEAD_CLICKS_AUTOCAPTURE: 'dead-clicks-autocapture', // owner: @pauldambra #team-replay } as const export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS] diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index 27e54bb9eee7f..23bc258fa0dc2 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -62,6 +62,10 @@ export const humanizeBytes = (fileSizeInBytes: number | null): string => { return convertedBytes.toFixed(2) + ' ' + byteUnits[i] } +export function toSentenceCase(str: string): string { + return str.replace(/\b\w/g, (c) => c.toUpperCase()) +} + export function toParams(obj: Record, explodeArrays: boolean = false): string { if (!obj) { return '' diff --git a/frontend/src/scenes/billing/Billing.tsx b/frontend/src/scenes/billing/Billing.tsx index 75038e5fbd1ba..fd06e8a67859d 100644 --- a/frontend/src/scenes/billing/Billing.tsx +++ b/frontend/src/scenes/billing/Billing.tsx @@ -11,7 +11,7 @@ import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' -import { humanFriendlyCurrency } from 'lib/utils' +import { humanFriendlyCurrency, toSentenceCase } from 'lib/utils' import { useEffect } from 'react' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { SceneExport } from 'scenes/sceneTypes' @@ -108,9 +108,13 @@ export function Billing(): JSX.Element { )} - {billing?.free_trial_until ? ( - - You are currently on a free trial until {billing.free_trial_until.format('LL')} + {billing?.trial ? ( + + You are currently on a free trial for {toSentenceCase(billing.trial.target)} plan until{' '} + {dayjs(billing.trial.expires_at).format('LL')}. At the end of the trial{' '} + {billing.trial.type === 'autosubscribe' + ? 'you will be automatically subscribed to the plan.' + : 'you will be asked to subscribe. If you choose not to, you will lose access to the features.'} ) : null} diff --git a/frontend/src/scenes/billing/BillingProductAddon.tsx b/frontend/src/scenes/billing/BillingProductAddon.tsx index df746aba9add9..918538cf31ce8 100644 --- a/frontend/src/scenes/billing/BillingProductAddon.tsx +++ b/frontend/src/scenes/billing/BillingProductAddon.tsx @@ -1,9 +1,12 @@ import { IconCheckCircle, IconPlus } from '@posthog/icons' -import { LemonButton, LemonSelectOptions, LemonTag, Link, Tooltip } from '@posthog/lemon-ui' +import { LemonButton, LemonModal, LemonSelectOptions, LemonTag, Link, Tooltip } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' +import { supportLogic } from 'lib/components/Support/supportLogic' +import { FEATURE_FLAGS, UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' +import { dayjs } from 'lib/dayjs' import { More } from 'lib/lemon-ui/LemonButton/More' -import { humanFriendlyCurrency } from 'lib/utils' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { humanFriendlyCurrency, toSentenceCase } from 'lib/utils' import { ReactNode, useMemo, useRef } from 'react' import { getProductIcon } from 'scenes/products/Products' @@ -31,12 +34,25 @@ const formatFlatRate = (flatRate: number, unit: string | null): string | ReactNo export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonType }): JSX.Element => { const productRef = useRef(null) const { billing, redirectPath, billingError, timeTotalInSeconds, timeRemainingInSeconds } = useValues(billingLogic) - const { isPricingModalOpen, currentAndUpgradePlans, surveyID, billingProductLoading } = useValues( - billingProductLogic({ product: addon, productRef }) - ) - const { toggleIsPricingModalOpen, reportSurveyShown, setSurveyResponse, initiateProductUpgrade } = useActions( - billingProductLogic({ product: addon }) - ) + const { + isPricingModalOpen, + currentAndUpgradePlans, + surveyID, + billingProductLoading, + trialModalOpen, + trialLoading, + } = useValues(billingProductLogic({ product: addon, productRef })) + const { + toggleIsPricingModalOpen, + reportSurveyShown, + setSurveyResponse, + initiateProductUpgrade, + setTrialModalOpen, + activateTrial, + } = useActions(billingProductLogic({ product: addon })) + const { openSupportForm } = useActions(supportLogic) + + const { featureFlags } = useValues(featureFlagLogic) const upgradePlan = currentAndUpgradePlans?.upgradePlan @@ -70,6 +86,21 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp addon.type === 'enhanced_persons' && addon.plans?.find((plan) => plan.plan_key === 'addon-20240404-og-customers') + const trialExperiment = featureFlags[FEATURE_FLAGS.BILLING_TRIAL_FLOW] + + const handleTrialActivation = (): void => { + if (trialExperiment === 'modal') { + // Modal - Show trial modal (default behavior) + setTrialModalOpen(true) + } else if (trialExperiment === 'control') { + // Direct - Activate trial immediately + activateTrial() + } else { + // No trial flow even without the feature flag + initiateProductUpgrade(addon, currentAndUpgradePlans?.upgradePlan, redirectPath) + } + } + return (
) + ) : billing?.trial?.target === addon.type ? ( +
+ + You are currently on a free trial for{' '} + {toSentenceCase(billing.trial.target)} until{' '} + {dayjs(billing.trial.expires_at).format('LL')}. At the end of the + trial{' '} + {billing.trial.type === 'autosubscribe' + ? 'you will be automatically subscribed to the plan.' + : 'you will be asked to subscribe. If you choose not to, you will lose access to the features.'} +

+ } + > + }> + You're on a trial for this add-on + +
+ {/* Comment out until we can make sure a customer can't activate a trial multiple times */} + {/* + Cancel trial + */} +
) : addon.included_with_main_product ? ( }> Included with plan @@ -152,9 +213,16 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp <> {currentAndUpgradePlans?.upgradePlan?.flat_rate ? (

- - {formatFlatRate(Number(upgradePlan?.unit_amount_usd), upgradePlan?.unit)} - + {addon.trial ? ( + {addon.trial.length} day free trial + ) : ( + + {formatFlatRate( + Number(upgradePlan?.unit_amount_usd), + upgradePlan?.unit + )} + + )}

) : ( )} - {!addon.inclusion_only && ( - } - size="small" - disableClientSideRouting - disabledReason={ - (billingError && billingError.message) || - (billing?.subscription_level === 'free' && 'Upgrade to add add-ons') - } - loading={billingProductLoading === addon.type} - onClick={() => - initiateProductUpgrade( - addon, - currentAndUpgradePlans?.upgradePlan, - redirectPath - ) - } - > - Add - - )} + {!addon.inclusion_only && + (addon.trial && !!trialExperiment ? ( + } + size="small" + disableClientSideRouting + disabledReason={ + (billingError && billingError.message) || + (billing?.subscription_level === 'free' && 'Upgrade to add add-ons') + } + loading={billingProductLoading === addon.type} + onClick={handleTrialActivation} + > + Start trial + + ) : ( + } + size="small" + disableClientSideRouting + disabledReason={ + (billingError && billingError.message) || + (billing?.subscription_level === 'free' && 'Upgrade to add add-ons') + } + loading={billingProductLoading === addon.type} + onClick={() => + initiateProductUpgrade( + addon, + currentAndUpgradePlans?.upgradePlan, + redirectPath + ) + } + > + Add + + ))} )}
- {!addon.inclusion_only && isProrated && !addon.contact_support && ( + {!addon.inclusion_only && !addon.trial && isProrated && !addon.contact_support && (

Pay ~${prorationAmount} today (prorated) and
@@ -199,6 +283,12 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp thereafter.

)} + {!!addon.trial && !billing?.trial && ( +

+ You'll have {addon.trial.length} days to try it out. Then you'll be charged{' '} + {formatFlatRate(Number(upgradePlan?.unit_amount_usd), upgradePlan?.unit)}. +

+ )}
@@ -237,6 +327,57 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp } /> {surveyID && } + setTrialModalOpen(false)} + title={`Start your ${addon.name} trial`} + description={`You'll have ${addon.trial?.length} days to try it out before being charged.`} + footer={ + <> + setTrialModalOpen(false)}> + Cancel + + + Start trial + + + } + > +

Here's some stuff about the trial:

+
    +
  • + 🎉 It's free! +
  • +
  • + 📅 The trial is for {addon.trial?.length} days +
  • +
  • + 🚀 You'll get access to all the features of the plan immediately +
  • +
  • + 📧 3 days before the trial ends, you'll be emailed a reminder that you'll be charged +
  • +
  • + 🚫 If you don't want to be charged, you can cancel anytime before the trial ends +
  • +
  • + 💵 At the end of the trial, you'll be be subscribed and charged{' '} + {formatFlatRate(Number(upgradePlan?.unit_amount_usd), upgradePlan?.unit)} +
  • +
  • + ☎️ If you have any questions, you can{' '} + { + setTrialModalOpen(false) + openSupportForm({ kind: 'support', target_area: 'billing' }) + }} + className="cursor-pointer" + > + contact us + +
  • +
+
) } diff --git a/frontend/src/scenes/billing/billingProductLogic.ts b/frontend/src/scenes/billing/billingProductLogic.ts index a2b3d9718fa46..9a4d54afea84d 100644 --- a/frontend/src/scenes/billing/billingProductLogic.ts +++ b/frontend/src/scenes/billing/billingProductLogic.ts @@ -1,6 +1,7 @@ -import { LemonDialog } from '@posthog/lemon-ui' +import { LemonDialog, lemonToast } from '@posthog/lemon-ui' import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { forms } from 'kea-forms' +import api from 'lib/api' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import posthog from 'posthog-js' import React from 'react' @@ -37,6 +38,7 @@ export const billingProductLogic = kea([ [ 'updateBillingLimits', 'updateBillingLimitsSuccess', + 'loadBilling', 'loadBillingSuccess', 'deactivateProduct', 'setProductSpecificAlert', @@ -75,6 +77,10 @@ export const billingProductLogic = kea([ products, redirectPath, }), + activateTrial: true, + cancelTrial: true, + setTrialModalOpen: (isOpen: boolean) => ({ isOpen }), + setTrialLoading: (loading: boolean) => ({ loading }), setUnsubscribeModalStep: (step: number) => ({ step }), resetUnsubscribeModalStep: true, setHedgehogSatisfied: (satisfied: boolean) => ({ satisfied }), @@ -156,6 +162,18 @@ export const billingProductLogic = kea([ toggleIsPlanComparisonModalOpen: (_, { highlightedFeatureKey }) => highlightedFeatureKey || null, }, ], + trialModalOpen: [ + false, + { + setTrialModalOpen: (_, { isOpen }) => isOpen, + }, + ], + trialLoading: [ + false, + { + setTrialLoading: (_, { loading }) => loading, + }, + ], unsubscribeModalStep: [ 1 as number, { @@ -349,6 +367,35 @@ export const billingProductLogic = kea([ redirectPath && `&redirect_path=${redirectPath}` }` }, + activateTrial: async () => { + actions.setTrialLoading(true) + try { + await api.create(`api/billing/trials/activate`, { + type: 'autosubscribe', + target: props.product.type, + }) + lemonToast.success('Your trial has been activated!') + } catch (e) { + lemonToast.error('There was an error activating your trial. Please try again or contact support.') + } finally { + actions.loadBilling() + actions.setTrialLoading(false) + actions.setTrialModalOpen(false) + } + }, + cancelTrial: async () => { + actions.setTrialLoading(true) + try { + await api.create(`api/billing/trials/cancel`) + lemonToast.success('Your trial has been cancelled!') + } catch (e) { + console.error(e) + lemonToast.error('There was an error cancelling your trial. Please try again or contact support.') + } finally { + actions.loadBilling() + actions.setTrialLoading(false) + } + }, triggerMoreHedgehogs: async (_, breakpoint) => { for (let i = 0; i < 5; i++) { props.hogfettiTrigger?.() diff --git a/frontend/src/types.ts b/frontend/src/types.ts index b1519d86e6f26..846b63401d2ac 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1618,6 +1618,10 @@ export interface BillingTierType { up_to: number | null } +export interface BillingTrialType { + length: number +} + export interface BillingProductV2Type { type: string usage_key: string | null @@ -1650,6 +1654,7 @@ export interface BillingProductV2Type { addons: BillingProductV2AddonType[] // addons-only: if this addon is included with the base product and not subscribed individually. for backwards compatibility. included_with_main_product?: boolean + trial?: BillingTrialType } export interface BillingProductV2AddonType { @@ -1680,6 +1685,7 @@ export interface BillingProductV2AddonType { features: BillingFeatureType[] included_if?: 'no_active_subscription' | 'has_subscription' | null usage_limit?: number | null + trial?: BillingTrialType } export interface BillingType { customer_id: string @@ -1707,6 +1713,12 @@ export interface BillingType { discount_percent?: number discount_amount_usd?: string amount_off_expires_at?: Dayjs + trial?: { + type: 'autosubscribe' | 'standard' + status: 'active' | 'expired' | 'cancelled' | 'converted' + target: 'paid' | 'teams' | 'enterprise' + expires_at: string + } } export interface BillingPlanType { diff --git a/funnel-udf/src/steps.rs b/funnel-udf/src/steps.rs index a5075ecc4e864..9330caf4e1cbd 100644 --- a/funnel-udf/src/steps.rs +++ b/funnel-udf/src/steps.rs @@ -8,6 +8,7 @@ use crate::PropVal; #[derive(Clone, Deserialize)] struct EnteredTimestamp { timestamp: f64, + excluded: bool, timings: Vec, uuids: Vec, } @@ -48,6 +49,7 @@ const MAX_REPLAY_EVENTS: usize = 10; const DEFAULT_ENTERED_TIMESTAMP: EnteredTimestamp = EnteredTimestamp { timestamp: 0.0, + excluded: false, timings: vec![], uuids: vec![], }; @@ -101,32 +103,29 @@ impl AggregateFunnelRow { let events_with_same_timestamp: Vec<_> = events_with_same_timestamp.collect(); vars.entered_timestamp[0] = EnteredTimestamp { timestamp, + excluded: false, timings: vec![], uuids: vec![], }; if events_with_same_timestamp.len() == 1 { - if !self.process_event( + self.process_event( args, &mut vars, &events_with_same_timestamp[0], prop_val, false - ) { - return; - } + ); } else if events_with_same_timestamp.iter().map(|x| &x.steps).all_equal() { // Deal with the most common case where they are all the same event (order doesn't matter) for event in events_with_same_timestamp { - if !self.process_event( + self.process_event( args, &mut vars, event, prop_val, false - ) { - return; - } + ); } } else { // Handle permutations for different events with the same timestamp @@ -144,15 +143,13 @@ impl AggregateFunnelRow { // Run exclusions, if they exist, then run matching events. for event in sorted_events { - if !self.process_event( + self.process_event( args, &mut vars, &event, &prop_val, true - ) { - return; - } + ); } } @@ -166,6 +163,11 @@ impl AggregateFunnelRow { let final_index = vars.max_step.0; let final_value = &vars.max_step.1; + if final_value.excluded { + self.results.push(Result(-1, prop_val.clone(), vec![], vec![])); + return; + } + for i in 0..final_index { //if event_uuids[i].len() >= MAX_REPLAY_EVENTS && !event_uuids[i].contains(&final_value.uuids[i]) { // Always put the actual event uuids first, we use it to extract timestamps @@ -188,7 +190,7 @@ impl AggregateFunnelRow { event: &Event, prop_val: &PropVal, processing_multiple_events: bool - ) -> bool { + ) { for step in event.steps.iter().rev() { let mut exclusion = false; let step = (if *step < 0 { @@ -199,35 +201,51 @@ impl AggregateFunnelRow { }) as usize; let in_match_window = (event.timestamp - vars.entered_timestamp[step - 1].timestamp) <= args.conversion_window_limit as f64; + let previous_step_excluded = vars.entered_timestamp[step-1].excluded; let already_reached_this_step = vars.entered_timestamp[step].timestamp == vars.entered_timestamp[step - 1].timestamp && vars.entered_timestamp[step].timestamp != 0.0; if in_match_window && !already_reached_this_step { if exclusion { - self.results.push(Result(-1, prop_val.clone(), vec![], vec![])); - return false; - } - let is_unmatched_step_attribution = self.breakdown_step.map(|breakdown_step| step == breakdown_step - 1).unwrap_or(false) && *prop_val != event.breakdown; - let already_used_event = processing_multiple_events && vars.entered_timestamp[step-1].uuids.contains(&event.uuid); - if !is_unmatched_step_attribution && !already_used_event { - vars.entered_timestamp[step] = EnteredTimestamp { - timestamp: vars.entered_timestamp[step - 1].timestamp, - timings: { - let mut timings = vars.entered_timestamp[step - 1].timings.clone(); - timings.push(event.timestamp); - timings - }, - uuids: { - let mut uuids = vars.entered_timestamp[step - 1].uuids.clone(); - uuids.push(event.uuid); - uuids - }, - }; - if vars.event_uuids[step - 1].len() < MAX_REPLAY_EVENTS - 1 { - vars.event_uuids[step - 1].push(event.uuid); + if !previous_step_excluded { + vars.entered_timestamp[step - 1].excluded = true; + if vars.max_step.0 == step - 1 { + let max_timestamp_in_match_window = (event.timestamp - vars.max_step.1.timestamp) <= args.conversion_window_limit as f64; + if max_timestamp_in_match_window { + vars.max_step.1.excluded = true; + } + } } - if step > vars.max_step.0 { - vars.max_step = (step, vars.entered_timestamp[step].clone()); + } else { + let is_unmatched_step_attribution = self.breakdown_step.map(|breakdown_step| step == breakdown_step - 1).unwrap_or(false) && *prop_val != event.breakdown; + let already_used_event = processing_multiple_events && vars.entered_timestamp[step - 1].uuids.contains(&event.uuid); + if !is_unmatched_step_attribution && !already_used_event { + let new_entered_timestamp = |vars: &Vars| -> EnteredTimestamp { + EnteredTimestamp { + timestamp: vars.entered_timestamp[step - 1].timestamp, + excluded: previous_step_excluded, + timings: { + let mut timings = vars.entered_timestamp[step - 1].timings.clone(); + timings.push(event.timestamp); + timings + }, + uuids: { + let mut uuids = vars.entered_timestamp[step - 1].uuids.clone(); + uuids.push(event.uuid); + uuids + }, + } + }; + if !previous_step_excluded { + vars.entered_timestamp[step] = new_entered_timestamp(vars); + if vars.event_uuids[step - 1].len() < MAX_REPLAY_EVENTS - 1 { + vars.event_uuids[step - 1].push(event.uuid); + } + } + + if step > vars.max_step.0 || (step == vars.max_step.0 && vars.max_step.1.excluded) { + vars.max_step = (step, new_entered_timestamp(vars)); + } } } } @@ -242,7 +260,5 @@ impl AggregateFunnelRow { } } } - - true } } \ No newline at end of file diff --git a/funnel-udf/src/trends.rs b/funnel-udf/src/trends.rs index 5f5da9a2ed953..fa7dc162c1261 100644 --- a/funnel-udf/src/trends.rs +++ b/funnel-udf/src/trends.rs @@ -16,7 +16,8 @@ where #[derive(Clone, Deserialize)] struct EnteredTimestamp { - timestamp: f64 + timestamp: f64, + excluded: bool, } #[derive(Clone, Deserialize)] @@ -40,12 +41,28 @@ struct Args { value: Vec, } +// The Exclusion enum is used to label the max step +// A full exclusion is when there has been an event, a matching exclusion, and a matching event +// A partial exclusion is when there has been an event, and a matching exclusion +#[derive(PartialEq)] +enum Exclusion { + Not, + Partial, + Full, +} + #[derive(Serialize)] struct ResultStruct(u64, i8, PropVal, Uuid); +struct MaxStep { + step: usize, + timestamp: f64, + excluded: Exclusion, + event_uuid: Uuid, +} + struct IntervalData { - max_step: usize, - max_step_event_uuid: Uuid, + max_step: MaxStep, entered_timestamp: Vec, } @@ -60,6 +77,7 @@ struct AggregateFunnelRow { const DEFAULT_ENTERED_TIMESTAMP: EnteredTimestamp = EnteredTimestamp { timestamp: 0.0, + excluded: false }; pub fn process_line(line: &str) -> Value { @@ -107,22 +125,23 @@ impl AggregateFunnelRow { for (_timestamp, events_with_same_timestamp) in &filtered_events { let events_with_same_timestamp: Vec<_> = events_with_same_timestamp.collect(); for event in events_with_same_timestamp { - if !self.process_event( + self.process_event( args, &mut vars, &event, prop_val, - ) { - return - } + ); } } - - // At this point, everything left in entered_timestamps is a failure, if it has made it to from_step - for interval_data in vars.interval_start_to_entered_timestamps.values() { - if !self.results.contains_key(&(interval_data.entered_timestamp[0].timestamp as u64)) && interval_data.max_step >= args.from_step + 1 { - self.results.insert(interval_data.entered_timestamp[0].timestamp as u64, ResultStruct(interval_data.entered_timestamp[0].timestamp as u64, -1, prop_val.clone(), interval_data.max_step_event_uuid)); + // At this point, everything left in entered_timestamps is an entry, but not an exit, if it has made it to from_step + // When there is an exclusion, we drop all partial matches and only return full matches + let fully_excluded = vars.interval_start_to_entered_timestamps.values().find(|interval_data| interval_data.max_step.excluded == Exclusion::Full); + if fully_excluded.is_none() { + for (interval_start, interval_data) in vars.interval_start_to_entered_timestamps.into_iter() { + if !self.results.contains_key(&interval_start) && interval_data.max_step.step >= args.from_step + 1 && interval_data.max_step.excluded != Exclusion::Partial { + self.results.insert(interval_start, ResultStruct(interval_start, -1, prop_val.clone(), interval_data.max_step.event_uuid)); + } } } } @@ -134,7 +153,7 @@ impl AggregateFunnelRow { vars: &mut Vars, event: &Event, prop_val: &PropVal, - ) -> bool { + ) { for step in event.steps.iter().rev() { let mut exclusion = false; let step = (if *step < 0 { @@ -145,42 +164,74 @@ impl AggregateFunnelRow { }) as usize; if step == 1 { - if !vars.interval_start_to_entered_timestamps.contains_key(&event.interval_start) && !self.results.contains_key(&event.interval_start) { - let mut entered_timestamp = vec![DEFAULT_ENTERED_TIMESTAMP.clone(); args.num_steps + 1]; - entered_timestamp[0] = EnteredTimestamp { timestamp: event.interval_start as f64 }; - entered_timestamp[1] = EnteredTimestamp { timestamp: event.timestamp }; - vars.interval_start_to_entered_timestamps.insert(event.interval_start, IntervalData { max_step: 1, max_step_event_uuid: event.uuid, entered_timestamp: entered_timestamp }); + if !self.results.contains_key(&event.interval_start) { + let entered_timestamp_one = EnteredTimestamp { timestamp: event.timestamp, excluded: false }; + let interval = vars.interval_start_to_entered_timestamps.get_mut(&event.interval_start); + if interval.is_none() || interval.as_ref().map( | interval | interval.max_step.step == 1 && interval.max_step.excluded != Exclusion::Not).unwrap() { + let mut entered_timestamp = vec![DEFAULT_ENTERED_TIMESTAMP.clone(); args.num_steps + 1]; + entered_timestamp[1] = entered_timestamp_one; + let interval_data = IntervalData { + max_step: MaxStep { + step: 1, + timestamp: event.timestamp, + excluded: Exclusion::Not, + event_uuid: event.uuid, + }, + entered_timestamp: entered_timestamp + }; + vars.interval_start_to_entered_timestamps.insert(event.interval_start, interval_data); + } else { + interval.unwrap().entered_timestamp[1] = entered_timestamp_one; + } } } else { - for interval_data in vars.interval_start_to_entered_timestamps.values_mut() { + vars.interval_start_to_entered_timestamps.retain(|&interval_start, interval_data| { let in_match_window = (event.timestamp - interval_data.entered_timestamp[step - 1].timestamp) <= args.conversion_window_limit as f64; + let previous_step_excluded = interval_data.entered_timestamp[step-1].excluded; let already_reached_this_step = interval_data.entered_timestamp[step].timestamp == interval_data.entered_timestamp[step - 1].timestamp; if in_match_window && !already_reached_this_step { if exclusion { - return false; - } - let is_unmatched_step_attribution = self.breakdown_step.map(|breakdown_step| step == breakdown_step - 1).unwrap_or(false) && *prop_val != event.breakdown; - if !is_unmatched_step_attribution { - interval_data.entered_timestamp[step] = EnteredTimestamp { - timestamp: interval_data.entered_timestamp[step - 1].timestamp - }; - // check if we have hit the goal. if we have, remove it from the list and add it to the successful_timestamps - if interval_data.entered_timestamp[args.num_steps].timestamp != 0.0 { - self.results.insert( - interval_data.entered_timestamp[0].timestamp as u64, - ResultStruct(interval_data.entered_timestamp[0].timestamp as u64, 1, prop_val.clone(), event.uuid) - ); - } else if step > interval_data.max_step { - interval_data.max_step = step; - interval_data.max_step_event_uuid = event.uuid; + if !previous_step_excluded { + interval_data.entered_timestamp[step - 1].excluded = true; + if interval_data.max_step.step == step - 1 { + let max_timestamp_in_match_window = (event.timestamp - interval_data.max_step.timestamp) <= args.conversion_window_limit as f64; + if max_timestamp_in_match_window { + interval_data.max_step.excluded = Exclusion::Partial; + } + } + } + } else { + let is_unmatched_step_attribution = self.breakdown_step.map(|breakdown_step| step == breakdown_step - 1).unwrap_or(false) && *prop_val != event.breakdown; + if !is_unmatched_step_attribution { + if !previous_step_excluded { + interval_data.entered_timestamp[step] = EnteredTimestamp { + timestamp: interval_data.entered_timestamp[step - 1].timestamp, + excluded: false, + }; + } + // check if we have hit the goal. if we have, remove it from the list and add it to the successful_timestamps + if interval_data.entered_timestamp[args.num_steps].timestamp != 0.0 { + self.results.insert( + interval_start, + ResultStruct(interval_start, 1, prop_val.clone(), event.uuid) + ); + return false; + } else if step > interval_data.max_step.step || (step == interval_data.max_step.step && interval_data.max_step.excluded == Exclusion::Partial) { + interval_data.max_step = MaxStep { + step: step, + event_uuid: event.uuid, + timestamp: event.timestamp, + excluded: if previous_step_excluded { Exclusion::Full } else { Exclusion::Not }, + }; + } } } } - } + true + }) } } // If a strict funnel, clear all of the steps that we didn't match to - // If we are processing multiple events, skip this step, because ordering makes it complicated if args.funnel_order_type == "strict" { for interval_data in vars.interval_start_to_entered_timestamps.values_mut() { for i in 1..interval_data.entered_timestamp.len() { @@ -190,6 +241,5 @@ impl AggregateFunnelRow { } } } - true } } \ No newline at end of file diff --git a/latest_migrations.manifest b/latest_migrations.manifest index bc50d0916fc37..dd3901be254da 100644 --- a/latest_migrations.manifest +++ b/latest_migrations.manifest @@ -5,7 +5,7 @@ contenttypes: 0002_remove_content_type_name ee: 0016_rolemembership_organization_member otp_static: 0002_throttling otp_totp: 0002_auto_20190420_0723 -posthog: 0505_productintent_activated_at_and_more +posthog: 0505_grouptypemapping_project sessions: 0001_initial social_django: 0010_uid_db_index two_factor: 0007_auto_20201201_1019 diff --git a/package.json b/package.json index 50ed3be1bfa3d..1f4a9a6547e7a 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "pmtiles": "^2.11.0", "postcss": "^8.4.31", "postcss-preset-env": "^9.3.0", - "posthog-js": "1.180.0", + "posthog-js": "1.180.1", "posthog-js-lite": "3.0.0", "prettier": "^2.8.8", "prop-types": "^15.7.2", diff --git a/plugin-server/functional_tests/api.ts b/plugin-server/functional_tests/api.ts index 2999170b38c9b..4df5eff0701e7 100644 --- a/plugin-server/functional_tests/api.ts +++ b/plugin-server/functional_tests/api.ts @@ -311,14 +311,14 @@ export const fetchGroups = async (teamId: number) => { return queryResult.data.map((group) => ({ ...group, group_properties: JSON.parse(group.group_properties) })) } -export const createGroupType = async (teamId: number, index: number, groupType: string) => { +export const createGroupType = async (teamId: number, projectId: number, index: number, groupType: string) => { await postgres.query( PostgresUse.COMMON_WRITE, ` - INSERT INTO posthog_grouptypemapping (team_id, group_type, group_type_index) - VALUES ($1, $2, $3) + INSERT INTO posthog_grouptypemapping (team_id, project_id, group_type, group_type_index) + VALUES ($1, $2, $3, $4) `, - [teamId, groupType, index], + [teamId, projectId, groupType, index], 'insertGroupType' ) } @@ -455,7 +455,7 @@ export const createOrganizationRaw = async (organizationProperties = {}) => { await postgres.query( PostgresUse.COMMON_WRITE, - `INSERT into posthog_organization + `INSERT into posthog_organization (${keys}) VALUES (${values}) `, diff --git a/plugin-server/functional_tests/webhooks.test.ts b/plugin-server/functional_tests/webhooks.test.ts index 4e26055c8de15..7acd5a8448bc3 100644 --- a/plugin-server/functional_tests/webhooks.test.ts +++ b/plugin-server/functional_tests/webhooks.test.ts @@ -45,7 +45,7 @@ test.concurrent(`webhooks: fires slack webhook`, async () => { }) const teamId = await createTeam(organizationId, `http://localhost:${server.address()?.port}`) const user = await createUser(teamId, new UUIDT().toString()) - await createGroupType(teamId, 0, 'organization') + await createGroupType(teamId, teamId, 0, 'organization') await createGroup(teamId, 0, 'TestWebhookOrg', { name: 'test-webhooks' }) const action = await createAction({ team_id: teamId, diff --git a/plugin-server/src/types.ts b/plugin-server/src/types.ts index 9ae7756af0fef..42d31b00adea5 100644 --- a/plugin-server/src/types.ts +++ b/plugin-server/src/types.ts @@ -620,6 +620,7 @@ export interface RawOrganization { /** Usable Team model. */ export interface Team { id: number + project_id: number uuid: string organization_id: string name: string diff --git a/plugin-server/src/utils/db/db.ts b/plugin-server/src/utils/db/db.ts index 84e8a98675ac4..04fad93de5102 100644 --- a/plugin-server/src/utils/db/db.ts +++ b/plugin-server/src/utils/db/db.ts @@ -1342,14 +1342,18 @@ export class DB { } public async getTeamsInOrganizationsWithRootPluginAccess(): Promise { - return ( - await this.postgres.query( - PostgresUse.COMMON_READ, - 'SELECT * from posthog_team WHERE organization_id = (SELECT id from posthog_organization WHERE plugins_access_level = $1)', - [OrganizationPluginsAccessLevel.ROOT], - 'getTeamsInOrganizationsWithRootPluginAccess' - ) - ).rows as Team[] + const selectResult = await this.postgres.query( + PostgresUse.COMMON_READ, + 'SELECT * from posthog_team WHERE organization_id = (SELECT id from posthog_organization WHERE plugins_access_level = $1)', + [OrganizationPluginsAccessLevel.ROOT], + 'getTeamsInOrganizationsWithRootPluginAccess' + ) + for (const row of selectResult.rows) { + // pg returns int8 as a string, since it can be larger than JS's max safe integer, + // but this is not a problem for project_id, which is a long long way from that limit. + row.project_id = parseInt(row.project_id as unknown as string) + } + return selectResult.rows } public async addOrUpdatePublicJob( diff --git a/plugin-server/src/worker/ingestion/group-type-manager.ts b/plugin-server/src/worker/ingestion/group-type-manager.ts index 7263a2429ccca..32a07e493a1bc 100644 --- a/plugin-server/src/worker/ingestion/group-type-manager.ts +++ b/plugin-server/src/worker/ingestion/group-type-manager.ts @@ -46,7 +46,11 @@ export class GroupTypeManager { } } - public async fetchGroupTypeIndex(teamId: TeamId, groupType: string): Promise { + public async fetchGroupTypeIndex( + teamId: TeamId, + projectId: TeamId, + groupType: string + ): Promise { const groupTypes = await this.fetchGroupTypes(teamId) if (groupType in groupTypes) { @@ -54,6 +58,7 @@ export class GroupTypeManager { } else { const [groupTypeIndex, isInsert] = await this.insertGroupType( teamId, + projectId, groupType, Object.keys(groupTypes).length ) @@ -70,6 +75,7 @@ export class GroupTypeManager { public async insertGroupType( teamId: TeamId, + projectId: TeamId, groupType: string, index: number ): Promise<[GroupTypeIndex | null, boolean]> { @@ -81,21 +87,21 @@ export class GroupTypeManager { PostgresUse.COMMON_WRITE, ` WITH insert_result AS ( - INSERT INTO posthog_grouptypemapping (team_id, group_type, group_type_index) - VALUES ($1, $2, $3) + INSERT INTO posthog_grouptypemapping (team_id, project_id, group_type, group_type_index) + VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING RETURNING group_type_index ) - SELECT group_type_index, 1 AS is_insert FROM insert_result + SELECT group_type_index, 1 AS is_insert FROM insert_result UNION - SELECT group_type_index, 0 AS is_insert FROM posthog_grouptypemapping WHERE team_id = $1 AND group_type = $2; + SELECT group_type_index, 0 AS is_insert FROM posthog_grouptypemapping WHERE team_id = $1 AND group_type = $3; `, - [teamId, groupType, index], + [teamId, projectId, groupType, index], 'insertGroupType' ) if (insertGroupTypeResult.rows.length == 0) { - return await this.insertGroupType(teamId, groupType, index + 1) + return await this.insertGroupType(teamId, projectId, groupType, index + 1) } const { group_type_index, is_insert } = insertGroupTypeResult.rows[0] diff --git a/plugin-server/src/worker/ingestion/groups.ts b/plugin-server/src/worker/ingestion/groups.ts index d65dd2ecabf41..0df055e7c8e6d 100644 --- a/plugin-server/src/worker/ingestion/groups.ts +++ b/plugin-server/src/worker/ingestion/groups.ts @@ -5,11 +5,12 @@ import { GroupTypeManager } from './group-type-manager' export async function addGroupProperties( teamId: TeamId, + projectId: TeamId, properties: Properties, groupTypeManager: GroupTypeManager ): Promise { for (const [groupType, groupIdentifier] of Object.entries(properties.$groups || {})) { - const columnIndex = await groupTypeManager.fetchGroupTypeIndex(teamId, groupType) + const columnIndex = await groupTypeManager.fetchGroupTypeIndex(teamId, projectId, groupType) if (columnIndex !== null) { // :TODO: Update event column instead properties[`$group_${columnIndex}`] = groupIdentifier diff --git a/plugin-server/src/worker/ingestion/process-event.ts b/plugin-server/src/worker/ingestion/process-event.ts index f03ca9d85fb04..fc53331c98eb4 100644 --- a/plugin-server/src/worker/ingestion/process-event.ts +++ b/plugin-server/src/worker/ingestion/process-event.ts @@ -155,7 +155,12 @@ export class EventsProcessor { if (this.pluginsServer.SKIP_UPDATE_EVENT_AND_PROPERTIES_STEP === false) { try { - await this.groupAndFirstEventManager.updateGroupsAndFirstEvent(team.id, event, properties) + await this.groupAndFirstEventManager.updateGroupsAndFirstEvent( + team.id, + team.project_id, + event, + properties + ) } catch (err) { Sentry.captureException(err, { tags: { team_id: team.id } }) status.warn('⚠️', 'Failed to update property definitions for an event', { @@ -168,10 +173,10 @@ export class EventsProcessor { if (processPerson) { // Adds group_0 etc values to properties - properties = await addGroupProperties(team.id, properties, this.groupTypeManager) + properties = await addGroupProperties(team.id, team.project_id, properties, this.groupTypeManager) if (event === '$groupidentify') { - await this.upsertGroup(team.id, properties, timestamp) + await this.upsertGroup(team.id, team.project_id, properties, timestamp) } } @@ -278,13 +283,18 @@ export class EventsProcessor { return [rawEvent, ack] } - private async upsertGroup(teamId: number, properties: Properties, timestamp: DateTime): Promise { + private async upsertGroup( + teamId: number, + projectId: number, + properties: Properties, + timestamp: DateTime + ): Promise { if (!properties['$group_type'] || !properties['$group_key']) { return } const { $group_type: groupType, $group_key: groupKey, $group_set: groupPropertiesToSet } = properties - const groupTypeIndex = await this.groupTypeManager.fetchGroupTypeIndex(teamId, groupType) + const groupTypeIndex = await this.groupTypeManager.fetchGroupTypeIndex(teamId, projectId, groupType) if (groupTypeIndex !== null) { await upsertGroup( diff --git a/plugin-server/src/worker/ingestion/property-definitions-manager.ts b/plugin-server/src/worker/ingestion/property-definitions-manager.ts index 98920ca7dab7e..7c3d4e8e67e31 100644 --- a/plugin-server/src/worker/ingestion/property-definitions-manager.ts +++ b/plugin-server/src/worker/ingestion/property-definitions-manager.ts @@ -27,7 +27,12 @@ export class GroupAndFirstEventManager { this.groupTypeManager = groupTypeManager } - public async updateGroupsAndFirstEvent(teamId: number, event: string, properties: Properties): Promise { + public async updateGroupsAndFirstEvent( + teamId: number, + projectId: number, + event: string, + properties: Properties + ): Promise { if (EVENTS_WITHOUT_EVENT_DEFINITION.includes(event)) { return } @@ -56,7 +61,9 @@ export class GroupAndFirstEventManager { const { $group_type: groupType, $group_set: groupPropertiesToSet } = properties if (groupType != null && groupPropertiesToSet != null) { // This "fetch" is side-effecty, it inserts a group-type and assigns an index if one isn't found - const groupPromise = this.groupTypeManager.fetchGroupTypeIndex(teamId, groupType).then(() => {}) + const groupPromise = this.groupTypeManager + .fetchGroupTypeIndex(teamId, projectId, groupType) + .then(() => {}) promises.push(groupPromise) } } diff --git a/plugin-server/src/worker/ingestion/team-manager.ts b/plugin-server/src/worker/ingestion/team-manager.ts index c050387f65869..9846071c094fd 100644 --- a/plugin-server/src/worker/ingestion/team-manager.ts +++ b/plugin-server/src/worker/ingestion/team-manager.ts @@ -154,6 +154,7 @@ export async function fetchTeam(client: PostgresRouter, teamId: Team['id']): Pro ` SELECT id, + project_id, uuid, organization_id, name, @@ -172,7 +173,13 @@ export async function fetchTeam(client: PostgresRouter, teamId: Team['id']): Pro [teamId], 'fetchTeam' ) - return selectResult.rows[0] ?? null + if (selectResult.rows.length === 0) { + return null + } + // pg returns int8 as a string, since it can be larger than JS's max safe integer, + // but this is not a problem for project_id, which is a long long way from that limit. + selectResult.rows[0].project_id = parseInt(selectResult.rows[0].project_id as unknown as string) + return selectResult.rows[0] } export async function fetchTeamByToken(client: PostgresRouter, token: string): Promise { @@ -181,6 +188,7 @@ export async function fetchTeamByToken(client: PostgresRouter, token: string): P ` SELECT id, + project_id, uuid, organization_id, name, @@ -199,7 +207,13 @@ export async function fetchTeamByToken(client: PostgresRouter, token: string): P [token], 'fetchTeamByToken' ) - return selectResult.rows[0] ?? null + if (selectResult.rows.length === 0) { + return null + } + // pg returns int8 as a string, since it can be larger than JS's max safe integer, + // but this is not a problem for project_id, which is a long long way from that limit. + selectResult.rows[0].project_id = parseInt(selectResult.rows[0].project_id as unknown as string) + return selectResult.rows[0] } export async function fetchTeamTokensWithRecordings(client: PostgresRouter): Promise> { diff --git a/plugin-server/tests/helpers/sql.ts b/plugin-server/tests/helpers/sql.ts index 5935cedb9c542..e815d008654d8 100644 --- a/plugin-server/tests/helpers/sql.ts +++ b/plugin-server/tests/helpers/sql.ts @@ -262,14 +262,16 @@ export async function createUserTeamAndOrganization( } export async function getTeams(hub: Hub): Promise { - return ( - await hub.db.postgres.query( - PostgresUse.COMMON_READ, - 'SELECT * FROM posthog_team ORDER BY id', - undefined, - 'fetchAllTeams' - ) - ).rows + const selectResult = await hub.db.postgres.query( + PostgresUse.COMMON_READ, + 'SELECT * FROM posthog_team ORDER BY id', + undefined, + 'fetchAllTeams' + ) + for (const row of selectResult.rows) { + row.project_id = parseInt(row.project_id as unknown as string) + } + return selectResult.rows } export async function getFirstTeam(hub: Hub): Promise { diff --git a/plugin-server/tests/main/db.test.ts b/plugin-server/tests/main/db.test.ts index fe4d090090acd..10e514d8323c9 100644 --- a/plugin-server/tests/main/db.test.ts +++ b/plugin-server/tests/main/db.test.ts @@ -855,6 +855,7 @@ describe('DB', () => { anonymize_ips: false, api_token: 'token1', id: teamId, + project_id: teamId, ingested_event: true, name: 'TEST PROJECT', organization_id: organizationId, @@ -884,6 +885,7 @@ describe('DB', () => { anonymize_ips: false, api_token: 'token2', id: teamId, + project_id: teamId, ingested_event: true, name: 'TEST PROJECT', organization_id: organizationId, diff --git a/plugin-server/tests/worker/ingestion/group-type-manager.test.ts b/plugin-server/tests/worker/ingestion/group-type-manager.test.ts index 5ee484a996db3..efbc630365bf1 100644 --- a/plugin-server/tests/worker/ingestion/group-type-manager.test.ts +++ b/plugin-server/tests/worker/ingestion/group-type-manager.test.ts @@ -33,8 +33,8 @@ describe('GroupTypeManager()', () => { expect(groupTypes).toEqual({}) jest.spyOn(global.Date, 'now').mockImplementation(() => new Date('2020-02-27 11:00:25').getTime()) - await groupTypeManager.insertGroupType(2, 'foo', 0) - await groupTypeManager.insertGroupType(2, 'bar', 1) + await groupTypeManager.insertGroupType(2, 2, 'foo', 0) + await groupTypeManager.insertGroupType(2, 2, 'bar', 1) jest.mocked(hub.db.postgres.query).mockClear() @@ -56,30 +56,30 @@ describe('GroupTypeManager()', () => { it('fetches group types that have been inserted', async () => { expect(await groupTypeManager.fetchGroupTypes(2)).toEqual({}) - expect(await groupTypeManager.insertGroupType(2, 'g0', 0)).toEqual([0, true]) - expect(await groupTypeManager.insertGroupType(2, 'g1', 1)).toEqual([1, true]) + expect(await groupTypeManager.insertGroupType(2, 2, 'g0', 0)).toEqual([0, true]) + expect(await groupTypeManager.insertGroupType(2, 2, 'g1', 1)).toEqual([1, true]) groupTypeManager['groupTypesCache'].clear() // Clear cache expect(await groupTypeManager.fetchGroupTypes(2)).toEqual({ g0: 0, g1: 1 }) }) it('handles conflicting by index when inserting and limits', async () => { - expect(await groupTypeManager.insertGroupType(2, 'g0', 0)).toEqual([0, true]) - expect(await groupTypeManager.insertGroupType(2, 'g1', 0)).toEqual([1, true]) - expect(await groupTypeManager.insertGroupType(2, 'g2', 0)).toEqual([2, true]) - expect(await groupTypeManager.insertGroupType(2, 'g3', 1)).toEqual([3, true]) - expect(await groupTypeManager.insertGroupType(2, 'g4', 0)).toEqual([4, true]) - expect(await groupTypeManager.insertGroupType(2, 'g5', 0)).toEqual([null, false]) - expect(await groupTypeManager.insertGroupType(2, 'g6', 0)).toEqual([null, false]) + expect(await groupTypeManager.insertGroupType(2, 2, 'g0', 0)).toEqual([0, true]) + expect(await groupTypeManager.insertGroupType(2, 2, 'g1', 0)).toEqual([1, true]) + expect(await groupTypeManager.insertGroupType(2, 2, 'g2', 0)).toEqual([2, true]) + expect(await groupTypeManager.insertGroupType(2, 2, 'g3', 1)).toEqual([3, true]) + expect(await groupTypeManager.insertGroupType(2, 2, 'g4', 0)).toEqual([4, true]) + expect(await groupTypeManager.insertGroupType(2, 2, 'g5', 0)).toEqual([null, false]) + expect(await groupTypeManager.insertGroupType(2, 2, 'g6', 0)).toEqual([null, false]) expect(await groupTypeManager.fetchGroupTypes(2)).toEqual({ g0: 0, g1: 1, g2: 2, g3: 3, g4: 4 }) }) it('handles conflict by name when inserting', async () => { - expect(await groupTypeManager.insertGroupType(2, 'group_name', 0)).toEqual([0, true]) - expect(await groupTypeManager.insertGroupType(2, 'group_name', 0)).toEqual([0, false]) - expect(await groupTypeManager.insertGroupType(2, 'group_name', 0)).toEqual([0, false]) - expect(await groupTypeManager.insertGroupType(2, 'foo', 0)).toEqual([1, true]) - expect(await groupTypeManager.insertGroupType(2, 'foo', 0)).toEqual([1, false]) + expect(await groupTypeManager.insertGroupType(2, 2, 'group_name', 0)).toEqual([0, true]) + expect(await groupTypeManager.insertGroupType(2, 2, 'group_name', 0)).toEqual([0, false]) + expect(await groupTypeManager.insertGroupType(2, 2, 'group_name', 0)).toEqual([0, false]) + expect(await groupTypeManager.insertGroupType(2, 2, 'foo', 0)).toEqual([1, true]) + expect(await groupTypeManager.insertGroupType(2, 2, 'foo', 0)).toEqual([1, false]) expect(await groupTypeManager.fetchGroupTypes(2)).toEqual({ group_name: 0, foo: 1 }) }) @@ -87,14 +87,14 @@ describe('GroupTypeManager()', () => { describe('fetchGroupTypeIndex()', () => { it('fetches an already existing value', async () => { - await groupTypeManager.insertGroupType(2, 'foo', 0) - await groupTypeManager.insertGroupType(2, 'bar', 1) + await groupTypeManager.insertGroupType(2, 2, 'foo', 0) + await groupTypeManager.insertGroupType(2, 2, 'bar', 1) jest.mocked(hub.db.postgres.query).mockClear() jest.mocked(groupTypeManager.insertGroupType).mockClear() - expect(await groupTypeManager.fetchGroupTypeIndex(2, 'foo')).toEqual(0) - expect(await groupTypeManager.fetchGroupTypeIndex(2, 'bar')).toEqual(1) + expect(await groupTypeManager.fetchGroupTypeIndex(2, 2, 'foo')).toEqual(0) + expect(await groupTypeManager.fetchGroupTypeIndex(2, 2, 'bar')).toEqual(1) expect(hub.db.postgres.query).toHaveBeenCalledTimes(1) expect(groupTypeManager.insertGroupType).toHaveBeenCalledTimes(0) @@ -102,12 +102,12 @@ describe('GroupTypeManager()', () => { }) it('inserts value if it does not exist yet at next index, resets cache', async () => { - await groupTypeManager.insertGroupType(2, 'foo', 0) + await groupTypeManager.insertGroupType(2, 2, 'foo', 0) jest.mocked(groupTypeManager.insertGroupType).mockClear() jest.mocked(hub.db.postgres.query).mockClear() - expect(await groupTypeManager.fetchGroupTypeIndex(2, 'second')).toEqual(1) + expect(await groupTypeManager.fetchGroupTypeIndex(2, 2, 'second')).toEqual(1) expect(groupTypeManager.insertGroupType).toHaveBeenCalledTimes(1) expect(hub.db.postgres.query).toHaveBeenCalledTimes(3) // FETCH + INSERT + Team lookup @@ -118,7 +118,7 @@ describe('GroupTypeManager()', () => { groupTypeIndex: 1, }) - expect(await groupTypeManager.fetchGroupTypeIndex(2, 'third')).toEqual(2) + expect(await groupTypeManager.fetchGroupTypeIndex(2, 2, 'third')).toEqual(2) jest.mocked(hub.db.postgres.query).mockClear() expect(await groupTypeManager.fetchGroupTypes(2)).toEqual({ @@ -126,8 +126,8 @@ describe('GroupTypeManager()', () => { second: 1, third: 2, }) - expect(await groupTypeManager.fetchGroupTypeIndex(2, 'second')).toEqual(1) - expect(await groupTypeManager.fetchGroupTypeIndex(2, 'third')).toEqual(2) + expect(await groupTypeManager.fetchGroupTypeIndex(2, 2, 'second')).toEqual(1) + expect(await groupTypeManager.fetchGroupTypeIndex(2, 2, 'third')).toEqual(2) expect(hub.db.postgres.query).toHaveBeenCalledTimes(1) }) @@ -135,8 +135,8 @@ describe('GroupTypeManager()', () => { it('handles raciness for inserting a new group', async () => { expect(await groupTypeManager.fetchGroupTypes(2)).toEqual({}) - await groupTypeManager.insertGroupType(2, 'foo', 0) // Emulate another thread inserting foo - expect(await groupTypeManager.fetchGroupTypeIndex(2, 'second')).toEqual(1) + await groupTypeManager.insertGroupType(2, 2, 'foo', 0) // Emulate another thread inserting foo + expect(await groupTypeManager.fetchGroupTypeIndex(2, 2, 'second')).toEqual(1) expect(await groupTypeManager.fetchGroupTypes(2)).toEqual({ foo: 0, second: 1, @@ -147,10 +147,10 @@ describe('GroupTypeManager()', () => { expect(await groupTypeManager.fetchGroupTypes(2)).toEqual({}) // Emulate another thread inserting group types - await groupTypeManager.insertGroupType(2, 'foo', 0) - await groupTypeManager.insertGroupType(2, 'bar', 0) + await groupTypeManager.insertGroupType(2, 2, 'foo', 0) + await groupTypeManager.insertGroupType(2, 2, 'bar', 0) - expect(await groupTypeManager.fetchGroupTypeIndex(2, 'bar')).toEqual(1) + expect(await groupTypeManager.fetchGroupTypeIndex(2, 2, 'bar')).toEqual(1) expect(await groupTypeManager.fetchGroupTypes(2)).toEqual({ foo: 0, bar: 1, @@ -158,13 +158,13 @@ describe('GroupTypeManager()', () => { }) it('returns null once limit is met', async () => { - await groupTypeManager.insertGroupType(2, 'g0', 0) - await groupTypeManager.insertGroupType(2, 'g1', 1) - await groupTypeManager.insertGroupType(2, 'g2', 2) - await groupTypeManager.insertGroupType(2, 'g3', 3) - await groupTypeManager.insertGroupType(2, 'g4', 4) + await groupTypeManager.insertGroupType(2, 2, 'g0', 0) + await groupTypeManager.insertGroupType(2, 2, 'g1', 1) + await groupTypeManager.insertGroupType(2, 2, 'g2', 2) + await groupTypeManager.insertGroupType(2, 2, 'g3', 3) + await groupTypeManager.insertGroupType(2, 2, 'g4', 4) - expect(await groupTypeManager.fetchGroupTypeIndex(2, 'new')).toEqual(null) + expect(await groupTypeManager.fetchGroupTypeIndex(2, 2, 'new')).toEqual(null) expect(await groupTypeManager.fetchGroupTypes(2)).toEqual({ g0: 0, g1: 1, diff --git a/plugin-server/tests/worker/ingestion/groups.test.ts b/plugin-server/tests/worker/ingestion/groups.test.ts index c9aad702ab72b..263d92eab4f2b 100644 --- a/plugin-server/tests/worker/ingestion/groups.test.ts +++ b/plugin-server/tests/worker/ingestion/groups.test.ts @@ -10,12 +10,12 @@ describe('addGroupProperties()', () => { foobar: null, } mockGroupTypeManager = { - fetchGroupTypeIndex: jest.fn().mockImplementation((teamId, key) => lookup[key]), + fetchGroupTypeIndex: jest.fn().mockImplementation((teamId, projectId, key) => lookup[key]), } }) it('does nothing if no group properties', async () => { - expect(await addGroupProperties(2, { foo: 'bar' }, mockGroupTypeManager)).toEqual({ foo: 'bar' }) + expect(await addGroupProperties(2, 2, { foo: 'bar' }, mockGroupTypeManager)).toEqual({ foo: 'bar' }) expect(mockGroupTypeManager.fetchGroupTypeIndex).not.toHaveBeenCalled() }) @@ -30,7 +30,7 @@ describe('addGroupProperties()', () => { }, } - expect(await addGroupProperties(2, properties, mockGroupTypeManager)).toEqual({ + expect(await addGroupProperties(2, 2, properties, mockGroupTypeManager)).toEqual({ foo: 'bar', $groups: { organization: 'PostHog', @@ -41,8 +41,8 @@ describe('addGroupProperties()', () => { $group_1: 'web', }) - expect(mockGroupTypeManager.fetchGroupTypeIndex).toHaveBeenCalledWith(2, 'organization') - expect(mockGroupTypeManager.fetchGroupTypeIndex).toHaveBeenCalledWith(2, 'project') - expect(mockGroupTypeManager.fetchGroupTypeIndex).toHaveBeenCalledWith(2, 'foobar') + expect(mockGroupTypeManager.fetchGroupTypeIndex).toHaveBeenCalledWith(2, 2, 'organization') + expect(mockGroupTypeManager.fetchGroupTypeIndex).toHaveBeenCalledWith(2, 2, 'project') + expect(mockGroupTypeManager.fetchGroupTypeIndex).toHaveBeenCalledWith(2, 2, 'foobar') }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 361398b20e98c..da4548d538096 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,8 +284,8 @@ dependencies: specifier: ^9.3.0 version: 9.3.0(postcss@8.4.31) posthog-js: - specifier: 1.180.0 - version: 1.180.0 + specifier: 1.180.1 + version: 1.180.1 posthog-js-lite: specifier: 3.0.0 version: 3.0.0 @@ -17781,8 +17781,8 @@ packages: resolution: {integrity: sha512-dyajjnfzZD1tht4N7p7iwf7nBnR1MjVaVu+MKr+7gBgA39bn28wizCIJZztZPtHy4PY0YwtSGgwfBCuG/hnHgA==} dev: false - /posthog-js@1.180.0: - resolution: {integrity: sha512-a+LTbmDUaHuskdIlRlTWWV1YOgQdfhhJJ8sSoW8+sCa+UrE8miD2B4Q2PtCj7mTcyNENu/ZR1VqkbfzIWRNWmQ==} + /posthog-js@1.180.1: + resolution: {integrity: sha512-LV65maVrpqkAh0wu32YvU7FpCSEjg6o+sZFYCs1+6tnEa9VvXuz8J6ReLiyRpJABI4j1qX/PB2jaVY5tDbLalQ==} dependencies: core-js: 3.39.0 fflate: 0.4.8 diff --git a/posthog/api/test/__snapshots__/test_api_docs.ambr b/posthog/api/test/__snapshots__/test_api_docs.ambr index 1558efeef9470..5ffc635eb9da7 100644 --- a/posthog/api/test/__snapshots__/test_api_docs.ambr +++ b/posthog/api/test/__snapshots__/test_api_docs.ambr @@ -13,7 +13,6 @@ '/home/runner/work/posthog/posthog/ee/clickhouse/views/experiment_holdouts.py: Warning [ExperimentHoldoutViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.experiment.ExperimentHoldout" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/clickhouse/views/experiment_saved_metrics.py: Warning [ExperimentSavedMetricViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.experiment.ExperimentSavedMetric" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/clickhouse/views/experiments.py: Warning [EnterpriseExperimentsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.experiment.Experiment" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/ee/clickhouse/views/groups.py: Warning [GroupsTypesViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.group_type_mapping.GroupTypeMapping" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/clickhouse/views/groups.py: Warning [GroupsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.group.group.Group" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/clickhouse/views/insights.py: Warning [EnterpriseInsightsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.insight.Insight" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/ee/clickhouse/views/person.py: Warning [EnterprisePersonViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.person.person.Person" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', diff --git a/posthog/api/test/__snapshots__/test_feature_flag.ambr b/posthog/api/test/__snapshots__/test_feature_flag.ambr index 0ba9846a238f0..451d10a005752 100644 --- a/posthog/api/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_feature_flag.ambr @@ -2248,6 +2248,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -2260,6 +2261,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -2272,6 +2274,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlation_actors_udf.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlation_actors_udf.ambr index 8deadafdab5a6..8c6788fe66107 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlation_actors_udf.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlation_actors_udf.ambr @@ -26,7 +26,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -81,7 +81,7 @@ JOIN (SELECT aggregation_target AS actor_id, matched_events_array[plus(step_reached, 1)] AS matching_events, (matched_events_array[1][1]).1 AS timestamp, nullIf((matched_events_array[2][1]).1, 0) AS final_timestamp, (matched_events_array[1][1]).1 AS first_timestamp, steps AS steps, final_timestamp, first_timestamp FROM - (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -157,7 +157,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -213,7 +213,7 @@ JOIN (SELECT aggregation_target AS actor_id, matched_events_array[plus(step_reached, 1)] AS matching_events, (matched_events_array[1][1]).1 AS timestamp, nullIf((matched_events_array[3][1]).1, 0) AS final_timestamp, (matched_events_array[1][1]).1 AS first_timestamp, steps AS steps, final_timestamp, first_timestamp FROM - (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -281,7 +281,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -339,7 +339,7 @@ FROM (SELECT aggregation_target AS actor_id, matched_events_array[plus(step_reached, 1)] AS matching_events, (matched_events_array[1][1]).1 AS timestamp, nullIf((matched_events_array[2][1]).1, 0) AS final_timestamp, (matched_events_array[1][1]).1 AS first_timestamp, steps AS steps, final_timestamp, first_timestamp FROM - (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlation_udf.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlation_udf.ambr index e26b1deb65b12..3b9dbd1c06fe2 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlation_udf.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlation_udf.ambr @@ -22,7 +22,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -74,7 +74,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -137,7 +137,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -202,7 +202,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -265,7 +265,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -323,7 +323,7 @@ FROM (SELECT aggregation_target AS actor_id, matched_events_array[plus(step_reached, 1)] AS matching_events, (matched_events_array[1][1]).1 AS timestamp, nullIf((matched_events_array[2][1]).1, 0) AS final_timestamp, (matched_events_array[1][1]).1 AS first_timestamp, steps AS steps, final_timestamp, first_timestamp FROM - (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -400,7 +400,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -458,7 +458,7 @@ FROM (SELECT aggregation_target AS actor_id, matched_events_array[plus(step_reached, 1)] AS matching_events, (matched_events_array[1][1]).1 AS timestamp, nullIf((matched_events_array[2][1]).1, 0) AS final_timestamp, (matched_events_array[1][1]).1 AS first_timestamp, steps AS steps, final_timestamp, first_timestamp FROM - (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -535,7 +535,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -593,7 +593,7 @@ FROM (SELECT aggregation_target AS actor_id, matched_events_array[plus(step_reached, 1)] AS matching_events, (matched_events_array[1][1]).1 AS timestamp, nullIf((matched_events_array[2][1]).1, 0) AS final_timestamp, (matched_events_array[1][1]).1 AS first_timestamp, steps AS steps, final_timestamp, first_timestamp FROM - (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -670,7 +670,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -728,7 +728,7 @@ FROM (SELECT aggregation_target AS actor_id, matched_events_array[plus(step_reached, 1)] AS matching_events, (matched_events_array[1][1]).1 AS timestamp, nullIf((matched_events_array[2][1]).1, 0) AS final_timestamp, (matched_events_array[1][1]).1 AS first_timestamp, steps AS steps, final_timestamp, first_timestamp FROM - (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -805,7 +805,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -870,7 +870,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -933,7 +933,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -991,7 +991,7 @@ FROM (SELECT aggregation_target AS actor_id, matched_events_array[plus(step_reached, 1)] AS matching_events, (matched_events_array[1][1]).1 AS timestamp, nullIf((matched_events_array[2][1]).1, 0) AS final_timestamp, (matched_events_array[1][1]).1 AS first_timestamp, steps AS steps, final_timestamp, first_timestamp FROM - (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1068,7 +1068,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1126,7 +1126,7 @@ FROM (SELECT aggregation_target AS actor_id, matched_events_array[plus(step_reached, 1)] AS matching_events, (matched_events_array[1][1]).1 AS timestamp, nullIf((matched_events_array[2][1]).1, 0) AS final_timestamp, (matched_events_array[1][1]).1 AS first_timestamp, steps AS steps, final_timestamp, first_timestamp FROM - (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1203,7 +1203,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1261,7 +1261,7 @@ FROM (SELECT aggregation_target AS actor_id, matched_events_array[plus(step_reached, 1)] AS matching_events, (matched_events_array[1][1]).1 AS timestamp, nullIf((matched_events_array[2][1]).1, 0) AS final_timestamp, (matched_events_array[1][1]).1 AS first_timestamp, steps AS steps, final_timestamp, first_timestamp FROM - (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1338,7 +1338,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1396,7 +1396,7 @@ FROM (SELECT aggregation_target AS actor_id, matched_events_array[plus(step_reached, 1)] AS matching_events, (matched_events_array[1][1]).1 AS timestamp, nullIf((matched_events_array[2][1]).1, 0) AS final_timestamp, (matched_events_array[1][1]).1 AS first_timestamp, steps AS steps, final_timestamp, first_timestamp FROM - (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1475,7 +1475,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1521,7 +1521,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1579,7 +1579,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1625,7 +1625,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1678,7 +1678,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1723,7 +1723,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1778,7 +1778,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1845,7 +1845,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1912,7 +1912,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1979,7 +1979,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2044,7 +2044,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2097,7 +2097,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2160,7 +2160,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2227,7 +2227,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2292,7 +2292,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2337,7 +2337,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2392,7 +2392,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2459,7 +2459,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2526,7 +2526,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2593,7 +2593,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2658,7 +2658,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2711,7 +2711,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2774,7 +2774,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2841,7 +2841,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2909,7 +2909,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -2966,7 +2966,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3020,7 +3020,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3094,7 +3094,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3168,7 +3168,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3242,7 +3242,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3318,7 +3318,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3375,7 +3375,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3431,7 +3431,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3488,7 +3488,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3542,7 +3542,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3616,7 +3616,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3690,7 +3690,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3764,7 +3764,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3840,7 +3840,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3897,7 +3897,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -3953,7 +3953,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4010,7 +4010,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4064,7 +4064,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4138,7 +4138,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4212,7 +4212,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4286,7 +4286,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4362,7 +4362,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4419,7 +4419,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4475,7 +4475,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4532,7 +4532,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4586,7 +4586,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4660,7 +4660,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4734,7 +4734,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4808,7 +4808,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4884,7 +4884,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4941,7 +4941,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -4997,7 +4997,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -5054,7 +5054,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -5108,7 +5108,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -5182,7 +5182,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -5256,7 +5256,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -5330,7 +5330,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -5406,7 +5406,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -5463,7 +5463,7 @@ first_timestamp FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_persons_udf.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_persons_udf.ambr index 9b0b5574f0e78..bfe0f81e29959 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_persons_udf.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_persons_udf.ambr @@ -9,7 +9,7 @@ matched_events_array[1] AS matching_events FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -87,7 +87,7 @@ matched_events_array[2] AS matching_events FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -165,7 +165,7 @@ matched_events_array[2] AS matching_events FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_persons_udf.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_persons_udf.ambr index 053faed42ef2b..9451ffa4690f6 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_persons_udf.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_persons_udf.ambr @@ -9,7 +9,7 @@ matched_events_array[1] AS matching_events FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'strict', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'strict', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -87,7 +87,7 @@ matched_events_array[2] AS matching_events FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'strict', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'strict', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -165,7 +165,7 @@ matched_events_array[2] AS matching_events FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'strict', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'strict', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_udf.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_udf.ambr index cbfa354980554..8c2f54558e15a 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_udf.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_udf.ambr @@ -15,7 +15,7 @@ if(ifNull(less(row_number, 25), 0), breakdown, ['Other']) AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, prop, arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'strict', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'strict', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -88,7 +88,7 @@ if(ifNull(less(row_number, 25), 0), breakdown, ['Other']) AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, prop, arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'step_1', 'strict', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'step_1', 'strict', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -168,7 +168,7 @@ if(ifNull(less(row_number, 25), 0), breakdown, ['Other']) AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, prop, arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'strict', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'strict', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -246,7 +246,7 @@ if(ifNull(less(row_number, 25), 0), breakdown, 'Other') AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, prop, arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_v2(3, 1209600, 'first_touch', 'strict', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_v3(3, 1209600, 'first_touch', 'strict', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -327,7 +327,7 @@ if(ifNull(less(row_number, 25), 0), breakdown, 'Other') AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, prop, arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_v2(3, 1209600, 'first_touch', 'strict', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_v3(3, 1209600, 'first_touch', 'strict', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -408,7 +408,7 @@ if(ifNull(less(row_number, 25), 0), breakdown, 'Other') AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, prop, arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_v2(3, 1209600, 'first_touch', 'strict', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_v3(3, 1209600, 'first_touch', 'strict', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_actors_udf.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_actors_udf.ambr index 2abc42d42537c..3070a213fe4da 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_actors_udf.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_actors_udf.ambr @@ -9,7 +9,7 @@ matching_events AS matching_events FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), toUInt64(toDateTime(toStartOfDay(timestamp), 'UTC')), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_trends_v2(0, 2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.5), 1), 0), ifNull(equals(x.5, x_before.5), isNull(x.5) + arrayJoin(aggregate_funnel_array_trends_v3(0, 2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.5), 1), 0), ifNull(equals(x.5, x_before.5), isNull(x.5) and isNull(x_before.5)), ifNull(equals(x.5, x_after.5), isNull(x.5) and isNull(x_after.5)), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) @@ -85,7 +85,7 @@ matching_events AS matching_events FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), toUInt64(toDateTime(toStartOfDay(timestamp), 'UTC')), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_trends_v2(0, 3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.5), 1), 0), ifNull(equals(x.5, x_before.5), isNull(x.5) + arrayJoin(aggregate_funnel_array_trends_v3(0, 3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.5), 1), 0), ifNull(equals(x.5, x_before.5), isNull(x.5) and isNull(x_before.5)), ifNull(equals(x.5, x_after.5), isNull(x.5) and isNull(x_after.5)), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) @@ -161,7 +161,7 @@ matching_events AS matching_events FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), toUInt64(toDateTime(toStartOfDay(timestamp), 'UTC')), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_trends_v2(0, 3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.5), 1), 0), ifNull(equals(x.5, x_before.5), isNull(x.5) + arrayJoin(aggregate_funnel_array_trends_v3(0, 3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.5), 1), 0), ifNull(equals(x.5, x_before.5), isNull(x.5) and isNull(x_before.5)), ifNull(equals(x.5, x_after.5), isNull(x.5) and isNull(x_after.5)), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_udf.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_udf.ambr index 36c13443b6ae2..90889f847b87b 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_udf.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_udf.ambr @@ -8,7 +8,7 @@ data.breakdown AS prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), toUInt64(toDateTime(toStartOfDay(timestamp), 'UTC')), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_trends_v2(0, 3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.5), 1), 0), ifNull(equals(x.5, x_before.5), isNull(x.5) + arrayJoin(aggregate_funnel_array_trends_v3(0, 3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.5), 1), 0), ifNull(equals(x.5, x_before.5), isNull(x.5) and isNull(x_before.5)), ifNull(equals(x.5, x_after.5), isNull(x.5) and isNull(x_after.5)), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) @@ -60,7 +60,7 @@ data.breakdown AS prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), toUInt64(toDateTime(toStartOfDay(timestamp), 'US/Pacific')), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_trends_v2(0, 3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.5), 1), 0), ifNull(equals(x.5, x_before.5), isNull(x.5) + arrayJoin(aggregate_funnel_array_trends_v3(0, 3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.5), 1), 0), ifNull(equals(x.5, x_before.5), isNull(x.5) and isNull(x_before.5)), ifNull(equals(x.5, x_after.5), isNull(x.5) and isNull(x_after.5)), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) @@ -112,7 +112,7 @@ data.breakdown AS prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), toUInt64(toDateTime(toStartOfWeek(timestamp, 0), 'UTC')), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_trends_v2(0, 3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.5), 1), 0), ifNull(equals(x.5, x_before.5), isNull(x.5) + arrayJoin(aggregate_funnel_array_trends_v3(0, 3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.5), 1), 0), ifNull(equals(x.5, x_before.5), isNull(x.5) and isNull(x_before.5)), ifNull(equals(x.5, x_after.5), isNull(x.5) and isNull(x_after.5)), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_udf.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_udf.ambr index 744282f8e82a7..54f2e510cdcc7 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_udf.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_udf.ambr @@ -20,7 +20,7 @@ breakdown AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 15, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 15, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -73,7 +73,7 @@ (SELECT aggregation_target AS actor_id FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 15, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 15, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -154,7 +154,7 @@ breakdown AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -233,7 +233,7 @@ breakdown AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -299,7 +299,7 @@ breakdown AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -363,7 +363,7 @@ (SELECT aggregation_target AS actor_id FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -435,7 +435,7 @@ (SELECT aggregation_target AS actor_id FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -507,7 +507,7 @@ (SELECT aggregation_target AS actor_id FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(3, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -594,7 +594,7 @@ breakdown AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -655,7 +655,7 @@ breakdown AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, [], arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', [[]], arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -713,7 +713,7 @@ if(ifNull(less(row_number, 25), 0), breakdown, ['Other']) AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, prop, arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -786,7 +786,7 @@ if(ifNull(less(row_number, 25), 0), breakdown, ['Other']) AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, prop, arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'step_1', 'ordered', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'step_1', 'ordered', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -866,7 +866,7 @@ if(ifNull(less(row_number, 25), 0), breakdown, ['Other']) AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, prop, arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1)])))) AS events_array, - arrayJoin(aggregate_funnel_array_v2(2, 1209600, 'first_touch', 'ordered', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_array_v3(2, 1209600, 'first_touch', 'ordered', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -944,7 +944,7 @@ if(ifNull(less(row_number, 25), 0), breakdown, 'Other') AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, prop, arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_v2(3, 1209600, 'first_touch', 'ordered', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_v3(3, 1209600, 'first_touch', 'ordered', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1025,7 +1025,7 @@ if(ifNull(less(row_number, 25), 0), breakdown, 'Other') AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, prop, arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_v2(3, 1209600, 'first_touch', 'ordered', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_v3(3, 1209600, 'first_touch', 'ordered', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) @@ -1106,7 +1106,7 @@ if(ifNull(less(row_number, 25), 0), breakdown, 'Other') AS final_prop FROM (SELECT arraySort(t -> t.1, groupArray(tuple(accurateCastOrNull(timestamp, 'Float64'), uuid, prop, arrayFilter(x -> ifNull(notEquals(x, 0), 1), [multiply(1, step_0), multiply(2, step_1), multiply(3, step_2)])))) AS events_array, - arrayJoin(aggregate_funnel_v2(3, 1209600, 'first_touch', 'ordered', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) + arrayJoin(aggregate_funnel_v3(3, 1209600, 'first_touch', 'ordered', groupUniqArray(prop), arrayFilter((x, x_before, x_after) -> not(and(ifNull(lessOrEquals(length(x.4), 1), 0), ifNull(equals(x.4, x_before.4), isNull(x.4) and isNull(x_before.4)), ifNull(equals(x.4, x_after.4), isNull(x.4) and isNull(x_after.4)), ifNull(equals(x.3, x_before.3), isNull(x.3) and isNull(x_before.3)), ifNull(equals(x.3, x_after.3), isNull(x.3) diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel.py b/posthog/hogql_queries/insights/funnels/test/test_funnel.py index 4b3b2e1aa2360..e3a37b78e6ef7 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel.py @@ -2049,6 +2049,161 @@ def test_funnel_exclusion_no_end_event(self): self.assertCountEqual(self._get_actor_ids_at_step(filters, 1), [person1.uuid, person4.uuid]) self.assertCountEqual(self._get_actor_ids_at_step(filters, 2), [person1.uuid]) + def test_funnel_exclusion_multiple_possible_no_end_event1(self): + journeys_for( + { + "user_one": [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 1), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 31), + }, + ], + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "steps", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(len(results), 2) + self.assertEqual(1, results[0]["count"]) + self.assertEqual(0, results[1]["count"]) + + def test_funnel_exclusion_multiple_possible_no_end_event2(self): + journeys_for( + { + "user_one": [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 31), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 32), + }, + ], + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "steps", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(len(results), 2) + self.assertEqual(1, results[0]["count"]) + self.assertEqual(0, results[1]["count"]) + + def test_funnel_exclusion_multiple_possible_no_end_event3(self): + journeys_for( + { + "user_one": [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 1), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 2), + }, + ], + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "steps", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + # There should be no events. UDF funnels returns an empty array and says "no events" + # Old style funnels returns a count of 0 + try: + self.assertEqual([], results) + except AssertionError: + self.assertEqual(len(results), 2) + self.assertEqual(0, results[0]["count"]) + self.assertEqual(0, results[1]["count"]) + @also_test_with_materialized_columns(["key"]) def test_funnel_exclusions_with_actions(self): sign_up_action = _create_action( @@ -4276,6 +4431,127 @@ def test_funnel_personless_events_are_supported(self): self.assertEqual(results[0]["count"], 1) self.assertEqual(results[1]["count"], 1) + def test_short_exclusions(self): + journeys_for( + { + "user_one": [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 1), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 1, 0, 0, 31), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 1, 0, 0), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 1, 1, 0, 29), + }, + ], + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "steps", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 30, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(1, results[1]["count"]) + self.assertEqual(29, results[1]["average_conversion_time"]) + + def test_excluded_completion(self): + events = [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + # Exclusion happens after time expires + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 11), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 12), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 13), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 1, 0, 0, 14), + }, + ] + journeys_for( + { + "user_one": events, + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "steps", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + # There should be no events. UDF funnels returns an empty array and says "no events" + # Old style funnels returns a count of 0 + try: + self.assertEqual([], results) + except AssertionError: + self.assertEqual(0, results[0]["count"]) + self.assertEqual(0, results[1]["count"]) + return TestGetFunnel diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends.py index cb1ab3e0653fd..5dc01e90fa1a4 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends.py @@ -18,6 +18,7 @@ ClickhouseTestMixin, _create_person, snapshot_clickhouse_queries, + _create_event, ) from posthog.test.test_journeys import journeys_for @@ -1623,6 +1624,724 @@ def test_parses_breakdown_correctly(self): self.assertEqual(len(results), 1) + def test_short_exclusions(self): + journeys_for( + { + "user_one": [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 1), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 1, 0, 0, 31), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 1, 0, 0), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 1, 1, 0, 29), + }, + ], + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "display": TRENDS_LINEAR, + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 30, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + "breakdown_type": "event", + "breakdown": "$browser", + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(len(results), 1) + self.assertEqual([100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], results[0]["data"]) + + def test_funnel_exclusion_no_end_event(self): + filters = { + "events": [ + {"id": "user signed up", "type": "events", "order": 0}, + {"id": "paid", "type": "events", "order": 1}, + ], + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "display": TRENDS_LINEAR, + "funnel_window_interval": 1, + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-14 00:00:00", + "exclusions": [ + { + "id": "x", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + # person 1 + _create_person(distinct_ids=["person1"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="person1", + timestamp="2021-05-01 01:00:00", + ) + _create_event( + team=self.team, + event="paid", + distinct_id="person1", + timestamp="2021-05-01 02:00:00", + ) + + # person 2 + _create_person(distinct_ids=["person2"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="person2", + timestamp="2021-05-01 03:00:00", + ) + _create_event( + team=self.team, + event="x", + distinct_id="person2", + timestamp="2021-05-01 03:30:00", + ) + _create_event( + team=self.team, + event="paid", + distinct_id="person2", + timestamp="2021-05-01 04:00:00", + ) + + # person 3 + _create_person(distinct_ids=["person3"], team_id=self.team.pk) + # should be discarded, even if nothing happened after x, since within conversion window + _create_event( + team=self.team, + event="user signed up", + distinct_id="person3", + timestamp="2021-05-01 05:00:00", + ) + _create_event( + team=self.team, + event="x", + distinct_id="person3", + timestamp="2021-05-01 06:00:00", + ) + + # person 4 - outside conversion window + _create_person(distinct_ids=["person4"], team_id=self.team.pk) + _create_event( + team=self.team, + event="user signed up", + distinct_id="person4", + timestamp="2021-05-01 07:00:00", + ) + _create_event( + team=self.team, + event="x", + distinct_id="person4", + timestamp="2021-05-02 08:00:00", + ) + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(len(results), 1) + # person2 and person3 should be excluded, person 1 and 4 should make it + self.assertEqual([50, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], results[0]["data"]) + + def test_funnel_exclusion_multiple_possible_no_end_event1(self): + journeys_for( + { + "user_one": [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 1), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 31), + }, + ], + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + self.assertEqual(1, results[0]["reached_from_step_count"]) + self.assertEqual(0, results[0]["reached_to_step_count"]) + + def test_funnel_exclusion_multiple_possible_no_end_event2(self): + journeys_for( + { + "user_one": [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 31), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 32), + }, + ], + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + self.assertEqual(1, results[0]["reached_from_step_count"]) + self.assertEqual(0, results[0]["reached_to_step_count"]) + + def test_funnel_exclusion_multiple_possible_no_end_event3(self): + journeys_for( + { + "user_one": [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 1), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 2), + }, + ], + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + self.assertEqual(0, results[0]["reached_from_step_count"]) + self.assertEqual(0, results[0]["reached_to_step_count"]) + + def test_exclusion_after_goal(self): + events = [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 1, 0, 0, 1), + }, + { + "event": "step three", + "timestamp": datetime(2021, 5, 1, 0, 0, 3), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 4), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 1, 0, 0, 5), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 6), + }, + { + "event": "step three", + "timestamp": datetime(2021, 5, 1, 0, 0, 7), + }, + ] + journeys_for( + { + "user_one": events, + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + {"id": "step three", "order": 2}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 1, + "funnel_to_step": 2, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + self.assertEqual(1, results[0]["reached_from_step_count"]) + self.assertEqual(1, results[0]["reached_to_step_count"]) + + def test_exclusion_multiday_completion_on_first_day(self): + events = [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 1, 0, 0, 1), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 2, 0, 0, 4), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 2, 0, 0, 5), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 2, 0, 0, 6), + }, + ] + journeys_for( + { + "user_one": events, + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + self.assertEqual(1, results[0]["reached_from_step_count"]) + self.assertEqual(1, results[0]["reached_to_step_count"]) + self.assertEqual(0, results[1]["reached_from_step_count"]) + self.assertEqual(0, results[1]["reached_to_step_count"]) + + def test_exclusion_multiday_completion_on_second_day(self): + events = [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 1), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 1, 0, 0, 2), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 2, 0, 0, 4), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 2, 0, 0, 6), + }, + ] + journeys_for( + { + "user_one": events, + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + self.assertEqual(0, results[0]["reached_from_step_count"]) + self.assertEqual(0, results[0]["reached_to_step_count"]) + self.assertEqual(1, results[1]["reached_from_step_count"]) + self.assertEqual(1, results[1]["reached_to_step_count"]) + + # When there is a partial match and then an exclusion, the partial match gets dropped + # When there is a full match and then an exclusion, the full match doesn't get dropped + def test_exclusion_multiday_partial_first_day_exclusion_second_day(self): + journeys_for( + { + "user_one": [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 2, 0, 0, 30), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 2, 0, 0, 31), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 2, 0, 0, 32), + }, + ], + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + self.assertEqual(0, results[0]["reached_from_step_count"]) + self.assertEqual(0, results[0]["reached_to_step_count"]) + self.assertEqual(0, results[1]["reached_from_step_count"]) + self.assertEqual(0, results[1]["reached_to_step_count"]) + + def test_exclusion_multiday_partial_first_day_open_exclusion_second_day(self): + journeys_for( + { + "user_one": [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 2, 0, 0, 30), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 2, 0, 0, 31), + }, + ], + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + self.assertEqual(1, results[0]["reached_from_step_count"]) + self.assertEqual(0, results[0]["reached_to_step_count"]) + self.assertEqual(0, results[1]["reached_from_step_count"]) + self.assertEqual(0, results[1]["reached_to_step_count"]) + + def test_open_exclusion_multiday(self): + events = [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 1, 0, 0, 1), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 2, 0, 0, 4), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 2, 0, 0, 5), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 2, 0, 0, 6), + }, + ] + journeys_for( + { + "user_one": events, + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + {"id": "step three", "order": 2}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 1, + "funnel_to_step": 2, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + self.assertEqual(1, results[0]["reached_from_step_count"]) + self.assertEqual(0, results[0]["reached_to_step_count"]) + self.assertEqual(0, results[1]["reached_from_step_count"]) + self.assertEqual(0, results[1]["reached_to_step_count"]) + + def test_excluded_completion(self): + events = [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + # Exclusion happens after time expires + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 11), + }, + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 12), + }, + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 13), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 1, 0, 0, 14), + }, + ] + journeys_for( + { + "user_one": events, + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + self.assertEqual(0, results[0]["reached_from_step_count"]) + self.assertEqual(0, results[0]["reached_to_step_count"]) + class TestFunnelTrends(BaseTestFunnelTrends): __test__ = True diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_udf.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_udf.py index 1b4f3e487003a..783bb30a53ab7 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_udf.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends_udf.py @@ -21,6 +21,7 @@ IntervalType, ) from posthog.test.base import _create_person, _create_event +from posthog.test.test_journeys import journeys_for @patch( @@ -222,3 +223,56 @@ def test_different_prop_val_in_strict_filter(self): assert len(results) == 2 assert all(data == 0 for result in results for data in result["data"]) + + # This is a change in behavior that only applies to UDFs - it seems more correct than what was happening before + # In old style UDFs, an exclusion like this would still count, even if it were outside of the match window + def test_excluded_after_time_expires(self): + events = [ + { + "event": "step one", + "timestamp": datetime.datetime(2021, 5, 1, 0, 0, 0), + }, + # Exclusion happens after time expires + { + "event": "exclusion", + "timestamp": datetime.datetime(2021, 5, 1, 0, 0, 11), + }, + { + "event": "step two", + "timestamp": datetime.datetime(2021, 5, 1, 0, 0, 12), + }, + ] + journeys_for( + { + "user_one": events, + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "trends", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + self.assertEqual(1, results[0]["reached_from_step_count"]) + self.assertEqual(0, results[0]["reached_to_step_count"]) diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_udf.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_udf.py index 2802aee785edd..285dd9d127fac 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_udf.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_udf.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import cast from unittest.mock import patch, Mock @@ -19,6 +20,7 @@ _create_event, _create_person, ) +from posthog.test.test_journeys import journeys_for from test_funnel import funnel_test_factory from posthog.hogql_queries.insights.funnels.test.conversion_time_cases import ( funnel_conversion_time_test_factory, @@ -138,6 +140,59 @@ def test_events_same_timestamp_no_exclusions(self): results = FunnelsQueryRunner(query=query, team=self.team).calculate().results self.assertEqual(1, results[-1]["count"]) + # This is a change in behavior that only applies to UDFs - it seems more correct than what was happening before + # In old style UDFs, an exclusion like this would still count, even if it were outside of the match window + def test_excluded_after_time_expires(self): + events = [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1, 0, 0, 0), + }, + # Exclusion happens after time expires + { + "event": "exclusion", + "timestamp": datetime(2021, 5, 1, 0, 0, 11), + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 1, 0, 0, 12), + }, + ] + journeys_for( + { + "user_one": events, + }, + self.team, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "funnel_viz_type": "steps", + "interval": "day", + "date_from": "2021-05-01 00:00:00", + "date_to": "2021-05-13 23:59:59", + "funnel_window_interval": 10, + "funnel_window_interval_unit": "second", + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 1}, + ], + "exclusions": [ + { + "id": "exclusion", + "type": "events", + "funnel_from_step": 0, + "funnel_to_step": 1, + } + ], + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results + + self.assertEqual(1, results[0]["count"]) + self.assertEqual(0, results[1]["count"]) + maxDiff = None diff --git a/posthog/management/commands/test_migrations_are_safe.py b/posthog/management/commands/test_migrations_are_safe.py index 6c7d832e97112..d7c593d1194d6 100644 --- a/posthog/management/commands/test_migrations_are_safe.py +++ b/posthog/management/commands/test_migrations_are_safe.py @@ -68,13 +68,13 @@ def validate_migration_sql(sql) -> bool: ) return True if ( - "CONSTRAINT" in operation_sql + " CONSTRAINT " in operation_sql # Ignore for new foreign key columns that are nullable, as their foreign key constraint does not lock - and not re.match(r"ADD COLUMN .+ NULL CONSTRAINT", operation_sql) + and not re.search(r"ADD COLUMN .+ NULL CONSTRAINT", operation_sql) and "-- existing-table-constraint-ignore" not in operation_sql and " NOT VALID" not in operation_sql - and " VALIDATE CONSTRAINT " - not in operation_sql # VALIDATE CONSTRAINT is a different, non-locking operation + # VALIDATE CONSTRAINT is a different, non-locking operation + and " VALIDATE CONSTRAINT " not in operation_sql and ( table_being_altered not in tables_created_so_far or _get_table("ALTER TABLE", operation_sql) not in new_tables # Ignore for brand-new tables @@ -85,7 +85,7 @@ def validate_migration_sql(sql) -> bool: "If adding a foreign key field, see `0415_pluginconfig_match_action` for an example of how to do this safely. " "If adding the constraint by itself, please use `AddConstraintNotValid()` of `django.contrib.postgres.operations` instead. " "See https://docs.djangoproject.com/en/4.2/ref/contrib/postgres/operations/#adding-constraints-without-enforcing-validation.\n" - "Source: `{operation_sql}`" + f"Source: `{operation_sql}`" ) return True if ( @@ -98,7 +98,7 @@ def validate_migration_sql(sql) -> bool: "If adding a foreign key field, see `0415_pluginconfig_match_action` for an example of how to do this safely. " "If adding the index by itself, please use `AddIndexConcurrently()` of `django.contrib.postgres.operations` instead. " "See https://docs.djangoproject.com/en/4.2/ref/contrib/postgres/operations/#concurrent-index-operations.\n" - "Source: `{operation_sql}`" + f"Source: `{operation_sql}`" ) return True diff --git a/posthog/migrations/0505_grouptypemapping_project.py b/posthog/migrations/0505_grouptypemapping_project.py new file mode 100644 index 0000000000000..0f0d4573dd93c --- /dev/null +++ b/posthog/migrations/0505_grouptypemapping_project.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.15 on 2024-10-15 13:32 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + atomic = False # Added to support concurrent index creation + dependencies = [ + ("posthog", "0504_add_dead_clicks_setting"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AddField( + model_name="grouptypemapping", + name="project", + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.CASCADE, to="posthog.project" + ), + ), + ], + database_operations=[ + migrations.RunSQL( + """ + ALTER TABLE "posthog_grouptypemapping" ADD COLUMN "project_id" bigint NULL CONSTRAINT "posthog_grouptypemap_project_id_239c0515_fk_posthog_p" REFERENCES "posthog_project"("id") DEFERRABLE INITIALLY DEFERRED; + SET CONSTRAINTS "posthog_grouptypemap_project_id_239c0515_fk_posthog_p" IMMEDIATE;""", + reverse_sql=""" + ALTER TABLE "posthog_grouptypemapping" DROP COLUMN IF EXISTS "project_id";""", + ), + # We add CONCURRENTLY to the create command + migrations.RunSQL( + """ + CREATE INDEX CONCURRENTLY "posthog_grouptypemapping_project_id_239c0515" ON "posthog_grouptypemapping" ("project_id");""", + reverse_sql=""" + DROP INDEX IF EXISTS "posthog_grouptypemapping_project_id_239c0515";""", + ), + ], + ), + ] diff --git a/posthog/models/group_type_mapping.py b/posthog/models/group_type_mapping.py index 8dcb9cd74f1fb..38cfe4e6e105f 100644 --- a/posthog/models/group_type_mapping.py +++ b/posthog/models/group_type_mapping.py @@ -5,6 +5,7 @@ # to add group keys class GroupTypeMapping(models.Model): team = models.ForeignKey("Team", on_delete=models.CASCADE) + project = models.ForeignKey("Project", on_delete=models.CASCADE, null=True) group_type = models.CharField(max_length=400, null=False, blank=False) group_type_index = models.IntegerField(null=False, blank=False) # Used to display in UI diff --git a/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr b/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr index fc4dd0038fedd..eed189b5adfc9 100644 --- a/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr +++ b/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr @@ -645,6 +645,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -770,6 +771,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -1949,6 +1951,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -2074,6 +2077,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -2534,6 +2538,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -2728,6 +2733,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -3201,6 +3207,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -3345,6 +3352,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -3833,6 +3841,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -3958,6 +3967,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -4510,6 +4520,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -4635,6 +4646,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -4934,6 +4946,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -5059,6 +5072,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -5527,6 +5541,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -5560,6 +5575,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -5664,6 +5680,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -6129,6 +6146,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -6327,6 +6345,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -6792,6 +6811,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -6917,6 +6937,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -6950,6 +6971,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -7398,6 +7420,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -7523,6 +7546,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", diff --git a/posthog/temporal/batch_exports/redshift_batch_export.py b/posthog/temporal/batch_exports/redshift_batch_export.py index e45e4df5cbc15..9a2ad891d2e1b 100644 --- a/posthog/temporal/batch_exports/redshift_batch_export.py +++ b/posthog/temporal/batch_exports/redshift_batch_export.py @@ -1,3 +1,4 @@ +import asyncio import collections.abc import contextlib import dataclasses @@ -7,6 +8,7 @@ import psycopg import pyarrow as pa +import structlog from psycopg import sql from temporalio import activity, workflow from temporalio.common import RetryPolicy @@ -26,8 +28,9 @@ default_fields, execute_batch_export_insert_activity, get_data_interval, - iter_model_records, + raise_on_produce_task_failure, start_batch_export_run, + start_produce_batch_export_record_batches, ) from posthog.temporal.batch_exports.metrics import get_rows_exported_metric from posthog.temporal.batch_exports.postgres_batch_export import ( @@ -36,10 +39,15 @@ PostgreSQLClient, PostgreSQLField, ) -from posthog.temporal.batch_exports.utils import JsonType, apeek_first_and_rewind, set_status_to_running_task +from posthog.temporal.batch_exports.utils import ( + JsonType, + apeek_first_and_rewind, + set_status_to_running_task, +) from posthog.temporal.common.clickhouse import get_client from posthog.temporal.common.heartbeat import Heartbeater -from posthog.temporal.common.logger import bind_temporal_worker_logger +from posthog.temporal.common.logger import configure_temporal_worker_logger +from posthog.temporal.common.utils import BatchExportHeartbeatDetails, should_resume_from_activity_heartbeat def remove_escaped_whitespace_recursive(value): @@ -221,6 +229,9 @@ def get_redshift_fields_from_record_schema( pg_schema: list[PostgreSQLField] = [] for name in record_schema.names: + if name == "_inserted_at": + continue + pa_field = record_schema.field(name) if pa.types.is_string(pa_field.type) or isinstance(pa_field.type, JsonType): @@ -261,11 +272,19 @@ def get_redshift_fields_from_record_schema( return pg_schema +@dataclasses.dataclass +class RedshiftHeartbeatDetails(BatchExportHeartbeatDetails): + """The Redshift batch export details included in every heartbeat.""" + + pass + + async def insert_records_to_redshift( - records: collections.abc.AsyncGenerator[dict[str, typing.Any], None], + records: collections.abc.AsyncGenerator[tuple[dict[str, typing.Any], dt.datetime], None], redshift_client: RedshiftClient, schema: str | None, table: str, + heartbeater: Heartbeater, batch_size: int = 100, use_super: bool = False, known_super_columns: list[str] | None = None, @@ -289,10 +308,11 @@ async def insert_records_to_redshift( make us go OOM or exceed Redshift's SQL statement size limit (16MB). Setting this too low can significantly affect performance due to Redshift's poor handling of INSERTs. """ - first_record_batch, records_iterator = await apeek_first_and_rewind(records) - if first_record_batch is None: + first_value, records_iterator = await apeek_first_and_rewind(records) + if first_value is None: return 0 + first_record_batch, _inserted_at = first_value columns = first_record_batch.keys() if schema: @@ -332,7 +352,7 @@ async def flush_to_redshift(batch): # the byte size of each batch the way things are currently written. We can revisit this # in the future if we decide it's useful enough. - async for record in records_iterator: + async for record, _inserted_at in records_iterator: for column in columns: if known_super_columns is not None and column in known_super_columns: record[column] = json.dumps(record[column], ensure_ascii=False) @@ -342,10 +362,12 @@ async def flush_to_redshift(batch): continue await flush_to_redshift(batch) + heartbeater.details = (str(_inserted_at),) batch = [] if len(batch) > 0: await flush_to_redshift(batch) + heartbeater.details = (str(_inserted_at),) return total_rows_exported @@ -378,7 +400,9 @@ async def insert_into_redshift_activity(inputs: RedshiftInsertInputs) -> Records the Redshift-specific properties_data_type to indicate the type of JSON-like fields. """ - logger = await bind_temporal_worker_logger(team_id=inputs.team_id, destination="Redshift") + logger = await configure_temporal_worker_logger( + logger=structlog.get_logger(), team_id=inputs.team_id, destination="Redshift" + ) await logger.ainfo( "Batch exporting range %s - %s to Redshift: %s.%s.%s", inputs.data_interval_start or "START", @@ -389,35 +413,72 @@ async def insert_into_redshift_activity(inputs: RedshiftInsertInputs) -> Records ) async with ( - Heartbeater(), + Heartbeater() as heartbeater, set_status_to_running_task(run_id=inputs.run_id, logger=logger), get_client(team_id=inputs.team_id) as client, ): if not await client.is_alive(): raise ConnectionError("Cannot establish connection to ClickHouse") + should_resume, details = await should_resume_from_activity_heartbeat(activity, RedshiftHeartbeatDetails, logger) + + if should_resume is True and details is not None: + data_interval_start: str | None = details.last_inserted_at.isoformat() + else: + data_interval_start = inputs.data_interval_start + model: BatchExportModel | BatchExportSchema | None = None if inputs.batch_export_schema is None and "batch_export_model" in { field.name for field in dataclasses.fields(inputs) }: model = inputs.batch_export_model - + if model is not None: + model_name = model.name + extra_query_parameters = model.schema["values"] if model.schema is not None else None + fields = model.schema["fields"] if model.schema is not None else None + else: + model_name = "events" + extra_query_parameters = None + fields = None else: model = inputs.batch_export_schema + model_name = "custom" + extra_query_parameters = model["values"] if model is not None else {} + fields = model["fields"] if model is not None else None - record_iterator = iter_model_records( + queue, produce_task = start_produce_batch_export_record_batches( client=client, - model=model, + model_name=model_name, + is_backfill=inputs.is_backfill, team_id=inputs.team_id, - interval_start=inputs.data_interval_start, + interval_start=data_interval_start, interval_end=inputs.data_interval_end, exclude_events=inputs.exclude_events, include_events=inputs.include_events, + fields=fields, destination_default_fields=redshift_default_fields(), - is_backfill=inputs.is_backfill, + extra_query_parameters=extra_query_parameters, ) - first_record_batch, record_iterator = await apeek_first_and_rewind(record_iterator) - if first_record_batch is None: + + get_schema_task = asyncio.create_task(queue.get_schema()) + await asyncio.wait( + [get_schema_task, produce_task], + return_when=asyncio.FIRST_COMPLETED, + ) + + # Finishing producing happens sequentially after putting to queue and setting the schema. + # So, either we finished producing and setting the schema tasks, or we finished without + # putting anything in the queue. + if get_schema_task.done(): + # In the first case, we'll land here. + # The schema is available, and the queue is not empty, so we can start the batch export. + record_batch_schema = get_schema_task.result() + else: + # In the second case, we'll land here: We finished producing without putting anything. + # Since we finished producing with an empty queue, there is nothing to batch export. + # We could have also failed, so we need to re-raise that exception to allow a retry if + # that's the case. + await raise_on_produce_task_failure(produce_task) return 0 known_super_columns = ["properties", "set", "set_once", "person_properties"] @@ -442,10 +503,8 @@ async def insert_into_redshift_activity(inputs: RedshiftInsertInputs) -> Records ("timestamp", "TIMESTAMP WITH TIME ZONE"), ] else: - column_names = [column for column in first_record_batch.schema.names if column != "_inserted_at"] - record_schema = first_record_batch.select(column_names).schema table_fields = get_redshift_fields_from_record_schema( - record_schema, known_super_columns=known_super_columns, use_super=properties_type == "SUPER" + record_batch_schema, known_super_columns=known_super_columns, use_super=properties_type == "SUPER" ) requires_merge = ( @@ -477,7 +536,7 @@ async def insert_into_redshift_activity(inputs: RedshiftInsertInputs) -> Records ): schema_columns = {field[0] for field in table_fields} - def map_to_record(row: dict) -> dict: + def map_to_record(row: dict) -> tuple[dict, dt.datetime]: """Map row to a record to insert to Redshift.""" record = {k: v for k, v in row.items() if k in schema_columns} @@ -486,10 +545,24 @@ def map_to_record(row: dict) -> dict: # TODO: We should be able to save a json.loads here. record[column] = remove_escaped_whitespace_recursive(json.loads(record[column])) - return record + return record, row["_inserted_at"] + + async def record_generator() -> ( + collections.abc.AsyncGenerator[tuple[dict[str, typing.Any], dt.datetime], None] + ): + while not queue.empty() or not produce_task.done(): + try: + record_batch = queue.get_nowait() + except asyncio.QueueEmpty: + if produce_task.done(): + await logger.adebug( + "Empty queue with no more events being produced, closing consumer loop" + ) + return + else: + await asyncio.sleep(0.1) + continue - async def record_generator() -> collections.abc.AsyncGenerator[dict[str, typing.Any], None]: - async for record_batch in record_iterator: for record in record_batch.to_pylist(): yield map_to_record(record) @@ -498,6 +571,7 @@ async def record_generator() -> collections.abc.AsyncGenerator[dict[str, typing. redshift_client, inputs.schema, redshift_stage_table if requires_merge else redshift_table, + heartbeater=heartbeater, use_super=properties_type == "SUPER", known_super_columns=known_super_columns, ) diff --git a/posthog/test/__snapshots__/test_feature_flag.ambr b/posthog/test/__snapshots__/test_feature_flag.ambr index 36ed6e1cce57e..fe95ff9b2ce28 100644 --- a/posthog/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/test/__snapshots__/test_feature_flag.ambr @@ -418,6 +418,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -470,6 +471,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", @@ -570,6 +572,7 @@ ''' SELECT "posthog_grouptypemapping"."id", "posthog_grouptypemapping"."team_id", + "posthog_grouptypemapping"."project_id", "posthog_grouptypemapping"."group_type", "posthog_grouptypemapping"."group_type_index", "posthog_grouptypemapping"."name_singular", diff --git a/posthog/udf_versioner.py b/posthog/udf_versioner.py index cb22ed9455be1..4b9554dba90a7 100644 --- a/posthog/udf_versioner.py +++ b/posthog/udf_versioner.py @@ -8,15 +8,15 @@ # For revertible cloud deploys: # 1. Develop using the python files at the top level of `user_scripts`, with schema defined in `docker/clickhouse/user_defined_function.xml` -# 2. If you're made breaking changes to UDFs (likely involving changing type definitions), when ready to deploy, increment the version below and run this file +# 2. If you're made any changes to UDFs, when ready to deploy, increment the version below and run this file # 3. Overwrite `user_defined_function.xml` in the `posthog-cloud-infra` repo (us, eu, and dev) with `user_scripts/latest_user_defined_function.xml` and deploy it # 4. Land a version of the posthog repo with the updated `user_scripts` folder from the new branch (make sure this PR doesn't include changes to this file with the new version) # 5. Run the `copy_udfs_to_clickhouse` action in the `posthog_cloud_infra` repo to deploy the `user_scripts` folder to clickhouse # 6. After that deploy goes out, it is safe to land and deploy the full changes to the `posthog` repo -UDF_VERSION = 2 # Last modified by: @aspicer, 2024-10-16 +UDF_VERSION = 3 # Last modified by: @aspicer, 2024-10-30 # Clean up all versions less than this -EARLIEST_UDF_VERSION = 1 +EARLIEST_UDF_VERSION = 2 CLICKHOUSE_XML_FILENAME = "user_defined_function.xml" ACTIVE_XML_CONFIG = "../../docker/clickhouse/user_defined_function.xml" diff --git a/posthog/user_scripts/aggregate_funnel_aarch64 b/posthog/user_scripts/aggregate_funnel_aarch64 index 2cf3ee037fe3a..6c490d2e58b86 100755 Binary files a/posthog/user_scripts/aggregate_funnel_aarch64 and b/posthog/user_scripts/aggregate_funnel_aarch64 differ diff --git a/posthog/user_scripts/aggregate_funnel_x86_64 b/posthog/user_scripts/aggregate_funnel_x86_64 index 030bbc0a3fe00..9570bf6d55e29 100755 Binary files a/posthog/user_scripts/aggregate_funnel_x86_64 and b/posthog/user_scripts/aggregate_funnel_x86_64 differ diff --git a/posthog/user_scripts/latest_user_defined_function.xml b/posthog/user_scripts/latest_user_defined_function.xml index c84d6f05a9722..a7da950ae23e2 100644 --- a/posthog/user_scripts/latest_user_defined_function.xml +++ b/posthog/user_scripts/latest_user_defined_function.xml @@ -1,8 +1,8 @@ - executable_pool - aggregate_funnel_v1 + aggregate_funnel_v2 Array(Tuple(Int8, Nullable(String), Array(Float64), Array(Array(UUID)))) result @@ -30,13 +30,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v1/aggregate_funnel steps + v2/aggregate_funnel steps 600 executable_pool - aggregate_funnel_cohort_v1 + aggregate_funnel_cohort_v2 Array(Tuple(Int8, UInt64, Array(Float64), Array(Array(UUID)))) result @@ -64,13 +64,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v1/aggregate_funnel steps + v2/aggregate_funnel steps 600 executable_pool - aggregate_funnel_array_v1 + aggregate_funnel_array_v2 Array(Tuple(Int8, Array(String), Array(Float64), Array(Array(UUID)))) result @@ -98,13 +98,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v1/aggregate_funnel steps + v2/aggregate_funnel steps 600 executable_pool - aggregate_funnel_test_v1 + aggregate_funnel_test_v2 String result @@ -132,14 +132,14 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v1/aggregate_funnel_test.py + v2/aggregate_funnel_test.py 600 executable_pool - aggregate_funnel_trends_v1 - Array(Tuple(UInt64, Int8, Nullable(String))) + aggregate_funnel_trends_v2 + Array(Tuple(UInt64, Int8, Nullable(String), UUID)) result UInt8 @@ -170,19 +170,19 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the prop_vals - Array(Tuple(Nullable(Float64), UInt64, Nullable(String), Array(Int8))) + Array(Tuple(Nullable(Float64), UInt64, UUID, Nullable(String), Array(Int8))) value JSONEachRow - v1/aggregate_funnel trends + v2/aggregate_funnel trends 600 executable_pool - aggregate_funnel_array_trends_v1 + aggregate_funnel_array_trends_v2 - Array(Tuple(UInt64, Int8, Array(String))) + Array(Tuple(UInt64, Int8, Array(String), UUID)) result UInt8 @@ -209,19 +209,19 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the prop_vals - Array(Tuple(Nullable(Float64), UInt64, Array(String), Array(Int8))) + Array(Tuple(Nullable(Float64), UInt64, UUID, Array(String), Array(Int8))) value JSONEachRow - v1/aggregate_funnel trends + v2/aggregate_funnel trends 600 executable_pool - aggregate_funnel_cohort_trends_v1 + aggregate_funnel_cohort_trends_v2 - Array(Tuple(UInt64, Int8, UInt64)) + Array(Tuple(UInt64, Int8, UInt64, UUID)) result UInt8 @@ -248,17 +248,17 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the prop_vals - Array(Tuple(Nullable(Float64), UInt64, UInt64, Array(Int8))) + Array(Tuple(Nullable(Float64), UInt64, UUID, UInt64, Array(Int8))) value JSONEachRow - v1/aggregate_funnel trends + v2/aggregate_funnel trends 600 executable_pool - aggregate_funnel_array_trends_test_v1 + aggregate_funnel_array_trends_test_v2 String result @@ -286,16 +286,16 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the prop_vals - Array(Tuple(Nullable(Float64), UInt64, Array(String), Array(Int8))) + Array(Tuple(Nullable(Float64), UInt64, UUID, Array(String), Array(Int8))) value JSONEachRow - v1/aggregate_funnel_array_trends_test.py + v2/aggregate_funnel_array_trends_test.py 600 executable_pool - aggregate_funnel_v2 + aggregate_funnel_v3 Array(Tuple(Int8, Nullable(String), Array(Float64), Array(Array(UUID)))) result @@ -323,13 +323,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v2/aggregate_funnel steps + v3/aggregate_funnel steps 600 executable_pool - aggregate_funnel_cohort_v2 + aggregate_funnel_cohort_v3 Array(Tuple(Int8, UInt64, Array(Float64), Array(Array(UUID)))) result @@ -357,13 +357,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v2/aggregate_funnel steps + v3/aggregate_funnel steps 600 executable_pool - aggregate_funnel_array_v2 + aggregate_funnel_array_v3 Array(Tuple(Int8, Array(String), Array(Float64), Array(Array(UUID)))) result @@ -391,13 +391,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v2/aggregate_funnel steps + v3/aggregate_funnel steps 600 executable_pool - aggregate_funnel_test_v2 + aggregate_funnel_test_v3 String result @@ -425,13 +425,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v2/aggregate_funnel_test.py + v3/aggregate_funnel_test.py 600 executable_pool - aggregate_funnel_trends_v2 + aggregate_funnel_trends_v3 Array(Tuple(UInt64, Int8, Nullable(String), UUID)) result @@ -467,13 +467,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v2/aggregate_funnel trends + v3/aggregate_funnel trends 600 executable_pool - aggregate_funnel_array_trends_v2 + aggregate_funnel_array_trends_v3 Array(Tuple(UInt64, Int8, Array(String), UUID)) result @@ -506,13 +506,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v2/aggregate_funnel trends + v3/aggregate_funnel trends 600 executable_pool - aggregate_funnel_cohort_trends_v2 + aggregate_funnel_cohort_trends_v3 Array(Tuple(UInt64, Int8, UInt64, UUID)) result @@ -545,13 +545,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v2/aggregate_funnel trends + v3/aggregate_funnel trends 600 executable_pool - aggregate_funnel_array_trends_test_v2 + aggregate_funnel_array_trends_test_v3 String result @@ -583,7 +583,7 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v2/aggregate_funnel_array_trends_test.py + v3/aggregate_funnel_array_trends_test.py 600 \ No newline at end of file diff --git a/posthog/user_scripts/v1/aggregate_funnel_aarch64 b/posthog/user_scripts/v1/aggregate_funnel_aarch64 deleted file mode 100755 index aabb3ff28f7cd..0000000000000 Binary files a/posthog/user_scripts/v1/aggregate_funnel_aarch64 and /dev/null differ diff --git a/posthog/user_scripts/v1/aggregate_funnel_x86_64 b/posthog/user_scripts/v1/aggregate_funnel_x86_64 deleted file mode 100755 index 8eb41e8979bf2..0000000000000 Binary files a/posthog/user_scripts/v1/aggregate_funnel_x86_64 and /dev/null differ diff --git a/posthog/user_scripts/v1/aggregate_funnel b/posthog/user_scripts/v3/aggregate_funnel similarity index 100% rename from posthog/user_scripts/v1/aggregate_funnel rename to posthog/user_scripts/v3/aggregate_funnel diff --git a/posthog/user_scripts/v3/aggregate_funnel_aarch64 b/posthog/user_scripts/v3/aggregate_funnel_aarch64 new file mode 100755 index 0000000000000..6c490d2e58b86 Binary files /dev/null and b/posthog/user_scripts/v3/aggregate_funnel_aarch64 differ diff --git a/posthog/user_scripts/v1/aggregate_funnel_array_trends_test.py b/posthog/user_scripts/v3/aggregate_funnel_array_trends_test.py similarity index 100% rename from posthog/user_scripts/v1/aggregate_funnel_array_trends_test.py rename to posthog/user_scripts/v3/aggregate_funnel_array_trends_test.py diff --git a/posthog/user_scripts/v1/aggregate_funnel_test.py b/posthog/user_scripts/v3/aggregate_funnel_test.py similarity index 100% rename from posthog/user_scripts/v1/aggregate_funnel_test.py rename to posthog/user_scripts/v3/aggregate_funnel_test.py diff --git a/posthog/user_scripts/v3/aggregate_funnel_x86_64 b/posthog/user_scripts/v3/aggregate_funnel_x86_64 new file mode 100755 index 0000000000000..9570bf6d55e29 Binary files /dev/null and b/posthog/user_scripts/v3/aggregate_funnel_x86_64 differ diff --git a/posthog/user_scripts/v1/user_defined_function.xml b/posthog/user_scripts/v3/user_defined_function.xml similarity index 56% rename from posthog/user_scripts/v1/user_defined_function.xml rename to posthog/user_scripts/v3/user_defined_function.xml index 42e4452293452..a7da950ae23e2 100644 --- a/posthog/user_scripts/v1/user_defined_function.xml +++ b/posthog/user_scripts/v3/user_defined_function.xml @@ -1,8 +1,8 @@ - executable_pool - aggregate_funnel + aggregate_funnel_v2 Array(Tuple(Int8, Nullable(String), Array(Float64), Array(Array(UUID)))) result @@ -30,13 +30,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - aggregate_funnel steps + v2/aggregate_funnel steps 600 executable_pool - aggregate_funnel_cohort + aggregate_funnel_cohort_v2 Array(Tuple(Int8, UInt64, Array(Float64), Array(Array(UUID)))) result @@ -64,13 +64,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - aggregate_funnel steps + v2/aggregate_funnel steps 600 executable_pool - aggregate_funnel_array + aggregate_funnel_array_v2 Array(Tuple(Int8, Array(String), Array(Float64), Array(Array(UUID)))) result @@ -98,13 +98,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - aggregate_funnel steps + v2/aggregate_funnel steps 600 executable_pool - aggregate_funnel_test + aggregate_funnel_test_v2 String result @@ -132,14 +132,14 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - aggregate_funnel_test.py + v2/aggregate_funnel_test.py 600 executable_pool - aggregate_funnel_trends - Array(Tuple(UInt64, Int8, Nullable(String))) + aggregate_funnel_trends_v2 + Array(Tuple(UInt64, Int8, Nullable(String), UUID)) result UInt8 @@ -170,19 +170,19 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the prop_vals - Array(Tuple(Nullable(Float64), UInt64, Nullable(String), Array(Int8))) + Array(Tuple(Nullable(Float64), UInt64, UUID, Nullable(String), Array(Int8))) value JSONEachRow - aggregate_funnel trends + v2/aggregate_funnel trends 600 executable_pool - aggregate_funnel_array_trends + aggregate_funnel_array_trends_v2 - Array(Tuple(UInt64, Int8, Array(String))) + Array(Tuple(UInt64, Int8, Array(String), UUID)) result UInt8 @@ -209,19 +209,19 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the prop_vals - Array(Tuple(Nullable(Float64), UInt64, Array(String), Array(Int8))) + Array(Tuple(Nullable(Float64), UInt64, UUID, Array(String), Array(Int8))) value JSONEachRow - aggregate_funnel trends + v2/aggregate_funnel trends 600 executable_pool - aggregate_funnel_cohort_trends + aggregate_funnel_cohort_trends_v2 - Array(Tuple(UInt64, Int8, UInt64)) + Array(Tuple(UInt64, Int8, UInt64, UUID)) result UInt8 @@ -248,17 +248,17 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the prop_vals - Array(Tuple(Nullable(Float64), UInt64, UInt64, Array(Int8))) + Array(Tuple(Nullable(Float64), UInt64, UUID, UInt64, Array(Int8))) value JSONEachRow - aggregate_funnel trends + v2/aggregate_funnel trends 600 executable_pool - aggregate_funnel_array_trends_test + aggregate_funnel_array_trends_test_v2 String result @@ -286,16 +286,16 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the prop_vals - Array(Tuple(Nullable(Float64), UInt64, Array(String), Array(Int8))) + Array(Tuple(Nullable(Float64), UInt64, UUID, Array(String), Array(Int8))) value JSONEachRow - aggregate_funnel_array_trends_test.py + v2/aggregate_funnel_array_trends_test.py 600 executable_pool - aggregate_funnel_v0 + aggregate_funnel_v3 Array(Tuple(Int8, Nullable(String), Array(Float64), Array(Array(UUID)))) result @@ -323,13 +323,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v0/aggregate_funnel steps + v3/aggregate_funnel steps 600 executable_pool - aggregate_funnel_cohort_v0 + aggregate_funnel_cohort_v3 Array(Tuple(Int8, UInt64, Array(Float64), Array(Array(UUID)))) result @@ -357,13 +357,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v0/aggregate_funnel steps + v3/aggregate_funnel steps 600 executable_pool - aggregate_funnel_array_v0 + aggregate_funnel_array_v3 Array(Tuple(Int8, Array(String), Array(Float64), Array(Array(UUID)))) result @@ -391,13 +391,13 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v0/aggregate_funnel steps + v3/aggregate_funnel steps 600 executable_pool - aggregate_funnel_test_v0 + aggregate_funnel_test_v3 String result @@ -425,14 +425,14 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the value JSONEachRow - v0/aggregate_funnel_test.py + v3/aggregate_funnel_test.py 600 executable_pool - aggregate_funnel_trends_v0 - Array(Tuple(UInt64, Int8, Nullable(String))) + aggregate_funnel_trends_v3 + Array(Tuple(UInt64, Int8, Nullable(String), UUID)) result UInt8 @@ -463,19 +463,19 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the prop_vals - Array(Tuple(Nullable(Float64), UInt64, Nullable(String), Array(Int8))) + Array(Tuple(Nullable(Float64), UInt64, UUID, Nullable(String), Array(Int8))) value JSONEachRow - v0/aggregate_funnel trends + v3/aggregate_funnel trends 600 executable_pool - aggregate_funnel_array_trends_v0 + aggregate_funnel_array_trends_v3 - Array(Tuple(UInt64, Int8, Array(String))) + Array(Tuple(UInt64, Int8, Array(String), UUID)) result UInt8 @@ -502,19 +502,19 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the prop_vals - Array(Tuple(Nullable(Float64), UInt64, Array(String), Array(Int8))) + Array(Tuple(Nullable(Float64), UInt64, UUID, Array(String), Array(Int8))) value JSONEachRow - v0/aggregate_funnel trends + v3/aggregate_funnel trends 600 executable_pool - aggregate_funnel_cohort_trends_v0 + aggregate_funnel_cohort_trends_v3 - Array(Tuple(UInt64, Int8, UInt64)) + Array(Tuple(UInt64, Int8, UInt64, UUID)) result UInt8 @@ -541,17 +541,17 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the prop_vals - Array(Tuple(Nullable(Float64), UInt64, UInt64, Array(Int8))) + Array(Tuple(Nullable(Float64), UInt64, UUID, UInt64, Array(Int8))) value JSONEachRow - v0/aggregate_funnel trends + v3/aggregate_funnel trends 600 executable_pool - aggregate_funnel_array_trends_test_v0 + aggregate_funnel_array_trends_test_v3 String result @@ -579,304 +579,11 @@ This file is autogenerated by udf_versioner.py. Do not edit this, only edit the prop_vals - Array(Tuple(Nullable(Float64), UInt64, Array(String), Array(Int8))) + Array(Tuple(Nullable(Float64), UInt64, UUID, Array(String), Array(Int8))) value JSONEachRow - v0/aggregate_funnel_array_trends_test.py - 600 - - - executable_pool - aggregate_funnel_v1 - Array(Tuple(Int8, Nullable(String), Array(Float64), Array(Array(UUID)))) - result - - UInt8 - num_steps - - - UInt64 - conversion_window_limit - - - String - breakdown_attribution_type - - - String - funnel_order_type - - - Array(Nullable(String)) - prop_vals - - - Array(Tuple(Nullable(Float64), UUID, Nullable(String), Array(Int8))) - value - - JSONEachRow - v1/aggregate_funnel steps - 600 - - - - executable_pool - aggregate_funnel_cohort_v1 - Array(Tuple(Int8, UInt64, Array(Float64), Array(Array(UUID)))) - result - - UInt8 - num_steps - - - UInt64 - conversion_window_limit - - - String - breakdown_attribution_type - - - String - funnel_order_type - - - Array(UInt64) - prop_vals - - - Array(Tuple(Nullable(Float64), UUID, UInt64, Array(Int8))) - value - - JSONEachRow - v1/aggregate_funnel steps - 600 - - - - executable_pool - aggregate_funnel_array_v1 - Array(Tuple(Int8, Array(String), Array(Float64), Array(Array(UUID)))) - result - - UInt8 - num_steps - - - UInt64 - conversion_window_limit - - - String - breakdown_attribution_type - - - String - funnel_order_type - - - Array(Array(String)) - prop_vals - - - Array(Tuple(Nullable(Float64), UUID, Array(String), Array(Int8))) - value - - JSONEachRow - v1/aggregate_funnel steps - 600 - - - - executable_pool - aggregate_funnel_test_v1 - String - result - - UInt8 - num_steps - - - UInt64 - conversion_window_limit - - - String - breakdown_attribution_type - - - String - funnel_order_type - - - Array(Array(String)) - prop_vals - - - Array(Tuple(Nullable(Float64), UUID, Nullable(String), Array(Int8))) - value - - JSONEachRow - v1/aggregate_funnel_test.py - 600 - - - - executable_pool - aggregate_funnel_trends_v1 - Array(Tuple(UInt64, Int8, Nullable(String))) - result - - UInt8 - from_step - - - UInt8 - num_steps - - - UInt8 - num_steps - - - UInt64 - conversion_window_limit - - - String - breakdown_attribution_type - - - String - funnel_order_type - - - Array(Nullable(String)) - prop_vals - - - Array(Tuple(Nullable(Float64), UInt64, Nullable(String), Array(Int8))) - value - - JSONEachRow - v1/aggregate_funnel trends - 600 - - - - executable_pool - aggregate_funnel_array_trends_v1 - - Array(Tuple(UInt64, Int8, Array(String))) - result - - UInt8 - from_step - - - UInt8 - num_steps - - - UInt64 - conversion_window_limit - - - String - breakdown_attribution_type - - - String - funnel_order_type - - - Array(Array(String)) - prop_vals - - - Array(Tuple(Nullable(Float64), UInt64, Array(String), Array(Int8))) - value - - JSONEachRow - v1/aggregate_funnel trends - 600 - - - - executable_pool - aggregate_funnel_cohort_trends_v1 - - Array(Tuple(UInt64, Int8, UInt64)) - result - - UInt8 - from_step - - - UInt8 - num_steps - - - UInt64 - conversion_window_limit - - - String - breakdown_attribution_type - - - String - funnel_order_type - - - Array(UInt64) - prop_vals - - - Array(Tuple(Nullable(Float64), UInt64, UInt64, Array(Int8))) - value - - JSONEachRow - v1/aggregate_funnel trends - 600 - - - - executable_pool - aggregate_funnel_array_trends_test_v1 - String - result - - UInt8 - from_step - - - UInt8 - num_steps - - - UInt64 - conversion_window_limit - - - String - breakdown_attribution_type - - - String - funnel_order_type - - - Array(Array(String)) - prop_vals - - - Array(Tuple(Nullable(Float64), UInt64, Array(String), Array(Int8))) - value - - JSONEachRow - v1/aggregate_funnel_array_trends_test.py + v3/aggregate_funnel_array_trends_test.py 600 \ No newline at end of file