From c55d50154f9fad9189664ca5d9d0c9388cca8c6f Mon Sep 17 00:00:00 2001 From: Ben White Date: Wed, 20 Mar 2024 12:20:45 +0100 Subject: [PATCH 01/51] chore: Remove link to support (#21013) --- ...es-other-password-reset--initial--dark.png | Bin 13189 -> 14460 bytes ...s-other-password-reset--initial--light.png | Bin 23644 -> 24879 bytes ...es-other-password-reset--success--dark.png | Bin 12758 -> 14405 bytes ...s-other-password-reset--success--light.png | Bin 25197 -> 26737 bytes ...-other-password-reset--throttled--dark.png | Bin 13757 -> 13999 bytes ...other-password-reset--throttled--light.png | Bin 27101 -> 27912 bytes .../scenes/authentication/PasswordReset.tsx | 6 +----- 7 files changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/__snapshots__/scenes-other-password-reset--initial--dark.png b/frontend/__snapshots__/scenes-other-password-reset--initial--dark.png index 9f1a52a1f90fa10773ee6979a2835f99f901f4df..6cb6eb6d746fc527112c981bf39847d3bfd78fbb 100644 GIT binary patch literal 14460 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 literal 13189 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�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

ugHWUqfPdDRT#3}P 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 diff --git a/frontend/__snapshots__/scenes-other-password-reset--success--dark.png b/frontend/__snapshots__/scenes-other-password-reset--success--dark.png index fb0de961d18c11755675b7982e7b9fff7cd86676..4bfe10a123e4fdd3561c6d5679cd550c6b6def94 100644 GIT binary patch literal 14405 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 h?+MEP4Url literal 12758 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 G2~7YP!b=qZ diff --git a/frontend/__snapshots__/scenes-other-password-reset--throttled--dark.png b/frontend/__snapshots__/scenes-other-password-reset--throttled--dark.png index 04c2aa439c2b4d074e3b925be2a0e2db79d89fc2..bb0239106bde1e4d0fb54ae704fd6b9d337ddf24 100644 GIT binary patch literal 13999 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

A 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 literal 27101 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 diff --git a/frontend/src/scenes/authentication/PasswordReset.tsx b/frontend/src/scenes/authentication/PasswordReset.tsx index d1ba200bc25cc..d04b22db8bddb 100644 --- a/frontend/src/scenes/authentication/PasswordReset.tsx +++ b/frontend/src/scenes/authentication/PasswordReset.tsx @@ -151,11 +151,7 @@ function ResetThrottled(): JSX.Element { return (

There have been too many reset requests for the email {requestPasswordReset?.email || 'you typed'}. - Please try again later or{' '} - - get in touch - {' '} - if you think this has been a mistake. + Please try again later or contact support if you think this has been a mistake.
Date: Wed, 20 Mar 2024 12:51:21 +0100 Subject: [PATCH 02/51] fix: Throw on password reset errors (#21007) --- frontend/src/scenes/authentication/passwordResetLogic.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/scenes/authentication/passwordResetLogic.ts b/frontend/src/scenes/authentication/passwordResetLogic.ts index a0240f016fa36..854a24a225d48 100644 --- a/frontend/src/scenes/authentication/passwordResetLogic.ts +++ b/frontend/src/scenes/authentication/passwordResetLogic.ts @@ -1,3 +1,4 @@ +import { captureException } from '@sentry/react' import { kea, path, reducers } from 'kea' import { forms } from 'kea-forms' import { loaders } from 'kea-loaders' @@ -69,7 +70,9 @@ export const passwordResetLogic = kea([ try { await api.create('api/reset/', { email }) } catch (e: any) { - actions.setRequestPasswordResetManualErrors(e) + actions.setRequestPasswordResetManualErrors({ email: e.detail ?? 'An error occurred' }) + captureException('Failed to reset password', { extra: { error: e } }) + throw e } }, }, @@ -104,6 +107,7 @@ export const passwordResetLogic = kea([ window.location.href = '/' // We need the refresh } catch (e: any) { actions.setPasswordResetManualErrors({ password: e.detail }) + throw e } }, }, From 0c5b8cb18e68a1271d422adcdab0d62c4b403a51 Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Wed, 20 Mar 2024 12:11:29 +0000 Subject: [PATCH 03/51] fix(insights): Fix set active view when changing insight (#21034) --- .../insights/InsightNav/insightNavLogic.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx b/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx index d2f28906bfe95..9dff8c87eeb8c 100644 --- a/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx +++ b/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx @@ -1,4 +1,5 @@ import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { urlToAction } from 'kea-router' import { FEATURE_FLAGS } from 'lib/constants' import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' @@ -41,7 +42,7 @@ import { isStickinessQuery, isTrendsQuery, } from '~/queries/utils' -import { BaseMathType, InsightLogicProps, InsightType } from '~/types' +import { BaseMathType, FilterType, InsightLogicProps, InsightType } from '~/types' import { MathAvailability } from '../filters/ActionFilter/ActionFilterRow/ActionFilterRow' import type { insightNavLogicType } from './insightNavLogicType' @@ -278,6 +279,23 @@ export const insightNavLogic = kea([ } }, })), + urlToAction(({ actions }) => ({ + '/insights/:shortId(/:mode)(/:subscriptionId)': ( + _, // url params + { dashboard, ...searchParams }, // search params + { filters: _filters } // hash params + ) => { + // capture any filters from the URL, either #filters={} or ?insight=X&bla=foo&bar=baz + const filters: Partial | null = + Object.keys(_filters || {}).length > 0 ? _filters : searchParams.insight ? searchParams : null + + if (!filters?.insight) { + return + } + + actions.setActiveView(filters?.insight) + }, + })), afterMount(({ values, actions }) => { if (values.query && isInsightVizNode(values.query)) { actions.updateQueryPropertyCache(cachePropertiesFromQuery(values.query.source, values.queryPropertyCache)) From 235f7c643bb4b33c40d5420ad6794a0cb0c78db0 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Wed, 20 Mar 2024 12:52:34 +0000 Subject: [PATCH 04/51] fix: padding reset for mobile replay (#21041) * fix: padding reset for mobile replay * fix * updat snapshot --- .../__snapshots__/transform.test.ts.snap | 44 +++++++++++++++++++ .../mobile-replay/transformer/transformers.ts | 1 + 2 files changed, 45 insertions(+) diff --git a/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap b/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap index 889de23db5174..a421f7ff220bf 100644 --- a/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap +++ b/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap @@ -55,6 +55,7 @@ exports[`replay/transform transform can convert images 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -214,6 +215,7 @@ exports[`replay/transform transform can convert navigation bar 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -395,6 +397,7 @@ exports[`replay/transform transform can convert rect with text 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -551,6 +554,7 @@ exports[`replay/transform transform can convert status bar 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -718,6 +722,7 @@ exports[`replay/transform transform can ignore unknown wireframe types 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -860,6 +865,7 @@ exports[`replay/transform transform can process unknown types without error 1`] border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -1006,6 +1012,7 @@ exports[`replay/transform transform can set background image to base64 png 1`] = border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -1199,6 +1206,7 @@ exports[`replay/transform transform child wireframes are processed 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -1747,6 +1755,7 @@ exports[`replay/transform transform inputs buttons with nested elements 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -1913,6 +1922,7 @@ exports[`replay/transform transform inputs input - $inputType - hello 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -2032,6 +2042,7 @@ exports[`replay/transform transform inputs input - button - click me 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -2168,6 +2179,7 @@ exports[`replay/transform transform inputs input - checkbox - $value 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -2315,6 +2327,7 @@ exports[`replay/transform transform inputs input - checkbox - $value 2`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -2461,6 +2474,7 @@ exports[`replay/transform transform inputs input - checkbox - $value 3`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -2609,6 +2623,7 @@ exports[`replay/transform transform inputs input - checkbox - $value 4`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -2740,6 +2755,7 @@ exports[`replay/transform transform inputs input - email - $value 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -2871,6 +2887,7 @@ exports[`replay/transform transform inputs input - number - $value 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -3002,6 +3019,7 @@ exports[`replay/transform transform inputs input - password - $value 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -3133,6 +3151,7 @@ exports[`replay/transform transform inputs input - progress - $value 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -3289,6 +3308,7 @@ exports[`replay/transform transform inputs input - progress - $value 2`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -3421,6 +3441,7 @@ exports[`replay/transform transform inputs input - progress - 0.75 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -3553,6 +3574,7 @@ exports[`replay/transform transform inputs input - progress - 0.75 2`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -3685,6 +3707,7 @@ exports[`replay/transform transform inputs input - search - $value 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -3816,6 +3839,7 @@ exports[`replay/transform transform inputs input - select - hello 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -3979,6 +4003,7 @@ exports[`replay/transform transform inputs input - tel - $value 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -4111,6 +4136,7 @@ exports[`replay/transform transform inputs input - text - $value 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -4242,6 +4268,7 @@ exports[`replay/transform transform inputs input - text - hello 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -4373,6 +4400,7 @@ exports[`replay/transform transform inputs input - textArea - $value 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -4504,6 +4532,7 @@ exports[`replay/transform transform inputs input - textArea - hello 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -4635,6 +4664,7 @@ exports[`replay/transform transform inputs input - toggle - $value 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -4814,6 +4844,7 @@ exports[`replay/transform transform inputs input - toggle - $value 2`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -4993,6 +5024,7 @@ exports[`replay/transform transform inputs input - toggle - $value 3`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -5172,6 +5204,7 @@ exports[`replay/transform transform inputs input - toggle - $value 4`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -5335,6 +5368,7 @@ exports[`replay/transform transform inputs input - url - https://example.io 1`] border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -5466,6 +5500,7 @@ exports[`replay/transform transform inputs input gets 0 padding by default but c border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -6918,6 +6953,7 @@ exports[`replay/transform transform inputs placeholder - $inputType - $value 1`] border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -7053,6 +7089,7 @@ exports[`replay/transform transform inputs progress rating 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -7686,6 +7723,7 @@ exports[`replay/transform transform inputs radio group - $inputType - $value 1`] border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -7805,6 +7843,7 @@ exports[`replay/transform transform inputs radio_group - $inputType - $value 1`] border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -7934,6 +7973,7 @@ exports[`replay/transform transform inputs radio_group 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -8063,6 +8103,7 @@ exports[`replay/transform transform inputs web_view - $inputType - $value 1`] = border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -8198,6 +8239,7 @@ exports[`replay/transform transform inputs web_view with URL 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -8333,6 +8375,7 @@ exports[`replay/transform transform inputs wrapping with labels 1`] = ` border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; @@ -8480,6 +8523,7 @@ exports[`replay/transform transform omitting x and y is equivalent to setting th border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; diff --git a/ee/frontend/mobile-replay/transformer/transformers.ts b/ee/frontend/mobile-replay/transformer/transformers.ts index 96aea3e36c06e..1527a24d7dbeb 100644 --- a/ee/frontend/mobile-replay/transformer/transformers.ts +++ b/ee/frontend/mobile-replay/transformer/transformers.ts @@ -1383,6 +1383,7 @@ function makeCSSReset(context: ConversionContext): serializedNodeWithId { border: 0; outline: 0; background: transparent; + padding-block: 0 !important; } .input:focus { outline: none; From 4073dc1cf90d162223c11d5ec7f1fccd0a293d73 Mon Sep 17 00:00:00 2001 From: Eric Duong Date: Wed, 20 Mar 2024 10:12:48 -0400 Subject: [PATCH 05/51] chore(data-warehouse): add field type on breakdown logic (#21028) --- posthog/hogql_queries/insights/trends/trends_query_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posthog/hogql_queries/insights/trends/trends_query_runner.py b/posthog/hogql_queries/insights/trends/trends_query_runner.py index b9acb5c37d000..29d29b55e8b0f 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/trends_query_runner.py @@ -709,7 +709,7 @@ def _is_breakdown_field_boolean(self): if not table_model: raise ValueError(f"Table {series.table_name} not found") - field_type = dict(table_model.columns)[self.query.breakdownFilter.breakdown] + field_type = dict(table_model.columns)[self.query.breakdownFilter.breakdown]["clickhouse"] if field_type.startswith("Nullable("): field_type = field_type.replace("Nullable(", "")[:-1] From bfe46620b452f22815ff006de8359251cf6a3ee5 Mon Sep 17 00:00:00 2001 From: PostHog Bot <69588470+posthog-bot@users.noreply.github.com> Date: Wed, 20 Mar 2024 11:00:36 -0400 Subject: [PATCH 06/51] chore(deps): Update posthog-js to 1.116.3 (#21047) --- package.json | 2 +- pnpm-lock.yaml | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 0a268fc208fd5..770be74997198 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "pmtiles": "^2.11.0", "postcss": "^8.4.31", "postcss-preset-env": "^9.3.0", - "posthog-js": "1.116.1", + "posthog-js": "1.116.3", "posthog-js-lite": "2.5.0", "prettier": "^2.8.8", "prop-types": "^15.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef81ba7d4c4d9..5f157ec8b039e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -251,8 +251,8 @@ dependencies: specifier: ^9.3.0 version: 9.3.0(postcss@8.4.31) posthog-js: - specifier: 1.116.1 - version: 1.116.1 + specifier: 1.116.3 + version: 1.116.3 posthog-js-lite: specifier: 2.5.0 version: 2.5.0 @@ -6793,7 +6793,7 @@ packages: '@storybook/csf': 0.1.3 '@storybook/global': 5.0.0 '@storybook/types': 7.6.17 - '@types/qs': 6.9.12 + '@types/qs': 6.9.13 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 @@ -8195,6 +8195,11 @@ packages: /@types/qs@6.9.12: resolution: {integrity: sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==} + dev: false + + /@types/qs@6.9.13: + resolution: {integrity: sha512-iLR+1vTTJ3p0QaOUq6ACbY1mzKTODFDT/XedZI8BksOotFmL4ForwDfRQ/DZeuTHR7/2i4lI1D203gdfxuqTlA==} + dev: true /@types/query-selector-shadow-dom@1.0.0: resolution: {integrity: sha512-cTGo8ZxW0WXFDV7gvL/XCq4213t6S/yWaSGqscnXUTNDWqwnsYKegB/VAzQDwzmACoLzIbGbYXdjJOgfPLu7Ig==} @@ -13625,7 +13630,7 @@ packages: hogan.js: 3.0.2 htm: 3.1.1 instantsearch-ui-components: 0.3.0 - preact: 10.19.6 + preact: 10.20.0 qs: 6.9.7 search-insights: 2.13.0 dev: false @@ -17441,19 +17446,19 @@ packages: resolution: {integrity: sha512-Urvlp0Vu9h3td0BVFWt0QXFJDoOZcaAD83XM9d91NKMKTVPZtfU0ysoxstIf5mw/ce9ZfuMgpWPaagrZI4rmSg==} dev: false - /posthog-js@1.116.1: - resolution: {integrity: sha512-tYKw6K23S3koa2sfX0sylno7jQQ6ET7u1Lw4KqowhciNhS0R5OWTo3HWEJPt64e9IzeWQGcgb9utJIWwrp5D0Q==} + /posthog-js@1.116.3: + resolution: {integrity: sha512-KakGsQ8rS/K/U5Q/tiBrRrFRCgGrR0oI9VSYw9hwNCY00EClwAU3EuykUuQTFdQ1EuYMrZDIMWDD4NW6zgf7wQ==} dependencies: fflate: 0.4.8 - preact: 10.19.6 + preact: 10.20.0 dev: false /potpack@2.0.0: resolution: {integrity: sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==} dev: false - /preact@10.19.6: - resolution: {integrity: sha512-gympg+T2Z1fG1unB8NH29yHJwnEaCH37Z32diPDku316OTnRPeMbiRV9kTrfZpocXjdfnWuFUl/Mj4BHaf6gnw==} + /preact@10.20.0: + resolution: {integrity: sha512-wU7iZw2BjsaKDal3pDRDy/HpPB6cuFOnVUCcw9aIPKG98+ZrXx3F+szkos8BVME5bquyKDKvRlOJFG8kMkcAbg==} dev: false /prelude-ls@1.2.1: From 4ca3c29b5d1dea94d79773cbe2cc76a168ee692d Mon Sep 17 00:00:00 2001 From: Ben White Date: Wed, 20 Mar 2024 16:12:51 +0100 Subject: [PATCH 07/51] feat: Add comma separator support for LemonInputSelect (#21031) * Add comma sepator * feat: Alt-click to edit inputselect items (#21032) --- ...ect--multiple-select-with-custom--dark.png | Bin 1646 -> 1357 bytes ...ct--multiple-select-with-custom--light.png | Bin 1696 -> 1427 bytes .../LemonInputSelect.stories.tsx | 17 +- .../LemonInputSelect/LemonInputSelect.tsx | 145 ++++++++++++------ 4 files changed, 118 insertions(+), 44 deletions(-) diff --git a/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--dark.png b/frontend/__snapshots__/lemon-ui-lemon-input-select--multiple-select-with-custom--dark.png index aa1ba92d1702f51e6269e217cc34fde1d097d853..de66482b3ee6e84a942c809cd11d49ae5d6cb709 100644 GIT binary patch delta 1326 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 N002ovPDHLkV1ixPIZ6Nk diff --git a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.stories.tsx b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.stories.tsx index 796d1794b4c89..f7c9212186d1f 100644 --- a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.stories.tsx @@ -54,9 +54,24 @@ MultipleSelect.args = { export const MultipleSelectWithCustom: Story = Template.bind({}) MultipleSelectWithCustom.args = { - placeholder: 'Enter any email...', + placeholder: 'Pick a url...', mode: 'multiple', allowCustomValues: true, + options: [ + { + key: 'http://posthog.com/docs', + label: 'http://posthog.com/docs', + }, + { + key: 'http://posthog.com/pricing', + label: 'http://posthog.com/pricing', + }, + + { + key: 'http://posthog.com/products', + label: 'http://posthog.com/products', + }, + ], } export const Disabled: Story = Template.bind({}) diff --git a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx index 967f18e323753..9e5240a275a68 100644 --- a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx +++ b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx @@ -1,3 +1,5 @@ +import { Tooltip } from '@posthog/lemon-ui' +import { useKeyHeld } from 'lib/hooks/useKeyHeld' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' import { LemonSnack } from 'lib/lemon-ui/LemonSnack/LemonSnack' import { range } from 'lib/utils' @@ -49,14 +51,21 @@ export function LemonInputSelect({ const inputRef = useRef(null) const [selectedIndex, setSelectedIndex] = useState(0) const values = value ?? [] + const altKeyHeld = useKeyHeld('Alt') + + const separateOnComma = allowCustomValues && mode === 'multiple' const visibleOptions = useMemo(() => { const res: LemonInputSelectOption[] = [] const customValues = [...values] + // We show the input value if custom values are allowed and it's not in the list + if (allowCustomValues && inputValue && !values.includes(inputValue)) { + customValues.unshift(inputValue) + } + options.forEach((option) => { // Remove from the custom values list if it's in the options - if (customValues.includes(option.key)) { customValues.splice(customValues.indexOf(option.key), 1) } @@ -75,14 +84,8 @@ export function LemonInputSelect({ res.unshift({ key: value, label: value }) }) } - - // Finally we show the input value if custom values are allowed and it's not in the list - if (allowCustomValues && inputValue && !values.includes(inputValue)) { - res.unshift({ key: inputValue, label: inputValue }) - } - return res - }, [options, inputValue, value]) + }, [options, inputValue, values]) // Reset the selected index when the visible options change useEffect(() => { @@ -90,33 +93,69 @@ export function LemonInputSelect({ }, [visibleOptions.length]) const setInputValue = (newValue: string): void => { + // Special case for multiple mode with custom values + if (separateOnComma && newValue.includes(',')) { + const newValues = [...values] + + newValue.split(',').forEach((value) => { + const trimmedValue = value.trim() + if (trimmedValue && !values.includes(trimmedValue)) { + newValues.push(trimmedValue) + } + }) + + onChange?.(newValues) + newValue = '' + } + _setInputValue(newValue) onInputChange?.(inputValue) } - const _onActionItem = (item: string): void => { + const _removeItem = (item: string): void => { let newValues = [...values] - if (values.includes(item)) { - // Remove the item - if (mode === 'single') { - newValues = [] - } else { - newValues.splice(values.indexOf(item), 1) - } + // Remove the item + if (mode === 'single') { + newValues = [] } else { - // Add the item - if (mode === 'single') { - newValues = [item] - } else { + newValues.splice(values.indexOf(item), 1) + } + + onChange?.(newValues) + } + + const _addItem = (item: string): void => { + let newValues = [...values] + // Add the item + if (mode === 'single') { + newValues = [item] + } else { + if (!newValues.includes(item)) { newValues.push(item) } - - setInputValue('') } + setInputValue('') onChange?.(newValues) } + const _onActionItem = (item: string): void => { + if (altKeyHeld && allowCustomValues) { + // In this case we want to remove it if added and set input to it + if (values.includes(item)) { + _removeItem(item) + } + setInputValue(item) + return + } + + if (values.includes(item)) { + _removeItem(item) + } else { + _addItem(item) + } + } + const _onBlur = (): void => { // We need to add a delay as a click could be in the popover or the input wrapper which refocuses setTimeout(() => { @@ -143,8 +182,8 @@ export function LemonInputSelect({ const _onKeyDown = (e: React.KeyboardEvent): void => { if (e.key === 'Enter') { e.preventDefault() - const itemToAdd = visibleOptions[selectedIndex]?.key + if (itemToAdd) { _onActionItem(visibleOptions[selectedIndex]?.key) } @@ -164,33 +203,51 @@ export function LemonInputSelect({ } } - // TRICKY: We don't want the popover to affect the snack buttons - const prefix = ( - - <> - {values.map((value) => { - const option = options.find((option) => option.key === value) ?? { - label: value, - labelComponent: null, - } - return ( - <> - _onActionItem(value)}> + const prefix = useMemo( + () => ( + // TRICKY: We don't want the popover to affect the snack buttons + + <> + {values.map((value) => { + const option = options.find((option) => option.key === value) ?? { + label: value, + labelComponent: null, + } + const snack = ( + _onActionItem(value)} + onClick={allowCustomValues ? () => _onActionItem(value) : undefined} + > {option?.labelComponent ?? option?.label} - - ) - })} - - + ) + return allowCustomValues ? ( + + + click to edit + + } + > + {snack} + + ) : ( + snack + ) + })} + + + ), + [values, options, altKeyHeld, allowCustomValues] ) return ( { popoverFocusRef.current = false setShowPopover(false) @@ -219,7 +276,9 @@ export function LemonInputSelect({ {isHighlighted ? ( {' '} - {!values.includes(option.key) + {altKeyHeld && allowCustomValues + ? 'edit' + : !values.includes(option.key) ? mode === 'single' ? 'select' : 'add' From 0bcf49f997c0ecbda1ac96861ce6a57eaf49b2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Oberm=C3=BCller?= Date: Wed, 20 Mar 2024 17:24:01 +0100 Subject: [PATCH 08/51] fix(hogql): properly handle dst transition for funnel conversion window (#21042) --- .../hogql_queries/insights/funnels/base.py | 10 +- .../funnel_correlation_query_runner.py | 2 +- .../insights/funnels/funnel_unordered.py | 6 +- .../test/__snapshots__/test_funnel.ambr | 84 ++--- ...test_funnel_breakdowns_by_current_url.ambr | 8 +- .../test_funnel_correlation.ambr | 326 +++++++++--------- .../test_funnel_correlations_persons.ambr | 26 +- .../__snapshots__/test_funnel_persons.ambr | 18 +- .../__snapshots__/test_funnel_strict.ambr | 30 +- .../test_funnel_strict_persons.ambr | 18 +- .../test_funnel_time_to_convert.ambr | 130 +++---- .../__snapshots__/test_funnel_trends.ambr | 18 +- .../test_funnel_trends_persons.ambr | 18 +- .../__snapshots__/test_funnel_unordered.ambr | 42 +-- .../test_funnel_unordered_persons.ambr | 18 +- .../insights/funnels/test/test_funnel.py | 65 +++- .../funnels/test/test_funnel_unordered.py | 54 +++ 17 files changed, 492 insertions(+), 381 deletions(-) diff --git a/posthog/hogql_queries/insights/funnels/base.py b/posthog/hogql_queries/insights/funnels/base.py index 5d84328d2063d..ef8782fade54a 100644 --- a/posthog/hogql_queries/insights/funnels/base.py +++ b/posthog/hogql_queries/insights/funnels/base.py @@ -830,7 +830,7 @@ def _get_step_times(self, max_steps: int) -> List[ast.Expr]: for i in range(1, max_steps): exprs.append( parse_expr( - f"if(isNotNull(latest_{i}) AND latest_{i} <= latest_{i-1} + INTERVAL {windowInterval} {windowIntervalUnit}, dateDiff('second', latest_{i - 1}, latest_{i}), NULL) step_{i}_conversion_time" + f"if(isNotNull(latest_{i}) AND latest_{i} <= toTimeZone(latest_{i-1}, 'UTC') + INTERVAL {windowInterval} {windowIntervalUnit}, dateDiff('second', latest_{i - 1}, latest_{i}), NULL) step_{i}_conversion_time" ), ) @@ -959,7 +959,7 @@ def _get_exclusion_condition(self) -> List[ast.Expr]: to_time = f"latest_{exclusion.funnelToStep}" exclusion_time = f"exclusion_{exclusion_id}_latest_{exclusion.funnelFromStep}" condition = parse_expr( - f"if( {exclusion_time} > {from_time} AND {exclusion_time} < if(isNull({to_time}), {from_time} + INTERVAL {windowInterval} {windowIntervalUnit}, {to_time}), 1, 0)" + f"if( {exclusion_time} > {from_time} AND {exclusion_time} < if(isNull({to_time}), toTimeZone({from_time}, 'UTC') + INTERVAL {windowInterval} {windowIntervalUnit}, {to_time}), 1, 0)" ) conditions.append(condition) @@ -988,7 +988,11 @@ def _get_sorting_condition(self, curr_index: int, max_steps: int) -> ast.Expr: duplicate_event = is_equal(series[i], series[i - 1]) or is_superset(series[i], series[i - 1]) conditions.append(parse_expr(f"latest_{i - 1} {'<' if duplicate_event else '<='} latest_{i}")) - conditions.append(parse_expr(f"latest_{i} <= latest_0 + INTERVAL {windowInterval} {windowIntervalUnit}")) + conditions.append( + parse_expr( + f"latest_{i} <= toTimeZone(latest_0, 'UTC') + INTERVAL {windowInterval} {windowIntervalUnit}" + ) + ) return ast.Call( name="if", diff --git a/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py b/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py index 945a1b7da5cce..2fef78a372324 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py +++ b/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py @@ -761,7 +761,7 @@ def _get_events_join_query(self) -> str: AND toTimeZone(toDateTime(event.timestamp), 'UTC') > funnel_actors.first_timestamp AND toTimeZone(toDateTime(event.timestamp), 'UTC') < coalesce( funnel_actors.final_timestamp, - funnel_actors.first_timestamp + INTERVAL {windowInterval} {windowIntervalUnit}, + toTimeZone(funnel_actors.first_timestamp, 'UTC') + INTERVAL {windowInterval} {windowIntervalUnit}, date_to) -- Ensure that the event is not outside the bounds of the funnel conversion window diff --git a/posthog/hogql_queries/insights/funnels/funnel_unordered.py b/posthog/hogql_queries/insights/funnels/funnel_unordered.py index 09fc322621eac..af3ed18d4f82e 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_unordered.py +++ b/posthog/hogql_queries/insights/funnels/funnel_unordered.py @@ -168,7 +168,7 @@ def _get_step_times(self, max_steps: int) -> List[ast.Expr]: for i in range(1, max_steps): exprs.append( parse_expr( - f"if(isNotNull(conversion_times[{i+1}]) AND conversion_times[{i+1}] <= conversion_times[{i}] + INTERVAL {windowInterval} {windowIntervalUnit}, dateDiff('second', conversion_times[{i}], conversion_times[{i+1}]), NULL) step_{i}_conversion_time" + f"if(isNotNull(conversion_times[{i+1}]) AND conversion_times[{i+1}] <= toTimeZone(conversion_times[{i}], 'UTC') + INTERVAL {windowInterval} {windowIntervalUnit}, dateDiff('second', conversion_times[{i}], conversion_times[{i+1}]), NULL) step_{i}_conversion_time" ) ) # array indices in ClickHouse are 1-based :shrug: @@ -190,7 +190,7 @@ def get_sorting_condition(self, max_steps: int) -> List[ast.Expr]: basic_conditions: List[str] = [] for i in range(1, max_steps): basic_conditions.append( - f"if(latest_0 < latest_{i} AND latest_{i} <= latest_0 + INTERVAL {windowInterval} {windowIntervalUnit}, 1, 0)" + f"if(latest_0 < latest_{i} AND latest_{i} <= toTimeZone(latest_0, 'UTC') + INTERVAL {windowInterval} {windowIntervalUnit}, 1, 0)" ) if basic_conditions: @@ -214,7 +214,7 @@ def _get_exclusion_condition(self) -> List[ast.Expr]: to_time = f"event_times[{exclusion.funnelToStep + 1}]" exclusion_time = f"exclusion_{exclusion_id}_latest_{exclusion.funnelFromStep}" condition = parse_expr( - f"if( {exclusion_time} > {from_time} AND {exclusion_time} < if(isNull({to_time}), {from_time} + INTERVAL {windowInterval} {windowIntervalUnit}, {to_time}), 1, 0)" + f"if( {exclusion_time} > {from_time} AND {exclusion_time} < if(isNull({to_time}), toTimeZone({from_time}, 'UTC') + INTERVAL {windowInterval} {windowIntervalUnit}, {to_time}), 1, 0)" ) conditions.append(condition) diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel.ambr index 29a1483cafc1c..8379672352c1c 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel.ambr @@ -30,9 +30,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalSecond(15))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalSecond(15))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalSecond(15))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalSecond(15))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalSecond(15))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalSecond(15))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalSecond(15))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalSecond(15))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalSecond(15))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalSecond(15))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -128,9 +128,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalSecond(15))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalSecond(15))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalSecond(15))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalSecond(15))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalSecond(15))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalSecond(15))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalSecond(15))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalSecond(15))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalSecond(15))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalSecond(15))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -238,9 +238,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -341,8 +341,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -413,9 +413,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -523,9 +523,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -637,9 +637,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -751,9 +751,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -860,8 +860,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -922,8 +922,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -1009,8 +1009,8 @@ step_1 AS step_1, latest_1 AS latest_1, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, @@ -1115,8 +1115,8 @@ step_1 AS step_1, latest_1 AS latest_1, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, @@ -1228,8 +1228,8 @@ step_1 AS step_1, latest_1 AS latest_1, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, @@ -1343,9 +1343,9 @@ step_2 AS step_2, latest_2 AS latest_2, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, @@ -1496,9 +1496,9 @@ step_2 AS step_2, latest_2 AS latest_2, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, @@ -1650,9 +1650,9 @@ step_2 AS step_2, latest_2 AS latest_2, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_breakdowns_by_current_url.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_breakdowns_by_current_url.ambr index 656a22a2aff51..a50033fda2f7f 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_breakdowns_by_current_url.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_breakdowns_by_current_url.ambr @@ -50,8 +50,8 @@ step_1 AS step_1, latest_1 AS latest_1, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, @@ -156,8 +156,8 @@ step_1 AS step_1, latest_1 AS latest_1, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlation.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlation.ambr index 5019f0f57f7b7..b5e3e020c1ec0 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlation.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlation.ambr @@ -41,8 +41,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -74,7 +74,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(event__pdi.person_id, funnel_actors.actor_id) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), notIn(event.event, [])) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), notIn(event.event, [])) GROUP BY name LIMIT 100 UNION ALL @@ -110,8 +110,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -186,8 +186,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -268,8 +268,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -362,8 +362,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -505,8 +505,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -648,8 +648,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -791,8 +791,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -918,8 +918,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -944,7 +944,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_1`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), in(event.event, ['positively_related', 'negatively_related']))) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), in(event.event, ['positively_related', 'negatively_related']))) GROUP BY name HAVING and(ifNull(greater(plus(success_count, failure_count), 2), 0), ifNull(notIn((prop).1, []), 0)) LIMIT 100 @@ -981,8 +981,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -1052,8 +1052,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -1078,7 +1078,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_1`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), in(event.event, ['positively_related', 'negatively_related']))) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), in(event.event, ['positively_related', 'negatively_related']))) GROUP BY name HAVING and(ifNull(greater(plus(success_count, failure_count), 2), 0), ifNull(notIn((prop).1, []), 0)) LIMIT 100 @@ -1115,8 +1115,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -1181,8 +1181,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -1207,7 +1207,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), notIn(event.event, [])) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), notIn(event.event, [])) GROUP BY name LIMIT 100 UNION ALL @@ -1243,8 +1243,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -1330,8 +1330,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -1375,7 +1375,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'positively_related'), ifNull(equals(funnel_actors.steps, 2), 0)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'positively_related'), ifNull(equals(funnel_actors.steps, 2), 0)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(groups.key, source.actor_id) ORDER BY source.actor_id ASC @@ -1441,8 +1441,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -1486,7 +1486,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'positively_related'), ifNull(notEquals(funnel_actors.steps, 2), 1)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'positively_related'), ifNull(notEquals(funnel_actors.steps, 2), 1)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(groups.key, source.actor_id) ORDER BY source.actor_id ASC @@ -1552,8 +1552,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -1597,7 +1597,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(equals(funnel_actors.steps, 2), 0)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(equals(funnel_actors.steps, 2), 0)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(groups.key, source.actor_id) ORDER BY source.actor_id ASC @@ -1663,8 +1663,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -1708,7 +1708,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(notEquals(funnel_actors.steps, 2), 1)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(notEquals(funnel_actors.steps, 2), 1)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(groups.key, source.actor_id) ORDER BY source.actor_id ASC @@ -1753,8 +1753,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -1787,7 +1787,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), notIn(event.event, [])) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), notIn(event.event, [])) GROUP BY name LIMIT 100 UNION ALL @@ -1823,8 +1823,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -1918,8 +1918,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -1963,7 +1963,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(equals(funnel_actors.steps, 2), 0)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(equals(funnel_actors.steps, 2), 0)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(groups.key, source.actor_id) ORDER BY source.actor_id ASC @@ -2029,8 +2029,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -2074,7 +2074,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(notEquals(funnel_actors.steps, 2), 1)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(notEquals(funnel_actors.steps, 2), 1)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(groups.key, source.actor_id) ORDER BY source.actor_id ASC @@ -2119,8 +2119,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -2145,7 +2145,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), notIn(event.event, [])) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), notIn(event.event, [])) GROUP BY name LIMIT 100 UNION ALL @@ -2181,8 +2181,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -2268,8 +2268,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -2313,7 +2313,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'positively_related'), ifNull(equals(funnel_actors.steps, 2), 0)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'positively_related'), ifNull(equals(funnel_actors.steps, 2), 0)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(groups.key, source.actor_id) ORDER BY source.actor_id ASC @@ -2379,8 +2379,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -2424,7 +2424,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'positively_related'), ifNull(notEquals(funnel_actors.steps, 2), 1)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'positively_related'), ifNull(notEquals(funnel_actors.steps, 2), 1)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(groups.key, source.actor_id) ORDER BY source.actor_id ASC @@ -2490,8 +2490,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -2535,7 +2535,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(equals(funnel_actors.steps, 2), 0)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(equals(funnel_actors.steps, 2), 0)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(groups.key, source.actor_id) ORDER BY source.actor_id ASC @@ -2601,8 +2601,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -2646,7 +2646,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(notEquals(funnel_actors.steps, 2), 1)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(notEquals(funnel_actors.steps, 2), 1)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(groups.key, source.actor_id) ORDER BY source.actor_id ASC @@ -2691,8 +2691,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -2725,7 +2725,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), notIn(event.event, [])) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), notIn(event.event, [])) GROUP BY name LIMIT 100 UNION ALL @@ -2761,8 +2761,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -2856,8 +2856,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -2901,7 +2901,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(equals(funnel_actors.steps, 2), 0)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(equals(funnel_actors.steps, 2), 0)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(groups.key, source.actor_id) ORDER BY source.actor_id ASC @@ -2967,8 +2967,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -3012,7 +3012,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(funnel_actors.actor_id, event.`$group_0`) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(notEquals(funnel_actors.steps, 2), 1)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-14 23:59:59', 6, 'UTC')))), notIn(event.event, ['paid', 'user signed up']), equals(event.event, 'negatively_related'), ifNull(notEquals(funnel_actors.steps, 2), 1)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(groups.key, source.actor_id) ORDER BY source.actor_id ASC @@ -3060,8 +3060,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -3134,8 +3134,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -3220,8 +3220,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -3338,8 +3338,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -3456,8 +3456,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -3574,8 +3574,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -3675,8 +3675,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -3749,8 +3749,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -3818,8 +3818,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -3892,8 +3892,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -3978,8 +3978,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -4096,8 +4096,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -4214,8 +4214,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -4332,8 +4332,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -4433,8 +4433,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -4507,8 +4507,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -4576,8 +4576,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -4650,8 +4650,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -4736,8 +4736,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -4854,8 +4854,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -4972,8 +4972,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -5090,8 +5090,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -5191,8 +5191,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -5265,8 +5265,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -5334,8 +5334,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -5408,8 +5408,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -5494,8 +5494,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -5612,8 +5612,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -5730,8 +5730,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -5848,8 +5848,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -5949,8 +5949,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -6023,8 +6023,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -6092,8 +6092,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -6166,8 +6166,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -6252,8 +6252,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -6370,8 +6370,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -6488,8 +6488,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -6606,8 +6606,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -6707,8 +6707,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -6781,8 +6781,8 @@ latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlations_persons.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlations_persons.ambr index fe7404ea9b7a1..62715e7d96b63 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlations_persons.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_correlations_persons.ambr @@ -63,8 +63,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -115,7 +115,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(event__pdi.person_id, funnel_actors.actor_id) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2021-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2021-01-08 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2021-01-08 23:59:59', 6, 'UTC')))), notIn(event.event, ['$pageview', 'insight analyzed']), equals(event.event, 'insight loaded'), ifNull(equals(funnel_actors.steps, 2), 0)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2021-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2021-01-08 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2021-01-08 23:59:59', 6, 'UTC')))), notIn(event.event, ['$pageview', 'insight analyzed']), equals(event.event, 'insight loaded'), ifNull(equals(funnel_actors.steps, 2), 0)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(persons.id, source.actor_id) ORDER BY persons.id ASC @@ -213,9 +213,9 @@ uuid_2 AS uuid_2, `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, @@ -321,7 +321,7 @@ and isNull(max_steps))) WHERE ifNull(in(steps, [1, 2, 3]), 0) ORDER BY aggregation_target ASC) AS funnel_actors ON equals(event__pdi.person_id, funnel_actors.actor_id) - WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2021-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2021-01-08 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(funnel_actors.first_timestamp, toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2021-01-08 23:59:59', 6, 'UTC')))), notIn(event.event, ['$pageview', 'insight analyzed', 'insight updated']), equals(event.event, 'insight loaded'), ifNull(notEquals(funnel_actors.steps, 3), 1)) + WHERE and(equals(event.team_id, 2), greaterOrEquals(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2021-01-01 00:00:00', 6, 'UTC'))), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2021-01-08 23:59:59', 6, 'UTC'))), equals(event.team_id, 2), greater(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), funnel_actors.first_timestamp), less(toTimeZone(toDateTime(toTimeZone(event.timestamp, 'UTC'), 'UTC'), 'UTC'), coalesce(funnel_actors.final_timestamp, plus(toTimeZone(funnel_actors.first_timestamp, 'UTC'), toIntervalDay(14)), assumeNotNull(parseDateTime64BestEffortOrNull('2021-01-08 23:59:59', 6, 'UTC')))), notIn(event.event, ['$pageview', 'insight analyzed', 'insight updated']), equals(event.event, 'insight loaded'), ifNull(notEquals(funnel_actors.steps, 3), 1)) GROUP BY actor_id ORDER BY actor_id ASC) AS source ON equals(persons.id, source.actor_id) ORDER BY persons.id ASC @@ -401,8 +401,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -544,8 +544,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event @@ -687,8 +687,8 @@ uuid_1 AS uuid_1, `$session_id_1` AS `$session_id_1`, `$window_id_1` AS `$window_id_1`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, if(isNull(latest_0), tuple(NULL, NULL, NULL, NULL), if(isNull(latest_1), step_0_matching_event, step_1_matching_event)) AS final_matching_event diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_persons.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_persons.ambr index a9a6fc5357f72..723165d9d8320 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_persons.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_persons.ambr @@ -52,9 +52,9 @@ uuid_2 AS uuid_2, `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, @@ -234,9 +234,9 @@ uuid_2 AS uuid_2, `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, @@ -416,9 +416,9 @@ uuid_2 AS uuid_2, `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict.ambr index 5b12c1d8d00e0..927a924d8a884 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict.ambr @@ -50,8 +50,8 @@ step_1 AS step_1, latest_1 AS latest_1, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -155,8 +155,8 @@ step_1 AS step_1, latest_1 AS latest_1, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -267,8 +267,8 @@ step_1 AS step_1, latest_1 AS latest_1, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -381,9 +381,9 @@ step_2 AS step_2, latest_2 AS latest_2, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, @@ -534,9 +534,9 @@ step_2 AS step_2, latest_2 AS latest_2, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, @@ -688,9 +688,9 @@ step_2 AS step_2, latest_2 AS latest_2, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_persons.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_persons.ambr index 0b2f21460022e..1496e07c65e02 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_persons.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_strict_persons.ambr @@ -52,9 +52,9 @@ uuid_2 AS uuid_2, `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, @@ -194,9 +194,9 @@ uuid_2 AS uuid_2, `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, @@ -336,9 +336,9 @@ uuid_2 AS uuid_2, `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_time_to_convert.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_time_to_convert.ambr index aae3049d382ec..2463fd084116f 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_time_to_convert.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_time_to_convert.ambr @@ -35,9 +35,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -102,7 +102,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 @@ -135,7 +135,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 @@ -168,7 +168,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 @@ -201,7 +201,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 @@ -249,9 +249,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -316,7 +316,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 @@ -349,7 +349,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 @@ -383,7 +383,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(7))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_2 @@ -450,9 +450,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -498,7 +498,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -526,7 +526,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -554,7 +554,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -582,7 +582,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -625,9 +625,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -673,7 +673,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -701,7 +701,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -730,7 +730,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -793,10 +793,10 @@ step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, - if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, + if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -836,10 +836,10 @@ step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, - if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, + if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -879,10 +879,10 @@ step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, - if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, + if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -928,7 +928,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -944,7 +944,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -960,7 +960,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -988,7 +988,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1004,7 +1004,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1020,7 +1020,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1048,7 +1048,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1064,7 +1064,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1080,7 +1080,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1108,7 +1108,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1124,7 +1124,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1140,7 +1140,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1184,10 +1184,10 @@ step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, - if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, + if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -1227,10 +1227,10 @@ step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, - if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, + if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -1270,10 +1270,10 @@ step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, - if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, + if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -1319,7 +1319,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1335,7 +1335,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1351,7 +1351,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1379,7 +1379,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1395,7 +1395,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1411,7 +1411,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1440,7 +1440,7 @@ FROM (SELECT aggregation_target AS aggregation_target, steps AS steps, max(steps) OVER (PARTITION BY aggregation_target) AS max_steps, step_1_conversion_time AS step_1_conversion_time, step_2_conversion_time AS step_2_conversion_time FROM - (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1456,7 +1456,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target @@ -1472,7 +1472,7 @@ HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-07 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2021-06-13 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('step one', 'step three', 'step two'))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))) WHERE ifNull(equals(step_0, 1), 0) - UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + UNION ALL SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, arraySort([latest_0, latest_1, latest_2]) AS event_times, arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, step_0 AS step_0, latest_0 AS latest_0, step_1 AS step_1, min(latest_1) OVER (PARTITION BY aggregation_target ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) AS latest_1, step_2 AS step_2, min(latest_2) OVER (PARTITION BY aggregation_target diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends.ambr index 5fa91548e3037..c86902d5df182 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends.ambr @@ -22,9 +22,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -109,9 +109,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -196,9 +196,9 @@ latest_1 AS latest_1, step_2 AS step_2, latest_2 AS latest_2, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_persons.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_persons.ambr index 9dcb793d30329..32518bdbf9bbc 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_persons.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_trends_persons.ambr @@ -39,9 +39,9 @@ uuid_2 AS uuid_2, `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, @@ -207,9 +207,9 @@ uuid_2 AS uuid_2, `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, @@ -375,9 +375,9 @@ uuid_2 AS uuid_2, `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, tuple(latest_0, uuid_0, `$session_id_0`, `$window_id_0`) AS step_0_matching_event, tuple(latest_1, uuid_1, `$session_id_1`, `$window_id_1`) AS step_1_matching_event, tuple(latest_2, uuid_2, `$session_id_2`, `$window_id_2`) AS step_2_matching_event, diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_unordered.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_unordered.ambr index 214583b03f081..52269964088e1 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_unordered.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_unordered.ambr @@ -49,9 +49,9 @@ latest_1 AS latest_1, prop AS prop, arraySort([latest_0, latest_1]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -101,9 +101,9 @@ latest_1 AS latest_1, prop AS prop, arraySort([latest_0, latest_1]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -206,9 +206,9 @@ latest_1 AS latest_1, prop AS prop, arraySort([latest_0, latest_1]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -265,9 +265,9 @@ latest_1 AS latest_1, prop AS prop, arraySort([latest_0, latest_1]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -377,9 +377,9 @@ latest_1 AS latest_1, prop AS prop, arraySort([latest_0, latest_1]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -429,9 +429,9 @@ latest_1 AS latest_1, prop AS prop, arraySort([latest_0, latest_1]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -544,9 +544,9 @@ step_2 AS step_2, latest_2 AS latest_2, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, @@ -697,9 +697,9 @@ step_2 AS step_2, latest_2 AS latest_2, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, @@ -851,9 +851,9 @@ step_2 AS step_2, latest_2 AS latest_2, prop AS prop, - if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 2, 1)) AS steps, - if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, - if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(latest_1, toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, + if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0), ifNull(lessOrEquals(latest_1, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 3, if(and(ifNull(lessOrEquals(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 2, 1)) AS steps, + if(and(isNotNull(latest_1), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time, + if(and(isNotNull(latest_2), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_1, 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', latest_1, latest_2), NULL) AS step_2_conversion_time, prop AS prop FROM (SELECT aggregation_target AS aggregation_target, diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_unordered_persons.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_unordered_persons.ambr index 6f308a54388e3..5ced3fb2b9766 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_unordered_persons.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel_unordered_persons.ambr @@ -45,10 +45,10 @@ `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, arraySort([latest_0, latest_1, latest_2]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, - if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, + if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -122,10 +122,10 @@ `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, arraySort([latest_0, latest_1, latest_2]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, - if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, + if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, @@ -199,10 +199,10 @@ `$session_id_2` AS `$session_id_2`, `$window_id_2` AS `$window_id_2`, arraySort([latest_0, latest_1, latest_2]) AS event_times, - arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(latest_0, toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, + arraySum([if(and(ifNull(less(latest_0, latest_1), 0), ifNull(lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), if(and(ifNull(less(latest_0, latest_2), 0), ifNull(lessOrEquals(latest_2, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14))), 0)), 1, 0), 1]) AS steps, arraySort([latest_0, latest_1, latest_2]) AS conversion_times, - if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(conversion_times[1], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, - if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(conversion_times[2], toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time + if(and(isNotNull(conversion_times[2]), ifNull(lessOrEquals(conversion_times[2], plus(toTimeZone(conversion_times[1], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[1], conversion_times[2]), NULL) AS step_1_conversion_time, + if(and(isNotNull(conversion_times[3]), ifNull(lessOrEquals(conversion_times[3], plus(toTimeZone(conversion_times[2], 'UTC'), toIntervalDay(14))), 0)), dateDiff('second', conversion_times[2], conversion_times[3]), NULL) AS step_2_conversion_time FROM (SELECT aggregation_target AS aggregation_target, timestamp AS timestamp, diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel.py b/posthog/hogql_queries/insights/funnels/test/test_funnel.py index d132927863542..98f4d060fb905 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel.py @@ -3523,6 +3523,59 @@ def test_funnel_aggregation_with_groups_with_cohort_filtering(self): self.assertEqual(results[1]["name"], "paid") self.assertEqual(results[1]["count"], 1) + def test_funnel_window_ignores_dst_transition(self): + _create_person( + distinct_ids=[f"user_1"], + team=self.team, + ) + + events_by_person = { + "user_1": [ + { + "event": "$pageview", + "timestamp": datetime(2024, 3, 1, 15, 10), # 1st March 15:10 + }, + { + "event": "user signed up", + "timestamp": datetime( + 2024, 3, 15, 14, 27 + ), # 15th March 14:27 (within 14 day conversion window that ends at 15:10) + }, + ], + } + journeys_for(events_by_person, self.team) + + filters = { + "events": [ + {"id": "$pageview", "type": "events", "order": 0}, + {"id": "user signed up", "type": "events", "order": 1}, + ], + "insight": INSIGHT_FUNNELS, + "date_from": "2024-02-17", + "date_to": "2024-03-18", + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(results[1]["name"], "user signed up") + self.assertEqual(results[1]["count"], 1) + self.assertEqual(results[1]["average_conversion_time"], 1_207_020) + self.assertEqual(results[1]["median_conversion_time"], 1_207_020) + + # there is a PST -> PDT transition on 10th of March + self.team.timezone = "US/Pacific" + self.team.save() + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + # we still should have the user here, as the conversion window should not be affected by DST + self.assertEqual(results[1]["name"], "user signed up") + self.assertEqual(results[1]["count"], 1) + self.assertEqual(results[1]["average_conversion_time"], 1_207_020) + self.assertEqual(results[1]["median_conversion_time"], 1_207_020) + return TestGetFunnel @@ -3550,8 +3603,8 @@ def test_smoke(self): latest_0, step_1, latest_1, - if(and(less(latest_0, latest_1), lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14)))), 2, 1) AS steps, - if(and(isNotNull(latest_1), lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14)))), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(less(latest_0, latest_1), lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14)))), 2, 1) AS steps, + if(and(isNotNull(latest_1), lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14)))), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target, @@ -3610,8 +3663,8 @@ def test_smoke(self): latest_0, step_1, latest_1, - if(and(less(latest_0, latest_1), lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14)))), 2, 1) AS steps, - if(and(isNotNull(latest_1), lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14)))), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(less(latest_0, latest_1), lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14)))), 2, 1) AS steps, + if(and(isNotNull(latest_1), lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14)))), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target, @@ -3681,8 +3734,8 @@ def test_smoke(self): latest_0, step_1, latest_1, - if(and(less(latest_0, latest_1), lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14)))), 2, 1) AS steps, - if(and(isNotNull(latest_1), lessOrEquals(latest_1, plus(latest_0, toIntervalDay(14)))), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time + if(and(less(latest_0, latest_1), lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14)))), 2, 1) AS steps, + if(and(isNotNull(latest_1), lessOrEquals(latest_1, plus(toTimeZone(latest_0, 'UTC'), toIntervalDay(14)))), dateDiff('second', latest_0, latest_1), NULL) AS step_1_conversion_time FROM (SELECT aggregation_target, diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_unordered.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_unordered.py index 36e5d87f39e49..49e495e69222d 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_unordered.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_unordered.py @@ -1620,3 +1620,57 @@ def test_funnel_unordered_entity_filters(self): self.assertEqual(results[0]["count"], 1) self.assertEqual(results[1]["count"], 1) + + def test_funnel_window_ignores_dst_transition(self): + _create_person( + distinct_ids=[f"user_1"], + team=self.team, + ) + + events_by_person = { + "user_1": [ + { + "event": "$pageview", + "timestamp": datetime(2024, 3, 1, 15, 10), # 1st March 15:10 + }, + { + "event": "user signed up", + "timestamp": datetime( + 2024, 3, 15, 14, 27 + ), # 15th March 14:27 (within 14 day conversion window that ends at 15:10) + }, + ], + } + journeys_for(events_by_person, self.team) + + filters = { + "events": [ + {"id": "$pageview", "type": "events", "order": 0}, + {"id": "user signed up", "type": "events", "order": 1}, + ], + "insight": INSIGHT_FUNNELS, + "funnel_order_type": "unordered", + "date_from": "2024-02-17", + "date_to": "2024-03-18", + } + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(results[1]["name"], "Completed 2 steps") + self.assertEqual(results[1]["count"], 1) + self.assertEqual(results[1]["average_conversion_time"], 1_207_020) + self.assertEqual(results[1]["median_conversion_time"], 1_207_020) + + # there is a PST -> PDT transition on 10th of March + self.team.timezone = "US/Pacific" + self.team.save() + + query = cast(FunnelsQuery, filter_to_query(filters)) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + # we still should have the user here, as the conversion window should not be affected by DST + self.assertEqual(results[1]["name"], "Completed 2 steps") + self.assertEqual(results[1]["count"], 1) + self.assertEqual(results[1]["average_conversion_time"], 1_207_020) + self.assertEqual(results[1]["median_conversion_time"], 1_207_020) From 590ef1cbd1ec49836c5d61efc707cbdb1a0b21b2 Mon Sep 17 00:00:00 2001 From: Bianca Yang <21014901+xrdt@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:44:04 -0700 Subject: [PATCH 09/51] fix: Swap out password reset token generator (#21026) swap out token generator --- posthog/admin/admins/user_admin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/posthog/admin/admins/user_admin.py b/posthog/admin/admins/user_admin.py index c1129ef334fa4..9872fad946f30 100644 --- a/posthog/admin/admins/user_admin.py +++ b/posthog/admin/admins/user_admin.py @@ -1,12 +1,11 @@ -from django.utils.html import format_html - from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin from django.contrib.auth.forms import UserChangeForm as DjangoUserChangeForm -from django.contrib.auth.tokens import default_token_generator +from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from posthog.admin.inlines.organization_member_inline import OrganizationMemberInline from posthog.admin.inlines.totp_device_inline import TOTPDeviceInline +from posthog.api.authentication import password_reset_token_generator from posthog.models import User @@ -18,7 +17,7 @@ def __init__(self, *args, **kwargs): # we have a link to the password reset page which the _user_ can use themselves. # This way if some user needs to reset their password and there's a problem with receiving the reset link email, # an admin can provide that reset link manually – much better than sending a new password in plain text. - password_reset_token = default_token_generator.make_token(self.instance) + password_reset_token = password_reset_token_generator.make_token(self.instance) self.fields["password"].help_text = ( "Raw passwords are not stored, so there is no way to see this user’s password, but you can send them " f'this password reset link ' From a22c8a6e505430abbad7e57a72b6ac84d4f47c7b Mon Sep 17 00:00:00 2001 From: Frank Hamand Date: Wed, 20 Mar 2024 17:11:39 +0000 Subject: [PATCH 10/51] fix: Revert healthcheck change (#21054) Revert healthcheck change We suspect this is causing pods to come up before they are ready to serve traffic --- unit.json | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/unit.json b/unit.json index 72e3d2f03edb6..472189f0c2880 100644 --- a/unit.json +++ b/unit.json @@ -17,14 +17,6 @@ }, "routes": { "posthog": [ - { - "match": { - "uri": ["/_health", "/_readyz", "/_livez"] - }, - "action": { - "pass": "applications/posthog-health" - } - }, { "action": { "pass": "applications/posthog" @@ -53,17 +45,6 @@ ] }, "applications": { - "posthog-health": { - "type": "python 3.10", - "processes": 1, - "working_directory": "/code", - "path": ".", - "module": "posthog.wsgi", - "user": "nobody", - "limits": { - "requests": 5000 - } - }, "posthog": { "type": "python 3.10", "processes": 4, From 921ee5a7253aa16ed5a4daea7f57773b0fbc100a Mon Sep 17 00:00:00 2001 From: Zach Waterfield Date: Wed, 20 Mar 2024 12:54:05 -0600 Subject: [PATCH 11/51] feat: add reverse proxy step to quick start steps + more quick start events (#20939) * Add reverse proxy to the quick start list * Add sidebar task skipped event * Add sidebar open/close events * Update quick start proxy copy * Move the sidebar closed call * Put reportActivationSideBarShown import back * Remove eventUsageLogic calls * remove eventUserLogic usage * spelling fix * Switch sidebar events * update task skipped key * Update UI snapshots for `chromium` (2) * Update UI snapshots for `chromium` (2) * rename sidebar events --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .../panels/activation/SidePanelActivation.tsx | 3 +++ .../panels/activation/activationLogic.ts | 19 ++++++++++++++++--- .../sidepanel/sidePanelStateLogic.tsx | 5 ++++- frontend/src/lib/utils/eventUsageLogic.ts | 12 ------------ 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/activation/SidePanelActivation.tsx b/frontend/src/layout/navigation-3000/sidepanel/panels/activation/SidePanelActivation.tsx index c78aa76f0faec..5aef55b51a523 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/activation/SidePanelActivation.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/activation/SidePanelActivation.tsx @@ -97,6 +97,9 @@ const ActivationTask = ({ if (url) { params.to = url params.targetBlank = true + params.onClick = () => { + reportActivationSideBarTaskClicked(id) + } } else { params.onClick = () => { runTask(id) diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/activation/activationLogic.ts b/frontend/src/layout/navigation-3000/sidepanel/panels/activation/activationLogic.ts index 7a182c47ba2b1..e8afd00aea1ed 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/activation/activationLogic.ts +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/activation/activationLogic.ts @@ -2,8 +2,8 @@ import { actions, connect, events, kea, listeners, path, reducers, selectors } f import { loaders } from 'kea-loaders' import { router } from 'kea-router' import api from 'lib/api' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { permanentlyMount } from 'lib/utils/kea-logic-builders' +import posthog from 'posthog-js' import { membersLogic } from 'scenes/organization/membersLogic' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic' @@ -25,6 +25,7 @@ export enum ActivationTasks { SetupSessionRecordings = 'setup_session_recordings', TrackCustomEvents = 'track_custom_events', InstallFirstApp = 'install_first_app', + SetUpReverseProxy = 'set_up_reverse_proxy', } export type ActivationTaskType = { @@ -65,8 +66,6 @@ export const activationLogic = kea([ ['loadPluginsSuccess', 'loadPluginsFailure'], sidePanelStateLogic, ['openSidePanel'], - eventUsageLogic, - ['reportActivationSideBarShown'], savedInsightsLogic, ['loadInsights', 'loadInsightsSuccess', 'loadInsightsFailure'], dashboardsModel, @@ -282,6 +281,17 @@ export const activationLogic = kea([ skipped: skippedTasks.includes(ActivationTasks.InstallFirstApp), }) break + case ActivationTasks.SetUpReverseProxy: + tasks.push({ + id: ActivationTasks.SetUpReverseProxy, + name: 'Set up a reverse proxy', + description: 'Send your events from your own domain to avoid tracking blockers', + completed: false, + canSkip: true, + skipped: skippedTasks.includes(ActivationTasks.SetUpReverseProxy), + url: 'https://posthog.com/docs/advanced/proxy', + }) + break default: break } @@ -342,6 +352,9 @@ export const activationLogic = kea([ } }, skipTask: ({ id }) => { + posthog.capture('activation sidebar task skipped', { + task: id, + }) if (values.currentTeam?.id) { actions.addSkippedTask(values.currentTeam.id, id) } diff --git a/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx b/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx index 6d1360cdf90ac..e4188d68842c7 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/sidePanelStateLogic.tsx @@ -1,6 +1,7 @@ import { actions, kea, listeners, path, reducers } from 'kea' import { actionToUrl, router, urlToAction } from 'kea-router' import { windowValues } from 'kea-window-values' +import posthog from 'posthog-js' import { SidePanelTab } from '~/types' @@ -55,10 +56,12 @@ export const sidePanelStateLogic = kea([ listeners(({ actions, values }) => ({ // NOTE: We explicitly reference the actions instead of connecting so that people don't accidentally // use this logic instead of sidePanelStateLogic - openSidePanel: () => { + openSidePanel: ({ tab }) => { + posthog.capture('sidebar opened', { tab }) actions.setSidePanelOpen(true) }, closeSidePanel: ({ tab }) => { + posthog.capture('sidebar closed', { tab }) if (!tab) { // If we aren't specifiying the tab we always close actions.setSidePanelOpen(false) diff --git a/frontend/src/lib/utils/eventUsageLogic.ts b/frontend/src/lib/utils/eventUsageLogic.ts index e88617ddd7bad..069d1758d0e56 100644 --- a/frontend/src/lib/utils/eventUsageLogic.ts +++ b/frontend/src/lib/utils/eventUsageLogic.ts @@ -457,11 +457,6 @@ export const eventUsageLogic = kea([ reportInstanceSettingChange: (name: string, value: string | boolean | number) => ({ name, value }), reportAxisUnitsChanged: (properties: Record) => ({ ...properties }), reportTeamSettingChange: (name: string, value: any) => ({ name, value }), - reportActivationSideBarShown: ( - activeTasksCount: number, - completedTasksCount: number, - completionPercent: number - ) => ({ activeTasksCount, completedTasksCount, completionPercent }), reportActivationSideBarTaskClicked: (key: string) => ({ key }), reportBillingUpgradeClicked: (plan: string) => ({ plan }), reportRoleCreated: (role: string) => ({ role }), @@ -1092,13 +1087,6 @@ export const eventUsageLogic = kea([ value, }) }, - reportActivationSideBarShown: ({ activeTasksCount, completedTasksCount, completionPercent }) => { - posthog.capture('activation sidebar shown', { - active_tasks_count: activeTasksCount, - completed_tasks_count: completedTasksCount, - completion_percent_count: completionPercent, - }) - }, reportActivationSideBarTaskClicked: ({ key }) => { posthog.capture('activation sidebar task clicked', { key, From 1b07dec7c6026a465d8c2e4b011f0d5e66dd5be7 Mon Sep 17 00:00:00 2001 From: Frank Hamand Date: Wed, 20 Mar 2024 20:48:55 +0000 Subject: [PATCH 12/51] fix: Switch back to direct routing instead of via route (#21058) Switch back to direct routing instead of via route We're getting segfault errors and this is one of the few potential causes --- unit.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/unit.json b/unit.json index 472189f0c2880..0b8de8774edf1 100644 --- a/unit.json +++ b/unit.json @@ -6,7 +6,7 @@ }, "listeners": { "*:8000": { - "pass": "routes/posthog" + "pass": "applications/posthog" }, "*:8001": { "pass": "routes/metrics" @@ -16,13 +16,6 @@ } }, "routes": { - "posthog": [ - { - "action": { - "pass": "applications/posthog" - } - } - ], "metrics": [ { "match": { From da9798527667a8fe3403573edbac4b1f2bac8313 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Wed, 20 Mar 2024 16:05:43 -0700 Subject: [PATCH 13/51] fix: allow reads/writes to description without team collab feature (#21027) * remove check for org feature for dashboard descriptions * use normal upgrad modal for editableField paywall * Update query snapshots --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- ee/api/test/test_dashboard.py | 46 +--- ...ickhouse_experiment_secondary_results.ambr | 2 +- .../EditableField/EditableField.tsx | 230 +++++++++--------- .../UpgradeModal/upgradeModalLogic.ts | 6 +- frontend/src/scenes/actions/ActionEdit.tsx | 4 +- .../src/scenes/dashboard/DashboardHeader.tsx | 8 +- .../definition/DefinitionView.tsx | 4 +- .../src/scenes/insights/InsightPageHeader.tsx | 4 +- posthog/api/dashboards/dashboard.py | 10 - 9 files changed, 138 insertions(+), 176 deletions(-) diff --git a/ee/api/test/test_dashboard.py b/ee/api/test/test_dashboard.py index 39098247d411f..8c39a17135db0 100644 --- a/ee/api/test/test_dashboard.py +++ b/ee/api/test/test_dashboard.py @@ -4,10 +4,8 @@ from rest_framework import status from ee.api.test.base import APILicensedTest -from ee.api.test.fixtures.available_product_features import AVAILABLE_PRODUCT_FEATURES from ee.models.explicit_team_membership import ExplicitTeamMembership from ee.models.license import License -from posthog.constants import AvailableFeature from posthog.models import OrganizationMembership from posthog.models.dashboard import Dashboard from posthog.models.sharing_configuration import SharingConfiguration @@ -269,7 +267,12 @@ def test_sharing_edits_limited_to_collaborators(self): self.permission_denied_response("You don't have edit permissions for this dashboard."), ) - def test_cannot_edit_dashboard_description_when_collaboration_not_available(self): + def test_can_edit_dashboard_description_when_collaboration_not_available(self): + """ + Team collaboration feature is only available on some plans, but if the feature is + not available, the user should still be able to read/write for migration purposes. + The access to the feature is blocked in the UI, so this is unlikely to be truly abused. + """ self.client.logout() self.organization.available_features = [] @@ -288,44 +291,11 @@ def test_cannot_edit_dashboard_description_when_collaboration_not_available(self name="example dashboard", ) - response = self.client.patch( - f"/api/projects/{self.team.id}/dashboards/{dashboard.id}", - { - "description": "i should not be allowed to edit this", - "name": "even though I am allowed to edit this", - }, - ) - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - dashboard.refresh_from_db() - self.assertEqual(dashboard.description, "") - self.assertEqual(dashboard.name, "example dashboard") - - def test_can_edit_dashboard_description_when_collaboration_is_available(self): - self.client.logout() - - self.organization.available_features = [AvailableFeature.TEAM_COLLABORATION] - self.organization.available_product_features = AVAILABLE_PRODUCT_FEATURES - self.organization.save() - self.team.access_control = True - self.team.save() - - user_with_collaboration = User.objects.create_and_join( - self.organization, "no-collaboration-feature@posthog.com", None - ) - self.client.force_login(user_with_collaboration) - - dashboard: Dashboard = Dashboard.objects.create( - team=self.team, - name="example dashboard", - ) - response = self.client.patch( f"/api/projects/{self.team.id}/dashboards/{dashboard.id}", { "description": "i should be allowed to edit this", - "name": "and so also to edit this", + "name": "as well as this", }, ) @@ -333,4 +303,4 @@ def test_can_edit_dashboard_description_when_collaboration_is_available(self): dashboard.refresh_from_db() self.assertEqual(dashboard.description, "i should be allowed to edit this") - self.assertEqual(dashboard.name, "and so also to edit this") + self.assertEqual(dashboard.name, "as well as this") diff --git a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr index 54b67fa7d359b..3c6b8ef78c385 100644 --- a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr +++ b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiment_secondary_results.ambr @@ -1,7 +1,7 @@ # serializer version: 1 # name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results ''' - /* user_id:108 celery:posthog.tasks.tasks.sync_insight_caching_state */ + /* user_id:107 celery:posthog.tasks.tasks.sync_insight_caching_state */ SELECT team_id, date_diff('second', max(timestamp), now()) AS age FROM events diff --git a/frontend/src/lib/components/EditableField/EditableField.tsx b/frontend/src/lib/components/EditableField/EditableField.tsx index 28cb2e4a247b7..0a83602cf5c5f 100644 --- a/frontend/src/lib/components/EditableField/EditableField.tsx +++ b/frontend/src/lib/components/EditableField/EditableField.tsx @@ -3,6 +3,7 @@ import './EditableField.scss' import { useMergeRefs } from '@floating-ui/react' import { IconPencil } from '@posthog/icons' import clsx from 'clsx' +import { useValues } from 'kea' import { useResizeObserver } from 'lib/hooks/useResizeObserver' import { IconMarkdown } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' @@ -12,6 +13,10 @@ import { pluralize } from 'lib/utils' import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' import TextareaAutosize from 'react-textarea-autosize' +import { AvailableFeature } from '~/types' + +import { upgradeModalLogic } from '../UpgradeModal/upgradeModalLogic' + export interface EditableFieldProps { /** What this field stands for. */ name: string @@ -28,7 +33,7 @@ export interface EditableFieldProps { markdown?: boolean compactButtons?: boolean | 'xsmall' // The 'xsmall' is somewhat hacky, but necessary for 3000 breadcrumbs /** Whether this field should be gated behind a "paywall". */ - paywall?: boolean + paywallFeature?: AvailableFeature /** Controlled mode. */ mode?: 'view' | 'edit' onModeToggle?: (newMode: 'view' | 'edit') => void @@ -58,7 +63,7 @@ export function EditableField({ multiline = false, markdown = false, compactButtons = false, - paywall = false, + paywallFeature, mode, onModeToggle, editingIndication = 'outlined', @@ -68,6 +73,7 @@ export function EditableField({ saveButtonText = 'Save', notice, }: EditableFieldProps): JSX.Element { + const { guardAvailableFeature } = useValues(upgradeModalLogic) const [localIsEditing, setLocalIsEditing] = useState(mode === 'edit') const [localTentativeValue, setLocalTentativeValue] = useState(value) const [isDisplayTooltipNeeded, setIsDisplayTooltipNeeded] = useState(false) @@ -125,7 +131,7 @@ export function EditableField({ onModeToggle?.('view') } - const isEditing = !paywall && (mode === 'edit' || localIsEditing) + const isEditing = mode === 'edit' || localIsEditing const handleKeyDown = (e: React.KeyboardEvent): void => { if (isEditing) { @@ -156,123 +162,117 @@ export function EditableField({ style={style} ref={containerRef} > - -
- {isEditing ? ( - <> - {multiline ? ( - { - onChange?.(e.target.value) - setLocalTentativeValue(e.target.value) - }} - onBlur={saveOnBlur ? (localTentativeValue !== value ? save : cancel) : undefined} - onKeyDown={handleKeyDown} - placeholder={placeholder} - minLength={minLength} - maxLength={maxLength} - autoFocus={autoFocus} - ref={inputRef as React.RefObject} - /> - ) : ( - { +
+ {isEditing ? ( + <> + {multiline ? ( + { + onChange?.(e.target.value) + setLocalTentativeValue(e.target.value) + }} + onBlur={saveOnBlur ? (localTentativeValue !== value ? save : cancel) : undefined} + onKeyDown={handleKeyDown} + placeholder={placeholder} + minLength={minLength} + maxLength={maxLength} + autoFocus={autoFocus} + ref={inputRef as React.RefObject} + /> + ) : ( + { + guardAvailableFeature(paywallFeature, () => { onChange?.(e.target.value) setLocalTentativeValue(e.target.value) - }} - onBlur={saveOnBlur ? (localTentativeValue !== value ? save : cancel) : undefined} - onKeyDown={handleKeyDown} - placeholder={placeholder} - minLength={minLength} - maxLength={maxLength} - autoFocus={autoFocus} - ref={inputRef as React.RefObject} - /> - )} - {(!mode || !!onModeToggle) && ( -
- {markdown && ( - - - - - - )} - - Cancel - - - {saveButtonText} - -
- )} - - ) : ( - <> - {localTentativeValue && markdown ? ( - {localTentativeValue} - ) : ( - } + /> + )} + {(!mode || !!onModeToggle) && ( +
+ {markdown && ( + + + + + + )} + + Cancel + + - - {localTentativeValue || {placeholder}} - - - )} - {(!mode || !!onModeToggle) && ( -
- } - size={compactButtons ? 'small' : undefined} - onClick={() => { + {saveButtonText} + +
+ )} + + ) : ( + <> + {localTentativeValue && markdown ? ( + {localTentativeValue} + ) : ( + + + {localTentativeValue || {placeholder}} + + + )} + {(!mode || !!onModeToggle) && ( +
+ } + size={compactButtons ? 'small' : undefined} + onClick={() => { + guardAvailableFeature(paywallFeature, () => { setLocalIsEditing(true) onModeToggle?.('edit') - }} - data-attr={`edit-prop-${name}`} - disabled={paywall} - noPadding - /> -
- )} - - )} -
-
+ }) + }} + data-attr={`edit-prop-${name}`} + noPadding + /> +
+ )} + + )} +
{!isEditing && notice && ( diff --git a/frontend/src/lib/components/UpgradeModal/upgradeModalLogic.ts b/frontend/src/lib/components/UpgradeModal/upgradeModalLogic.ts index 9542ac6a208dc..51c6cf6fb670c 100644 --- a/frontend/src/lib/components/UpgradeModal/upgradeModalLogic.ts +++ b/frontend/src/lib/components/UpgradeModal/upgradeModalLogic.ts @@ -9,7 +9,7 @@ import { AvailableFeature } from '~/types' import type { upgradeModalLogicType } from './upgradeModalLogicType' export type GuardAvailableFeatureFn = ( - featureKey: AvailableFeature, + featureKey?: AvailableFeature, featureAvailableCallback?: () => void, options?: { guardOnCloud?: boolean @@ -60,6 +60,10 @@ export const upgradeModalLogic = kea([ (s) => [s.preflight, s.hasAvailableFeature], (preflight, hasAvailableFeature): GuardAvailableFeatureFn => { return (featureKey, featureAvailableCallback, options): boolean => { + if (!featureKey) { + featureAvailableCallback?.() + return true + } const { guardOnCloud = true, guardOnSelfHosted = true, diff --git a/frontend/src/scenes/actions/ActionEdit.tsx b/frontend/src/scenes/actions/ActionEdit.tsx index 9da6187358372..40623d01883fc 100644 --- a/frontend/src/scenes/actions/ActionEdit.tsx +++ b/frontend/src/scenes/actions/ActionEdit.tsx @@ -15,7 +15,6 @@ import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { compactNumber, uuid } from 'lib/utils' import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' -import { userLogic } from 'scenes/userLogic' import { tagsModel } from '~/models/tagsModel' import { ActionStepType, AvailableFeature } from '~/types' @@ -32,7 +31,6 @@ export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps): const { action, actionLoading, actionCount, actionCountLoading } = useValues(logic) const { submitAction, deleteAction } = useActions(logic) const { currentTeam } = useValues(teamLogic) - const { hasAvailableFeature } = useValues(userLogic) const { tags } = useValues(tagsModel) const slackEnabled = currentTeam?.slack_incoming_webhook @@ -96,7 +94,7 @@ export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps): className="action-description" compactButtons maxLength={600} // No limit on backend model, but enforce shortish description - paywall={!hasAvailableFeature(AvailableFeature.INGESTION_TAXONOMY)} + paywallFeature={AvailableFeature.INGESTION_TAXONOMY} /> )} diff --git a/frontend/src/scenes/dashboard/DashboardHeader.tsx b/frontend/src/scenes/dashboard/DashboardHeader.tsx index 231b3516f4674..4c15d085d0317 100644 --- a/frontend/src/scenes/dashboard/DashboardHeader.tsx +++ b/frontend/src/scenes/dashboard/DashboardHeader.tsx @@ -308,7 +308,11 @@ export function DashboardHeader(): JSX.Element | null { multiline name="description" markdown - value={dashboard.description || ''} + value={ + (hasAvailableFeature(AvailableFeature.TEAM_COLLABORATION) && + dashboard.description) || + '' + } placeholder="Description (optional)" onSave={(value) => updateDashboard({ id: dashboard.id, description: value, allowUndo: true }) @@ -316,7 +320,7 @@ export function DashboardHeader(): JSX.Element | null { saveOnBlur={true} compactButtons mode={!canEditDashboard ? 'view' : undefined} - paywall={!hasAvailableFeature(AvailableFeature.TEAM_COLLABORATION)} + paywallFeature={AvailableFeature.TEAM_COLLABORATION} /> )} {dashboard?.tags && ( diff --git a/frontend/src/scenes/data-management/definition/DefinitionView.tsx b/frontend/src/scenes/data-management/definition/DefinitionView.tsx index 93be7c2e0910c..f307d61f7d13a 100644 --- a/frontend/src/scenes/data-management/definition/DefinitionView.tsx +++ b/frontend/src/scenes/data-management/definition/DefinitionView.tsx @@ -17,7 +17,6 @@ import { definitionLogic, DefinitionLogicProps } from 'scenes/data-management/de import { EventDefinitionProperties } from 'scenes/data-management/events/EventDefinitionProperties' import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { userLogic } from 'scenes/userLogic' import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils' import { Query } from '~/queries/Query/Query' @@ -37,7 +36,6 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element { const { definition, definitionLoading, definitionMissing, hasTaxonomyFeatures, singular, isEvent, isProperty } = useValues(logic) const { deleteDefinition } = useActions(logic) - const { hasAvailableFeature } = useValues(userLogic) if (definitionLoading) { return @@ -146,7 +144,7 @@ export function DefinitionView(props: DefinitionLogicProps = {}): JSX.Element { className="definition-description" compactButtons maxLength={600} - paywall={!hasAvailableFeature(AvailableFeature.INGESTION_TAXONOMY)} + paywallFeature={AvailableFeature.INGESTION_TAXONOMY} /> )} {canEditInsight ? ( diff --git a/posthog/api/dashboards/dashboard.py b/posthog/api/dashboards/dashboard.py index 8524ab8618b4b..100e8745b8db1 100644 --- a/posthog/api/dashboards/dashboard.py +++ b/posthog/api/dashboards/dashboard.py @@ -7,7 +7,6 @@ from django.utils.timezone import now from rest_framework import exceptions, serializers, viewsets from rest_framework.decorators import action -from rest_framework.exceptions import PermissionDenied from rest_framework.permissions import SAFE_METHODS, BasePermission from rest_framework.request import Request from rest_framework.response import Response @@ -22,14 +21,12 @@ from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.api.tagged_item import TaggedItemSerializerMixin, TaggedItemViewSetMixin -from posthog.constants import AvailableFeature from posthog.event_usage import report_user_action from posthog.helpers import create_dashboard_from_template from posthog.helpers.dashboard_templates import create_from_template from posthog.models import Dashboard, DashboardTile, Insight, Text from posthog.models.dashboard_templates import DashboardTemplate from posthog.models.tagged_item import TaggedItem -from posthog.models.team.team import check_is_feature_available_for_team from posthog.models.user import User from posthog.user_permissions import UserPermissionsSerializerMixin @@ -158,13 +155,6 @@ class Meta: ] read_only_fields = ["creation_mode", "effective_restriction_level", "is_shared"] - def validate_description(self, value: str) -> str: - if value and not check_is_feature_available_for_team( - self.context["team_id"], AvailableFeature.TEAM_COLLABORATION - ): - raise PermissionDenied("You must have paid for dashboard collaboration to set the dashboard description") - return value - def validate_filters(self, value) -> Dict: if not isinstance(value, dict): raise serializers.ValidationError("Filters must be a dictionary") From cca6ccb218c66bfadc5d974842d9ed7c546aa8ec Mon Sep 17 00:00:00 2001 From: Ben White Date: Thu, 21 Mar 2024 11:04:59 +0100 Subject: [PATCH 14/51] fix: Loop requests in toolbar (#21061) --- frontend/src/toolbar/actions/ActionsListView.tsx | 6 ------ frontend/src/toolbar/actions/ActionsToolbarMenu.tsx | 7 ++++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/toolbar/actions/ActionsListView.tsx b/frontend/src/toolbar/actions/ActionsListView.tsx index 6054c0bc65241..fb999ed600949 100644 --- a/frontend/src/toolbar/actions/ActionsListView.tsx +++ b/frontend/src/toolbar/actions/ActionsListView.tsx @@ -1,7 +1,6 @@ import { Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { Spinner } from 'lib/lemon-ui/Spinner' -import { useEffect } from 'react' import { actionsLogic } from '~/toolbar/actions/actionsLogic' import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' @@ -13,13 +12,8 @@ interface ActionsListViewProps { export function ActionsListView({ actions }: ActionsListViewProps): JSX.Element { const { allActionsLoading, searchTerm } = useValues(actionsLogic) - const { getActions } = useActions(actionsLogic) const { selectAction } = useActions(actionsTabLogic) - useEffect(() => { - getActions() - }, []) - return (
{actions.length ? ( diff --git a/frontend/src/toolbar/actions/ActionsToolbarMenu.tsx b/frontend/src/toolbar/actions/ActionsToolbarMenu.tsx index ecbb3800c9260..87ff37b719ee8 100644 --- a/frontend/src/toolbar/actions/ActionsToolbarMenu.tsx +++ b/frontend/src/toolbar/actions/ActionsToolbarMenu.tsx @@ -5,6 +5,7 @@ import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonInput } from 'lib/lemon-ui/LemonInput' import { Link } from 'lib/lemon-ui/Link' import { Spinner } from 'lib/lemon-ui/Spinner' +import { useEffect } from 'react' import { urls } from 'scenes/urls' import { ActionsEditingToolbarMenu } from '~/toolbar/actions/ActionsEditingToolbarMenu' @@ -16,13 +17,17 @@ import { toolbarConfigLogic } from '~/toolbar/toolbarConfigLogic' const ActionsListToolbarMenu = (): JSX.Element => { const { searchTerm } = useValues(actionsLogic) - const { setSearchTerm } = useActions(actionsLogic) + const { setSearchTerm, getActions } = useActions(actionsLogic) const { newAction } = useActions(actionsTabLogic) const { allActions, sortedActions, allActionsLoading } = useValues(actionsLogic) const { apiURL } = useValues(toolbarConfigLogic) + useEffect(() => { + getActions() + }, []) + return ( From 6741039ec10a165d59fb9810b7eb44d1805b5508 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Thu, 21 Mar 2024 11:12:29 +0100 Subject: [PATCH 15/51] feat(blobby): run overflow detection and report result as a gauge (#21046) --- plugin-server/src/config/config.ts | 3 ++ .../services/overflow-detection.ts | 45 +++++++++++++++++++ .../session-recordings-consumer.ts | 13 ++++++ .../session-recording/types.ts | 1 + .../session-recording/utils.ts | 2 + plugin-server/src/types.ts | 4 ++ .../__snapshots__/utils.test.ts.snap | 4 ++ .../session-recording/utils.test.ts | 12 ++--- 8 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 plugin-server/src/main/ingestion-queues/session-recording/services/overflow-detection.ts diff --git a/plugin-server/src/config/config.ts b/plugin-server/src/config/config.ts index 8e9b50afb9528..b5fa1ce899f63 100644 --- a/plugin-server/src/config/config.ts +++ b/plugin-server/src/config/config.ts @@ -163,6 +163,9 @@ export function getDefaultConfig(): PluginsServerConfig { SESSION_RECORDING_DEBUG_PARTITION: undefined, SESSION_RECORDING_KAFKA_DEBUG: undefined, SESSION_RECORDING_MAX_PARALLEL_FLUSHES: 10, + SESSION_RECORDING_OVERFLOW_ENABLED: false, + SESSION_RECORDING_OVERFLOW_BUCKET_REPLENISH_RATE: 1_000_000, // 1MB/second uncompressed, sustained + SESSION_RECORDING_OVERFLOW_BUCKET_CAPACITY: 100_000_000, // 100MB burst } } diff --git a/plugin-server/src/main/ingestion-queues/session-recording/services/overflow-detection.ts b/plugin-server/src/main/ingestion-queues/session-recording/services/overflow-detection.ts new file mode 100644 index 0000000000000..8b478b781bc95 --- /dev/null +++ b/plugin-server/src/main/ingestion-queues/session-recording/services/overflow-detection.ts @@ -0,0 +1,45 @@ +import LRUCache from 'lru-cache' +import { Gauge } from 'prom-client' + +import { Limiter } from '../../../../utils/token-bucket' + +export enum OverflowState { + Okay, + Triggered, // Recently triggered the overflow detection + Cooldown, // Already triggered the overflow detection earlier than cooldownSeconds +} + +export const overflowTriggeredGauge = new Gauge({ + name: 'overflow_detection_triggered_total', + help: 'Number of entities that triggered overflow detection.', +}) + +/** + * OverflowDetection handles consumer-side detection of hot partitions by + * accounting for data volumes per entity (a session_id, a distinct_id...). + * + * The first time that the observed spike crosses the thresholds set via burstCapacity + * and replenishRate, observe returns Triggered. Subsequent calls will return Cooldown + * until cooldownSeconds is reached. + */ +export class OverflowDetection { + private limiter: Limiter + private triggered: LRUCache + + constructor(burstCapacity: number, replenishRate: number, cooldownSeconds: number) { + this.limiter = new Limiter(burstCapacity, replenishRate) + this.triggered = new LRUCache({ max: 1_000_000, maxAge: cooldownSeconds * 1000 }) + } + + public observe(key: string, quantity: number, now?: number): OverflowState { + if (this.triggered.has(key)) { + return OverflowState.Cooldown + } + if (this.limiter.consume(key, quantity, now)) { + return OverflowState.Okay + } + this.triggered.set(key, true) + overflowTriggeredGauge.inc(1) + return OverflowState.Triggered + } +} diff --git a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer.ts b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer.ts index 30aaab4a023d5..491044652d80f 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer.ts @@ -20,6 +20,7 @@ import { addSentryBreadcrumbsEventListeners } from '../kafka-metrics' import { eventDroppedCounter } from '../metrics' import { ConsoleLogsIngester } from './services/console-logs-ingester' import { OffsetHighWaterMarker } from './services/offset-high-water-marker' +import { OverflowDetection } from './services/overflow-detection' import { RealtimeManager } from './services/realtime-manager' import { ReplayEventsIngester } from './services/replay-events-ingester' import { BUCKETS_KB_WRITTEN, SessionManager } from './services/session-manager' @@ -128,6 +129,7 @@ export class SessionRecordingIngester { sessionHighWaterMarker: OffsetHighWaterMarker persistentHighWaterMarker: OffsetHighWaterMarker realtimeManager: RealtimeManager + overflowDetection?: OverflowDetection replayEventsIngester?: ReplayEventsIngester consoleLogsIngester?: ConsoleLogsIngester batchConsumer?: BatchConsumer @@ -160,6 +162,14 @@ export class SessionRecordingIngester { this.realtimeManager = new RealtimeManager(this.redisPool, this.config) + if (globalServerConfig.SESSION_RECORDING_OVERFLOW_ENABLED) { + this.overflowDetection = new OverflowDetection( + globalServerConfig.SESSION_RECORDING_OVERFLOW_BUCKET_CAPACITY, + globalServerConfig.SESSION_RECORDING_OVERFLOW_BUCKET_REPLENISH_RATE, + 24 * 3600 // One day + ) + } + // We create a hash of the cluster to use as a unique identifier for the high-water marks // This enables us to swap clusters without having to worry about resetting the high-water marks const kafkaClusterIdentifier = crypto.createHash('md5').update(this.config.KAFKA_HOSTS).digest('hex') @@ -275,6 +285,9 @@ export class SessionRecordingIngester { return } + // TODO: update Redis if this triggers + this.overflowDetection?.observe(key, event.metadata.rawSize, event.metadata.timestamp) + if (!this.sessions[key]) { const { partition, topic } = event.metadata diff --git a/plugin-server/src/main/ingestion-queues/session-recording/types.ts b/plugin-server/src/main/ingestion-queues/session-recording/types.ts index 254e3f0897ee7..d61dadda9279e 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/types.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/types.ts @@ -6,6 +6,7 @@ export type IncomingRecordingMessage = { metadata: { topic: string partition: number + rawSize: number lowOffset: number highOffset: number timestamp: number diff --git a/plugin-server/src/main/ingestion-queues/session-recording/utils.ts b/plugin-server/src/main/ingestion-queues/session-recording/utils.ts index 4b4345d43b48d..53ce953e5bd92 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/utils.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/utils.ts @@ -225,6 +225,7 @@ export const parseKafkaMessage = async ( metadata: { partition: message.partition, topic: message.topic, + rawSize: message.size, lowOffset: message.offset, highOffset: message.offset, timestamp: message.timestamp, @@ -267,6 +268,7 @@ export const reduceRecordingMessages = (messages: IncomingRecordingMessage[]): I existingMessage.eventsByWindowId[windowId] = events } } + existingMessage.metadata.rawSize += clonedMessage.metadata.rawSize // Update the events ranges existingMessage.metadata.lowOffset = Math.min( diff --git a/plugin-server/src/types.ts b/plugin-server/src/types.ts index b8eeb5b296a9e..114547cfe605f 100644 --- a/plugin-server/src/types.ts +++ b/plugin-server/src/types.ts @@ -230,6 +230,10 @@ export interface PluginsServerConfig { // a single partition which will output many more log messages to the console // useful when that partition is lagging unexpectedly SESSION_RECORDING_DEBUG_PARTITION: string | undefined + // overflow detection, updating Redis for capture to move the traffic away + SESSION_RECORDING_OVERFLOW_ENABLED: boolean + SESSION_RECORDING_OVERFLOW_BUCKET_CAPACITY: number + SESSION_RECORDING_OVERFLOW_BUCKET_REPLENISH_RATE: number // Dedicated infra values SESSION_RECORDING_KAFKA_HOSTS: string | undefined diff --git a/plugin-server/tests/main/ingestion-queues/session-recording/__snapshots__/utils.test.ts.snap b/plugin-server/tests/main/ingestion-queues/session-recording/__snapshots__/utils.test.ts.snap index 9962eb544bc6d..87ca515b22bd6 100644 --- a/plugin-server/tests/main/ingestion-queues/session-recording/__snapshots__/utils.test.ts.snap +++ b/plugin-server/tests/main/ingestion-queues/session-recording/__snapshots__/utils.test.ts.snap @@ -33,6 +33,7 @@ Array [ "highOffset": 3, "lowOffset": 1, "partition": 1, + "rawSize": 12, "timestamp": 1, "topic": "the_topic", }, @@ -59,6 +60,7 @@ Array [ "highOffset": 4, "lowOffset": 4, "partition": 1, + "rawSize": 30, "timestamp": 4, "topic": "the_topic", }, @@ -85,6 +87,7 @@ Array [ "highOffset": 5, "lowOffset": 5, "partition": 1, + "rawSize": 31, "timestamp": 5, "topic": "the_topic", }, @@ -130,6 +133,7 @@ Object { "highOffset": 1, "lowOffset": 1, "partition": 1, + "rawSize": 42, "timestamp": 1, "topic": "the_topic", }, diff --git a/plugin-server/tests/main/ingestion-queues/session-recording/utils.test.ts b/plugin-server/tests/main/ingestion-queues/session-recording/utils.test.ts index b8e6dc59284e7..c5a3851486d93 100644 --- a/plugin-server/tests/main/ingestion-queues/session-recording/utils.test.ts +++ b/plugin-server/tests/main/ingestion-queues/session-recording/utils.test.ts @@ -57,7 +57,7 @@ describe('session-recording utils', () => { }) ), timestamp: 1, - size: 1, + size: 42, topic: 'the_topic', offset: 1, partition: 1, @@ -257,7 +257,7 @@ describe('session-recording utils', () => { distinct_id: '1', eventsRange: { start: 1, end: 1 }, eventsByWindowId: { window_1: [{ timestamp: 1, type: 1, data: {} }] }, - metadata: { lowOffset: 1, highOffset: 1, partition: 1, timestamp: 1, topic: 'the_topic' }, + metadata: { lowOffset: 1, highOffset: 1, partition: 1, timestamp: 1, topic: 'the_topic', rawSize: 5 }, session_id: '1', team_id: 1, snapshot_source: null, @@ -266,7 +266,7 @@ describe('session-recording utils', () => { distinct_id: '1', eventsRange: { start: 2, end: 2 }, eventsByWindowId: { window_1: [{ timestamp: 2, type: 2, data: {} }] }, - metadata: { lowOffset: 2, highOffset: 2, partition: 1, timestamp: 2, topic: 'the_topic' }, + metadata: { lowOffset: 2, highOffset: 2, partition: 1, timestamp: 2, topic: 'the_topic', rawSize: 4 }, session_id: '1', team_id: 1, snapshot_source: null, @@ -276,7 +276,7 @@ describe('session-recording utils', () => { distinct_id: '1', eventsRange: { start: 3, end: 3 }, eventsByWindowId: { window_2: [{ timestamp: 3, type: 3, data: {} }] }, - metadata: { lowOffset: 3, highOffset: 3, partition: 1, timestamp: 3, topic: 'the_topic' }, + metadata: { lowOffset: 3, highOffset: 3, partition: 1, timestamp: 3, topic: 'the_topic', rawSize: 3 }, session_id: '1', team_id: 1, snapshot_source: null, @@ -286,7 +286,7 @@ describe('session-recording utils', () => { distinct_id: '1', eventsRange: { start: 4, end: 4 }, eventsByWindowId: { window_1: [{ timestamp: 4, type: 4, data: {} }] }, - metadata: { lowOffset: 4, highOffset: 4, partition: 1, timestamp: 4, topic: 'the_topic' }, + metadata: { lowOffset: 4, highOffset: 4, partition: 1, timestamp: 4, topic: 'the_topic', rawSize: 30 }, session_id: '1', team_id: 2, snapshot_source: null, @@ -296,7 +296,7 @@ describe('session-recording utils', () => { distinct_id: '1', eventsRange: { start: 5, end: 5 }, eventsByWindowId: { window_1: [{ timestamp: 5, type: 5, data: {} }] }, - metadata: { lowOffset: 5, highOffset: 5, partition: 1, timestamp: 5, topic: 'the_topic' }, + metadata: { lowOffset: 5, highOffset: 5, partition: 1, timestamp: 5, topic: 'the_topic', rawSize: 31 }, session_id: '2', team_id: 1, snapshot_source: null, From 8082d7ce0e1fd62fe300b55f9d610ca7670c9317 Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Thu, 21 Mar 2024 11:28:12 +0000 Subject: [PATCH 16/51] feat(hogql): Add matched recordings to trends actors query (#21045) --- .../insights/views/BoldNumber/BoldNumber.tsx | 8 ++++++++ .../trends/persons-modal/PersonsModal.tsx | 1 + .../scenes/trends/viz/ActionsHorizontalBar.tsx | 4 ++++ .../src/scenes/trends/viz/ActionsLineGraph.tsx | 4 ++++ frontend/src/scenes/trends/viz/ActionsPie.tsx | 4 ++++ .../__snapshots__/test_dashboard.ambr | 18 ++++++++++++++++++ .../insights/trends/trends_query_builder.py | 15 ++++++++++++--- 7 files changed, 51 insertions(+), 3 deletions(-) diff --git a/frontend/src/scenes/insights/views/BoldNumber/BoldNumber.tsx b/frontend/src/scenes/insights/views/BoldNumber/BoldNumber.tsx index 3642648298642..f18fbeebfefb1 100644 --- a/frontend/src/scenes/insights/views/BoldNumber/BoldNumber.tsx +++ b/frontend/src/scenes/insights/views/BoldNumber/BoldNumber.tsx @@ -120,6 +120,10 @@ export function BoldNumber({ showPersonsModal = true }: ChartParams): JSX.Elemen kind: NodeKind.InsightActorsQuery, source: query.source, }, + additionalSelect: { + value_at_data_point: 'event_count', + matched_recordings: 'matched_recordings', + }, }) } else if (resultSeries.persons?.url) { openPersonsModal({ @@ -213,6 +217,10 @@ function BoldNumberComparison({ showPersonsModal }: Pick : null} type="secondary" + status={matchedRecordings.length > 1 ? 'alt' : undefined} size="small" > {matchedRecordings.length > 1 ? `${matchedRecordings.length} recordings` : 'View recording'} diff --git a/frontend/src/scenes/trends/viz/ActionsHorizontalBar.tsx b/frontend/src/scenes/trends/viz/ActionsHorizontalBar.tsx index 03743f0c4dd29..ded94e7815490 100644 --- a/frontend/src/scenes/trends/viz/ActionsHorizontalBar.tsx +++ b/frontend/src/scenes/trends/viz/ActionsHorizontalBar.tsx @@ -115,6 +115,10 @@ export function ActionsHorizontalBar({ showPersonsModal = true }: ChartParams): kind: NodeKind.InsightActorsQuery, source: query.source, }, + additionalSelect: { + value_at_data_point: 'event_count', + matched_recordings: 'matched_recordings', + }, }) } else if (selectedUrl) { openPersonsModal({ diff --git a/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx b/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx index b930608587e22..b7f3ba8a46d0f 100644 --- a/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx +++ b/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx @@ -152,6 +152,10 @@ export function ActionsLineGraph({ breakdown: dataset.breakdown_value, compare: dataset.compare_label, }, + additionalSelect: { + value_at_data_point: 'event_count', + matched_recordings: 'matched_recordings', + }, }) } else { const datasetUrls = urlsForDatasets( diff --git a/frontend/src/scenes/trends/viz/ActionsPie.tsx b/frontend/src/scenes/trends/viz/ActionsPie.tsx index 86804b11e487f..839e06f0d71b6 100644 --- a/frontend/src/scenes/trends/viz/ActionsPie.tsx +++ b/frontend/src/scenes/trends/viz/ActionsPie.tsx @@ -118,6 +118,10 @@ export function ActionsPie({ kind: NodeKind.InsightActorsQuery, source: query.source, }, + additionalSelect: { + value_at_data_point: 'event_count', + matched_recordings: 'matched_recordings', + }, }) } else if (selectedUrl) { openPersonsModal({ diff --git a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr index 642602f396f8d..cccef08bc4a1f 100644 --- a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr +++ b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr @@ -11959,6 +11959,24 @@ 5 /* ... */) /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ ''' # --- +# name: TestDashboard.test_retrieve_dashboard_list.33 + ''' + SELECT "posthog_sharingconfiguration"."id", + "posthog_sharingconfiguration"."team_id", + "posthog_sharingconfiguration"."dashboard_id", + "posthog_sharingconfiguration"."insight_id", + "posthog_sharingconfiguration"."recording_id", + "posthog_sharingconfiguration"."created_at", + "posthog_sharingconfiguration"."enabled", + "posthog_sharingconfiguration"."access_token" + FROM "posthog_sharingconfiguration" + WHERE "posthog_sharingconfiguration"."dashboard_id" IN (1, + 2, + 3, + 4, + 5 /* ... */) /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ + ''' +# --- # name: TestDashboard.test_retrieve_dashboard_list.4 ''' SELECT "posthog_dashboardtile"."id" diff --git a/posthog/hogql_queries/insights/trends/trends_query_builder.py b/posthog/hogql_queries/insights/trends/trends_query_builder.py index ed5d867b48b75..4a2536750cb2c 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_builder.py +++ b/posthog/hogql_queries/insights/trends/trends_query_builder.py @@ -74,8 +74,12 @@ def build_actors_query( return parse_select( """ - SELECT DISTINCT actor_id + SELECT + actor_id, + count() as event_count, + groupUniqArray(100)((timestamp, uuid, $session_id, $window_id)) as matching_events FROM {subquery} + GROUP BY actor_id """, placeholders={ "subquery": self._get_events_subquery( @@ -225,8 +229,13 @@ def _get_events_subquery( # TODO: Move this logic into the below branches when working on adding breakdown support for the person modal if is_actors_query: - default_query.select = [ast.Alias(alias="actor_id", expr=self._aggregation_operation.actor_id())] - default_query.distinct = True + default_query.select = [ + ast.Alias(alias="actor_id", expr=self._aggregation_operation.actor_id()), + ast.Field(chain=["e", "timestamp"]), + ast.Field(chain=["e", "uuid"]), + ast.Field(chain=["e", "$session_id"]), + ast.Field(chain=["e", "$window_id"]), + ] default_query.group_by = [] # No breakdowns and no complex series aggregation From 3f93d5fdaf6fdfa14bd17155b91ffb88853d1b1c Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 21 Mar 2024 12:16:07 +0000 Subject: [PATCH 17/51] chore(ci): use depot 4 cpu runner (#21066) chore: use depot 4 cpu runner --- .github/workflows/ci-backend-depot.yml | 10 +++++----- .github/workflows/ci-e2e-depot.yml | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-backend-depot.yml b/.github/workflows/ci-backend-depot.yml index e555082455c9f..3cf935ced141e 100644 --- a/.github/workflows/ci-backend-depot.yml +++ b/.github/workflows/ci-backend-depot.yml @@ -39,7 +39,7 @@ jobs: # Job to decide if we should run backend ci # See https://github.com/dorny/paths-filter#conditional-execution for more details changes: - runs-on: depot-ubuntu-latest + runs-on: depot-ubuntu-latest-4 timeout-minutes: 5 if: github.repository == 'PostHog/posthog' name: Determine need to run backend checks @@ -90,7 +90,7 @@ jobs: timeout-minutes: 30 name: Python code quality checks - runs-on: depot-ubuntu-latest + runs-on: depot-ubuntu-latest-4 steps: # If this run wasn't initiated by the bot (meaning: snapshot update) and we've determined @@ -174,7 +174,7 @@ jobs: timeout-minutes: 10 name: Validate Django migrations - runs-on: depot-ubuntu-latest + runs-on: depot-ubuntu-latest-4 steps: - uses: actions/checkout@v3 @@ -237,7 +237,7 @@ jobs: timeout-minutes: 30 name: Django tests – ${{ matrix.segment }} (persons-on-events ${{ matrix.person-on-events && 'on' || 'off' }}), Py ${{ matrix.python-version }}, ${{ matrix.clickhouse-server-image }} (${{matrix.group}}/${{ matrix.concurrency }}) (depot) - runs-on: depot-ubuntu-latest + runs-on: depot-ubuntu-latest-4 strategy: fail-fast: false @@ -318,7 +318,7 @@ jobs: matrix: clickhouse-server-image: ['clickhouse/clickhouse-server:23.11.2.11-alpine'] if: needs.changes.outputs.backend == 'true' - runs-on: depot-ubuntu-latest + runs-on: depot-ubuntu-latest-4 steps: - name: 'Checkout repo' uses: actions/checkout@v3 diff --git a/.github/workflows/ci-e2e-depot.yml b/.github/workflows/ci-e2e-depot.yml index 2134d4d70f18f..4985dac9d746a 100644 --- a/.github/workflows/ci-e2e-depot.yml +++ b/.github/workflows/ci-e2e-depot.yml @@ -14,7 +14,7 @@ concurrency: jobs: changes: - runs-on: depot-ubuntu-latest + runs-on: depot-ubuntu-latest-4 timeout-minutes: 5 if: github.repository == 'PostHog/posthog' name: Determine need to run E2E checks @@ -55,7 +55,7 @@ jobs: chunks: needs: changes name: Cypress preparation - runs-on: depot-ubuntu-latest + runs-on: depot-ubuntu-latest-4 timeout-minutes: 5 outputs: chunks: ${{ steps.chunk.outputs.chunks }} @@ -70,7 +70,7 @@ jobs: container: name: Build and cache container image - runs-on: depot-ubuntu-latest + runs-on: depot-ubuntu-latest-4 timeout-minutes: 60 needs: [changes] permissions: @@ -94,7 +94,7 @@ jobs: cypress: name: Cypress E2E tests (${{ strategy.job-index }}) (depot) - runs-on: depot-ubuntu-latest + runs-on: depot-ubuntu-latest-4 timeout-minutes: 60 needs: [chunks, changes, container] permissions: From c491bf2cf5c7cc4eac862a15ab5c5b3fbf6d385f Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Thu, 21 Mar 2024 12:44:12 +0000 Subject: [PATCH 18/51] fix(hogql): Optimize querying for date frame in trends actors query (#21060) --- mypy-baseline.txt | 4 +- .../insights/insight_actors_query_runner.py | 4 +- .../insights/trends/trends_query_builder.py | 60 ++++++++----------- .../insights/trends/trends_query_runner.py | 2 +- .../hogql_queries/utils/query_date_range.py | 47 +++++++++------ 5 files changed, 59 insertions(+), 58 deletions(-) diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 781ad2980830b..2143c119ea5c2 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -377,11 +377,11 @@ posthog/hogql_queries/insights/trends/trends_query_runner.py:0: error: Signature posthog/hogql_queries/insights/trends/trends_query_runner.py:0: note: Superclass: posthog/hogql_queries/insights/trends/trends_query_runner.py:0: note: def to_actors_query(self) -> SelectQuery | SelectUnionQuery posthog/hogql_queries/insights/trends/trends_query_runner.py:0: note: Subclass: -posthog/hogql_queries/insights/trends/trends_query_runner.py:0: note: def to_actors_query(self, time_frame: str | int | None, series_index: int, breakdown_value: str | int | None = ..., compare: Compare | None = ...) -> SelectQuery | SelectUnionQuery +posthog/hogql_queries/insights/trends/trends_query_runner.py:0: note: def to_actors_query(self, time_frame: str | None, series_index: int, breakdown_value: str | int | None = ..., compare: Compare | None = ...) -> SelectQuery | SelectUnionQuery posthog/hogql_queries/insights/trends/trends_query_runner.py:0: note: Superclass: posthog/hogql_queries/insights/trends/trends_query_runner.py:0: note: def to_actors_query(self) -> SelectQuery | SelectUnionQuery posthog/hogql_queries/insights/trends/trends_query_runner.py:0: note: Subclass: -posthog/hogql_queries/insights/trends/trends_query_runner.py:0: note: def to_actors_query(self, time_frame: str | int | None, series_index: int, breakdown_value: str | int | None = ..., compare: Compare | None = ...) -> SelectQuery | SelectUnionQuery +posthog/hogql_queries/insights/trends/trends_query_runner.py:0: note: def to_actors_query(self, time_frame: str | None, series_index: int, breakdown_value: str | int | None = ..., compare: Compare | None = ...) -> SelectQuery | SelectUnionQuery posthog/hogql_queries/insights/trends/trends_query_runner.py:0: error: Statement is unreachable [unreachable] posthog/hogql_queries/insights/trends/trends_query_runner.py:0: error: Argument 1 to "_event_property" of "TrendsQueryRunner" has incompatible type "str | float | list[str | float] | None"; expected "str" [arg-type] posthog/hogql_queries/insights/retention_query_runner.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "Call") [assignment] diff --git a/posthog/hogql_queries/insights/insight_actors_query_runner.py b/posthog/hogql_queries/insights/insight_actors_query_runner.py index 782dd5b054a0e..a0e38abae1776 100644 --- a/posthog/hogql_queries/insights/insight_actors_query_runner.py +++ b/posthog/hogql_queries/insights/insight_actors_query_runner.py @@ -1,5 +1,5 @@ from datetime import timedelta -from typing import cast +from typing import cast, Optional from posthog.hogql import ast from posthog.hogql.query import execute_hogql_query @@ -37,7 +37,7 @@ def to_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: trends_runner = cast(TrendsQueryRunner, self.source_runner) query = cast(InsightActorsQuery, self.query) return trends_runner.to_actors_query( - time_frame=query.day, + time_frame=cast(Optional[str], query.day), # Other runner accept day as int, but not this one series_index=query.series or 0, breakdown_value=query.breakdown, compare=query.compare, diff --git a/posthog/hogql_queries/insights/trends/trends_query_builder.py b/posthog/hogql_queries/insights/trends/trends_query_builder.py index 4a2536750cb2c..7be735d3b0a8b 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_builder.py +++ b/posthog/hogql_queries/insights/trends/trends_query_builder.py @@ -68,7 +68,7 @@ def build_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: return full_query def build_actors_query( - self, time_frame: Optional[str | int] = None, breakdown_filter: Optional[str | int] = None + self, time_frame: Optional[str] = None, breakdown_filter: Optional[str | int] = None ) -> ast.SelectQuery | ast.SelectUnionQuery: breakdown = self._breakdown(is_actors_query=True, breakdown_values_override=breakdown_filter) @@ -169,7 +169,7 @@ def _get_events_subquery( is_actors_query: bool, breakdown: Breakdown, breakdown_values_override: Optional[str | int] = None, - actors_query_time_frame: Optional[str | int] = None, + actors_query_time_frame: Optional[str] = None, ) -> ast.SelectQuery: day_start = ast.Alias( alias="day_start", @@ -186,31 +186,16 @@ def _get_events_subquery( actors_query_time_frame=actors_query_time_frame, ) - default_query = cast( - ast.SelectQuery, - parse_select( - """ - SELECT - {aggregation_operation} AS total - FROM {table} AS e - WHERE {events_filter} - """ - if isinstance(self.series, DataWarehouseNode) - else """ - SELECT - {aggregation_operation} AS total - FROM {table} AS e - SAMPLE {sample} - WHERE {events_filter} - """, - placeholders={ - "table": self._table_expr, - "events_filter": events_filter, - "aggregation_operation": self._aggregation_operation.select_aggregation(), - "sample": self._sample_value(), - }, - ), + default_query = ast.SelectQuery( + select=[ast.Alias(alias="total", expr=self._aggregation_operation.select_aggregation())], + select_from=ast.JoinExpr(table=self._table_expr, alias="e"), + where=events_filter, ) + if not isinstance(self.series, DataWarehouseNode): + assert default_query.select_from is not None + default_query.select_from.sample = ast.SampleExpr( + sample_value=self._sample_value(), + ) default_query.group_by = [] @@ -463,20 +448,27 @@ def _events_filter( breakdown: Breakdown | None, ignore_breakdowns: bool = False, breakdown_values_override: Optional[str | int] = None, - actors_query_time_frame: Optional[str | int] = None, + actors_query_time_frame: Optional[str] = None, ) -> ast.Expr: series = self.series filters: List[ast.Expr] = [] # Dates if is_actors_query and actors_query_time_frame is not None: - to_start_of_time_frame = f"toStartOf{self.query_date_range.interval_name.capitalize()}" - filters.append( - ast.CompareOperation( - left=ast.Call(name=to_start_of_time_frame, args=[ast.Field(chain=["timestamp"])]), - op=ast.CompareOperationOp.Eq, - right=ast.Call(name="toDateTime", args=[ast.Constant(value=actors_query_time_frame)]), - ) + actors_from, actors_to = self.query_date_range.interval_bounds_from_str(actors_query_time_frame) + filters.extend( + [ + ast.CompareOperation( + left=ast.Field(chain=["timestamp"]), + op=ast.CompareOperationOp.GtEq, + right=ast.Constant(value=actors_from), + ), + ast.CompareOperation( + left=ast.Field(chain=["timestamp"]), + op=ast.CompareOperationOp.Lt, + right=ast.Constant(value=actors_to), + ), + ] ) elif not self._aggregation_operation.requires_query_orchestration(): filters.extend( diff --git a/posthog/hogql_queries/insights/trends/trends_query_runner.py b/posthog/hogql_queries/insights/trends/trends_query_runner.py index 29d29b55e8b0f..40f26664c2585 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/trends_query_runner.py @@ -142,7 +142,7 @@ def to_queries(self) -> List[ast.SelectQuery | ast.SelectUnionQuery]: def to_actors_query( self, - time_frame: Optional[str | int], + time_frame: Optional[str], series_index: int, breakdown_value: Optional[str | int] = None, compare: Optional[Compare] = None, diff --git a/posthog/hogql_queries/utils/query_date_range.py b/posthog/hogql_queries/utils/query_date_range.py index f2e5cef3d82a3..5453d878b4017 100644 --- a/posthog/hogql_queries/utils/query_date_range.py +++ b/posthog/hogql_queries/utils/query_date_range.py @@ -4,6 +4,7 @@ from typing import Literal, Optional, Dict, List from zoneinfo import ZoneInfo +from dateutil.parser import parse from dateutil.relativedelta import relativedelta from posthog.hogql.errors import HogQLException @@ -116,36 +117,39 @@ def interval_type(self) -> IntervalType: def interval_name(self) -> Literal["hour", "day", "week", "month"]: return self.interval_type.name - def all_values(self) -> List[str]: - start: datetime = self.date_from() - end: datetime = self.date_to() - interval = self.interval_name - - if interval == "hour": - start = start.replace(minute=0, second=0, microsecond=0) - elif interval == "day": - start = start.replace(hour=0, minute=0, second=0, microsecond=0) - elif interval == "week": + def align_with_interval(self, start: datetime) -> datetime: + if self.interval_name == "hour": + return start.replace(minute=0, second=0, microsecond=0) + elif self.interval_name == "day": + return start.replace(hour=0, minute=0, second=0, microsecond=0) + elif self.interval_name == "week": start = start.replace(hour=0, minute=0, second=0, microsecond=0) week_start_alignment_days = start.isoweekday() % 7 if self._team.week_start_day == WeekStartDay.MONDAY: week_start_alignment_days = start.weekday() start -= timedelta(days=week_start_alignment_days) - elif interval == "month": - start = start.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + return start + elif self.interval_name == "month": + return start.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + + def interval_relativedelta(self) -> relativedelta: + return relativedelta( + days=1 if self.interval_name == "day" else 0, + weeks=1 if self.interval_name == "week" else 0, + months=1 if self.interval_name == "month" else 0, + hours=1 if self.interval_name == "hour" else 0, + ) + def all_values(self) -> List[str]: + start = self.align_with_interval(self.date_from()) + end: datetime = self.date_to() values: List[str] = [] while start <= end: - if interval == "hour": + if self.interval_name == "hour": values.append(start.strftime("%Y-%m-%d %H:%M:%S")) else: values.append(start.strftime("%Y-%m-%d")) - start += relativedelta( - days=1 if interval == "day" else 0, - weeks=1 if interval == "week" else 0, - months=1 if interval == "month" else 0, - hours=1 if interval == "hour" else 0, - ) + start += self.interval_relativedelta() return values def date_to_as_hogql(self) -> ast.Expr: @@ -257,6 +261,11 @@ def to_placeholders(self) -> Dict[str, ast.Expr]: else self.date_from_as_hogql(), } + def interval_bounds_from_str(self, time_frame: str) -> tuple[datetime, datetime]: + date_from = parse(time_frame, tzinfos={None: self._team.timezone_info}) + date_to = date_from + self.interval_relativedelta() + return date_from, date_to + class QueryDateRangeWithIntervals(QueryDateRange): def __init__( From 4c4a3fc02c5784d5ffcd7959eaff2d727c4037c1 Mon Sep 17 00:00:00 2001 From: Tom Owers Date: Thu, 21 Mar 2024 15:10:07 +0100 Subject: [PATCH 19/51] fix(hogql): Allow missing aggregate errors to reach the UI (#21070) Allow missing aggregate errors to reach the UI --- posthog/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posthog/errors.py b/posthog/errors.py index 39f07be762d00..afa0cdd8648e7 100644 --- a/posthog/errors.py +++ b/posthog/errors.py @@ -151,7 +151,7 @@ def look_up_error_code_meta(error: ServerException) -> ErrorCodeMeta: 60: ErrorCodeMeta("UNKNOWN_TABLE"), 61: ErrorCodeMeta("ONLY_FILTER_COLUMN_IN_BLOCK"), 62: ErrorCodeMeta("SYNTAX_ERROR"), - 63: ErrorCodeMeta("UNKNOWN_AGGREGATE_FUNCTION"), + 63: ErrorCodeMeta("UNKNOWN_AGGREGATE_FUNCTION", user_safe=True), 64: ErrorCodeMeta("CANNOT_READ_AGGREGATE_FUNCTION_FROM_TEXT"), 65: ErrorCodeMeta("CANNOT_WRITE_AGGREGATE_FUNCTION_AS_TEXT"), 66: ErrorCodeMeta("NOT_A_COLUMN"), From 8016a383ddda5e6fda866a6ca25d95d5931e411f Mon Sep 17 00:00:00 2001 From: David Newell Date: Thu, 21 Mar 2024 14:57:50 +0000 Subject: [PATCH 20/51] chore: remove unused feature flag based prompt (#21038) --- frontend/src/layout/GlobalModals.tsx | 5 - frontend/src/lib/constants.tsx | 1 - frontend/src/lib/logic/newPrompt/Prompt.tsx | 123 ------------ frontend/src/lib/logic/newPrompt/prompt.scss | 14 -- .../lib/logic/newPrompt/prompt.stories.tsx | 60 ------ .../src/lib/logic/newPrompt/promptLogic.tsx | 179 ----------------- .../team_id/insights/dataTableEvents.json | 187 ------------------ .../scenes/events/__mocks__/eventsQuery.json | 10 - 8 files changed, 579 deletions(-) delete mode 100644 frontend/src/lib/logic/newPrompt/Prompt.tsx delete mode 100644 frontend/src/lib/logic/newPrompt/prompt.scss delete mode 100644 frontend/src/lib/logic/newPrompt/prompt.stories.tsx delete mode 100644 frontend/src/lib/logic/newPrompt/promptLogic.tsx diff --git a/frontend/src/layout/GlobalModals.tsx b/frontend/src/layout/GlobalModals.tsx index 7ef5f0d546afb..1ef66f6c78899 100644 --- a/frontend/src/layout/GlobalModals.tsx +++ b/frontend/src/layout/GlobalModals.tsx @@ -1,9 +1,7 @@ import { LemonModal } from '@posthog/lemon-ui' import { actions, kea, path, reducers, useActions, useValues } from 'kea' -import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { HedgehogBuddyWithLogic } from 'lib/components/HedgehogBuddy/HedgehogBuddyWithLogic' import { UpgradeModal } from 'lib/components/UpgradeModal/UpgradeModal' -import { Prompt } from 'lib/logic/newPrompt/Prompt' import { Setup2FA } from 'scenes/authentication/Setup2FA' import { CreateOrganizationModal } from 'scenes/organization/CreateOrganizationModal' import { membersLogic } from 'scenes/organization/membersLogic' @@ -72,9 +70,6 @@ export function GlobalModals(): JSX.Element { /> )} - - - ) diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 9d667db41b2ed..53036d5c6c511 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -147,7 +147,6 @@ export const FEATURE_FLAGS = { QUERY_TIMINGS: 'query-timings', // owner: @mariusandra QUERY_ASYNC: 'query-async', // owner: @webjunkie POSTHOG_3000_NAV: 'posthog-3000-nav', // owner: @Twixes - ENABLE_PROMPTS: 'enable-prompts', // owner: @lharries HEDGEHOG_MODE: 'hedgehog-mode', // owner: @benjackwhite HEDGEHOG_MODE_DEBUG: 'hedgehog-mode-debug', // owner: @benjackwhite GENERIC_SIGNUP_BENEFITS: 'generic-signup-benefits', // experiment, owner: @raquelmsmith diff --git a/frontend/src/lib/logic/newPrompt/Prompt.tsx b/frontend/src/lib/logic/newPrompt/Prompt.tsx deleted file mode 100644 index 8392dfc95aa35..0000000000000 --- a/frontend/src/lib/logic/newPrompt/Prompt.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import './prompt.scss' - -import { LemonButton, LemonModal } from '@posthog/lemon-ui' -import clsx from 'clsx' -import { useActions, useValues } from 'kea' -import { FallbackCoverImage } from 'lib/components/FallbackCoverImage/FallbackCoverImage' - -import { PromptButtonType, PromptFlag, PromptPayload } from '~/types' - -import { promptLogic } from './promptLogic' - -export function ModalPrompt({ - payload, - closePrompt, - openPromptFlag, - inline = false, -}: { - payload: PromptPayload - closePrompt: (promptFlag: PromptFlag, buttonType: PromptButtonType) => void - openPromptFlag: PromptFlag - inline?: boolean -}): JSX.Element { - return ( - closePrompt(openPromptFlag, 'secondary')} - footer={ - (payload.secondaryButtonText || payload.primaryButtonText) && ( -
- closePrompt(openPromptFlag, 'secondary')} type="secondary"> - {payload.secondaryButtonText || 'Dismiss'} - - {payload.primaryButtonText && ( - closePrompt(openPromptFlag, 'primary')} type="primary"> - {payload.primaryButtonText} - - )} -
- ) - } - inline={inline} - > -
-
-
- -
-
- {payload.title &&

{payload.title}

} - - {payload.body && ( -
- )} -
- - ) -} - -export function PopupPrompt({ - payload, - openPromptFlag, - closePrompt, - inline = false, -}: { - payload: PromptPayload - openPromptFlag: PromptFlag - closePrompt: (promptFlag: PromptFlag, buttonType: PromptButtonType) => void - inline?: boolean -}): JSX.Element { - return ( -
- {payload.image && ( - - )} -
- {payload.title &&

{payload.title}

} - {payload.body && ( -
- )} -
-
-
- {payload?.secondaryButtonText && ( - closePrompt(openPromptFlag, 'secondary')} type="secondary"> - {payload.secondaryButtonText} - - )} - {payload.primaryButtonText && ( - closePrompt(openPromptFlag, 'primary')} type="primary"> - {payload.primaryButtonText} - - )} -
-
-
- ) -} - -export function Prompt(): JSX.Element { - const { payload, openPromptFlag } = useValues(promptLogic) - const { closePrompt } = useActions(promptLogic) - - if (!payload || !openPromptFlag) { - return <> - } - - if (payload.type === 'modal') { - return - } - - return -} diff --git a/frontend/src/lib/logic/newPrompt/prompt.scss b/frontend/src/lib/logic/newPrompt/prompt.scss deleted file mode 100644 index 86a2aad37d1cc..0000000000000 --- a/frontend/src/lib/logic/newPrompt/prompt.scss +++ /dev/null @@ -1,14 +0,0 @@ -.PromptPopup { - position: fixed; - right: 10px; - bottom: 10px; - z-index: 2000; - flex-direction: column; - min-width: 300px; - min-height: 100px; - padding-top: 5px; - background: white; - border: 1px solid #f0f0f0; - border-radius: 8px; - box-shadow: -6px 0 16px -8px rgb(0 0 0 / 8%), -9px 0 28px 0 rgb(0 0 0 / 5%), -12px 0 48px 16px rgb(0 0 0 / 3%); -} diff --git a/frontend/src/lib/logic/newPrompt/prompt.stories.tsx b/frontend/src/lib/logic/newPrompt/prompt.stories.tsx deleted file mode 100644 index 58eb6c9647db9..0000000000000 --- a/frontend/src/lib/logic/newPrompt/prompt.stories.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Meta } from '@storybook/react' -import { useActions } from 'kea' -import BlankDashboardHog from 'public/blank-dashboard-hog.png' - -import { PromptFlag, PromptPayload } from '~/types' - -import { ModalPrompt, PopupPrompt, Prompt } from './Prompt' -import { promptLogic } from './promptLogic' - -const meta: Meta = { - title: 'Components/Prompts', - component: Prompt, -} -export default meta -export function ModalPrompt_(): JSX.Element { - // Ideally we'd instead mock the feature flag and payload but I couldn't get that to work - const payload = { - title: 'New hedgehog spotted!', - body: "We have exciting news, there's a new hedge hog that has arrived!.", - image: BlankDashboardHog, - type: 'modal', - primaryButtonText: 'Join the search!', - primaryButtonURL: 'https://google.com', - } as PromptPayload - const openPromptFlag = { - flag: 'new-hedgehog', - payload: payload, - showingPrompt: true, - } as PromptFlag - const { closePrompt } = useActions(promptLogic) - - return ( -
- -
- ) -} - -export function PopupPrompt_(): JSX.Element { - const payload = { - title: 'New hedgehog spotted!', - body: "We have exciting news, there's a new hedge hog that has arrived!.", - image: BlankDashboardHog, - type: 'popup', - primaryButtonText: 'Join the search!', - primaryButtonURL: 'https://google.com', - } as PromptPayload - const openPromptFlag = { - flag: 'new-hedgehog', - payload: payload, - showingPrompt: true, - } as PromptFlag - const { closePrompt } = useActions(promptLogic) - - return ( -
- -
- ) -} diff --git a/frontend/src/lib/logic/newPrompt/promptLogic.tsx b/frontend/src/lib/logic/newPrompt/promptLogic.tsx deleted file mode 100644 index 064fcff1c78cd..0000000000000 --- a/frontend/src/lib/logic/newPrompt/promptLogic.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' -import { router } from 'kea-router' -import posthog from 'posthog-js' - -import { PromptButtonType, PromptFlag, PromptPayload } from '~/types' - -import { featureFlagLogic } from '../featureFlagLogic' -import type { promptLogicType } from './promptLogicType' - -const PROMPT_PREFIX = 'prompt' -const LAST_SEEN = 'last-seen' -const MINIMUM_DAYS_BETWEEN_PROMPTS = 1 - -function getFeatureSessionStorageKey(featureFlagName: string): string { - return `${PROMPT_PREFIX}-${featureFlagName}` -} - -function getLastSeenSessionStorageKey(): string { - return `${PROMPT_PREFIX}-${LAST_SEEN}` -} - -function hasSeenPromptRecently(): boolean { - const lastSeenPopup = localStorage.getItem(getLastSeenSessionStorageKey()) - const lastSeenPopupDate = lastSeenPopup ? new Date(lastSeenPopup) : null - const oneDayAgo = new Date() - oneDayAgo.setDate(oneDayAgo.getDate() - MINIMUM_DAYS_BETWEEN_PROMPTS) - - let seenRecently = false - - if (lastSeenPopupDate && lastSeenPopupDate > oneDayAgo) { - seenRecently = true - } - return seenRecently -} - -function shouldShowPopup(featureFlagName: string): boolean { - // The feature flag should be disabled for the user once the prompt has been closed through the user properties - // This is a second check for shorter-term preventing of the prompt from showing - const flagShownBefore = localStorage.getItem(getFeatureSessionStorageKey(featureFlagName)) - - const seenRecently = hasSeenPromptRecently() - - return !flagShownBefore && !seenRecently -} - -function sendPopupEvent( - event: string, - promptFlag: PromptFlag, - buttonType: PromptButtonType | undefined = undefined -): void { - const properties = { - flagName: promptFlag.flag, - flagPayload: promptFlag.payload, - } - - if (buttonType) { - properties['buttonPressed'] = buttonType - } - - posthog.capture(event, properties) -} - -export const promptLogic = kea([ - path(['lib', 'logic', 'newPrompt', 'promptLogic']), - actions({ - closePrompt: (promptFlag: PromptFlag, buttonType: PromptButtonType) => ({ promptFlag, buttonType }), - setPromptFlags: (promptFlags: PromptFlag[]) => ({ promptFlags }), - searchForValidFlags: true, - setOpenPromptFlag: (promptFlag: PromptFlag) => ({ promptFlag }), - // hide the prompt without sending an event or setting the localstorage - // used for when the user navigates away from the page - hidePromptWithoutSaving: (promptFlag: PromptFlag) => ({ promptFlag }), - }), - connect({ - actions: [featureFlagLogic, ['setFeatureFlags'], router, ['locationChanged']], - }), - reducers({ - promptFlags: [ - [] as PromptFlag[], - { - setPromptFlags: (_, { promptFlags }) => promptFlags, - setOpenPromptFlag: (promptFlags, { promptFlag }) => { - return promptFlags.map((flag: PromptFlag) => { - if (flag.flag === promptFlag.flag) { - return { ...flag, showingPrompt: true } - } - return flag - }) - }, - closePrompt: (promptFlags) => { - return promptFlags.map((flag: PromptFlag) => { - return { ...flag, showingPrompt: false } - }) - }, - hidePromptWithoutSaving: (promptFlags, { promptFlag }) => { - return promptFlags.map((flag: PromptFlag) => { - if (flag.flag === promptFlag.flag) { - return { ...flag, showingPrompt: false } - } - return flag - }) - }, - }, - ], - }), - listeners(({ actions, values }) => ({ - // TODO: on url change, check if there's a prompt to show - setFeatureFlags: async ({ flags }, breakpoint) => { - await breakpoint(100) - const promptFlags: PromptFlag[] = [] - flags.forEach((flag: string) => { - if (flag.startsWith(PROMPT_PREFIX) && posthog.isFeatureEnabled(flag)) { - const payload = posthog.getFeatureFlagPayload(flag) as PromptPayload - if (!payload || !payload.type) { - // indicates that it's not a valid prompt - return - } - promptFlags.push({ - flag, - payload, - showingPrompt: false, - }) - } - }) - actions.setPromptFlags(promptFlags) - actions.searchForValidFlags() - }, - searchForValidFlags: async () => { - for (const promptFlag of values.promptFlags) { - if (!promptFlag.payload.url_match || window.location.href.match(promptFlag.payload.url_match)) { - if (shouldShowPopup(promptFlag.flag)) { - actions.setOpenPromptFlag(promptFlag) - return // only show one prompt at a time - } - } - } - }, - setOpenPromptFlag: async ({ promptFlag }, breakpoint) => { - await breakpoint(1000) - sendPopupEvent('Prompt shown', promptFlag) - }, - closePrompt: async ({ promptFlag, buttonType }) => { - if (promptFlag) { - sendPopupEvent('Prompt closed', promptFlag, buttonType) - localStorage.setItem(getFeatureSessionStorageKey(promptFlag.flag), new Date().toDateString()) - localStorage.setItem(getLastSeenSessionStorageKey(), new Date().toDateString()) - posthog.people.set({ ['$' + promptFlag.flag]: new Date().toDateString() }) - - if (promptFlag?.payload.primaryButtonURL && buttonType === 'primary') { - window.open(promptFlag.payload.primaryButtonURL, '_blank') - } - } - }, - locationChanged: async (_, breakpoint) => { - await breakpoint(100) - if (values.openPromptFlag && values.openPromptFlag.payload.url_match) { - if (!window.location.href.match(values.openPromptFlag.payload.url_match)) { - actions.hidePromptWithoutSaving(values.openPromptFlag) - } - } - - actions.searchForValidFlags() - }, - })), - selectors({ - openPromptFlag: [ - (s) => [s.promptFlags], - (promptFlags) => { - return promptFlags.find((flag: PromptFlag) => flag.showingPrompt) - }, - ], - payload: [ - (s) => [s.openPromptFlag], - (openPromptFlag: PromptFlag) => { - return openPromptFlag?.payload - }, - ], - }), -]) diff --git a/frontend/src/mocks/fixtures/api/projects/team_id/insights/dataTableEvents.json b/frontend/src/mocks/fixtures/api/projects/team_id/insights/dataTableEvents.json index 638a1618d5fb2..f186af09cdff1 100644 --- a/frontend/src/mocks/fixtures/api/projects/team_id/insights/dataTableEvents.json +++ b/frontend/src/mocks/fixtures/api/projects/team_id/insights/dataTableEvents.json @@ -96,7 +96,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -136,7 +135,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -453,7 +451,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -493,7 +490,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -683,7 +679,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -723,7 +718,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -899,7 +893,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -939,7 +932,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -1119,7 +1111,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -1159,7 +1150,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -1422,7 +1412,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -1462,7 +1451,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -1800,7 +1788,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -1840,7 +1827,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -2146,7 +2132,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -2186,7 +2171,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -2542,7 +2526,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -2582,7 +2565,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -2758,7 +2740,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -2798,7 +2779,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -2975,7 +2955,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -3015,7 +2994,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -3371,7 +3349,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -3411,7 +3388,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -3588,7 +3564,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -3628,7 +3603,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -3818,7 +3792,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -3858,7 +3831,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -4036,7 +4008,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -4076,7 +4047,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -4254,7 +4224,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -4294,7 +4263,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -4470,7 +4438,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -4510,7 +4477,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -4879,7 +4845,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -4919,7 +4884,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -5109,7 +5073,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -5149,7 +5112,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -5325,7 +5287,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -5365,7 +5326,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -5694,7 +5654,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -5734,7 +5693,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -5912,7 +5870,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -5952,7 +5909,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -6128,7 +6084,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -6168,7 +6123,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -6348,7 +6302,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -6388,7 +6341,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -6566,7 +6518,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -6606,7 +6557,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -6908,7 +6858,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -6948,7 +6897,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -7130,7 +7078,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -7170,7 +7117,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -7346,7 +7292,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -7386,7 +7331,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -7564,7 +7508,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -7604,7 +7547,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -7890,7 +7832,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -7930,7 +7871,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -8108,7 +8048,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -8148,7 +8087,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -8326,7 +8264,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -8366,7 +8303,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -8544,7 +8480,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -8584,7 +8519,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -8761,7 +8695,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -8801,7 +8734,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -8979,7 +8911,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -9019,7 +8950,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -9197,7 +9127,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -9237,7 +9166,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -9415,7 +9343,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -9455,7 +9382,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -9633,7 +9559,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -9673,7 +9598,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -9709,7 +9633,6 @@ }, "$referrer": "$direct", "$referring_domain": "$direct", - "$feature_flag": "enable-prompts", "$feature_flag_response": true, "token": "phc_IdfzBh09RdfsZyvdjYbq8ml2NR0AD0SnFqcUl4Itwwp", "$session_id": "188906a04ee2c10-0132aa872fb6f9-1c525634-384000-188906a04ef1cdc", @@ -9938,7 +9861,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -9978,7 +9900,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -10164,7 +10085,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -10204,7 +10124,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -10391,7 +10310,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -10431,7 +10349,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -10609,7 +10526,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -10649,7 +10565,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -10825,7 +10740,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -10865,7 +10779,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -11041,7 +10954,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -11081,7 +10993,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -11257,7 +11168,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -11297,7 +11207,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -11584,7 +11493,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -11624,7 +11532,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -11805,7 +11712,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -11845,7 +11751,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -12021,7 +11926,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -12061,7 +11965,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -12239,7 +12142,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -12279,7 +12181,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -12457,7 +12358,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -12497,7 +12397,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -12675,7 +12574,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -12715,7 +12613,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -12893,7 +12790,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -12933,7 +12829,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -13110,7 +13005,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -13150,7 +13044,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -13328,7 +13221,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -13368,7 +13260,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -13546,7 +13437,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -13586,7 +13476,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -13764,7 +13653,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -13804,7 +13692,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -13840,7 +13727,6 @@ }, "$referrer": "$direct", "$referring_domain": "$direct", - "$feature_flag": "enable-prompts", "$feature_flag_response": true, "token": "phc_IdfzBh09RdfsZyvdjYbq8ml2NR0AD0SnFqcUl4Itwwp", "$session_id": "1889068fbce1060-03042ca0405c1f-1c525634-384000-1889068fbcf1371", @@ -13982,7 +13868,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -14022,7 +13907,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -14287,7 +14171,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -14327,7 +14210,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -14513,7 +14395,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -14553,7 +14434,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -14740,7 +14620,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -14780,7 +14659,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -14958,7 +14836,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -14998,7 +14875,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -15174,7 +15050,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -15214,7 +15089,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -15390,7 +15264,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -15430,7 +15303,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -15606,7 +15478,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -15646,7 +15517,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -15828,7 +15698,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -15868,7 +15737,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -16151,7 +16019,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -16191,7 +16058,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -16367,7 +16233,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -16407,7 +16272,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -16588,7 +16452,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -16628,7 +16491,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -16806,7 +16668,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -16846,7 +16707,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -17024,7 +16884,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -17064,7 +16923,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -17242,7 +17100,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -17282,7 +17139,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -17459,7 +17315,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -17499,7 +17354,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -17677,7 +17531,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -17717,7 +17570,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -17895,7 +17747,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -17935,7 +17786,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -18113,7 +17963,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -18153,7 +18002,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -18189,7 +18037,6 @@ }, "$referrer": "$direct", "$referring_domain": "$direct", - "$feature_flag": "enable-prompts", "$feature_flag_response": true, "token": "phc_IdfzBh09RdfsZyvdjYbq8ml2NR0AD0SnFqcUl4Itwwp", "$session_id": "18890672ccd2bb9-07e026efe4e2a7-1c525634-384000-18890672cce22bc", @@ -18331,7 +18178,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -18371,7 +18217,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -18636,7 +18481,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -18676,7 +18520,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -18862,7 +18705,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -18902,7 +18744,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -19089,7 +18930,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -19129,7 +18969,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -19305,7 +19144,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -19345,7 +19183,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -19523,7 +19360,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -19563,7 +19399,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -19739,7 +19574,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -19779,7 +19613,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -19955,7 +19788,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -19995,7 +19827,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -20177,7 +20008,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -20217,7 +20047,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -20394,7 +20223,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -20434,7 +20262,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -20610,7 +20437,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -20650,7 +20476,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -21003,7 +20828,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -21043,7 +20867,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -21219,7 +21042,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -21259,7 +21081,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -21439,7 +21260,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -21479,7 +21299,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -21809,7 +21628,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -21849,7 +21667,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -22031,7 +21848,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -22071,7 +21887,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -22363,7 +22178,6 @@ "sampling", "recordings-v2-recorder", "posthog-3000", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -22403,7 +22217,6 @@ "$feature/sampling": true, "$feature/recordings-v2-recorder": true, "$feature/posthog-3000": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, diff --git a/frontend/src/scenes/events/__mocks__/eventsQuery.json b/frontend/src/scenes/events/__mocks__/eventsQuery.json index 2ec1ff533774c..a55c2a0d29a14 100644 --- a/frontend/src/scenes/events/__mocks__/eventsQuery.json +++ b/frontend/src/scenes/events/__mocks__/eventsQuery.json @@ -150,7 +150,6 @@ "query_running_time", "recording-debugging", "sampling", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -189,7 +188,6 @@ "$feature/recording-debugging": true, "$feature/sampling": true, "$feature/recordings-v2-recorder": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -362,7 +360,6 @@ "query_running_time", "recording-debugging", "sampling", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -401,7 +398,6 @@ "$feature/recording-debugging": true, "$feature/sampling": true, "$feature/recordings-v2-recorder": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -574,7 +570,6 @@ "query_running_time", "recording-debugging", "sampling", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -613,7 +608,6 @@ "$feature/recording-debugging": true, "$feature/sampling": true, "$feature/recordings-v2-recorder": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -786,7 +780,6 @@ "query_running_time", "recording-debugging", "sampling", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -825,7 +818,6 @@ "$feature/recording-debugging": true, "$feature/sampling": true, "$feature/recordings-v2-recorder": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, @@ -1012,7 +1004,6 @@ "query_running_time", "recording-debugging", "sampling", - "enable-prompts", "feedback-scene", "hogql", "notebooks", @@ -1051,7 +1042,6 @@ "$feature/recording-debugging": true, "$feature/sampling": true, "$feature/recordings-v2-recorder": true, - "$feature/enable-prompts": true, "$feature/feedback-scene": true, "$feature/hogql": true, "$feature/notebooks": true, From cbe486f50e71aafe2e45e54539035e92c5fda07e Mon Sep 17 00:00:00 2001 From: Tom Owers Date: Thu, 21 Mar 2024 16:18:12 +0100 Subject: [PATCH 21/51] feat(data-warehouse): Adds a Zendesk integration (#21068) * WIP * Finishing touches for zendesk * Updated mypy * Removed limit * Comment on the comments * Added migration for source type --- frontend/public/zendesk-logo.png | Bin 0 -> 7002 bytes .../data-warehouse/external/SourceModal.tsx | 8 + .../external/sourceModalLogic.tsx | 32 ++ frontend/src/types.ts | 2 +- latest_migrations.manifest | 2 +- mypy-baseline.txt | 9 +- ...98_alter_externaldatasource_source_type.py | 25 + .../data_imports/external_data_job.py | 14 + .../data_imports/pipelines/pipeline.py | 8 +- .../data_imports/pipelines/schemas.py | 4 + .../pipelines/zendesk/api_helpers.py | 103 ++++ .../pipelines/zendesk/credentials.py | 49 ++ .../data_imports/pipelines/zendesk/helpers.py | 444 ++++++++++++++++++ .../pipelines/zendesk/settings.py | 73 +++ .../pipelines/zendesk/talk_api.py | 114 +++++ posthog/warehouse/api/external_data_source.py | 29 ++ .../warehouse/models/external_data_source.py | 1 + 17 files changed, 911 insertions(+), 6 deletions(-) create mode 100644 frontend/public/zendesk-logo.png create mode 100644 posthog/migrations/0398_alter_externaldatasource_source_type.py create mode 100644 posthog/temporal/data_imports/pipelines/zendesk/api_helpers.py create mode 100644 posthog/temporal/data_imports/pipelines/zendesk/credentials.py create mode 100644 posthog/temporal/data_imports/pipelines/zendesk/helpers.py create mode 100644 posthog/temporal/data_imports/pipelines/zendesk/settings.py create mode 100644 posthog/temporal/data_imports/pipelines/zendesk/talk_api.py diff --git a/frontend/public/zendesk-logo.png b/frontend/public/zendesk-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..080bd000a41e004ba81a61de7f83578d3ac7c6ef GIT binary patch literal 7002 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 G2>UO@)thqw literal 0 HcmV?d00001 diff --git a/frontend/src/scenes/data-warehouse/external/SourceModal.tsx b/frontend/src/scenes/data-warehouse/external/SourceModal.tsx index ae546ea9eae21..204b0ef71982e 100644 --- a/frontend/src/scenes/data-warehouse/external/SourceModal.tsx +++ b/frontend/src/scenes/data-warehouse/external/SourceModal.tsx @@ -5,6 +5,7 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import hubspotLogo from 'public/hubspot-logo.svg' import postgresLogo from 'public/postgres-logo.svg' import stripeLogo from 'public/stripe-logo.svg' +import zendeskLogo from 'public/zendesk-logo.png' import { DatawarehouseTableForm } from '../new_table/DataWarehouseTableForm' import PostgresSchemaForm from './forms/PostgresSchemaForm' @@ -103,6 +104,13 @@ function FirstStep(): JSX.Element { ) } + if (config.name === 'Zendesk') { + return ( + + Zendesk logo + + ) + } return <> } diff --git a/frontend/src/scenes/data-warehouse/external/sourceModalLogic.tsx b/frontend/src/scenes/data-warehouse/external/sourceModalLogic.tsx index d710ed397811e..38f7c9b543938 100644 --- a/frontend/src/scenes/data-warehouse/external/sourceModalLogic.tsx +++ b/frontend/src/scenes/data-warehouse/external/sourceModalLogic.tsx @@ -119,6 +119,38 @@ export const SOURCE_DETAILS: Record = { }, ], }, + Zendesk: { + name: 'Zendesk', + caption: ( + <> + Enter your Zendesk API key to automatically pull your Zendesk support data into the PostHog Data + warehouse. + + ), + fields: [ + { + name: 'subdomain', + label: 'Zendesk Subdomain', + type: 'text', + required: true, + placeholder: '', + }, + { + name: 'api_key', + label: 'API Key', + type: 'text', + required: true, + placeholder: '', + }, + { + name: 'email_address', + label: 'Zendesk Email Address', + type: 'text', + required: true, + placeholder: '', + }, + ], + }, } export const sourceModalLogic = kea([ diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 2aaaba4809a87..26c15fd69d17d 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -3542,7 +3542,7 @@ export interface DataWarehouseViewLink { created_at?: string | null } -export type ExternalDataSourceType = 'Stripe' | 'Hubspot' | 'Postgres' +export type ExternalDataSourceType = 'Stripe' | 'Hubspot' | 'Postgres' | 'Zendesk' export interface ExternalDataSourceCreatePayload { source_type: ExternalDataSourceType diff --git a/latest_migrations.manifest b/latest_migrations.manifest index f232dbc8c186c..f88359530eb78 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: 0397_projects_backfill +posthog: 0398_alter_externaldatasource_source_type sessions: 0001_initial social_django: 0010_uid_db_index two_factor: 0007_auto_20201201_1019 diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 2143c119ea5c2..180d259fc3c20 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -1,6 +1,14 @@ posthog/temporal/common/utils.py:0: error: Argument 1 to "abstractclassmethod" has incompatible type "Callable[[HeartbeatDetails, Any], Any]"; expected "Callable[[type[Never], Any], Any]" [arg-type] posthog/temporal/common/utils.py:0: note: This is likely because "from_activity" has named arguments: "cls". Consider marking them positional-only posthog/temporal/common/utils.py:0: error: Argument 2 to "__get__" of "classmethod" has incompatible type "type[HeartbeatType]"; expected "type[Never]" [arg-type] +posthog/temporal/data_imports/pipelines/zendesk/talk_api.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "str") [assignment] +posthog/temporal/data_imports/pipelines/zendesk/helpers.py:0: error: Argument 1 to "ensure_pendulum_datetime" has incompatible type "DateTime | Date | datetime | date | str | float | int | None"; expected "DateTime | Date | datetime | date | str | float | int" [arg-type] +posthog/temporal/data_imports/pipelines/zendesk/helpers.py:0: error: Argument 1 to "ensure_pendulum_datetime" has incompatible type "str | None"; expected "DateTime | Date | datetime | date | str | float | int" [arg-type] +posthog/temporal/data_imports/pipelines/zendesk/helpers.py:0: error: Argument 1 to "ensure_pendulum_datetime" has incompatible type "DateTime | Date | datetime | date | str | float | int | None"; expected "DateTime | Date | datetime | date | str | float | int" [arg-type] +posthog/temporal/data_imports/pipelines/zendesk/helpers.py:0: error: Argument 1 to "ensure_pendulum_datetime" has incompatible type "str | None"; expected "DateTime | Date | datetime | date | str | float | int" [arg-type] +posthog/temporal/data_imports/pipelines/zendesk/helpers.py:0: error: Argument 1 to "ensure_pendulum_datetime" has incompatible type "DateTime | Date | datetime | date | str | float | int | None"; expected "DateTime | Date | datetime | date | str | float | int" [arg-type] +posthog/temporal/data_imports/pipelines/zendesk/helpers.py:0: error: Item "None" of "DateTime | None" has no attribute "int_timestamp" [union-attr] +posthog/temporal/data_imports/pipelines/zendesk/helpers.py:0: error: Argument 1 to "ensure_pendulum_datetime" has incompatible type "str | None"; expected "DateTime | Date | datetime | date | str | float | int" [arg-type] posthog/hogql/database/argmax.py:0: error: Argument "chain" to "Field" has incompatible type "list[str]"; expected "list[str | int]" [arg-type] posthog/hogql/database/argmax.py:0: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql/database/argmax.py:0: note: Consider using "Sequence" instead, which is covariant @@ -243,7 +251,6 @@ posthog/hogql/resolver.py:0: error: Argument 1 to "join" of "str" has incompatib posthog/temporal/data_imports/external_data_job.py:0: error: Argument "team_id" has incompatible type "int"; expected "str" [arg-type] posthog/temporal/data_imports/external_data_job.py:0: error: Unused "type: ignore" comment [unused-ignore] posthog/temporal/data_imports/external_data_job.py:0: error: Argument "team_id" has incompatible type "int"; expected "str" [arg-type] -posthog/temporal/data_imports/external_data_job.py:0: error: Argument 2 to "DataImportPipeline" has incompatible type "DltSource"; expected "DltResource" [arg-type] posthog/hogql/transforms/lazy_tables.py:0: error: Incompatible default for argument "context" (default has type "None", argument has type "HogQLContext") [assignment] posthog/hogql/transforms/lazy_tables.py:0: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True posthog/hogql/transforms/lazy_tables.py:0: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase diff --git a/posthog/migrations/0398_alter_externaldatasource_source_type.py b/posthog/migrations/0398_alter_externaldatasource_source_type.py new file mode 100644 index 0000000000000..af95cd44eef98 --- /dev/null +++ b/posthog/migrations/0398_alter_externaldatasource_source_type.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.13 on 2024-03-21 13:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("posthog", "0397_projects_backfill"), + ] + + operations = [ + migrations.AlterField( + model_name="externaldatasource", + name="source_type", + field=models.CharField( + choices=[ + ("Stripe", "Stripe"), + ("Hubspot", "Hubspot"), + ("Postgres", "Postgres"), + ("Zendesk", "Zendesk"), + ], + max_length=128, + ), + ), + ] diff --git a/posthog/temporal/data_imports/external_data_job.py b/posthog/temporal/data_imports/external_data_job.py index db99eeb1de315..bf78c99e9d9e0 100644 --- a/posthog/temporal/data_imports/external_data_job.py +++ b/posthog/temporal/data_imports/external_data_job.py @@ -10,6 +10,7 @@ # TODO: remove dependency from posthog.temporal.batch_exports.base import PostHogWorkflow +from posthog.temporal.data_imports.pipelines.zendesk.credentials import ZendeskCredentialsToken from posthog.warehouse.data_load.source_templates import create_warehouse_templates_for_source from posthog.warehouse.data_load.validate_schema import validate_schema_and_update_table @@ -220,7 +221,20 @@ async def run_external_data_job(inputs: ExternalDataJobInputs) -> TSchemaTables: schema=schema, table_names=endpoints, ) + elif model.pipeline.source_type == ExternalDataSource.Type.ZENDESK: + from posthog.temporal.data_imports.pipelines.zendesk.helpers import zendesk_support + credentials = ZendeskCredentialsToken() + credentials.token = model.pipeline.job_inputs.get("zendesk_api_key") + credentials.subdomain = model.pipeline.job_inputs.get("zendesk_subdomain") + credentials.email = model.pipeline.job_inputs.get("zendesk_email_address") + + data_support = zendesk_support(credentials=credentials, endpoints=tuple(endpoints), team_id=inputs.team_id) + # Uncomment to support zendesk chat and talk + # data_chat = zendesk_chat() + # data_talk = zendesk_talk() + + source = data_support else: raise ValueError(f"Source type {model.pipeline.source_type} not supported") diff --git a/posthog/temporal/data_imports/pipelines/pipeline.py b/posthog/temporal/data_imports/pipelines/pipeline.py index 5297f2e39ac29..6a922d2d96a67 100644 --- a/posthog/temporal/data_imports/pipelines/pipeline.py +++ b/posthog/temporal/data_imports/pipelines/pipeline.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Literal from uuid import UUID import dlt @@ -9,7 +10,7 @@ import os from posthog.settings.base_variables import TEST from structlog.typing import FilteringBoundLogger -from dlt.sources import DltResource +from dlt.sources import DltSource @dataclass @@ -23,9 +24,9 @@ class PipelineInputs: class DataImportPipeline: - loader_file_format = "parquet" + loader_file_format: Literal["parquet"] = "parquet" - def __init__(self, inputs: PipelineInputs, source: DltResource, logger: FilteringBoundLogger): + def __init__(self, inputs: PipelineInputs, source: DltSource, logger: FilteringBoundLogger): self.inputs = inputs self.logger = logger self.source = source @@ -47,6 +48,7 @@ def _get_destination(self): credentials = { "aws_access_key_id": settings.AIRBYTE_BUCKET_KEY, "aws_secret_access_key": settings.AIRBYTE_BUCKET_SECRET, + "region_name": settings.AIRBYTE_BUCKET_REGION, } return dlt.destinations.filesystem( diff --git a/posthog/temporal/data_imports/pipelines/schemas.py b/posthog/temporal/data_imports/pipelines/schemas.py index 371f8087b7966..1caea1364899a 100644 --- a/posthog/temporal/data_imports/pipelines/schemas.py +++ b/posthog/temporal/data_imports/pipelines/schemas.py @@ -1,3 +1,4 @@ +from posthog.temporal.data_imports.pipelines.zendesk.settings import BASE_ENDPOINTS, SUPPORT_ENDPOINTS from posthog.warehouse.models import ExternalDataSource from posthog.temporal.data_imports.pipelines.stripe.settings import ENDPOINTS as STRIPE_ENDPOINTS from posthog.temporal.data_imports.pipelines.hubspot.settings import ENDPOINTS as HUBSPOT_ENDPOINTS @@ -5,5 +6,8 @@ PIPELINE_TYPE_SCHEMA_DEFAULT_MAPPING = { ExternalDataSource.Type.STRIPE: STRIPE_ENDPOINTS, ExternalDataSource.Type.HUBSPOT: HUBSPOT_ENDPOINTS, + ExternalDataSource.Type.ZENDESK: tuple( + list(BASE_ENDPOINTS) + [resource for resource, endpoint_url, data_key, cursor_paginated in SUPPORT_ENDPOINTS] + ), ExternalDataSource.Type.POSTGRES: (), } diff --git a/posthog/temporal/data_imports/pipelines/zendesk/api_helpers.py b/posthog/temporal/data_imports/pipelines/zendesk/api_helpers.py new file mode 100644 index 0000000000000..c6e4eb4809ee7 --- /dev/null +++ b/posthog/temporal/data_imports/pipelines/zendesk/api_helpers.py @@ -0,0 +1,103 @@ +from typing import Optional, TypedDict, Dict + +from dlt.common import pendulum +from dlt.common.time import ensure_pendulum_datetime +from dlt.common.typing import DictStrAny, DictStrStr, TDataItem + + +class TCustomFieldInfo(TypedDict): + title: str + options: DictStrStr + + +def _parse_date_or_none(value: Optional[str]) -> Optional[pendulum.DateTime]: + if not value: + return None + return ensure_pendulum_datetime(value) + + +def process_ticket( + ticket: DictStrAny, + custom_fields: Dict[str, TCustomFieldInfo], + pivot_custom_fields: bool = True, +) -> DictStrAny: + """ + Helper function that processes a ticket object and returns a dictionary of ticket data. + + Args: + ticket: The ticket dict object returned by a Zendesk API call. + custom_fields: A dictionary containing all the custom fields available for tickets. + pivot_custom_fields: A boolean indicating whether to pivot all custom fields or not. + Defaults to True. + + Returns: + DictStrAny: A dictionary containing cleaned data about a ticket. + """ + # Commented out due to how slow this processing code is, and how often it'd break the pipeline. + # to be revisited on whether we want/need this pre-processing and figure out the best way to do it. + + # pivot custom field if indicated as such + # get custom fields + # pivoted_fields = set() + # for custom_field in ticket.get("custom_fields", []): + # if pivot_custom_fields: + # cus_field_id = str(custom_field["id"]) + # field = custom_fields.get(cus_field_id, None) + # if field is None: + # logger.warning( + # "Custom field with ID %s does not exist in fields state. It may have been created after the pipeline run started.", + # cus_field_id, + # ) + # custom_field["ticket_id"] = ticket["id"] + # continue + + # pivoted_fields.add(cus_field_id) + # field_name = field["title"] + # current_value = custom_field["value"] + # options = field["options"] + # # Map dropdown values to labels + # if not current_value or not options: + # ticket[field_name] = current_value + # elif isinstance(current_value, list): # Multiple choice field has a list of values + # ticket[field_name] = [options.get(key, key) for key in current_value] + # else: + # ticket[field_name] = options.get(current_value) + # else: + # custom_field["ticket_id"] = ticket["id"] + # # delete fields that are not needed for pivoting + # if pivot_custom_fields: + # ticket["custom_fields"] = [f for f in ticket.get("custom_fields", []) if str(f["id"]) not in pivoted_fields] + # if not ticket.get("custom_fields"): + # del ticket["custom_fields"] + # del ticket["fields"] + + # modify dates to return datetime objects instead + ticket["updated_at"] = _parse_date_or_none(ticket["updated_at"]) + ticket["created_at"] = _parse_date_or_none(ticket["created_at"]) + ticket["due_at"] = _parse_date_or_none(ticket["due_at"]) + return ticket + + +def process_ticket_field(field: DictStrAny, custom_fields_state: Dict[str, TCustomFieldInfo]) -> TDataItem: + """Update custom field mapping in dlt state for the given field.""" + # grab id and update state dict + # if the id is new, add a new key to indicate that this is the initial value for title + # New dropdown options are added to existing field but existing options are not changed + return_dict = field.copy() + field_id = str(field["id"]) + + options = field.get("custom_field_options", []) + new_options = {o["value"]: o["name"] for o in options} + existing_field = custom_fields_state.get(field_id) + if existing_field: + existing_options = existing_field["options"] + if return_options := return_dict.get("custom_field_options"): + for item in return_options: + item["name"] = existing_options.get(item["value"], item["name"]) + for key, value in new_options.items(): + if key not in existing_options: + existing_options[key] = value + else: + custom_fields_state[field_id] = dict(title=field["title"], options=new_options) + return_dict["initial_title"] = field["title"] + return return_dict diff --git a/posthog/temporal/data_imports/pipelines/zendesk/credentials.py b/posthog/temporal/data_imports/pipelines/zendesk/credentials.py new file mode 100644 index 0000000000000..1f8110ae9b911 --- /dev/null +++ b/posthog/temporal/data_imports/pipelines/zendesk/credentials.py @@ -0,0 +1,49 @@ +""" +This module handles how credentials are read in dlt sources +""" +from typing import ClassVar, List, Union +from dlt.common.configuration import configspec +from dlt.common.configuration.specs import CredentialsConfiguration +from dlt.common.typing import TSecretValue + + +@configspec +class ZendeskCredentialsBase(CredentialsConfiguration): + """ + The Base version of all the ZendeskCredential classes. + """ + + subdomain: str + __config_gen_annotations__: ClassVar[List[str]] = [] + + +@configspec +class ZendeskCredentialsEmailPass(ZendeskCredentialsBase): + """ + This class is used to store credentials for Email + Password Authentication + """ + + email: str + password: TSecretValue + + +@configspec +class ZendeskCredentialsOAuth(ZendeskCredentialsBase): + """ + This class is used to store credentials for OAuth Token Authentication + """ + + oauth_token: TSecretValue + + +@configspec +class ZendeskCredentialsToken(ZendeskCredentialsBase): + """ + This class is used to store credentials for Token Authentication + """ + + email: str + token: TSecretValue + + +TZendeskCredentials = Union[ZendeskCredentialsEmailPass, ZendeskCredentialsToken, ZendeskCredentialsOAuth] diff --git a/posthog/temporal/data_imports/pipelines/zendesk/helpers.py b/posthog/temporal/data_imports/pipelines/zendesk/helpers.py new file mode 100644 index 0000000000000..a3e0328c8ab28 --- /dev/null +++ b/posthog/temporal/data_imports/pipelines/zendesk/helpers.py @@ -0,0 +1,444 @@ +from typing import Iterator, List, Optional, Iterable, Tuple +from itertools import chain + +import dlt +from dlt.common import pendulum +from dlt.common.time import ensure_pendulum_datetime +from dlt.common.typing import TDataItem, TAnyDateTime, TDataItems +from dlt.sources import DltResource + +from .api_helpers import process_ticket, process_ticket_field +from .talk_api import PaginationType, ZendeskAPIClient +from .credentials import TZendeskCredentials, ZendeskCredentialsOAuth + +from .settings import ( + DEFAULT_START_DATE, + CUSTOM_FIELDS_STATE_KEY, + SUPPORT_ENDPOINTS, + TALK_ENDPOINTS, + INCREMENTAL_TALK_ENDPOINTS, + SUPPORT_EXTRA_ENDPOINTS, +) + + +@dlt.source(max_table_nesting=0) +def zendesk_talk( + credentials: TZendeskCredentials = dlt.secrets.value, + start_date: Optional[TAnyDateTime] = DEFAULT_START_DATE, + end_date: Optional[TAnyDateTime] = None, +) -> Iterable[DltResource]: + """ + Retrieves data from Zendesk Talk for phone calls and voicemails. + + `start_date` argument can be used on its own or together with `end_date`. When both are provided + data is limited to items updated in that time range. + The range is "half-open", meaning elements equal and higher than `start_date` and elements lower than `end_date` are included. + All resources opt-in to use Airflow scheduler if run as Airflow task + + Args: + credentials: The credentials for authentication. Defaults to the value in the `dlt.secrets` object. + start_date: The start time of the range for which to load. Defaults to January 1st 2000. + end_date: The end time of the range for which to load data. + If end time is not provided, the incremental loading will be enabled and after initial run, only new data will be retrieved + Yields: + DltResource: Data resources from Zendesk Talk. + """ + + # use the credentials to authenticate with the ZendeskClient + zendesk_client = ZendeskAPIClient(credentials) + start_date_obj = ensure_pendulum_datetime(start_date) + end_date_obj = ensure_pendulum_datetime(end_date) if end_date else None + + # regular endpoints + for key, talk_endpoint, item_name, cursor_paginated in TALK_ENDPOINTS: + yield dlt.resource( + talk_resource( + zendesk_client, + key, + item_name or talk_endpoint, + PaginationType.CURSOR if cursor_paginated else PaginationType.OFFSET, + ), + name=key, + write_disposition="replace", + ) + + # adding incremental endpoints + for key, talk_incremental_endpoint in INCREMENTAL_TALK_ENDPOINTS.items(): + yield dlt.resource( + talk_incremental_resource, + name=f"{key}_incremental", + primary_key="id", + write_disposition="merge", + )( + zendesk_client=zendesk_client, + talk_endpoint_name=key, + talk_endpoint=talk_incremental_endpoint, + updated_at=dlt.sources.incremental[str]( + "updated_at", + initial_value=start_date_obj.isoformat(), + end_value=end_date_obj.isoformat() if end_date_obj else None, + allow_external_schedulers=True, + ), + ) + + +def talk_resource( + zendesk_client: ZendeskAPIClient, + talk_endpoint_name: str, + talk_endpoint: str, + pagination_type: PaginationType, +) -> Iterator[TDataItem]: + """ + Loads data from a Zendesk Talk endpoint. + + Args: + zendesk_client: An instance of ZendeskAPIClient for making API calls to Zendesk Talk. + talk_endpoint_name: The name of the talk_endpoint. + talk_endpoint: The actual URL ending of the endpoint. + pagination: Type of pagination type used by endpoint + + Yields: + TDataItem: Dictionary containing the data from the endpoint. + """ + # send query and process it + yield from zendesk_client.get_pages(talk_endpoint, talk_endpoint_name, pagination_type) + + +def talk_incremental_resource( + zendesk_client: ZendeskAPIClient, + talk_endpoint_name: str, + talk_endpoint: str, + updated_at: dlt.sources.incremental[str], +) -> Iterator[TDataItem]: + """ + Loads data from a Zendesk Talk endpoint with incremental loading. + + Args: + zendesk_client: An instance of ZendeskAPIClient for making API calls to Zendesk Talk. + talk_endpoint_name: The name of the talk_endpoint. + talk_endpoint: The actual URL ending of the endpoint. + updated_at: Source for the last updated timestamp. + + Yields: + TDataItem: Dictionary containing the data from the endpoint. + """ + # send the request and process it + for page in zendesk_client.get_pages( + talk_endpoint, + talk_endpoint_name, + PaginationType.START_TIME, + params={"start_time": ensure_pendulum_datetime(updated_at.last_value).int_timestamp}, + ): + yield page + if updated_at.end_out_of_range: + return + + +@dlt.source(max_table_nesting=0) +def zendesk_chat( + credentials: ZendeskCredentialsOAuth = dlt.secrets.value, + start_date: Optional[TAnyDateTime] = DEFAULT_START_DATE, + end_date: Optional[TAnyDateTime] = None, +) -> Iterable[DltResource]: + """ + Retrieves data from Zendesk Chat for chat interactions. + + `start_date` argument can be used on its own or together with `end_date`. When both are provided + data is limited to items updated in that time range. + The range is "half-open", meaning elements equal and higher than `start_date` and elements lower than `end_date` are included. + All resources opt-in to use Airflow scheduler if run as Airflow task + + Args: + credentials: The credentials for authentication. Defaults to the value in the `dlt.secrets` object. + start_date: The start time of the range for which to load. Defaults to January 1st 2000. + end_date: The end time of the range for which to load data. + If end time is not provided, the incremental loading will be enabled and after initial run, only new data will be retrieved + + Yields: + DltResource: Data resources from Zendesk Chat. + """ + + # Authenticate + zendesk_client = ZendeskAPIClient(credentials, url_prefix="https://www.zopim.com") + start_date_obj = ensure_pendulum_datetime(start_date) + end_date_obj = ensure_pendulum_datetime(end_date) if end_date else None + + yield dlt.resource(chats_table_resource, name="chats", write_disposition="merge")( + zendesk_client, + dlt.sources.incremental[str]( + "update_timestamp|updated_timestamp", + initial_value=start_date_obj.isoformat(), + end_value=end_date_obj.isoformat() if end_date_obj else None, + allow_external_schedulers=True, + ), + ) + + +def chats_table_resource( + zendesk_client: ZendeskAPIClient, + update_timestamp: dlt.sources.incremental[str], +) -> Iterator[TDataItems]: + """ + Resource for Chats + + Args: + zendesk_client: The Zendesk API client instance, used to make calls to Zendesk API. + update_timestamp: Incremental source specifying the timestamp for incremental loading. + + Yields: + dict: A dictionary representing each row of data. + """ + chat_pages = zendesk_client.get_pages( + "/api/v2/incremental/chats", + "chats", + PaginationType.START_TIME, + params={ + "start_time": ensure_pendulum_datetime(update_timestamp.last_value).int_timestamp, + "fields": "chats(*)", + }, + ) + for page in chat_pages: + yield page + + if update_timestamp.end_out_of_range: + return + + +@dlt.source(max_table_nesting=0) +def zendesk_support( + team_id: int, + credentials: TZendeskCredentials = dlt.secrets.value, + endpoints: Tuple[str, ...] = (), + pivot_ticket_fields: bool = True, + start_date: Optional[TAnyDateTime] = DEFAULT_START_DATE, + end_date: Optional[TAnyDateTime] = None, +) -> Iterable[DltResource]: + """ + Retrieves data from Zendesk Support for tickets, users, brands, organizations, and groups. + + `start_date` argument can be used on its own or together with `end_date`. When both are provided + data is limited to items updated in that time range. + The range is "half-open", meaning elements equal and higher than `start_date` and elements lower than `end_date` are included. + All resources opt-in to use Airflow scheduler if run as Airflow task + + Args: + credentials: The credentials for authentication. Defaults to the value in the `dlt.secrets` object. + load_all: Whether to load extra resources for the API. Defaults to True. + pivot_ticket_fields: Whether to pivot the custom fields in tickets. Defaults to True. + start_date: The start time of the range for which to load. Defaults to January 1st 2000. + end_date: The end time of the range for which to load data. + If end time is not provided, the incremental loading will be enabled and after initial run, only new data will be retrieved + + Returns: + Sequence[DltResource]: Multiple dlt resources. + """ + + start_date_obj = ensure_pendulum_datetime(start_date) + end_date_obj = ensure_pendulum_datetime(end_date) if end_date else None + + start_date_ts = start_date_obj.int_timestamp + start_date_iso_str = start_date_obj.isoformat() + end_date_ts: Optional[int] = None + end_date_iso_str: Optional[str] = None + if end_date_obj: + end_date_ts = end_date_obj.int_timestamp + end_date_iso_str = end_date_obj.isoformat() + + @dlt.resource(name="ticket_events", primary_key="id", write_disposition="append") + def ticket_events( + zendesk_client: ZendeskAPIClient, + timestamp: dlt.sources.incremental[int] = dlt.sources.incremental( # noqa: B008 + "timestamp", + initial_value=start_date_ts, + end_value=end_date_ts, + allow_external_schedulers=True, + ), + ) -> Iterator[TDataItem]: + # URL For ticket events + # 'https://d3v-dlthub.zendesk.com/api/v2/incremental/ticket_events.json?start_time=946684800' + event_pages = zendesk_client.get_pages( + "/api/v2/incremental/ticket_events.json", + "ticket_events", + PaginationType.STREAM, + params={"start_time": timestamp.last_value}, + ) + for page in event_pages: + yield page + if timestamp.end_out_of_range: + return + + @dlt.resource( + name="tickets", + primary_key="id", + write_disposition="merge", + columns={ + "tags": {"data_type": "complex"}, + "custom_fields": {"data_type": "complex"}, + }, + ) + def ticket_table( + zendesk_client: ZendeskAPIClient, + pivot_fields: bool = True, + updated_at: dlt.sources.incremental[pendulum.DateTime] = dlt.sources.incremental( # noqa: B008 + "updated_at", + initial_value=start_date_obj, + end_value=end_date_obj, + allow_external_schedulers=True, + ), + ) -> Iterator[TDataItem]: + """ + Resource for tickets table. Uses DLT state to handle column renaming of custom fields to prevent changing the names of said columns. + This resource uses pagination, loading and side loading to make API calls more efficient. + + Args: + zendesk_client: The Zendesk API client instance, used to make calls to Zendesk API. + pivot_fields: Indicates whether to pivot the custom fields in tickets. Defaults to True. + per_page: The number of Ticket objects to load per page. Defaults to 1000. + updated_at: Incremental source for the 'updated_at' column. + Defaults to dlt.sources.incremental("updated_at", initial_value=start_date). + + Yields: + TDataItem: Dictionary containing the ticket data. + """ + # grab the custom fields from dlt state if any + if pivot_fields: + load_ticket_fields_state(zendesk_client) + fields_dict = dlt.current.source_state().setdefault(CUSTOM_FIELDS_STATE_KEY, {}) + # include_objects = ["users", "groups", "organisation", "brands"] + ticket_pages = zendesk_client.get_pages( + "/api/v2/incremental/tickets", + "tickets", + PaginationType.STREAM, + params={"start_time": updated_at.last_value.int_timestamp}, + ) + for page in ticket_pages: + yield [process_ticket(ticket, fields_dict, pivot_custom_fields=pivot_fields) for ticket in page] + + # stop loading when using end_value and end is reached + if updated_at.end_out_of_range: + return + + @dlt.resource(name="ticket_metric_events", primary_key="id", write_disposition="append") + def ticket_metric_table( + zendesk_client: ZendeskAPIClient, + time: dlt.sources.incremental[str] = dlt.sources.incremental( # noqa: B008 + "time", + initial_value=start_date_iso_str, + end_value=end_date_iso_str, + allow_external_schedulers=True, + ), + ) -> Iterator[TDataItem]: + """ + Resource for ticket metric events table. Returns all the ticket metric events from the starting date, + with the default starting date being January 1st of the current year. + + Args: + zendesk_client: The Zendesk API client instance, used to make calls to Zendesk API. + time: Incremental source for the 'time' column, + indicating the starting date for retrieving ticket metric events. + Defaults to dlt.sources.incremental("time", initial_value=start_date_iso_str). + + Yields: + TDataItem: Dictionary containing the ticket metric event data. + """ + # "https://example.zendesk.com/api/v2/incremental/ticket_metric_events?start_time=1332034771" + metric_event_pages = zendesk_client.get_pages( + "/api/v2/incremental/ticket_metric_events", + "ticket_metric_events", + PaginationType.CURSOR, + params={ + "start_time": ensure_pendulum_datetime(time.last_value).int_timestamp, + }, + ) + for page in metric_event_pages: + yield page + + if time.end_out_of_range: + return + + def ticket_fields_table(zendesk_client: ZendeskAPIClient) -> Iterator[TDataItem]: + """ + Loads ticket fields data from Zendesk API. + + Args: + zendesk_client: The Zendesk API client instance, used to make calls to Zendesk API. + + Yields: + TDataItem: Dictionary containing the ticket fields data. + """ + # get dlt state + ticket_custom_fields = dlt.current.source_state().setdefault(CUSTOM_FIELDS_STATE_KEY, {}) + # get all custom fields and update state if needed, otherwise just load dicts into tables + all_fields = list( + chain.from_iterable( + zendesk_client.get_pages("/api/v2/ticket_fields.json", "ticket_fields", PaginationType.OFFSET) + ) + ) + # all_fields = zendesk_client.ticket_fields() + for field in all_fields: + yield process_ticket_field(field, ticket_custom_fields) + + def load_ticket_fields_state( + zendesk_client: ZendeskAPIClient, + ) -> None: + for _ in ticket_fields_table(zendesk_client): + pass + + ticket_fields_resource = dlt.resource(name="ticket_fields", write_disposition="replace")(ticket_fields_table) + + # Authenticate + zendesk_client = ZendeskAPIClient(credentials) + + all_endpoints = SUPPORT_ENDPOINTS + SUPPORT_EXTRA_ENDPOINTS + resource_list: List[DltResource] = [] + + for endpoint in endpoints: + # loading base tables + if endpoint == "ticket_fields": + resource_list.append(ticket_fields_resource(zendesk_client=zendesk_client)) + elif endpoint == "ticket_events": + resource_list.append(ticket_events(zendesk_client=zendesk_client)) + elif endpoint == "tickets": + resource_list.append(ticket_table(zendesk_client=zendesk_client, pivot_fields=pivot_ticket_fields)) + elif endpoint == "ticket_metric_events": + resource_list.append(ticket_metric_table(zendesk_client=zendesk_client)) + else: + # other tables to be loaded + for resource, endpoint_url, data_key, cursor_paginated in all_endpoints: + if endpoint == resource: + resource_list.append( + dlt.resource( + basic_resource(zendesk_client, endpoint_url, data_key or resource, cursor_paginated), + name=resource, + write_disposition="replace", + ) + ) + break + + return resource_list + + +def basic_resource( + zendesk_client: ZendeskAPIClient, + endpoint_url: str, + data_key: str, + cursor_paginated: bool, +) -> Iterator[TDataItem]: + """ + Basic loader for most endpoints offered by Zenpy. Supports pagination. Expects to be called as a DLT Resource. + + Args: + zendesk_client: The Zendesk API client instance, used to make calls to Zendesk API. + resource: The Zenpy endpoint to retrieve data from, usually directly linked to a Zendesk API endpoint. + cursor_paginated: Tells to use CURSOR pagination or OFFSET/no pagination + + Yields: + TDataItem: Dictionary containing the resource data. + """ + + pages = zendesk_client.get_pages( + endpoint_url, + data_key, + PaginationType.CURSOR if cursor_paginated else PaginationType.OFFSET, + ) + yield from pages diff --git a/posthog/temporal/data_imports/pipelines/zendesk/settings.py b/posthog/temporal/data_imports/pipelines/zendesk/settings.py new file mode 100644 index 0000000000000..aa44df7c20297 --- /dev/null +++ b/posthog/temporal/data_imports/pipelines/zendesk/settings.py @@ -0,0 +1,73 @@ +"""Zendesk source settings and constants""" + +from dlt.common import pendulum + +DEFAULT_START_DATE = pendulum.datetime(year=2000, month=1, day=1) +PAGE_SIZE = 100 +INCREMENTAL_PAGE_SIZE = 1000 + + +CUSTOM_FIELDS_STATE_KEY = "ticket_custom_fields_v2" + +# Resources that will always get pulled +BASE_ENDPOINTS = ["ticket_fields", "ticket_events", "tickets", "ticket_metric_events"] + +# Tuples of (Resource name, endpoint URL, data_key, supports pagination) +# data_key is the key which data list is nested under in responses +# if the data key is None it is assumed to be the same as the resource name +# The last element of the tuple says if endpoint supports cursor pagination +SUPPORT_ENDPOINTS = [ + ("users", "/api/v2/users.json", "users", True), + ("sla_policies", "/api/v2/slas/policies.json", None, False), + ("groups", "/api/v2/groups.json", None, True), + ("organizations", "/api/v2/organizations.json", None, True), + ("brands", "/api/v2/brands.json", None, True), +] + +SUPPORT_EXTRA_ENDPOINTS = [ + ("activities", "/api/v2/activities.json", None, True), + ("automations", "/api/v2/automations.json", None, True), + ("custom_agent_roles", "/api/v2/custom_roles.json", "custom_roles", False), + ("dynamic_content", "/api/v2/dynamic_content/items.json", "items", True), + ("group_memberships", "/api/v2/group_memberships.json", None, True), + ("job_status", "/api/v2/job_statuses.json", "job_statuses", True), + ("macros", "/api/v2/macros.json", None, True), + ("organization_fields", "/api/v2/organization_fields.json", None, True), + ("organization_memberships", "/api/v2/organization_memberships.json", None, True), + ("recipient_addresses", "/api/v2/recipient_addresses.json", None, True), + ("requests", "/api/v2/requests.json", None, True), + ("satisfaction_ratings", "/api/v2/satisfaction_ratings.json", None, True), + ("sharing_agreements", "/api/v2/sharing_agreements.json", None, False), + ("skips", "/api/v2/skips.json", None, True), + ("suspended_tickets", "/api/v2/suspended_tickets.json", None, True), + ("targets", "/api/v2/targets.json", None, False), + ("ticket_forms", "/api/v2/ticket_forms.json", None, False), + ("ticket_metrics", "/api/v2/ticket_metrics.json", None, True), + ("triggers", "/api/v2/triggers.json", None, True), + ("user_fields", "/api/v2/user_fields.json", None, True), + ("views", "/api/v2/views.json", None, True), + ("tags", "/api/v2/tags.json", None, True), +] + +TALK_ENDPOINTS = [ + ("calls", "/api/v2/channels/voice/calls", None, False), + ("addresses", "/api/v2/channels/voice/addresses", None, False), + ("greeting_categories", "/api/v2/channels/voice/greeting_categories", None, False), + ("greetings", "/api/v2/channels/voice/greetings", None, False), + ("ivrs", "/api/v2/channels/voice/ivr", None, False), + ("phone_numbers", "/api/v2/channels/voice/phone_numbers", None, False), + ("settings", "/api/v2/channels/voice/settings", None, False), + ("lines", "/api/v2/channels/voice/lines", None, False), + ("agents_activity", "/api/v2/channels/voice/stats/agents_activity", None, False), + ( + "current_queue_activity", + "/api/v2/channels/voice/stats/current_queue_activity", + None, + False, + ), +] + +INCREMENTAL_TALK_ENDPOINTS = { + "calls": "/api/v2/channels/voice/stats/incremental/calls.json", + "legs": "/api/v2/channels/voice/stats/incremental/legs.json", +} diff --git a/posthog/temporal/data_imports/pipelines/zendesk/talk_api.py b/posthog/temporal/data_imports/pipelines/zendesk/talk_api.py new file mode 100644 index 0000000000000..5db9a28eafc74 --- /dev/null +++ b/posthog/temporal/data_imports/pipelines/zendesk/talk_api.py @@ -0,0 +1,114 @@ +from enum import Enum +from typing import Dict, Iterator, Optional, Tuple, Any +from dlt.common.typing import DictStrStr, TDataItems, TSecretValue +from dlt.sources.helpers.requests import client + +from . import settings +from .credentials import ( + ZendeskCredentialsEmailPass, + ZendeskCredentialsOAuth, + ZendeskCredentialsToken, + TZendeskCredentials, +) + + +class PaginationType(Enum): + OFFSET = 0 + CURSOR = 1 + STREAM = 2 + START_TIME = 3 + + +class ZendeskAPIClient: + """ + API client used to make requests to Zendesk talk, support and chat API + """ + + subdomain: str = "" + url: str = "" + headers: Optional[DictStrStr] + auth: Optional[Tuple[str, TSecretValue]] + + def __init__(self, credentials: TZendeskCredentials, url_prefix: Optional[str] = None) -> None: + """ + Initializer for the API client which is then used to make API calls to the ZendeskAPI + + Args: + credentials: ZendeskCredentials object which contains the necessary credentials to authenticate to ZendeskAPI + """ + # oauth token is the preferred way to authenticate, followed by api token and then email + password combo + # fill headers and auth for every possibility of credentials given, raise error if credentials are of incorrect type + if isinstance(credentials, ZendeskCredentialsOAuth): + self.headers = {"Authorization": f"Bearer {credentials.oauth_token}"} + self.auth = None + elif isinstance(credentials, ZendeskCredentialsToken): + self.headers = None + self.auth = (f"{credentials.email}/token", credentials.token) + elif isinstance(credentials, ZendeskCredentialsEmailPass): + self.auth = (credentials.email, credentials.password) + self.headers = None + else: + raise TypeError( + "Wrong credentials type provided to ZendeskAPIClient. The credentials need to be of type: ZendeskCredentialsOAuth, ZendeskCredentialsToken or ZendeskCredentialsEmailPass" + ) + + # If url_prefix is set it overrides the default API URL (e.g. chat api uses zopim.com domain) + if url_prefix: + self.url = url_prefix + else: + self.subdomain = credentials.subdomain + self.url = f"https://{self.subdomain}.zendesk.com" + + def get_pages( + self, + endpoint: str, + data_point_name: str, + pagination: PaginationType, + params: Optional[Dict[str, Any]] = None, + ) -> Iterator[TDataItems]: + """ + Makes a request to a paginated endpoint and returns a generator of data items per page. + + Args: + endpoint: The url to the endpoint, e.g. /api/v2/calls + data_point_name: The key which data items are nested under in the response object (e.g. calls) + params: Optional dict of query params to include in the request + pagination: Type of pagination type used by endpoint + + Returns: + Generator of pages, each page is a list of dict data items + """ + + # update the page size to enable cursor pagination + params = params or {} + if pagination == PaginationType.CURSOR: + params["page[size]"] = settings.PAGE_SIZE + elif pagination == PaginationType.STREAM: + params["per_page"] = settings.INCREMENTAL_PAGE_SIZE + elif pagination == PaginationType.START_TIME: + params["limit"] = settings.INCREMENTAL_PAGE_SIZE + + # make request and keep looping until there is no next page + get_url = f"{self.url}{endpoint}" + while get_url: + response = client.get(get_url, headers=self.headers, auth=self.auth, params=params) + response.raise_for_status() + response_json = response.json() + result = response_json[data_point_name] + yield result + + get_url = None + if pagination == PaginationType.CURSOR: + if response_json["meta"]["has_more"]: + get_url = response_json["links"]["next"] + elif pagination == PaginationType.OFFSET: + get_url = response_json.get("next_page", None) + elif pagination == PaginationType.STREAM: + # See https://developer.zendesk.com/api-reference/ticketing/ticket-management/incremental_exports/#json-format + if not response_json["end_of_stream"]: + get_url = response_json["next_page"] + elif pagination == PaginationType.START_TIME: + if response_json["count"] > 0: + get_url = response_json["next_page"] + + params = {} diff --git a/posthog/warehouse/api/external_data_source.py b/posthog/warehouse/api/external_data_source.py index 1586f1051379e..6eb06abcad70f 100644 --- a/posthog/warehouse/api/external_data_source.py +++ b/posthog/warehouse/api/external_data_source.py @@ -136,6 +136,8 @@ def create(self, request: Request, *args: Any, **kwargs: Any) -> Response: new_source_model = self._handle_stripe_source(request, *args, **kwargs) elif source_type == ExternalDataSource.Type.HUBSPOT: new_source_model = self._handle_hubspot_source(request, *args, **kwargs) + elif source_type == ExternalDataSource.Type.ZENDESK: + new_source_model = self._handle_zendesk_source(request, *args, **kwargs) elif source_type == ExternalDataSource.Type.POSTGRES: try: new_source_model, table_names = self._handle_postgres_source(request, *args, **kwargs) @@ -190,6 +192,33 @@ def _handle_stripe_source(self, request: Request, *args: Any, **kwargs: Any) -> return new_source_model + def _handle_zendesk_source(self, request: Request, *args: Any, **kwargs: Any) -> ExternalDataSource: + payload = request.data["payload"] + api_key = payload.get("api_key") + subdomain = payload.get("subdomain") + email_address = payload.get("email_address") + prefix = request.data.get("prefix", None) + source_type = request.data["source_type"] + + # TODO: remove dummy vars + new_source_model = ExternalDataSource.objects.create( + source_id=str(uuid.uuid4()), + connection_id=str(uuid.uuid4()), + destination_id=str(uuid.uuid4()), + team=self.team, + status="Running", + source_type=source_type, + job_inputs={ + "zendesk_login_method": "api_key", # We should support the Zendesk OAuth flow in the future, and so with this we can do backwards compatibility + "zendesk_api_key": api_key, + "zendesk_subdomain": subdomain, + "zendesk_email_address": email_address, + }, + prefix=prefix, + ) + + return new_source_model + def _handle_hubspot_source(self, request: Request, *args: Any, **kwargs: Any) -> ExternalDataSource: payload = request.data["payload"] code = payload.get("code") diff --git a/posthog/warehouse/models/external_data_source.py b/posthog/warehouse/models/external_data_source.py index df668c5abfc54..0a044c0b06315 100644 --- a/posthog/warehouse/models/external_data_source.py +++ b/posthog/warehouse/models/external_data_source.py @@ -12,6 +12,7 @@ class Type(models.TextChoices): STRIPE = "Stripe", "Stripe" HUBSPOT = "Hubspot", "Hubspot" POSTGRES = "Postgres", "Postgres" + ZENDESK = "Zendesk", "Zendesk" class Status(models.TextChoices): RUNNING = "Running", "Running" From ab7c09910b60b697106e9b4eab647f89747c57e1 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 21 Mar 2024 15:30:11 +0000 Subject: [PATCH 22/51] fix: nobody sees these buttons (#21072) --- .../player/inspector/PlayerInspectorControls.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx index 93b272ede828b..bf3fd911e604d 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx @@ -174,6 +174,7 @@ export function PlayerInspectorControls({ onClose }: { onClose: () => void }): J
setTimestampMode(timestampMode === 'absolute' ? 'relative' : 'absolute')} tooltipPlacement="left" @@ -191,14 +192,15 @@ export function PlayerInspectorControls({ onClose }: { onClose: () => void }): J { - // If the user has syncScrolling on but it is paused due to interacting with the Inspector, we want to resume it + // If the user has syncScrolling on, but it is paused due to interacting with the Inspector, we want to resume it if (syncScroll && syncScrollingPaused) { setSyncScrollPaused(false) } else { - // Otherwise we are just toggling the settting + // Otherwise we are just toggling the setting setSyncScroll(!syncScroll) } }} From ed01ee36219f723dbbfd45a106f2b774a7d89256 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Thu, 21 Mar 2024 17:50:07 +0100 Subject: [PATCH 23/51] chore(blobby): raise overflow threshold (#21079) --- plugin-server/src/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin-server/src/config/config.ts b/plugin-server/src/config/config.ts index b5fa1ce899f63..dcaebe4c1097a 100644 --- a/plugin-server/src/config/config.ts +++ b/plugin-server/src/config/config.ts @@ -164,7 +164,7 @@ export function getDefaultConfig(): PluginsServerConfig { SESSION_RECORDING_KAFKA_DEBUG: undefined, SESSION_RECORDING_MAX_PARALLEL_FLUSHES: 10, SESSION_RECORDING_OVERFLOW_ENABLED: false, - SESSION_RECORDING_OVERFLOW_BUCKET_REPLENISH_RATE: 1_000_000, // 1MB/second uncompressed, sustained + SESSION_RECORDING_OVERFLOW_BUCKET_REPLENISH_RATE: 2_000_000, // 2MB/second uncompressed, sustained SESSION_RECORDING_OVERFLOW_BUCKET_CAPACITY: 100_000_000, // 100MB burst } } From 21922ff9e3e38560046f31f30811bfad06ddd005 Mon Sep 17 00:00:00 2001 From: David Newell Date: Thu, 21 Mar 2024 16:59:21 +0000 Subject: [PATCH 24/51] feat: create playlists from errors (#21037) --- ee/session_recordings/ai/error_clustering.py | 6 +-- .../errors/SessionRecordingErrors.tsx | 45 +++++++++++++------ .../errors/sessionRecordingErrorsLogic.ts | 20 ++++++++- frontend/src/types.ts | 3 +- 4 files changed, 55 insertions(+), 19 deletions(-) diff --git a/ee/session_recordings/ai/error_clustering.py b/ee/session_recordings/ai/error_clustering.py index 03ea4f62d2789..edc06ff471355 100644 --- a/ee/session_recordings/ai/error_clustering.py +++ b/ee/session_recordings/ai/error_clustering.py @@ -76,12 +76,12 @@ def construct_response(df: pd.DataFrame, team: Team, user: User): clusters = [] for cluster, rows in df.groupby("cluster"): session_ids = rows["session_id"].unique() - sample = rows.sample(n=1)[["session_id", "input"]].rename(columns={"input": "error"}).to_dict("records") + sample = rows.sample(n=1)[["session_id", "input"]].rename(columns={"input": "error"}).to_dict("records")[0] clusters.append( { "cluster": cluster, - "sample": sample, - "session_ids": session_ids, + "sample": sample.get("error"), + "session_ids": np.random.choice(session_ids, size=DBSCAN_MIN_SAMPLES - 1), "occurrences": rows.size, "unique_sessions": len(session_ids), "viewed": len(np.intersect1d(session_ids, viewed_session_ids, assume_unique=True)), diff --git a/frontend/src/scenes/session-recordings/errors/SessionRecordingErrors.tsx b/frontend/src/scenes/session-recordings/errors/SessionRecordingErrors.tsx index 56eac1213d91a..4b2dd2d1abed3 100644 --- a/frontend/src/scenes/session-recordings/errors/SessionRecordingErrors.tsx +++ b/frontend/src/scenes/session-recordings/errors/SessionRecordingErrors.tsx @@ -14,7 +14,7 @@ const MAX_TITLE_LENGTH = 75 export function SessionRecordingErrors(): JSX.Element { const { openSessionPlayer } = useActions(sessionPlayerModalLogic) const { errors, errorsLoading } = useValues(sessionRecordingErrorsLogic) - const { loadErrorClusters } = useActions(sessionRecordingErrorsLogic) + const { loadErrorClusters, createPlaylist } = useActions(sessionRecordingErrorsLogic) if (errorsLoading) { return @@ -36,7 +36,7 @@ export function SessionRecordingErrors(): JSX.Element { title: 'Error', dataIndex: 'cluster', render: (_, cluster) => { - const displayTitle = parseTitle(cluster.sample.error) + const displayTitle = parseTitle(cluster.sample) return (
{displayTitle} @@ -68,23 +68,40 @@ export function SessionRecordingErrors(): JSX.Element { title: 'Actions', render: function Render(_, cluster) { return ( - { - e.preventDefault() - openSessionPlayer({ id: cluster.sample.session_id }) - }} - className="p-2 whitespace-nowrap" - type="primary" - > - Watch example - +
+ { + e.preventDefault() + openSessionPlayer({ id: cluster.session_ids[0] }) + }} + className="whitespace-nowrap" + type="primary" + > + Watch example + + { + createPlaylist( + `Examples of '${parseTitle(cluster.sample)}'`, + cluster.session_ids + ) + }} + className="whitespace-nowrap" + type="secondary" + tooltip="Create a playlist of recordings containing this issue" + > + Create playlist + +
) }, }, ]} dataSource={errors} - expandable={{ expandedRowRender: (cluster) => }} + expandable={{ + expandedRowRender: (cluster) => , + }} /> diff --git a/frontend/src/scenes/session-recordings/errors/sessionRecordingErrorsLogic.ts b/frontend/src/scenes/session-recordings/errors/sessionRecordingErrorsLogic.ts index 45b887fd33cbb..49de62c7bf5c4 100644 --- a/frontend/src/scenes/session-recordings/errors/sessionRecordingErrorsLogic.ts +++ b/frontend/src/scenes/session-recordings/errors/sessionRecordingErrorsLogic.ts @@ -1,13 +1,19 @@ -import { afterMount, kea, path } from 'kea' +import { actions, afterMount, kea, listeners, path } from 'kea' import { loaders } from 'kea-loaders' +import { router } from 'kea-router' import api from 'lib/api' +import { urls } from 'scenes/urls' import { ErrorClusterResponse } from '~/types' +import { createPlaylist } from '../playlist/playlistUtils' import type { sessionRecordingErrorsLogicType } from './sessionRecordingErrorsLogicType' export const sessionRecordingErrorsLogic = kea([ path(['scenes', 'session-recordings', 'detail', 'sessionRecordingErrorsLogic']), + actions({ + createPlaylist: (name: string, sessionIds: string[]) => ({ name, sessionIds }), + }), loaders(() => ({ errors: [ null as ErrorClusterResponse, @@ -19,7 +25,19 @@ export const sessionRecordingErrorsLogic = kea( }, ], })), + listeners(() => ({ + createPlaylist: async ({ name, sessionIds }) => { + const playlist = await createPlaylist({ name: name }) + if (playlist) { + const samples = sessionIds.slice(0, 10) + await Promise.all( + samples.map((sessionId) => api.recordings.addRecordingToPlaylist(playlist.short_id, sessionId)) + ) + router.actions.push(urls.replayPlaylist(playlist.short_id)) + } + }, + })), afterMount(({ actions }) => { actions.loadErrorClusters(false) }), diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 26c15fd69d17d..a7f65e84c5473 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -903,8 +903,9 @@ export interface SessionRecordingsResponse { export type ErrorCluster = { cluster: number - sample: { session_id: string; error: string } + sample: string occurrences: number + session_ids: string[] unique_sessions: number viewed: number } From 08acfba72ee852c6ac5af957223447ccf6457de0 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 21 Mar 2024 18:09:22 +0100 Subject: [PATCH 25/51] fix(insights): more trends hogql fixes (#21001) --- .../trends/test/test_trends_query_runner.py | 45 +++++++++++++++ .../insights/trends/trends_query_runner.py | 56 +++++++++++-------- 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py index 104e232a01406..09a7389e74c7b 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py @@ -364,6 +364,19 @@ def test_trends_query_formula(self): self.assertEqual("Formula (A+B)", response.results[0]["label"]) self.assertEqual([1, 0, 2, 4, 4, 0, 2, 1, 1, 0, 1], response.results[0]["data"]) + def test_trends_query_formula_breakdown_no_data(self): + self._create_test_events() + + response = self._run_trends_query( + self.default_date_from, + self.default_date_to, + IntervalType.day, + [EventsNode(event="$pageviewxxx"), EventsNode(event="$pageleavexxx")], + TrendsFilter(formula="A+B"), + BreakdownFilter(breakdown_type=BreakdownType.person, breakdown="$browser"), + ) + self.assertEqual([], response.results) + def test_trends_query_formula_aggregate(self): self._create_test_events() @@ -1114,6 +1127,38 @@ def test_breakdown_values_limit(self): ) self.assertEqual(len(response.results), 11) + def test_breakdown_values_unknown_property(self): + # same as above test, just without creating the property definition + for value in list(range(30)): + _create_event( + team=self.team, + event="$pageview", + distinct_id=f"person_{value}", + timestamp="2020-01-11T12:00:00Z", + properties={"breakdown_value": f"{value}"}, + ) + + response = self._run_trends_query( + "2020-01-09", + "2020-01-20", + IntervalType.day, + [EventsNode(event="$pageview")], + TrendsFilter(display=ChartDisplayType.ActionsLineGraph), + BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.event), + ) + + self.assertEqual(len(response.results), 26) + + response = self._run_trends_query( + "2020-01-09", + "2020-01-20", + IntervalType.day, + [EventsNode(event="$pageview")], + TrendsFilter(display=ChartDisplayType.ActionsLineGraph), + BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.event, breakdown_limit=10), + ) + self.assertEqual(len(response.results), 11) + def test_breakdown_values_world_map_limit(self): PropertyDefinition.objects.create(team=self.team, name="breakdown_value", property_type="String") diff --git a/posthog/hogql_queries/insights/trends/trends_query_runner.py b/posthog/hogql_queries/insights/trends/trends_query_runner.py index 40f26664c2585..b698e095e8b09 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/trends_query_runner.py @@ -666,25 +666,27 @@ def apply_formula(self, formula: str, results: List[Dict[str, Any]]) -> List[Dic res.append(new_result) return res - if self._trends_display.should_aggregate_values(): - series_data = list(map(lambda s: [s["aggregated_value"]], results)) - new_series_data = FormulaAST(series_data).call(formula) - - new_result = results[0] - new_result["aggregated_value"] = float(sum(new_series_data)) - new_result["data"] = None - new_result["count"] = 0 - new_result["label"] = f"Formula ({formula})" - else: - series_data = list(map(lambda s: s["data"], results)) - new_series_data = FormulaAST(series_data).call(formula) + if len(results) > 0: + if self._trends_display.should_aggregate_values(): + series_data = list(map(lambda s: [s["aggregated_value"]], results)) + new_series_data = FormulaAST(series_data).call(formula) + + new_result = results[0] + new_result["aggregated_value"] = float(sum(new_series_data)) + new_result["data"] = None + new_result["count"] = 0 + new_result["label"] = f"Formula ({formula})" + else: + series_data = list(map(lambda s: s["data"], results)) + new_series_data = FormulaAST(series_data).call(formula) - new_result = results[0] - new_result["data"] = new_series_data - new_result["count"] = float(sum(new_series_data)) - new_result["label"] = f"Formula ({formula})" + new_result = results[0] + new_result["data"] = new_series_data + new_result["count"] = float(sum(new_series_data)) + new_result["label"] = f"Formula ({formula})" - return [new_result] + return [new_result] + return [] def _is_breakdown_field_boolean(self): if not self.query.breakdownFilter or not self.query.breakdownFilter.breakdown_type: @@ -741,13 +743,19 @@ def _event_property( field: str, field_type: PropertyDefinition.Type, group_type_index: Optional[int], - ): - return PropertyDefinition.objects.get( - name=field, - team=self.team, - type=field_type, - group_type_index=group_type_index if field_type == PropertyDefinition.Type.GROUP else None, - ).property_type + ) -> str: + try: + return ( + PropertyDefinition.objects.get( + name=field, + team=self.team, + type=field_type, + group_type_index=group_type_index if field_type == PropertyDefinition.Type.GROUP else None, + ).property_type + or "String" + ) + except PropertyDefinition.DoesNotExist: + return "String" # TODO: Move this to posthog/hogql_queries/legacy_compatibility/query_to_filter.py def _query_to_filter(self) -> Dict[str, Any]: From f5e4b17201afea02baddeb5a02f7d1f489747884 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Thu, 21 Mar 2024 19:20:42 +0100 Subject: [PATCH 26/51] fix(insights): Make HogQL queries `limit_context`-aware (#21022) * fix(insights): Make HogQL queries `limit_context`-aware * Add some tests * Update `execute_hogql_query` shape * Fix minor issues * Update mypy-baseline.txt --- mypy-baseline.txt | 1 - posthog/hogql/query.py | 1 + .../funnel_correlation_query_runner.py | 1 + .../insights/funnels/funnels_query_runner.py | 1 + .../insights/insight_actors_query_runner.py | 1 + .../insights/lifecycle_query_runner.py | 1 + posthog/hogql_queries/insights/paginators.py | 3 ++- .../insights/paths_query_runner.py | 1 + .../insights/retention_query_runner.py | 1 + .../insights/stickiness_query_runner.py | 1 + .../insights/test/test_paginators.py | 6 +++-- .../test/test_retention_query_runner.py | 16 ++++++++++-- .../test/test_stickiness_query_runner.py | 14 ++++++++++- .../trends/test/test_trends_query_runner.py | 25 ++++++++++++++++--- .../insights/trends/trends_query_runner.py | 1 + .../sessions_timeline_query_runner.py | 1 + .../web_analytics/test/test_web_overview.py | 23 +++++++++++++++-- .../hogql_queries/web_analytics/top_clicks.py | 1 + .../web_analytics_query_runner.py | 1 + .../web_analytics/web_overview.py | 1 + 20 files changed, 89 insertions(+), 12 deletions(-) diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 180d259fc3c20..f0a464d26c546 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -549,7 +549,6 @@ posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py:0: err posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py:0: error: Item "SelectQuery" of "SelectQuery | SelectUnionQuery | Field | Any | None" has no attribute "chain" [union-attr] posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py:0: error: Item "SelectUnionQuery" of "SelectQuery | SelectUnionQuery | Field | Any | None" has no attribute "chain" [union-attr] posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py:0: error: Item "None" of "SelectQuery | SelectUnionQuery | Field | Any | None" has no attribute "chain" [union-attr] -posthog/hogql_queries/insights/test/test_paginators.py:0: error: Argument 2 to "execute_hogql_query" of "HogQLHasMorePaginator" has incompatible type "SelectQuery | SelectUnionQuery"; expected "SelectQuery" [arg-type] posthog/hogql_queries/insights/test/test_paginators.py:0: error: Value of type "object" is not indexable [index] posthog/hogql_queries/insights/test/test_paginators.py:0: error: Value of type "object" is not indexable [index] posthog/hogql_queries/insights/test/test_paginators.py:0: error: Value of type "object" is not indexable [index] diff --git a/posthog/hogql/query.py b/posthog/hogql/query.py index e7bc3f7984205..69b5656020904 100644 --- a/posthog/hogql/query.py +++ b/posthog/hogql/query.py @@ -28,6 +28,7 @@ def execute_hogql_query( query: Union[str, ast.SelectQuery, ast.SelectUnionQuery], team: Team, + *, query_type: str = "hogql_query", filters: Optional[HogQLFilters] = None, placeholders: Optional[Dict[str, ast.Expr]] = None, diff --git a/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py b/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py index 2fef78a372324..bcb362ff3d4f9 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py +++ b/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py @@ -236,6 +236,7 @@ def _calculate(self) -> tuple[List[EventOddsRatio], bool, str, HogQLQueryRespons team=self.team, timings=self.timings, modifiers=self.modifiers, + limit_context=self.limit_context, ) assert response.results diff --git a/posthog/hogql_queries/insights/funnels/funnels_query_runner.py b/posthog/hogql_queries/insights/funnels/funnels_query_runner.py index 38e04603f2725..b1ca8dfd6dd54 100644 --- a/posthog/hogql_queries/insights/funnels/funnels_query_runner.py +++ b/posthog/hogql_queries/insights/funnels/funnels_query_runner.py @@ -92,6 +92,7 @@ def calculate(self): team=self.team, timings=self.timings, modifiers=self.modifiers, + limit_context=self.limit_context, ) results = self.funnel_class._format_results(response.results) diff --git a/posthog/hogql_queries/insights/insight_actors_query_runner.py b/posthog/hogql_queries/insights/insight_actors_query_runner.py index a0e38abae1776..d58f36cb6f7ee 100644 --- a/posthog/hogql_queries/insights/insight_actors_query_runner.py +++ b/posthog/hogql_queries/insights/insight_actors_query_runner.py @@ -102,6 +102,7 @@ def calculate(self) -> HogQLQueryResponse: team=self.team, timings=self.timings, modifiers=self.modifiers, + limit_context=self.limit_context, ) def _is_stale(self, cached_result_package): diff --git a/posthog/hogql_queries/insights/lifecycle_query_runner.py b/posthog/hogql_queries/insights/lifecycle_query_runner.py index 24bbe36f1c6bf..62968f5349e0e 100644 --- a/posthog/hogql_queries/insights/lifecycle_query_runner.py +++ b/posthog/hogql_queries/insights/lifecycle_query_runner.py @@ -157,6 +157,7 @@ def calculate(self) -> LifecycleQueryResponse: team=self.team, timings=self.timings, modifiers=self.modifiers, + limit_context=self.limit_context, ) # TODO: can we move the data conversion part into the query as well? It would make it easier to swap diff --git a/posthog/hogql_queries/insights/paginators.py b/posthog/hogql_queries/insights/paginators.py index 6dbdb1543b929..0dfda79ced617 100644 --- a/posthog/hogql_queries/insights/paginators.py +++ b/posthog/hogql_queries/insights/paginators.py @@ -54,8 +54,9 @@ def trim_results(self) -> list[Any]: def execute_hogql_query( self, - query_type: str, query: ast.SelectQuery, + *, + query_type: str, **kwargs, ) -> HogQLQueryResponse: self.response = cast( diff --git a/posthog/hogql_queries/insights/paths_query_runner.py b/posthog/hogql_queries/insights/paths_query_runner.py index c10a5a2320207..c454feb8e56ac 100644 --- a/posthog/hogql_queries/insights/paths_query_runner.py +++ b/posthog/hogql_queries/insights/paths_query_runner.py @@ -725,6 +725,7 @@ def calculate(self) -> PathsQueryResponse: team=self.team, timings=self.timings, modifiers=self.modifiers, + limit_context=self.limit_context, ) response.results = self.validate_results(response.results) diff --git a/posthog/hogql_queries/insights/retention_query_runner.py b/posthog/hogql_queries/insights/retention_query_runner.py index 221cb976757d2..3ac2c5b4b5462 100644 --- a/posthog/hogql_queries/insights/retention_query_runner.py +++ b/posthog/hogql_queries/insights/retention_query_runner.py @@ -313,6 +313,7 @@ def calculate(self) -> RetentionQueryResponse: team=self.team, timings=self.timings, modifiers=self.modifiers, + limit_context=self.limit_context, ) result_dict = { diff --git a/posthog/hogql_queries/insights/stickiness_query_runner.py b/posthog/hogql_queries/insights/stickiness_query_runner.py index d0b4b65c67f9b..184e3c0af02df 100644 --- a/posthog/hogql_queries/insights/stickiness_query_runner.py +++ b/posthog/hogql_queries/insights/stickiness_query_runner.py @@ -212,6 +212,7 @@ def calculate(self): team=self.team, timings=self.timings, modifiers=self.modifiers, + limit_context=self.limit_context, ) if response.timings is not None: diff --git a/posthog/hogql_queries/insights/test/test_paginators.py b/posthog/hogql_queries/insights/test/test_paginators.py index ac83efb45b353..6698115c46535 100644 --- a/posthog/hogql_queries/insights/test/test_paginators.py +++ b/posthog/hogql_queries/insights/test/test_paginators.py @@ -1,3 +1,5 @@ +from typing import cast +from posthog.hogql.ast import SelectQuery from posthog.hogql.constants import ( LimitContext, get_default_limit_for_context, @@ -136,8 +138,8 @@ def test_response_params_consistency(self): """Test consistency of response_params method.""" paginator = HogQLHasMorePaginator(limit=5, offset=10) paginator.response = paginator.execute_hogql_query( - "test_query", - parse_select("SELECT * FROM persons"), + cast(SelectQuery, parse_select("SELECT * FROM persons")), + query_type="test_query", team=self.team, ) params = paginator.response_params() diff --git a/posthog/hogql_queries/insights/test/test_retention_query_runner.py b/posthog/hogql_queries/insights/test/test_retention_query_runner.py index 30edb32102f76..04c108dd779f1 100644 --- a/posthog/hogql_queries/insights/test/test_retention_query_runner.py +++ b/posthog/hogql_queries/insights/test/test_retention_query_runner.py @@ -1,3 +1,5 @@ +from typing import Optional +from unittest.mock import MagicMock, patch import uuid from datetime import datetime @@ -6,11 +8,14 @@ from django.test import override_settings from rest_framework import status +from posthog.clickhouse.client.execute import sync_execute from posthog.constants import ( RETENTION_FIRST_TIME, TREND_FILTER_TYPE_ACTIONS, TREND_FILTER_TYPE_EVENTS, ) +from posthog.hogql.constants import LimitContext +from posthog.hogql.query import INCREASED_MAX_EXECUTION_TIME from posthog.hogql_queries.insights.retention_query_runner import RetentionQueryRunner from posthog.hogql_queries.actors_query_runner import ActorsQueryRunner from posthog.models import Action, ActionStep @@ -1685,10 +1690,10 @@ def test_day_interval_sampled(self): class TestClickhouseRetentionGroupAggregation(ClickhouseTestMixin, APIBaseTest): - def run_query(self, query): + def run_query(self, query, *, limit_context: Optional[LimitContext] = None): if not query.get("retentionFilter"): query["retentionFilter"] = {} - runner = RetentionQueryRunner(team=self.team, query=query) + runner = RetentionQueryRunner(team=self.team, query=query, limit_context=limit_context) return runner.calculate().model_dump()["results"] def run_actors_query(self, interval, query, select=None, actor="person"): @@ -1920,3 +1925,10 @@ def test_groups_aggregating_person_on_events(self): [1], ], ) + + @patch("posthog.hogql.query.sync_execute", wraps=sync_execute) + def test_limit_is_context_aware(self, mock_sync_execute: MagicMock): + self.run_query(query={}, limit_context=LimitContext.QUERY_ASYNC) + + mock_sync_execute.assert_called_once() + self.assertIn(f" max_execution_time={INCREASED_MAX_EXECUTION_TIME},", mock_sync_execute.call_args[0][0]) diff --git a/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py b/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py index 3de1fb6ce865e..6e25827e6ecba 100644 --- a/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py +++ b/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py @@ -1,8 +1,12 @@ from dataclasses import dataclass from typing import Dict, List, Optional, Union +from unittest.mock import MagicMock, patch from django.test import override_settings from freezegun import freeze_time +from posthog.clickhouse.client.execute import sync_execute +from posthog.hogql.constants import LimitContext +from posthog.hogql.query import INCREASED_MAX_EXECUTION_TIME from posthog.hogql_queries.insights.stickiness_query_runner import StickinessQueryRunner from posthog.models.action.action import Action from posthog.models.action_step import ActionStep @@ -197,6 +201,7 @@ def _run_query( properties: Optional[StickinessProperties] = None, filters: Optional[StickinessFilter] = None, filter_test_accounts: Optional[bool] = False, + limit_context: Optional[LimitContext] = None, ): query_series: List[EventsNode | ActionsNode] = [EventsNode(event="$pageview")] if series is None else series query_date_from = date_from or self.default_date_from @@ -211,7 +216,7 @@ def _run_query( stickinessFilter=filters, filterTestAccounts=filter_test_accounts, ) - return StickinessQueryRunner(team=self.team, query=query).calculate() + return StickinessQueryRunner(team=self.team, query=query, limit_context=limit_context).calculate() def test_stickiness_runs(self): self._create_test_events() @@ -580,3 +585,10 @@ def test_hogql_aggregations(self): 1, 0, ] + + @patch("posthog.hogql.query.sync_execute", wraps=sync_execute) + def test_limit_is_context_aware(self, mock_sync_execute: MagicMock): + self._run_query(limit_context=LimitContext.QUERY_ASYNC) + + mock_sync_execute.assert_called_once() + self.assertIn(f" max_execution_time={INCREASED_MAX_EXECUTION_TIME},", mock_sync_execute.call_args[0][0]) diff --git a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py index 09a7389e74c7b..433df7c7df23b 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py @@ -1,11 +1,13 @@ from dataclasses import dataclass from typing import Dict, List, Optional -from unittest.mock import patch +from unittest.mock import MagicMock, patch from django.test import override_settings from freezegun import freeze_time +from posthog.clickhouse.client.execute import sync_execute from posthog.hogql import ast -from posthog.hogql.constants import MAX_SELECT_RETURNED_ROWS +from posthog.hogql.constants import MAX_SELECT_RETURNED_ROWS, LimitContext from posthog.hogql.modifiers import create_default_modifiers_for_team +from posthog.hogql.query import INCREASED_MAX_EXECUTION_TIME from posthog.hogql_queries.insights.trends.trends_query_runner import TrendsQueryRunner from posthog.models.cohort.cohort import Cohort from posthog.models.property_definition import PropertyDefinition @@ -175,6 +177,7 @@ def _create_query_runner( breakdown: Optional[BreakdownFilter] = None, filter_test_accounts: Optional[bool] = None, hogql_modifiers: Optional[HogQLQueryModifiers] = None, + limit_context: Optional[LimitContext] = None, ) -> TrendsQueryRunner: query_series: List[EventsNode | ActionsNode] = [EventsNode(event="$pageview")] if series is None else series query = TrendsQuery( @@ -185,7 +188,7 @@ def _create_query_runner( breakdownFilter=breakdown, filterTestAccounts=filter_test_accounts, ) - return TrendsQueryRunner(team=self.team, query=query, modifiers=hogql_modifiers) + return TrendsQueryRunner(team=self.team, query=query, modifiers=hogql_modifiers, limit_context=limit_context) def _run_trends_query( self, @@ -195,8 +198,10 @@ def _run_trends_query( series: Optional[List[EventsNode | ActionsNode]], trends_filters: Optional[TrendsFilter] = None, breakdown: Optional[BreakdownFilter] = None, + *, filter_test_accounts: Optional[bool] = None, hogql_modifiers: Optional[HogQLQueryModifiers] = None, + limit_context: Optional[LimitContext] = None, ): return self._create_query_runner( date_from=date_from, @@ -207,6 +212,7 @@ def _run_trends_query( breakdown=breakdown, filter_test_accounts=filter_test_accounts, hogql_modifiers=hogql_modifiers, + limit_context=limit_context, ).calculate() def test_trends_query_label(self): @@ -1694,3 +1700,16 @@ def test_to_actors_query_options_breakdowns_hogql(self): BreakdownItem(label="Safari", value="Safari"), BreakdownItem(label="Edge", value="Edge"), ] + + @patch("posthog.hogql.query.sync_execute", wraps=sync_execute) + def test_limit_is_context_aware(self, mock_sync_execute: MagicMock): + self._run_trends_query( + "2020-01-09", + "2020-01-20", + IntervalType.day, + [EventsNode(event="$pageview")], + limit_context=LimitContext.QUERY_ASYNC, + ) + + mock_sync_execute.assert_called_once() + self.assertIn(f" max_execution_time={INCREASED_MAX_EXECUTION_TIME},", mock_sync_execute.call_args[0][0]) diff --git a/posthog/hogql_queries/insights/trends/trends_query_runner.py b/posthog/hogql_queries/insights/trends/trends_query_runner.py index b698e095e8b09..d66110298ffab 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/trends_query_runner.py @@ -292,6 +292,7 @@ def run(index: int, query: ast.SelectQuery | ast.SelectUnionQuery, is_parallel: team=self.team, timings=self.timings, modifiers=self.modifiers, + limit_context=self.limit_context, ) timings_matrix[index] = response.timings diff --git a/posthog/hogql_queries/sessions_timeline_query_runner.py b/posthog/hogql_queries/sessions_timeline_query_runner.py index d920ec7cf94fd..cda9433d63efa 100644 --- a/posthog/hogql_queries/sessions_timeline_query_runner.py +++ b/posthog/hogql_queries/sessions_timeline_query_runner.py @@ -135,6 +135,7 @@ def calculate(self) -> SessionsTimelineQueryResponse: query_type="SessionsTimelineQuery", timings=self.timings, modifiers=self.modifiers, + limit_context=self.limit_context, ) assert query_result.results is not None timeline_entries_map: Dict[str, TimelineEntry] = {} diff --git a/posthog/hogql_queries/web_analytics/test/test_web_overview.py b/posthog/hogql_queries/web_analytics/test/test_web_overview.py index 63a26ffea9233..dcafe660fc72d 100644 --- a/posthog/hogql_queries/web_analytics/test/test_web_overview.py +++ b/posthog/hogql_queries/web_analytics/test/test_web_overview.py @@ -1,6 +1,11 @@ +from typing import Optional +from unittest.mock import MagicMock, patch from freezegun import freeze_time from parameterized import parameterized +from posthog.clickhouse.client.execute import sync_execute +from posthog.hogql.constants import LimitContext +from posthog.hogql.query import INCREASED_MAX_EXECUTION_TIME from posthog.hogql_queries.web_analytics.web_overview import WebOverviewQueryRunner from posthog.schema import WebOverviewQuery, DateRange from posthog.test.base import ( @@ -36,14 +41,21 @@ def _create_events(self, data, event="$pageview"): ) return person_result - def _run_web_overview_query(self, date_from, date_to, use_sessions_table=False, compare=True): + def _run_web_overview_query( + self, + date_from: str, + date_to: str, + use_sessions_table: bool = False, + compare: bool = True, + limit_context: Optional[LimitContext] = None, + ): query = WebOverviewQuery( dateRange=DateRange(date_from=date_from, date_to=date_to), properties=[], compare=compare, useSessionsTable=use_sessions_table, ) - runner = WebOverviewQueryRunner(team=self.team, query=query) + runner = WebOverviewQueryRunner(team=self.team, query=query, limit_context=limit_context) return runner.calculate() @parameterized.expand([(True,), (False,)]) @@ -185,3 +197,10 @@ def test_correctly_counts_pageviews_in_long_running_session(self, use_sessions_t sessions = results[2] self.assertEqual(1, sessions.value) + + @patch("posthog.hogql.query.sync_execute", wraps=sync_execute) + def test_limit_is_context_aware(self, mock_sync_execute: MagicMock): + self._run_web_overview_query("2023-12-01", "2023-12-03", limit_context=LimitContext.QUERY_ASYNC) + + mock_sync_execute.assert_called_once() + self.assertIn(f" max_execution_time={INCREASED_MAX_EXECUTION_TIME},", mock_sync_execute.call_args[0][0]) diff --git a/posthog/hogql_queries/web_analytics/top_clicks.py b/posthog/hogql_queries/web_analytics/top_clicks.py index 3218e68975f7a..192d7b279b704 100644 --- a/posthog/hogql_queries/web_analytics/top_clicks.py +++ b/posthog/hogql_queries/web_analytics/top_clicks.py @@ -51,6 +51,7 @@ def calculate(self): team=self.team, timings=self.timings, modifiers=self.modifiers, + limit_context=self.limit_context, ) return WebTopClicksQueryResponse( diff --git a/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py b/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py index da4f98edcbf32..12ef703271c51 100644 --- a/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py +++ b/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py @@ -211,6 +211,7 @@ def _get_or_calculate_sample_ratio(self) -> SamplingRate: query=event_count, team=self.team, timings=self.timings, + limit_context=self.limit_context, ) if not response.results or not response.results[0] or not response.results[0][0]: diff --git a/posthog/hogql_queries/web_analytics/web_overview.py b/posthog/hogql_queries/web_analytics/web_overview.py index 38388315c8f0b..2da015a60ac4e 100644 --- a/posthog/hogql_queries/web_analytics/web_overview.py +++ b/posthog/hogql_queries/web_analytics/web_overview.py @@ -285,6 +285,7 @@ def calculate(self): team=self.team, timings=self.timings, modifiers=self.modifiers, + limit_context=self.limit_context, ) assert response.results From 7ac5d2b7c4f7b9ccb8d051e9deeecba2d20be042 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 21 Mar 2024 12:41:23 -0600 Subject: [PATCH 27/51] chore(batch-exports): remove log about empty heartbeat (#21075) cleanup(batch-exports): remove log about empty heartbeat --- posthog/temporal/common/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/posthog/temporal/common/utils.py b/posthog/temporal/common/utils.py index 1b61a356dc898..022c8270d7748 100644 --- a/posthog/temporal/common/utils.py +++ b/posthog/temporal/common/utils.py @@ -119,10 +119,9 @@ async def should_resume_from_activity_heartbeat( heartbeat_details = heartbeat_type.from_activity(activity) except EmptyHeartbeatError: - # We don't log this as a warning/error because it's the expected exception when heartbeat is empty. + # We don't log this as it's the expected exception when heartbeat is empty. heartbeat_details = None received = False - logger.debug("Did not receive details from previous activity execution") except NotEnoughHeartbeatValuesError: heartbeat_details = None From 888f2eee7b0b5ab2d8ef424b24e3c00ed850b914 Mon Sep 17 00:00:00 2001 From: Eric Duong Date: Thu, 21 Mar 2024 15:12:05 -0400 Subject: [PATCH 28/51] fix(data-warehouse): make sure taxonomic filter and popup are populated when selected a data warehouse filter (#21021) * add fix * use props correctly * Update UI snapshots for `chromium` (1) * check null * Update UI snapshots for `chromium` (1) --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .../DefinitionPopover/DefinitionPopoverContents.tsx | 9 +++++++++ .../src/lib/components/TaxonomicFilter/InfiniteList.tsx | 1 - .../lib/components/TaxonomicFilter/TaxonomicFilter.tsx | 2 ++ .../components/TaxonomicFilter/taxonomicFilterLogic.tsx | 3 ++- frontend/src/lib/components/TaxonomicFilter/types.ts | 2 ++ .../lib/components/TaxonomicPopover/TaxonomicPopover.tsx | 4 ++++ .../ActionFilter/ActionFilterRow/ActionFilterRow.tsx | 1 + 7 files changed, 20 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/DefinitionPopover/DefinitionPopoverContents.tsx b/frontend/src/lib/components/DefinitionPopover/DefinitionPopoverContents.tsx index 1c9e4928ecb22..8be1a72543f44 100644 --- a/frontend/src/lib/components/DefinitionPopover/DefinitionPopoverContents.tsx +++ b/frontend/src/lib/components/DefinitionPopover/DefinitionPopoverContents.tsx @@ -111,8 +111,16 @@ function DefinitionView({ group }: { group: TaxonomicFilterGroup }): JSX.Element } = useValues(definitionPopoverLogic) const { setLocalDefinition } = useActions(definitionPopoverLogic) + const { selectedItemMeta } = useValues(taxonomicFilterLogic) const { selectItem } = useActions(taxonomicFilterLogic) + // Use effect here to make definition view stateful. TaxonomicFilterLogic won't mount within definitionPopoverLogic + useEffect(() => { + if (selectedItemMeta && definition.name == selectedItemMeta.id) { + setLocalDefinition(selectedItemMeta) + } + }, [definition]) + if (!definition) { return <> } @@ -280,6 +288,7 @@ function DefinitionView({ group }: { group: TaxonomicFilterGroup }): JSX.Element value: column.key, })) const itemValue = localDefinition ? group?.getValue?.(localDefinition) : null + return (
diff --git a/frontend/src/lib/components/TaxonomicFilter/InfiniteList.tsx b/frontend/src/lib/components/TaxonomicFilter/InfiniteList.tsx index eca954d86f94f..8e0237d36f252 100644 --- a/frontend/src/lib/components/TaxonomicFilter/InfiniteList.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/InfiniteList.tsx @@ -173,7 +173,6 @@ export function InfiniteList({ popupAnchorElement }: InfiniteListProps): JSX.Ele const { mouseInteractionsEnabled, activeTab, searchQuery, value, groupType, eventNames } = useValues(taxonomicFilterLogic) const { selectItem } = useActions(taxonomicFilterLogic) - const { isLoading, results, diff --git a/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.tsx b/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.tsx index bd2f56b8dcfc9..52e99e1e432e6 100644 --- a/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/TaxonomicFilter.tsx @@ -21,6 +21,7 @@ export function TaxonomicFilter({ taxonomicFilterLogicKey: taxonomicFilterLogicKeyInput, groupType, value, + filter, onChange, onClose, taxonomicGroupTypes, @@ -48,6 +49,7 @@ export function TaxonomicFilter({ taxonomicFilterLogicKey, groupType, value, + filter, onChange, taxonomicGroupTypes, optionsFromProp, diff --git a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx index 9809e801308f6..b5904b8957056 100644 --- a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx @@ -141,6 +141,7 @@ export const taxonomicFilterLogic = kea([ ], })), selectors({ + selectedItemMeta: [() => [(_, props) => props.filter], (filter) => filter], taxonomicFilterLogicKey: [ (_, p) => [p.taxonomicFilterLogicKey], (taxonomicFilterLogicKey) => taxonomicFilterLogicKey, @@ -218,7 +219,7 @@ export const taxonomicFilterLogic = kea([ logic: dataWarehouseSceneLogic, value: 'externalTables', getName: (table: DataWarehouseTableType) => table.name, - getValue: (table: DataWarehouseTableType) => table.id, + getValue: (table: DataWarehouseTableType) => table.name, getPopoverHeader: () => 'Data Warehouse Table', getIcon: () => , }, diff --git a/frontend/src/lib/components/TaxonomicFilter/types.ts b/frontend/src/lib/components/TaxonomicFilter/types.ts index cde2e9d678ded..a3edff51c16f0 100644 --- a/frontend/src/lib/components/TaxonomicFilter/types.ts +++ b/frontend/src/lib/components/TaxonomicFilter/types.ts @@ -1,6 +1,7 @@ import Fuse from 'fuse.js' import { BuiltLogic, LogicWrapper } from 'kea' import { DataWarehouseTableType } from 'scenes/data-warehouse/types' +import { LocalFilter } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' import { AnyDataNode, DatabaseSchemaQueryResponseField } from '~/queries/schema' import { @@ -22,6 +23,7 @@ export interface TaxonomicFilterProps { value?: TaxonomicFilterValue onChange?: (group: TaxonomicFilterGroup, value: TaxonomicFilterValue, item: any) => void onClose?: () => void + filter?: LocalFilter taxonomicGroupTypes: TaxonomicFilterGroupType[] taxonomicFilterLogicKey?: string optionsFromProp?: Partial> diff --git a/frontend/src/lib/components/TaxonomicPopover/TaxonomicPopover.tsx b/frontend/src/lib/components/TaxonomicPopover/TaxonomicPopover.tsx index 4fe515646323c..b0e6cfc85a508 100644 --- a/frontend/src/lib/components/TaxonomicPopover/TaxonomicPopover.tsx +++ b/frontend/src/lib/components/TaxonomicPopover/TaxonomicPopover.tsx @@ -4,6 +4,7 @@ import { TaxonomicFilterGroupType, TaxonomicFilterValue } from 'lib/components/T import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' import { LemonDropdown } from 'lib/lemon-ui/LemonDropdown' import { useEffect, useState } from 'react' +import { LocalFilter } from 'scenes/insights/filters/ActionFilter/entityFilterLogic' import { AnyDataNode, DatabaseSchemaQueryResponseField } from '~/queries/schema' @@ -13,6 +14,7 @@ export interface TaxonomicPopoverProps void + filter?: LocalFilter groupTypes?: TaxonomicFilterGroupType[] renderValue?: (value: ValueType) => JSX.Element | null eventNames?: string[] @@ -41,6 +43,7 @@ export function TaxonomicStringPopover(props: TaxonomicPopoverProps): JS export function TaxonomicPopover({ groupType, value, + filter, onChange, renderValue, groupTypes, @@ -81,6 +84,7 @@ export function TaxonomicPopover { onChange?.(payload as ValueType, type, item) setVisible(false) diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx index 3f6d31c9489b2..bca5a483baf48 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx @@ -228,6 +228,7 @@ export function ActionFilterRow({ fullWidth groupType={filter.type as TaxonomicFilterGroupType} value={getValue(value, filter)} + filter={filter} onChange={(changedValue, taxonomicGroupType, item) => { const groupType = taxonomicFilterGroupTypeToEntityType(taxonomicGroupType) if (groupType === EntityTypes.DATA_WAREHOUSE) { From d35f67ec53d369df51597acea9cecb67b223ded0 Mon Sep 17 00:00:00 2001 From: Tiina Turban Date: Thu, 21 Mar 2024 21:52:15 +0100 Subject: [PATCH 29/51] fix: batch export PG form checkbox initial edit state (#21088) --- .../batch_exports/BatchExportEditForm.tsx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx b/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx index 7fbbc8cc29d69..b015659cfaef1 100644 --- a/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx +++ b/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx @@ -375,17 +375,21 @@ export function BatchExportsEditFields({ - - Does your Postgres instance have a self-signed SSL certificate? - - - - - } - /> + {({ value, onChange }) => ( + + Does your Postgres instance have a self-signed SSL certificate? + + + + + } + checked={!!value} + onChange={onChange} + /> + )} From fa5a1d61029bea66f91f237a47ce9f72808eed1b Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 21 Mar 2024 23:55:28 +0100 Subject: [PATCH 30/51] feat(insights): string breakdowns (#21023) --- frontend/src/queries/query.ts | 8 +- .../views/InsightsTable/InsightsTable.tsx | 3 +- mypy-baseline.txt | 14 +- package.json | 1 + pnpm-lock.yaml | 8 + .../insights/trends/breakdown.py | 115 +++----- .../insights/trends/breakdown_values.py | 23 +- .../test/__snapshots__/test_trends.ambr | 260 +++++++++--------- .../test_trends_data_warehouse_query.ambr | 16 +- .../insights/trends/test/test_trends.py | 4 +- .../trends/test/test_trends_query_runner.py | 19 +- .../insights/trends/trends_query_builder.py | 10 +- .../insights/trends/trends_query_runner.py | 28 +- 13 files changed, 234 insertions(+), 275 deletions(-) diff --git a/frontend/src/queries/query.ts b/frontend/src/queries/query.ts index 78778cec4322a..f866b2f336d31 100644 --- a/frontend/src/queries/query.ts +++ b/frontend/src/queries/query.ts @@ -220,7 +220,13 @@ export async function query( (hogQLInsightsFunnelsFlagEnabled && isFunnelsQuery(queryNode)) ) { if (hogQLInsightsLiveCompareEnabled) { - const legacyFunction = legacyUrl ? fetchLegacyUrl : fetchLegacyInsights + const legacyFunction = (): any => { + try { + return legacyUrl ? fetchLegacyUrl : fetchLegacyInsights + } catch (e) { + console.error('Error fetching legacy insights', e) + } + } let legacyResponse: any ;[response, legacyResponse] = await Promise.all([ executeQuery(queryNode, methodOptions, refresh, queryId), diff --git a/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.tsx b/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.tsx index d426ca87f525c..c6d037c3d1792 100644 --- a/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.tsx +++ b/frontend/src/scenes/insights/views/InsightsTable/InsightsTable.tsx @@ -3,6 +3,7 @@ import './InsightsTable.scss' import { useActions, useValues } from 'kea' import { getSeriesColor } from 'lib/colors' import { LemonTable, LemonTableColumn } from 'lib/lemon-ui/LemonTable' +import { compare as compareFn } from 'natural-orderby' import { insightLogic } from 'scenes/insights/insightLogic' import { insightSceneLogic } from 'scenes/insights/insightSceneLogic' import { isTrendsFilter } from 'scenes/insights/sharedUtils' @@ -157,7 +158,7 @@ export function InsightsTable({ } const labelA = formatItemBreakdownLabel(a) const labelB = formatItemBreakdownLabel(b) - return labelA.localeCompare(labelB) + return compareFn()(labelA, labelB) }, }) if (isTrends && display === ChartDisplayType.WorldMap) { diff --git a/mypy-baseline.txt b/mypy-baseline.txt index f0a464d26c546..9d5fe48cb5777 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -346,20 +346,12 @@ posthog/hogql_queries/sessions_timeline_query_runner.py:0: error: Statement is u posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown_type" [union-attr] posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown_histogram_bin_count" [union-attr] posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown_type" [union-attr] -posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown" [union-attr] -posthog/hogql_queries/insights/trends/breakdown.py:0: error: Argument 1 to "parse_expr" has incompatible type "str | float | list[str | float] | Any | None"; expected "str" [arg-type] -posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown_type" [union-attr] -posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown_type" [union-attr] -posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown" [union-attr] -posthog/hogql_queries/insights/trends/breakdown.py:0: error: Argument 1 to "parse_expr" has incompatible type "str | float | list[str | float] | Any | None"; expected "str" [arg-type] -posthog/hogql_queries/insights/trends/breakdown.py:0: error: Statement is unreachable [unreachable] posthog/hogql_queries/insights/trends/breakdown.py:0: error: Argument "exprs" to "Or" has incompatible type "list[CompareOperation]"; expected "list[Expr]" [arg-type] posthog/hogql_queries/insights/trends/breakdown.py:0: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql_queries/insights/trends/breakdown.py:0: note: Consider using "Sequence" instead, which is covariant -posthog/hogql_queries/insights/trends/breakdown.py:0: error: Incompatible types in assignment (expression has type "float", variable has type "int") [assignment] -posthog/hogql_queries/insights/trends/breakdown.py:0: error: Incompatible types in assignment (expression has type "float", variable has type "int") [assignment] -posthog/hogql_queries/insights/trends/breakdown.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] -posthog/hogql_queries/insights/trends/breakdown.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] +posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown_type" [union-attr] +posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown" [union-attr] +posthog/hogql_queries/insights/trends/breakdown.py:0: error: Argument 1 to "parse_expr" has incompatible type "str | float | list[str | float] | Any | None"; expected "str" [arg-type] posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown_type" [union-attr] posthog/hogql_queries/insights/trends/breakdown.py:0: error: Item "None" of "BreakdownFilter | None" has no attribute "breakdown" [union-attr] posthog/hogql_queries/insights/trends/breakdown.py:0: error: Argument "breakdown_field" to "get_properties_chain" has incompatible type "str | float | list[str | float] | Any | None"; expected "str" [arg-type] diff --git a/package.json b/package.json index 770be74997198..79946a087fbf4 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,7 @@ "maplibre-gl": "^3.5.1", "md5": "^2.3.0", "monaco-editor": "^0.39.0", + "natural-orderby": "^3.0.2", "papaparse": "^5.4.1", "pmtiles": "^2.11.0", "postcss": "^8.4.31", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f157ec8b039e..7fc1d7cbd22a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -238,6 +238,9 @@ dependencies: monaco-editor: specifier: ^0.39.0 version: 0.39.0 + natural-orderby: + specifier: ^3.0.2 + version: 3.0.2 papaparse: specifier: ^5.4.1 version: 5.4.1 @@ -15917,6 +15920,11 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /natural-orderby@3.0.2: + resolution: {integrity: sha512-x7ZdOwBxZCEm9MM7+eQCjkrNLrW3rkBKNHVr78zbtqnMGVNlnDi6C/eUEYgxHNrcbu0ymvjzcwIL/6H1iHri9g==} + engines: {node: '>=18'} + dev: false + /needle@3.3.1: resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==} engines: {node: '>= 4.4.x'} diff --git a/posthog/hogql_queries/insights/trends/breakdown.py b/posthog/hogql_queries/insights/trends/breakdown.py index 45a3a8421e8d8..bde2cd807b6a7 100644 --- a/posthog/hogql_queries/insights/trends/breakdown.py +++ b/posthog/hogql_queries/insights/trends/breakdown.py @@ -3,9 +3,7 @@ from posthog.hogql.parser import parse_expr from posthog.hogql.timings import HogQLTimings from posthog.hogql_queries.insights.trends.breakdown_values import ( - BREAKDOWN_NULL_NUMERIC_LABEL, BREAKDOWN_NULL_STRING_LABEL, - BREAKDOWN_OTHER_NUMERIC_LABEL, BREAKDOWN_OTHER_STRING_LABEL, BreakdownValues, ) @@ -19,6 +17,10 @@ from posthog.schema import ActionsNode, EventsNode, DataWarehouseNode, HogQLQueryModifiers, InCohortVia, TrendsQuery +def hogql_to_string(expr: ast.Expr) -> ast.Call: + return ast.Call(name="toString", args=[expr]) + + class Breakdown: query: TrendsQuery team: Team @@ -27,7 +29,7 @@ class Breakdown: timings: HogQLTimings modifiers: HogQLQueryModifiers events_filter: ast.Expr - breakdown_values_override: Optional[List[str | int | float]] + breakdown_values_override: Optional[List[str]] def __init__( self, @@ -38,7 +40,7 @@ def __init__( timings: HogQLTimings, modifiers: HogQLQueryModifiers, events_filter: ast.Expr, - breakdown_values_override: Optional[List[str | int | float]] = None, + breakdown_values_override: Optional[List[str]] = None, ): self.team = team self.query = query @@ -70,19 +72,15 @@ def placeholders(self) -> Dict[str, ast.Expr]: return {"cross_join_breakdown_values": ast.Alias(alias="breakdown_value", expr=values)} - def column_expr(self) -> ast.Expr: + def column_expr(self) -> ast.Alias: if self.is_histogram_breakdown: return ast.Alias(alias="breakdown_value", expr=self._get_breakdown_histogram_multi_if()) - elif self.query.breakdownFilter.breakdown_type == "hogql": - return ast.Alias( - alias="breakdown_value", - expr=parse_expr(self.query.breakdownFilter.breakdown), - ) - elif self.query.breakdownFilter.breakdown_type == "cohort": + + if self.query.breakdownFilter.breakdown_type == "cohort": if self.modifiers.inCohortVia == InCohortVia.leftjoin_conjoined: return ast.Alias( alias="breakdown_value", - expr=ast.Field(chain=["__in_cohort", "cohort_id"]), + expr=hogql_to_string(ast.Field(chain=["__in_cohort", "cohort_id"])), ) cohort_breakdown = ( @@ -90,19 +88,9 @@ def column_expr(self) -> ast.Expr: ) return ast.Alias( alias="breakdown_value", - expr=ast.Constant(value=cohort_breakdown), - ) - - if self.query.breakdownFilter.breakdown_type == "hogql": - return ast.Alias( - alias="breakdown_value", - expr=parse_expr(self.query.breakdownFilter.breakdown), + expr=hogql_to_string(ast.Constant(value=cohort_breakdown)), ) - # If there's no breakdown values - if len(self._breakdown_values) == 1 and self._breakdown_values[0] is None: - return ast.Alias(alias="breakdown_value", expr=ast.Field(chain=self._properties_chain)) - return ast.Alias(alias="breakdown_value", expr=self._get_breakdown_transform_func) def events_where_filter(self) -> ast.Expr | None: @@ -148,15 +136,14 @@ def events_where_filter(self) -> ast.Expr | None: else: left = ast.Field(chain=self._properties_chain) + if not self.is_histogram_breakdown: + left = hogql_to_string(left) + compare_ops = [] for _value in self._breakdown_values: - value: Optional[str | int | float] = _value + value: Optional[str] = str(_value) # non-cohorts are always strings # If the value is one of the "other" values, then use the `transform()` func - if ( - value == BREAKDOWN_OTHER_STRING_LABEL - or value == BREAKDOWN_OTHER_NUMERIC_LABEL - or value == float(BREAKDOWN_OTHER_NUMERIC_LABEL) - ): + if value == BREAKDOWN_OTHER_STRING_LABEL: transform_func = self._get_breakdown_transform_func compare_ops.append( ast.CompareOperation( @@ -164,11 +151,7 @@ def events_where_filter(self) -> ast.Expr | None: ) ) else: - if ( - value == BREAKDOWN_NULL_STRING_LABEL - or value == BREAKDOWN_NULL_NUMERIC_LABEL - or value == float(BREAKDOWN_NULL_NUMERIC_LABEL) - ): + if value == BREAKDOWN_NULL_STRING_LABEL: value = None compare_ops.append( @@ -184,30 +167,25 @@ def events_where_filter(self) -> ast.Expr | None: @cached_property def _get_breakdown_transform_func(self) -> ast.Call: - values = self._breakdown_values - all_values_are_ints_or_none = all(isinstance(value, int) or value is None for value in values) - all_values_are_floats_or_none = all(isinstance(value, float) or value is None for value in values) - - if all_values_are_ints_or_none: - breakdown_other_value = BREAKDOWN_OTHER_NUMERIC_LABEL - breakdown_null_value = BREAKDOWN_NULL_NUMERIC_LABEL - elif all_values_are_floats_or_none: - breakdown_other_value = float(BREAKDOWN_OTHER_NUMERIC_LABEL) - breakdown_null_value = float(BREAKDOWN_NULL_NUMERIC_LABEL) - else: - breakdown_other_value = BREAKDOWN_OTHER_STRING_LABEL - breakdown_null_value = BREAKDOWN_NULL_STRING_LABEL + if self.query.breakdownFilter.breakdown_type == "hogql": + return self._get_breakdown_values_transform(parse_expr(self.query.breakdownFilter.breakdown)) + return self._get_breakdown_values_transform(ast.Field(chain=self._properties_chain)) + def _get_breakdown_values_transform(self, node: ast.Expr) -> ast.Call: + breakdown_values = self._breakdown_values_ast return ast.Call( name="transform", args=[ ast.Call( name="ifNull", - args=[ast.Field(chain=self._properties_chain), ast.Constant(value=breakdown_null_value)], + args=[ + hogql_to_string(node), + ast.Constant(value=BREAKDOWN_NULL_STRING_LABEL), + ], ), - self._breakdown_values_ast, - self._breakdown_values_ast, - ast.Constant(value=breakdown_other_value), + breakdown_values, + breakdown_values, + ast.Constant(value=BREAKDOWN_OTHER_STRING_LABEL), ], ) @@ -220,15 +198,21 @@ def _breakdown_buckets_ast(self) -> ast.Array: return ast.Array(exprs=list(map(lambda v: ast.Constant(value=v), values))) - @cached_property + @property def _breakdown_values_ast(self) -> ast.Array: - return ast.Array(exprs=[ast.Constant(value=v) for v in self._breakdown_values]) + exprs: list[ast.Expr] = [] + for value in self._breakdown_values: + if isinstance(value, str): + exprs.append(ast.Constant(value=value)) + else: + exprs.append(hogql_to_string(ast.Constant(value=value))) + return ast.Array(exprs=exprs) @cached_property - def _all_breakdown_values(self) -> List[str | int | float | None]: + def _all_breakdown_values(self) -> List[str | int | None]: # Used in the actors query if self.breakdown_values_override is not None: - return cast(List[str | int | float | None], self.breakdown_values_override) + return cast(List[str | int | None], self.breakdown_values_override) if self.query.breakdownFilter is None: return [] @@ -243,25 +227,12 @@ def _all_breakdown_values(self) -> List[str | int | float | None]: query_date_range=self.query_date_range, modifiers=self.modifiers, ) - return cast(List[str | int | float | None], breakdown.get_breakdown_values()) + return cast(List[str | int | None], breakdown.get_breakdown_values()) @cached_property - def _breakdown_values(self) -> List[str | int | float]: - values = self._all_breakdown_values - if len(values) == 0 or all(value is None for value in values): - return [] - - if None in values: - all_values_are_ints_or_none = all(isinstance(value, int) or value is None for value in values) - all_values_are_floats_or_none = all(isinstance(value, float) or value is None for value in values) - - if all_values_are_ints_or_none: - values = [v if v is not None else BREAKDOWN_NULL_NUMERIC_LABEL for v in values] - elif all_values_are_floats_or_none: - values = [v if v is not None else float(BREAKDOWN_NULL_NUMERIC_LABEL) for v in values] - else: - values = [v if v is not None else BREAKDOWN_NULL_STRING_LABEL for v in values] - return cast(List[str | int | float], values) + def _breakdown_values(self) -> List[str | int]: + values = [BREAKDOWN_NULL_STRING_LABEL if v is None else v for v in self._all_breakdown_values] + return cast(List[str | int], values) @cached_property def has_breakdown_values(self) -> bool: diff --git a/posthog/hogql_queries/insights/trends/breakdown_values.py b/posthog/hogql_queries/insights/trends/breakdown_values.py index 7b1522d5f25c5..d9ab11891f210 100644 --- a/posthog/hogql_queries/insights/trends/breakdown_values.py +++ b/posthog/hogql_queries/insights/trends/breakdown_values.py @@ -97,6 +97,9 @@ def get_breakdown_values(self) -> List[str | int]: ), ) + if not self.histogram_bin_count: + select_field.expr = ast.Call(name="toString", args=[select_field.expr]) + if self.chart_display_type == ChartDisplayType.WorldMap: breakdown_limit = BREAKDOWN_VALUES_LIMIT_FOR_COUNTRIES else: @@ -211,23 +214,9 @@ def get_breakdown_values(self) -> List[str | int]: # Add "other" value if "other" is not hidden and we're not bucketing numeric values if self.hide_other_aggregation is not True and self.histogram_bin_count is None: - all_values_are_ints_or_none = all(isinstance(value, int) or value is None for value in values) - all_values_are_floats_or_none = all(isinstance(value, float) or value is None for value in values) - all_values_are_string_or_none = all(isinstance(value, str) or value is None for value in values) - - if all_values_are_string_or_none: - values = [BREAKDOWN_NULL_STRING_LABEL if value in (None, "") else value for value in values] - if needs_other: - values.insert(0, BREAKDOWN_OTHER_STRING_LABEL) - elif all_values_are_ints_or_none or all_values_are_floats_or_none: - if all_values_are_ints_or_none: - values = [BREAKDOWN_NULL_NUMERIC_LABEL if value is None else value for value in values] - if needs_other: - values.insert(0, BREAKDOWN_OTHER_NUMERIC_LABEL) - else: - values = [float(BREAKDOWN_NULL_NUMERIC_LABEL) if value is None else value for value in values] - if needs_other: - values.insert(0, float(BREAKDOWN_OTHER_NUMERIC_LABEL)) + values = [BREAKDOWN_NULL_STRING_LABEL if value in (None, "") else value for value in values] + if needs_other: + values = [BREAKDOWN_OTHER_STRING_LABEL] + values if len(values) == 0: values.insert(0, None) diff --git a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr index f6eb3748afb2b..e2ec22fb9fb1c 100644 --- a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr +++ b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr @@ -187,7 +187,7 @@ # --- # name: TestTrends.test_breakdown_by_group_props_person_on_events ''' - SELECT e__group_0.properties___industry AS value, + SELECT toString(e__group_0.properties___industry) AS value, count(e.uuid) AS count FROM events AS e LEFT JOIN @@ -210,7 +210,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -231,7 +231,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.uuid) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(e__group_0.properties___industry, '$$_posthog_breakdown_null_$$'), ['finance', 'technology'], ['finance', 'technology'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(e__group_0.properties___industry), '$$_posthog_breakdown_null_$$'), ['finance', 'technology'], ['finance', 'technology'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 LEFT JOIN (SELECT argMax(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(groups.group_properties, 'industry'), ''), 'null'), '^"|"$', ''), groups._timestamp) AS properties___industry, @@ -241,7 +241,7 @@ WHERE and(equals(groups.team_id, 2), ifNull(equals(index, 0), 0)) GROUP BY groups.group_type_index, groups.group_key) AS e__group_0 ON equals(e.`$group_0`, e__group_0.key) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(e__group_0.properties___industry, 'finance'), 0), ifNull(equals(e__group_0.properties___industry, 'technology'), 0))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(toString(e__group_0.properties___industry), 'finance'), 0), ifNull(equals(toString(e__group_0.properties___industry), 'technology'), 0))) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -287,7 +287,7 @@ # --- # name: TestTrends.test_breakdown_by_group_props_with_person_filter_person_on_events ''' - SELECT e__group_0.properties___industry AS value, + SELECT toString(e__group_0.properties___industry) AS value, count(e.uuid) AS count FROM events AS e LEFT JOIN @@ -310,7 +310,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -331,7 +331,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.uuid) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(e__group_0.properties___industry, '$$_posthog_breakdown_null_$$'), ['finance'], ['finance'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(e__group_0.properties___industry), '$$_posthog_breakdown_null_$$'), ['finance'], ['finance'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 LEFT JOIN (SELECT argMax(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(groups.group_properties, 'industry'), ''), 'null'), '^"|"$', ''), groups._timestamp) AS properties___industry, @@ -341,7 +341,7 @@ WHERE and(equals(groups.team_id, 2), ifNull(equals(index, 0), 0)) GROUP BY groups.group_type_index, groups.group_key) AS e__group_0 ON equals(e.`$group_0`, e__group_0.key) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.person_properties, 'key'), ''), 'null'), '^"|"$', ''), 'value'), 0), ifNull(equals(e__group_0.properties___industry, 'finance'), 0)) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.person_properties, 'key'), ''), 'null'), '^"|"$', ''), 'value'), 0), ifNull(equals(toString(e__group_0.properties___industry), 'finance'), 0)) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -356,7 +356,7 @@ # --- # name: TestTrends.test_breakdown_filtering_with_properties_in_new_format ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$current_url'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$current_url'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-22 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-22 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$browser'), ''), 'null'), '^"|"$', ''), 'Firefox'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Windows'), 0)), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Mac'), 0))) @@ -371,7 +371,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -392,9 +392,9 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.uuid) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$current_url'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['second url'], ['second url'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$current_url'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['second url'], ['second url'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-22 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$browser'), ''), 'null'), '^"|"$', ''), 'Firefox'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Windows'), 0)), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Mac'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$current_url'), ''), 'null'), '^"|"$', ''), 'second url'), 0)) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-22 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$browser'), ''), 'null'), '^"|"$', ''), 'Firefox'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Windows'), 0)), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Mac'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$current_url'), ''), 'null'), '^"|"$', '')), 'second url'), 0)) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -409,7 +409,7 @@ # --- # name: TestTrends.test_breakdown_filtering_with_properties_in_new_format.2 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$current_url'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$current_url'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-22 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-22 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), and(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$browser'), ''), 'null'), '^"|"$', ''), 'Firefox'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Windows'), 0)), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Mac'), 0))) @@ -423,24 +423,38 @@ # name: TestTrends.test_breakdown_filtering_with_properties_in_new_format.3 ''' SELECT groupArray(day_start) AS date, - groupArray(count) AS total + groupArray(count) AS total, + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, - day_start AS day_start + day_start AS day_start, + breakdown_value AS breakdown_value FROM (SELECT 0 AS total, - minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), toIntervalDay(numbers.number)) AS day_start - FROM numbers(coalesce(dateDiff('day', assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-22 00:00:00', 6, 'UTC')), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), 0)) AS numbers - UNION ALL SELECT 0 AS total, - toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-22 00:00:00', 6, 'UTC'))) AS day_start + ticks.day_start AS day_start, + sec.breakdown_value AS breakdown_value + FROM + (SELECT minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), toIntervalDay(numbers.number)) AS day_start + FROM numbers(coalesce(dateDiff('day', assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-22 00:00:00', 6, 'UTC')), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), 0)) AS numbers + UNION ALL SELECT toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-22 00:00:00', 6, 'UTC'))) AS day_start) AS ticks + CROSS JOIN + (SELECT breakdown_value + FROM + (SELECT ['$$_posthog_breakdown_null_$$'] AS breakdown_value) ARRAY + JOIN breakdown_value AS breakdown_value) AS sec + ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.uuid) AS total, - toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start + toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$current_url'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_null_$$'], ['$$_posthog_breakdown_null_$$'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-22 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), and(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$browser'), ''), 'null'), '^"|"$', ''), 'Firefox'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Windows'), 0)), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Mac'), 0)) - GROUP BY day_start) - GROUP BY day_start - ORDER BY day_start ASC) - ORDER BY sum(count) DESC + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-22 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), and(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$browser'), ''), 'null'), '^"|"$', ''), 'Firefox'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Windows'), 0)), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Mac'), 0), isNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$current_url'), ''), 'null'), '^"|"$', '')))) + GROUP BY day_start, + breakdown_value) + GROUP BY day_start, + breakdown_value + ORDER BY day_start ASC, breakdown_value ASC) + GROUP BY breakdown_value + ORDER BY sum(count) DESC, breakdown_value ASC LIMIT 10000 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -448,7 +462,7 @@ # --- # name: TestTrends.test_breakdown_weekly_active_users_aggregated ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')) AS value, count(DISTINCT e__pdi.person_id) AS count FROM events AS e INNER JOIN @@ -480,7 +494,7 @@ CROSS JOIN (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, e__pdi.person_id AS actor_id, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['val', 'bor'], ['val', 'bor'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['val', 'bor'], ['val', 'bor'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -489,7 +503,7 @@ WHERE equals(person_distinct_id2.team_id, 2) GROUP BY person_distinct_id2.distinct_id HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) - WHERE and(equals(e.team_id, 2), and(equals(e.event, '$pageview'), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'val'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'bor'), 0))), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-11 00:00:00', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-11 23:59:59', 6, 'UTC'))), 0)) + WHERE and(equals(e.team_id, 2), and(equals(e.event, '$pageview'), or(ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')), 'val'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')), 'bor'), 0))), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-11 00:00:00', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-11 23:59:59', 6, 'UTC'))), 0)) GROUP BY timestamp, actor_id, breakdown_value) AS e WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(6))), 0)) @@ -506,7 +520,7 @@ # --- # name: TestTrends.test_breakdown_weekly_active_users_aggregated_materialized ''' - SELECT nullIf(nullIf(e.mat_key, ''), 'null') AS value, + SELECT toString(nullIf(nullIf(e.mat_key, ''), 'null')) AS value, count(DISTINCT e__pdi.person_id) AS count FROM events AS e INNER JOIN @@ -538,7 +552,7 @@ CROSS JOIN (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, e__pdi.person_id AS actor_id, - transform(ifNull(nullIf(nullIf(e.mat_key, ''), 'null'), '$$_posthog_breakdown_null_$$'), ['val', 'bor'], ['val', 'bor'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(nullIf(nullIf(e.mat_key, ''), 'null')), '$$_posthog_breakdown_null_$$'), ['val', 'bor'], ['val', 'bor'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -547,7 +561,7 @@ WHERE equals(person_distinct_id2.team_id, 2) GROUP BY person_distinct_id2.distinct_id HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) - WHERE and(equals(e.team_id, 2), and(equals(e.event, '$pageview'), or(ifNull(equals(nullIf(nullIf(e.mat_key, ''), 'null'), 'val'), 0), ifNull(equals(nullIf(nullIf(e.mat_key, ''), 'null'), 'bor'), 0))), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-11 00:00:00', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-11 23:59:59', 6, 'UTC'))), 0)) + WHERE and(equals(e.team_id, 2), and(equals(e.event, '$pageview'), or(ifNull(equals(toString(nullIf(nullIf(e.mat_key, ''), 'null')), 'val'), 0), ifNull(equals(toString(nullIf(nullIf(e.mat_key, ''), 'null')), 'bor'), 0))), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-11 00:00:00', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-11 23:59:59', 6, 'UTC'))), 0)) GROUP BY timestamp, actor_id, breakdown_value) AS e WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(6))), 0)) @@ -584,7 +598,7 @@ # --- # name: TestTrends.test_breakdown_weekly_active_users_daily_based_on_action.2 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')) AS value, count(DISTINCT e__pdi.person_id) AS count FROM events AS e INNER JOIN @@ -622,7 +636,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -654,7 +668,7 @@ CROSS JOIN (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, e__pdi.person_id AS actor_id, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['val'], ['val'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['val'], ['val'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -679,7 +693,7 @@ FROM cohortpeople WHERE and(equals(cohortpeople.team_id, 2), equals(cohortpeople.cohort_id, 2)) GROUP BY cohortpeople.person_id, cohortpeople.cohort_id, cohortpeople.version - HAVING ifNull(greater(sum(cohortpeople.sign), 0), 0))), 0))), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'val'), 0)), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), 0)) + HAVING ifNull(greater(sum(cohortpeople.sign), 0), 0))), 0))), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')), 'val'), 0)), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), 0)) GROUP BY timestamp, actor_id, breakdown_value) AS e WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(6))), 0)) @@ -699,7 +713,7 @@ # --- # name: TestTrends.test_breakdown_with_filter_groups_person_on_events ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e LEFT JOIN @@ -722,7 +736,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -743,7 +757,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.uuid) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['uh', 'oh'], ['uh', 'oh'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['uh', 'oh'], ['uh', 'oh'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 LEFT JOIN (SELECT argMax(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(groups.group_properties, 'industry'), ''), 'null'), '^"|"$', ''), groups._timestamp) AS properties___industry, @@ -753,7 +767,7 @@ WHERE and(equals(groups.team_id, 2), ifNull(equals(index, 0), 0)) GROUP BY groups.group_type_index, groups.group_key) AS e__group_0 ON equals(e.`$group_0`, e__group_0.key) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), ifNull(equals(e__group_0.properties___industry, 'finance'), 0), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'uh'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'oh'), 0))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), ifNull(equals(e__group_0.properties___industry, 'finance'), 0), or(ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')), 'uh'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')), 'oh'), 0))) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -782,7 +796,7 @@ # --- # name: TestTrends.test_breakdown_with_filter_groups_person_on_events_v2.1 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e LEFT JOIN @@ -805,7 +819,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -826,7 +840,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(DISTINCT ifNull(nullIf(e__override.override_person_id, '00000000-0000-0000-0000-000000000000'), e.person_id)) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['uh', 'oh'], ['uh', 'oh'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['uh', 'oh'], ['uh', 'oh'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 LEFT OUTER JOIN (SELECT argMax(person_overrides.override_person_id, person_overrides.version) AS override_person_id, @@ -842,7 +856,7 @@ WHERE and(equals(groups.team_id, 2), ifNull(equals(index, 0), 0)) GROUP BY groups.group_type_index, groups.group_key) AS e__group_0 ON equals(e.`$group_0`, e__group_0.key) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), ifNull(equals(e__group_0.properties___industry, 'finance'), 0), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'uh'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'oh'), 0))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), ifNull(equals(e__group_0.properties___industry, 'finance'), 0), or(ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')), 'uh'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', '')), 'oh'), 0))) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -857,7 +871,7 @@ # --- # name: TestTrends.test_dau_with_breakdown_filtering_with_sampling ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'))) @@ -872,7 +886,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -893,7 +907,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(DISTINCT e__pdi.person_id) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['other_value', '$$_posthog_breakdown_null_$$', 'value'], ['other_value', '$$_posthog_breakdown_null_$$', 'value'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['other_value', '$$_posthog_breakdown_null_$$', 'value'], ['other_value', '$$_posthog_breakdown_null_$$', 'value'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1.0 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -902,7 +916,7 @@ WHERE equals(person_distinct_id2.team_id, 2) GROUP BY person_distinct_id2.distinct_id HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'other_value'), 0), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value'), 0))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'other_value'), 0), isNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''))), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value'), 0))) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -917,7 +931,7 @@ # --- # name: TestTrends.test_dau_with_breakdown_filtering_with_sampling.2 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'))) @@ -932,7 +946,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -953,7 +967,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(DISTINCT e__pdi.person_id) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['other_value', '$$_posthog_breakdown_null_$$', 'value'], ['other_value', '$$_posthog_breakdown_null_$$', 'value'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['other_value', '$$_posthog_breakdown_null_$$', 'value'], ['other_value', '$$_posthog_breakdown_null_$$', 'value'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1.0 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -962,7 +976,7 @@ WHERE equals(person_distinct_id2.team_id, 2) GROUP BY person_distinct_id2.distinct_id HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'other_value'), 0), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value'), 0))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'other_value'), 0), isNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''))), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value'), 0))) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -1242,7 +1256,7 @@ # --- # name: TestTrends.test_mau_with_breakdown_filtering_and_prop_filter ''' - SELECT e__pdi__person.`properties___$some_prop` AS value, + SELECT toString(e__pdi__person.`properties___$some_prop`) AS value, count(DISTINCT e__pdi.person_id) AS count FROM events AS e INNER JOIN @@ -1276,7 +1290,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -1308,7 +1322,7 @@ CROSS JOIN (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, e__pdi.person_id AS actor_id, - transform(ifNull(e__pdi__person.`properties___$some_prop`, '$$_posthog_breakdown_null_$$'), ['some_val2', 'some_val'], ['some_val2', 'some_val'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(e__pdi__person.`properties___$some_prop`), '$$_posthog_breakdown_null_$$'), ['some_val2', 'some_val'], ['some_val2', 'some_val'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -1329,7 +1343,7 @@ WHERE equals(person.team_id, 2) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) - WHERE and(equals(e.team_id, 2), and(equals(e.event, 'sign up'), ifNull(equals(e__pdi__person.properties___filter_prop, 'filter_val'), 0), or(ifNull(equals(e__pdi__person.`properties___$some_prop`, 'some_val2'), 0), ifNull(equals(e__pdi__person.`properties___$some_prop`, 'some_val'), 0))), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), toIntervalDay(30))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), 0)) + WHERE and(equals(e.team_id, 2), and(equals(e.event, 'sign up'), ifNull(equals(e__pdi__person.properties___filter_prop, 'filter_val'), 0), or(ifNull(equals(toString(e__pdi__person.`properties___$some_prop`), 'some_val2'), 0), ifNull(equals(toString(e__pdi__person.`properties___$some_prop`), 'some_val'), 0))), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), toIntervalDay(30))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), 0)) GROUP BY timestamp, actor_id, breakdown_value) AS e WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(29))), 0)) @@ -1349,7 +1363,7 @@ # --- # name: TestTrends.test_mau_with_breakdown_filtering_and_prop_filter_poe_v2 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.person_properties, '$some_prop'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.person_properties, '$some_prop'), ''), 'null'), '^"|"$', '')) AS value, count(DISTINCT ifNull(nullIf(e__override.override_person_id, '00000000-0000-0000-0000-000000000000'), e.person_id)) AS count FROM events AS e LEFT OUTER JOIN @@ -1370,7 +1384,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -1402,7 +1416,7 @@ CROSS JOIN (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp, ifNull(nullIf(e__override.override_person_id, '00000000-0000-0000-0000-000000000000'), e.person_id) AS actor_id, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.person_properties, '$some_prop'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['some_val2', 'some_val'], ['some_val2', 'some_val'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.person_properties, '$some_prop'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['some_val2', 'some_val'], ['some_val2', 'some_val'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 LEFT OUTER JOIN (SELECT argMax(person_overrides.override_person_id, person_overrides.version) AS override_person_id, @@ -1410,7 +1424,7 @@ FROM person_overrides WHERE equals(person_overrides.team_id, 2) GROUP BY person_overrides.old_person_id) AS e__override ON equals(e.person_id, e__override.old_person_id) - WHERE and(equals(e.team_id, 2), and(equals(e.event, 'sign up'), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.person_properties, 'filter_prop'), ''), 'null'), '^"|"$', ''), 'filter_val'), 0), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.person_properties, '$some_prop'), ''), 'null'), '^"|"$', ''), 'some_val2'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.person_properties, '$some_prop'), ''), 'null'), '^"|"$', ''), 'some_val'), 0))), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), toIntervalDay(30))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), 0)) + WHERE and(equals(e.team_id, 2), and(equals(e.event, 'sign up'), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.person_properties, 'filter_prop'), ''), 'null'), '^"|"$', ''), 'filter_val'), 0), or(ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.person_properties, '$some_prop'), ''), 'null'), '^"|"$', '')), 'some_val2'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.person_properties, '$some_prop'), ''), 'null'), '^"|"$', '')), 'some_val'), 0))), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), toIntervalDay(30))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), 0)) GROUP BY timestamp, actor_id, breakdown_value) AS e WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(29))), 0)) @@ -1476,7 +1490,7 @@ # --- # name: TestTrends.test_person_filtering_in_cohort_in_action.2 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e INNER JOIN @@ -1503,7 +1517,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -1524,7 +1538,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.uuid) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -1538,7 +1552,7 @@ FROM cohortpeople WHERE and(equals(cohortpeople.team_id, 2), equals(cohortpeople.cohort_id, 2)) GROUP BY cohortpeople.person_id, cohortpeople.cohort_id, cohortpeople.version - HAVING ifNull(greater(sum(cohortpeople.sign), 0), 0))), 0)), or(isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'other_value'), 0))) + HAVING ifNull(greater(sum(cohortpeople.sign), 0), 0))), 0)), or(isNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''))), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'other_value'), 0))) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -1573,7 +1587,7 @@ # --- # name: TestTrends.test_person_filtering_in_cohort_in_action_poe_v2.2 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e LEFT OUTER JOIN @@ -1599,7 +1613,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -1620,7 +1634,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.uuid) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 LEFT OUTER JOIN (SELECT argMax(person_overrides.override_person_id, person_overrides.version) AS override_person_id, @@ -1633,7 +1647,7 @@ FROM cohortpeople WHERE and(equals(cohortpeople.team_id, 2), equals(cohortpeople.cohort_id, 2)) GROUP BY cohortpeople.person_id, cohortpeople.cohort_id, cohortpeople.version - HAVING ifNull(greater(sum(cohortpeople.sign), 0), 0))), 0)), or(isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'other_value'), 0))) + HAVING ifNull(greater(sum(cohortpeople.sign), 0), 0))), 0)), or(isNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''))), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'other_value'), 0))) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -2217,7 +2231,7 @@ # --- # name: TestTrends.test_timezones_daily.4 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-29 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-29 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'))) @@ -2232,7 +2246,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -2253,7 +2267,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(DISTINCT e__pdi.person_id) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['Mac'], ['Mac'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['Mac'], ['Mac'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -2262,7 +2276,7 @@ WHERE equals(person_distinct_id2.team_id, 2) GROUP BY person_distinct_id2.distinct_id HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-29 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Mac'), 0)) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-29 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', '')), 'Mac'), 0)) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -2408,7 +2422,7 @@ # --- # name: TestTrends.test_timezones_daily_minus_utc.4 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'America/Phoenix'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-29 00:00:00', 6, 'America/Phoenix')))), lessOrEquals(toTimeZone(e.timestamp, 'America/Phoenix'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'America/Phoenix')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'America/Phoenix'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-29 00:00:00', 6, 'America/Phoenix')))), lessOrEquals(toTimeZone(e.timestamp, 'America/Phoenix'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'America/Phoenix'))), equals(e.event, 'sign up'))) @@ -2423,7 +2437,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -2444,7 +2458,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(DISTINCT e__pdi.person_id) AS total, toStartOfDay(toTimeZone(e.timestamp, 'America/Phoenix')) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['Mac'], ['Mac'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['Mac'], ['Mac'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -2453,7 +2467,7 @@ WHERE equals(person_distinct_id2.team_id, 2) GROUP BY person_distinct_id2.distinct_id HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'America/Phoenix'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-29 00:00:00', 6, 'America/Phoenix')))), lessOrEquals(toTimeZone(e.timestamp, 'America/Phoenix'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'America/Phoenix'))), equals(e.event, 'sign up'), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Mac'), 0)) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'America/Phoenix'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-29 00:00:00', 6, 'America/Phoenix')))), lessOrEquals(toTimeZone(e.timestamp, 'America/Phoenix'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'America/Phoenix'))), equals(e.event, 'sign up'), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', '')), 'Mac'), 0)) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -2599,7 +2613,7 @@ # --- # name: TestTrends.test_timezones_daily_plus_utc.4 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'Asia/Tokyo'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-29 00:00:00', 6, 'Asia/Tokyo')))), lessOrEquals(toTimeZone(e.timestamp, 'Asia/Tokyo'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'Asia/Tokyo')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'Asia/Tokyo'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-29 00:00:00', 6, 'Asia/Tokyo')))), lessOrEquals(toTimeZone(e.timestamp, 'Asia/Tokyo'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'Asia/Tokyo'))), equals(e.event, 'sign up'))) @@ -2614,7 +2628,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -2635,7 +2649,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(DISTINCT e__pdi.person_id) AS total, toStartOfDay(toTimeZone(e.timestamp, 'Asia/Tokyo')) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['Mac'], ['Mac'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['Mac'], ['Mac'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -2644,7 +2658,7 @@ WHERE equals(person_distinct_id2.team_id, 2) GROUP BY person_distinct_id2.distinct_id HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'Asia/Tokyo'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-29 00:00:00', 6, 'Asia/Tokyo')))), lessOrEquals(toTimeZone(e.timestamp, 'Asia/Tokyo'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'Asia/Tokyo'))), equals(e.event, 'sign up'), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', ''), 'Mac'), 0)) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'Asia/Tokyo'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-29 00:00:00', 6, 'Asia/Tokyo')))), lessOrEquals(toTimeZone(e.timestamp, 'Asia/Tokyo'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-05 23:59:59', 6, 'Asia/Tokyo'))), equals(e.event, 'sign up'), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$os'), ''), 'null'), '^"|"$', '')), 'Mac'), 0)) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -2992,7 +3006,7 @@ # --- # name: TestTrends.test_trend_breakdown_user_props_with_filter_with_partial_property_pushdowns ''' - SELECT e__pdi__person.properties___email AS value, + SELECT toString(e__pdi__person.properties___email) AS value, count(e.uuid) AS count FROM events AS e INNER JOIN @@ -3027,7 +3041,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -3048,7 +3062,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.uuid) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(e__pdi__person.properties___email, '$$_posthog_breakdown_null_$$'), ['test2@posthog.com', 'test@gmail.com', 'test5@posthog.com', 'test4@posthog.com', 'test3@posthog.com'], ['test2@posthog.com', 'test@gmail.com', 'test5@posthog.com', 'test4@posthog.com', 'test3@posthog.com'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(e__pdi__person.properties___email), '$$_posthog_breakdown_null_$$'), ['test2@posthog.com', 'test@gmail.com', 'test5@posthog.com', 'test4@posthog.com', 'test3@posthog.com'], ['test2@posthog.com', 'test@gmail.com', 'test5@posthog.com', 'test4@posthog.com', 'test3@posthog.com'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, @@ -3070,7 +3084,7 @@ WHERE equals(person.team_id, 2) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-07-01 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), and(or(ifNull(notILike(e__pdi__person.properties___email, '%@posthog.com%'), 1), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'val'), 0)), or(ifNull(equals(e__pdi__person.`properties___$os`, 'android'), 0), ifNull(equals(e__pdi__person.`properties___$browser`, 'safari'), 0))), or(ifNull(equals(e__pdi__person.properties___email, 'test2@posthog.com'), 0), ifNull(equals(e__pdi__person.properties___email, 'test@gmail.com'), 0), ifNull(equals(e__pdi__person.properties___email, 'test5@posthog.com'), 0), ifNull(equals(e__pdi__person.properties___email, 'test4@posthog.com'), 0), ifNull(equals(e__pdi__person.properties___email, 'test3@posthog.com'), 0))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-07-01 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), and(or(ifNull(notILike(e__pdi__person.properties___email, '%@posthog.com%'), 1), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'val'), 0)), or(ifNull(equals(e__pdi__person.`properties___$os`, 'android'), 0), ifNull(equals(e__pdi__person.`properties___$browser`, 'safari'), 0))), or(ifNull(equals(toString(e__pdi__person.properties___email), 'test2@posthog.com'), 0), ifNull(equals(toString(e__pdi__person.properties___email), 'test@gmail.com'), 0), ifNull(equals(toString(e__pdi__person.properties___email), 'test5@posthog.com'), 0), ifNull(equals(toString(e__pdi__person.properties___email), 'test4@posthog.com'), 0), ifNull(equals(toString(e__pdi__person.properties___email), 'test3@posthog.com'), 0))) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -3085,7 +3099,7 @@ # --- # name: TestTrends.test_trend_breakdown_user_props_with_filter_with_partial_property_pushdowns.2 ''' - SELECT e__pdi__person.properties___email AS value, + SELECT toString(e__pdi__person.properties___email) AS value, count(e.uuid) AS count FROM events AS e INNER JOIN @@ -3120,7 +3134,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -3141,7 +3155,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.uuid) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(e__pdi__person.properties___email, '$$_posthog_breakdown_null_$$'), ['test2@posthog.com'], ['test2@posthog.com'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(e__pdi__person.properties___email), '$$_posthog_breakdown_null_$$'), ['test2@posthog.com'], ['test2@posthog.com'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, @@ -3163,7 +3177,7 @@ WHERE equals(person.team_id, 2) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-07-01 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), and(ifNull(equals(e__pdi__person.`properties___$os`, 'android'), 0), ifNull(equals(e__pdi__person.`properties___$browser`, 'chrome'), 0)), and(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'val'), 0), ifNull(ilike(e__pdi__person.properties___email, '%@posthog.com%'), 0)), ifNull(equals(e__pdi__person.properties___email, 'test2@posthog.com'), 0)) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-07-01 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), and(ifNull(equals(e__pdi__person.`properties___$os`, 'android'), 0), ifNull(equals(e__pdi__person.`properties___$browser`, 'chrome'), 0)), and(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'val'), 0), ifNull(ilike(e__pdi__person.properties___email, '%@posthog.com%'), 0)), ifNull(equals(toString(e__pdi__person.properties___email), 'test2@posthog.com'), 0)) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -3248,7 +3262,7 @@ # --- # name: TestTrends.test_trends_aggregate_by_distinct_id.2 ''' - SELECT e__pdi__person.`properties___$some_prop` AS value, + SELECT toString(e__pdi__person.`properties___$some_prop`) AS value, count(e.uuid) AS count FROM events AS e INNER JOIN @@ -3281,7 +3295,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -3302,7 +3316,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(DISTINCT e.distinct_id) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(e__pdi__person.`properties___$some_prop`, '$$_posthog_breakdown_null_$$'), ['some_val', '$$_posthog_breakdown_null_$$'], ['some_val', '$$_posthog_breakdown_null_$$'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(e__pdi__person.`properties___$some_prop`), '$$_posthog_breakdown_null_$$'), ['some_val', '$$_posthog_breakdown_null_$$'], ['some_val', '$$_posthog_breakdown_null_$$'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, @@ -3322,7 +3336,7 @@ WHERE equals(person.team_id, 2) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-24 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-31 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(e__pdi__person.`properties___$some_prop`, 'some_val'), 0), isNull(e__pdi__person.`properties___$some_prop`))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-24 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-31 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(toString(e__pdi__person.`properties___$some_prop`), 'some_val'), 0), isNull(toString(e__pdi__person.`properties___$some_prop`)))) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -3415,7 +3429,7 @@ # --- # name: TestTrends.test_trends_aggregate_by_distinct_id.6 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_prop'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_prop'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-24 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-31 23:59:59', 6, 'UTC')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-24 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-31 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'))) @@ -3430,7 +3444,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -3451,9 +3465,9 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(DISTINCT e.distinct_id) AS total, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_prop'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_null_$$'], ['$$_posthog_breakdown_null_$$'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_prop'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_null_$$'], ['$$_posthog_breakdown_null_$$'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-24 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-31 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_prop'), ''), 'null'), '^"|"$', ''))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-24 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-31 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), isNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_prop'), ''), 'null'), '^"|"$', '')))) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -3520,7 +3534,7 @@ # --- # name: TestTrends.test_trends_breakdown_cumulative ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'))) @@ -3535,7 +3549,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT day_start AS day_start, sum(count) OVER (PARTITION BY breakdown_value @@ -3561,7 +3575,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(DISTINCT e__pdi.person_id) AS total, min(toStartOfDay(toTimeZone(e.timestamp, 'UTC'))) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -3570,7 +3584,7 @@ WHERE equals(person_distinct_id2.team_id, 2) GROUP BY person_distinct_id2.distinct_id HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'other_value'), 0))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(isNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''))), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'other_value'), 0))) GROUP BY e__pdi.person_id, breakdown_value) GROUP BY day_start, @@ -3585,7 +3599,7 @@ # --- # name: TestTrends.test_trends_breakdown_cumulative_poe_v2 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'))) @@ -3600,7 +3614,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT day_start AS day_start, sum(count) OVER (PARTITION BY breakdown_value @@ -3626,7 +3640,7 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(DISTINCT ifNull(nullIf(e__override.override_person_id, '00000000-0000-0000-0000-000000000000'), e.person_id)) AS total, min(toStartOfDay(toTimeZone(e.timestamp, 'UTC'))) AS day_start, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], ['$$_posthog_breakdown_null_$$', 'value', 'other_value'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 LEFT OUTER JOIN (SELECT argMax(person_overrides.override_person_id, person_overrides.version) AS override_person_id, @@ -3634,7 +3648,7 @@ FROM person_overrides WHERE equals(person_overrides.team_id, 2) GROUP BY person_overrides.old_person_id) AS e__override ON equals(e.person_id, e__override.old_person_id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'other_value'), 0))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(isNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''))), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'other_value'), 0))) GROUP BY ifNull(nullIf(e__override.override_person_id, '00000000-0000-0000-0000-000000000000'), e.person_id), breakdown_value) GROUP BY day_start, @@ -3649,7 +3663,7 @@ # --- # name: TestTrends.test_trends_breakdown_with_session_property_single_aggregate_math_and_breakdown ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')) AS value, max(e__session.duration) AS count FROM events AS e INNER JOIN @@ -3672,7 +3686,7 @@ breakdown_value AS breakdown_value FROM (SELECT any(e__session.duration) AS session_duration, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['value2', 'value1', '$$_posthog_breakdown_null_$$'], ['value2', 'value1', '$$_posthog_breakdown_null_$$'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['value2', 'value1', '$$_posthog_breakdown_null_$$'], ['value2', 'value1', '$$_posthog_breakdown_null_$$'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT events.`$session_id` AS id, @@ -3680,7 +3694,7 @@ FROM events WHERE and(equals(events.team_id, 2), greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), 0)), lessOrEquals(toTimeZone(events.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), ifNull(notEquals(id, ''), 1)) GROUP BY id) AS e__session ON equals(e.`$session_id`, e__session.id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), 0)), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value2'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value1'), 0), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), 0)), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value2'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value1'), 0), isNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''))))) GROUP BY e__session.id, breakdown_value) GROUP BY breakdown_value @@ -3691,7 +3705,7 @@ # --- # name: TestTrends.test_trends_breakdown_with_session_property_single_aggregate_math_and_breakdown.2 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')) AS value, max(e__session.duration) AS count FROM events AS e INNER JOIN @@ -3714,7 +3728,7 @@ breakdown_value AS breakdown_value FROM (SELECT any(e__session.duration) AS session_duration, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['value2', 'value1', '$$_posthog_breakdown_null_$$'], ['value2', 'value1', '$$_posthog_breakdown_null_$$'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['value2', 'value1', '$$_posthog_breakdown_null_$$'], ['value2', 'value1', '$$_posthog_breakdown_null_$$'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT events.`$session_id` AS id, @@ -3722,7 +3736,7 @@ FROM events WHERE and(equals(events.team_id, 2), greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(events.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), ifNull(notEquals(id, ''), 1)) GROUP BY id) AS e__session ON equals(e.`$session_id`, e__session.id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value2'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value1'), 0), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value2'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value1'), 0), isNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''))))) GROUP BY e__session.id, breakdown_value) GROUP BY breakdown_value @@ -3854,7 +3868,7 @@ # --- # name: TestTrends.test_trends_count_per_user_average_aggregated_with_event_property_breakdown_with_sampling ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'color'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'color'), ''), 'null'), '^"|"$', '')) AS value, count(e.uuid) AS count FROM events AS e WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-07 23:59:59', 6, 'UTC')))), equals(e.event, 'viewed video')) @@ -3874,7 +3888,7 @@ breakdown_value AS breakdown_value FROM (SELECT count(e.uuid) AS total, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'color'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['red', 'blue', '$$_posthog_breakdown_null_$$'], ['red', 'blue', '$$_posthog_breakdown_null_$$'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'color'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['red', 'blue', '$$_posthog_breakdown_null_$$'], ['red', 'blue', '$$_posthog_breakdown_null_$$'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1.0 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, @@ -3883,7 +3897,7 @@ WHERE equals(person_distinct_id2.team_id, 2) GROUP BY person_distinct_id2.distinct_id HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) - WHERE and(equals(e.team_id, 2), and(equals(e.event, 'viewed video'), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'color'), ''), 'null'), '^"|"$', ''), 'red'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'color'), ''), 'null'), '^"|"$', ''), 'blue'), 0), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'color'), ''), 'null'), '^"|"$', '')))), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')), toIntervalDay(0))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-07 23:59:59', 6, 'UTC')))) + WHERE and(equals(e.team_id, 2), and(equals(e.event, 'viewed video'), or(ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'color'), ''), 'null'), '^"|"$', '')), 'red'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'color'), ''), 'null'), '^"|"$', '')), 'blue'), 0), isNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'color'), ''), 'null'), '^"|"$', ''))))), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')), toIntervalDay(0))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-07 23:59:59', 6, 'UTC')))) GROUP BY e__pdi.person_id, breakdown_value) GROUP BY breakdown_value) @@ -4098,7 +4112,7 @@ # --- # name: TestTrends.test_trends_person_breakdown_with_session_property_single_aggregate_math_and_breakdown ''' - SELECT e__pdi__person.`properties___$some_prop` AS value, + SELECT toString(e__pdi__person.`properties___$some_prop`) AS value, max(e__session.duration) AS count FROM events AS e INNER JOIN @@ -4139,7 +4153,7 @@ breakdown_value AS breakdown_value FROM (SELECT any(e__session.duration) AS session_duration, - transform(ifNull(e__pdi__person.`properties___$some_prop`, '$$_posthog_breakdown_null_$$'), ['some_val', 'another_val'], ['some_val', 'another_val'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(e__pdi__person.`properties___$some_prop`), '$$_posthog_breakdown_null_$$'), ['some_val', 'another_val'], ['some_val', 'another_val'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN (SELECT events.`$session_id` AS id, @@ -4165,7 +4179,7 @@ WHERE equals(person.team_id, 2) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), 0)), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(e__pdi__person.`properties___$some_prop`, 'some_val'), 0), ifNull(equals(e__pdi__person.`properties___$some_prop`, 'another_val'), 0))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), 0)), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(toString(e__pdi__person.`properties___$some_prop`), 'some_val'), 0), ifNull(equals(toString(e__pdi__person.`properties___$some_prop`), 'another_val'), 0))) GROUP BY e__session.id, breakdown_value) GROUP BY breakdown_value @@ -4316,7 +4330,7 @@ # --- # name: TestTrends.test_trends_with_session_property_total_volume_math_with_breakdowns ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')) AS value, max(e__session.duration) AS count FROM events AS e INNER JOIN @@ -4337,7 +4351,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -4361,7 +4375,7 @@ breakdown_value AS breakdown_value FROM (SELECT any(e__session.duration) AS session_duration, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['value2', 'value1'], ['value2', 'value1'], '$$_posthog_breakdown_other_$$') AS breakdown_value, + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['value2', 'value1'], ['value2', 'value1'], '$$_posthog_breakdown_other_$$') AS breakdown_value, toStartOfWeek(toTimeZone(e.timestamp, 'UTC'), 0) AS day_start FROM events AS e SAMPLE 1 INNER JOIN @@ -4370,7 +4384,7 @@ FROM events WHERE and(equals(events.team_id, 2), greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), 0)), lessOrEquals(toTimeZone(events.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), ifNull(notEquals(id, ''), 1)) GROUP BY id) AS e__session ON equals(e.`$session_id`, e__session.id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), 0)), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value2'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value1'), 0))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), 0)), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value2'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value1'), 0))) GROUP BY day_start, e__session.id, breakdown_value, @@ -4389,7 +4403,7 @@ # --- # name: TestTrends.test_trends_with_session_property_total_volume_math_with_breakdowns.2 ''' - SELECT replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '') AS value, + SELECT toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')) AS value, max(e__session.duration) AS count FROM events AS e INNER JOIN @@ -4410,7 +4424,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -4434,7 +4448,7 @@ breakdown_value AS breakdown_value FROM (SELECT any(e__session.duration) AS session_duration, - transform(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), '$$_posthog_breakdown_null_$$'), ['value2', 'value1'], ['value2', 'value1'], '$$_posthog_breakdown_other_$$') AS breakdown_value, + transform(ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), '$$_posthog_breakdown_null_$$'), ['value2', 'value1'], ['value2', 'value1'], '$$_posthog_breakdown_other_$$') AS breakdown_value, toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start FROM events AS e SAMPLE 1 INNER JOIN @@ -4443,7 +4457,7 @@ FROM events WHERE and(equals(events.team_id, 2), greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(events.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), ifNull(notEquals(id, ''), 1)) GROUP BY id) AS e__session ON equals(e.`$session_id`, e__session.id) - WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value2'), 0), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', ''), 'value1'), 0))) + WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value2'), 0), ifNull(equals(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, '$some_property'), ''), 'null'), '^"|"$', '')), 'value1'), 0))) GROUP BY day_start, e__session.id, breakdown_value, diff --git a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends_data_warehouse_query.ambr b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends_data_warehouse_query.ambr index db9e8e1d45000..1e3bc1b5cbad6 100644 --- a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends_data_warehouse_query.ambr +++ b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends_data_warehouse_query.ambr @@ -1,7 +1,7 @@ # serializer version: 1 # name: TestTrendsDataWarehouseQuery.test_trends_breakdown ''' - SELECT e.prop_1 AS value, + SELECT toString(e.prop_1) AS value, count(e.id) AS count FROM s3('http://host.docker.internal:19000/posthog/test_storage_bucket-posthog.hogql.datawarehouse.trendquery/*.parquet', 'object_storage_root_user', 'object_storage_root_password', 'Parquet', 'id String, prop_1 String, prop_2 String, created DateTime64(3, \'UTC\')') AS e WHERE and(and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0)), and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0))) @@ -16,7 +16,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -37,9 +37,9 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.id) AS total, toStartOfDay(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC')) AS day_start, - transform(ifNull(e.prop_1, '$$_posthog_breakdown_null_$$'), ['d', 'c', 'b', 'a'], ['d', 'c', 'b', 'a'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(e.prop_1), '$$_posthog_breakdown_null_$$'), ['d', 'c', 'b', 'a'], ['d', 'c', 'b', 'a'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM s3('http://host.docker.internal:19000/posthog/test_storage_bucket-posthog.hogql.datawarehouse.trendquery/*.parquet', 'object_storage_root_user', 'object_storage_root_password', 'Parquet', 'id String, prop_1 String, prop_2 String, created DateTime64(3, \'UTC\')') AS e - WHERE and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), or(equals(e.prop_1, 'd'), equals(e.prop_1, 'c'), equals(e.prop_1, 'b'), equals(e.prop_1, 'a'))) + WHERE and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), or(ifNull(equals(toString(e.prop_1), 'd'), 0), ifNull(equals(toString(e.prop_1), 'c'), 0), ifNull(equals(toString(e.prop_1), 'b'), 0), ifNull(equals(toString(e.prop_1), 'a'), 0))) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -54,7 +54,7 @@ # --- # name: TestTrendsDataWarehouseQuery.test_trends_breakdown_with_property ''' - SELECT e.prop_1 AS value, + SELECT toString(e.prop_1) AS value, count(e.id) AS count FROM s3('http://host.docker.internal:19000/posthog/test_storage_bucket-posthog.hogql.datawarehouse.trendquery/*.parquet', 'object_storage_root_user', 'object_storage_root_password', 'Parquet', 'id String, prop_1 String, prop_2 String, created DateTime64(3, \'UTC\')') AS e WHERE and(and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0)), and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), equals(e.prop_1, 'a'))) @@ -69,7 +69,7 @@ ''' SELECT groupArray(day_start) AS date, groupArray(count) AS total, - ifNull(toString(breakdown_value), '') AS breakdown_value + ifNull(toString(breakdown_value), '$$_posthog_breakdown_null_$$') AS breakdown_value FROM (SELECT sum(total) AS count, day_start AS day_start, @@ -90,9 +90,9 @@ ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.id) AS total, toStartOfDay(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC')) AS day_start, - transform(ifNull(e.prop_1, '$$_posthog_breakdown_null_$$'), ['a'], ['a'], '$$_posthog_breakdown_other_$$') AS breakdown_value + transform(ifNull(toString(e.prop_1), '$$_posthog_breakdown_null_$$'), ['a'], ['a'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM s3('http://host.docker.internal:19000/posthog/test_storage_bucket-posthog.hogql.datawarehouse.trendquery/*.parquet', 'object_storage_root_user', 'object_storage_root_password', 'Parquet', 'id String, prop_1 String, prop_2 String, created DateTime64(3, \'UTC\')') AS e - WHERE and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), equals(e.prop_1, 'a'), equals(e.prop_1, 'a')) + WHERE and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), equals(e.prop_1, 'a'), ifNull(equals(toString(e.prop_1), 'a'), 0)) GROUP BY day_start, breakdown_value) GROUP BY day_start, diff --git a/posthog/hogql_queries/insights/trends/test/test_trends.py b/posthog/hogql_queries/insights/trends/test/test_trends.py index 1ac54e16de629..9e885fbadcc1d 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends.py @@ -5180,7 +5180,9 @@ def test_breakdown_filtering_with_properties_in_new_format(self): ) response = sorted(response, key=lambda x: x["label"]) - self.assertEqual(len(response), 0) + self.assertEqual(len(response), 1) + self.assertEqual(response[0]["label"], "$$_posthog_breakdown_null_$$") + self.assertEqual(response[0]["count"], 0) @also_test_with_person_on_events_v2 @snapshot_clickhouse_queries diff --git a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py index 433df7c7df23b..8d14950ec23b2 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py @@ -381,7 +381,7 @@ def test_trends_query_formula_breakdown_no_data(self): TrendsFilter(formula="A+B"), BreakdownFilter(breakdown_type=BreakdownType.person, breakdown="$browser"), ) - self.assertEqual([], response.results) + self.assertEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], response.results[0]["data"]) def test_trends_query_formula_aggregate(self): self._create_test_events() @@ -714,16 +714,7 @@ def test_trends_breakdowns_multiple_hogql(self): breakdown_labels = [result["breakdown_value"] for result in response.results] assert len(response.results) == 8 - assert breakdown_labels == [ - "Chrome", - "Firefox", - "Edge", - "Safari", - "Chrome", - "Edge", - "Firefox", - "Safari", - ] + assert breakdown_labels == ["Chrome", "Firefox", "Edge", "Safari", "Chrome", "Edge", "Firefox", "Safari"] assert response.results[0]["label"] == f"$pageview - Chrome" assert response.results[1]["label"] == f"$pageview - Firefox" assert response.results[2]["label"] == f"$pageview - Edge" @@ -823,6 +814,7 @@ def test_trends_breakdown_and_aggregation_query_orchestration(self): 10, 0, ] + assert response.results[1]["data"] == [ 20, 0, @@ -1606,9 +1598,8 @@ def test_to_actors_query_options_breakdowns_boolean(self): assert response.series == [InsightActorsQuerySeries(label="$pageview", value=0)] assert response.breakdown == [ - # BreakdownItem(label="Other", value="$$_posthog_breakdown_other_$$"), # TODO: Add when "Other" works - BreakdownItem(label="true", value=1), - BreakdownItem(label="false", value=0), + BreakdownItem(label="true", value="true"), + BreakdownItem(label="false", value="false"), ] def test_to_actors_query_options_breakdowns_histogram(self): diff --git a/posthog/hogql_queries/insights/trends/trends_query_builder.py b/posthog/hogql_queries/insights/trends/trends_query_builder.py index 7be735d3b0a8b..a911e4bf8302a 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_builder.py +++ b/posthog/hogql_queries/insights/trends/trends_query_builder.py @@ -14,6 +14,7 @@ from posthog.models.action.action import Action from posthog.models.filters.mixins.utils import cached_property from posthog.models.team.team import Team +from posthog.queries.trends.breakdown import BREAKDOWN_NULL_STRING_LABEL from posthog.schema import ( ActionsNode, DataWarehouseNode, @@ -68,7 +69,7 @@ def build_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: return full_query def build_actors_query( - self, time_frame: Optional[str] = None, breakdown_filter: Optional[str | int] = None + self, time_frame: Optional[str] = None, breakdown_filter: Optional[str] = None ) -> ast.SelectQuery | ast.SelectUnionQuery: breakdown = self._breakdown(is_actors_query=True, breakdown_values_override=breakdown_filter) @@ -292,7 +293,8 @@ def _get_events_subquery( # Just breakdowns elif breakdown.enabled: if not is_actors_query: - default_query.select.append(breakdown.column_expr()) + breakdown_expr = breakdown.column_expr() + default_query.select.append(breakdown_expr) default_query.group_by.append(ast.Field(chain=["breakdown_value"])) # Just session duration math property elif self._aggregation_operation.aggregating_on_session_duration(): @@ -369,7 +371,7 @@ def _outer_select_query(self, breakdown: Breakdown, inner_query: ast.SelectQuery name="ifNull", args=[ ast.Call(name="toString", args=[ast.Field(chain=["breakdown_value"])]), - ast.Constant(value=""), + ast.Constant(value=BREAKDOWN_NULL_STRING_LABEL), ], ), ) @@ -565,7 +567,7 @@ def session_duration_math_property_wrapper(self, default_query: ast.SelectQuery) query.group_by = [] return query - def _breakdown(self, is_actors_query: bool, breakdown_values_override: Optional[str | int] = None): + def _breakdown(self, is_actors_query: bool, breakdown_values_override: Optional[str] = None): return Breakdown( team=self.team, query=self.query, diff --git a/posthog/hogql_queries/insights/trends/trends_query_runner.py b/posthog/hogql_queries/insights/trends/trends_query_runner.py index d66110298ffab..d61720740f52b 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/trends_query_runner.py @@ -23,9 +23,7 @@ from posthog.hogql.query import execute_hogql_query from posthog.hogql.timings import HogQLTimings from posthog.hogql_queries.insights.trends.breakdown_values import ( - BREAKDOWN_NULL_NUMERIC_LABEL, BREAKDOWN_NULL_STRING_LABEL, - BREAKDOWN_OTHER_NUMERIC_LABEL, BREAKDOWN_OTHER_STRING_LABEL, ) from posthog.hogql_queries.insights.trends.display import TrendsDisplay @@ -175,7 +173,7 @@ def to_actors_query( modifiers=self.modifiers, ) - query = query_builder.build_actors_query(time_frame=time_frame, breakdown_filter=breakdown_value) + query = query_builder.build_actors_query(time_frame=time_frame, breakdown_filter=str(breakdown_value)) return query @@ -240,14 +238,10 @@ def to_actors_query_options(self) -> InsightActorsQueryOptionsResponse: cohort_name = "all users" if str(value) == "0" else Cohort.objects.get(pk=value).name label = cohort_name value = value - elif value == BREAKDOWN_OTHER_STRING_LABEL or value == BREAKDOWN_OTHER_NUMERIC_LABEL: - # label = "Other" - # value = BREAKDOWN_OTHER_STRING_LABEL - continue # TODO: Add support for "other" breakdowns - elif value == BREAKDOWN_NULL_STRING_LABEL or value == BREAKDOWN_NULL_NUMERIC_LABEL: - # label = "Null" - # value = BREAKDOWN_NULL_STRING_LABEL - continue # TODO: Add support for "null" breakdowns + elif value == BREAKDOWN_OTHER_STRING_LABEL: + label = "Other (Groups all remaining values)" + elif value == BREAKDOWN_NULL_STRING_LABEL: + label = "None (No value)" elif is_boolean_breakdown: label = self._convert_boolean(value) else: @@ -501,18 +495,6 @@ def get_value(name: str, val: Any): series_object["breakdown_value"] = remapped_label - # If the breakdown value is the numeric "other", then set it to the string version - if ( - remapped_label == BREAKDOWN_OTHER_NUMERIC_LABEL - or remapped_label == str(BREAKDOWN_OTHER_NUMERIC_LABEL) - or remapped_label == float(BREAKDOWN_OTHER_NUMERIC_LABEL) - ): - series_object["breakdown_value"] = BREAKDOWN_OTHER_STRING_LABEL - if real_series_count > 1 or self._is_breakdown_field_boolean(): - series_object["label"] = "{} - {}".format(series_label or "All events", "Other") - else: - series_object["label"] = "Other" - res.append(series_object) return res From 6eea1a6554b090925f1117e2adee053bc7a37146 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 21 Mar 2024 23:15:17 +0000 Subject: [PATCH 31/51] chore: only run depot runner comparison when labelled (#21092) * chore: only run depot runner comparison when labelled * fix --- .github/workflows/ci-backend-depot.yml | 10 +--------- .github/workflows/ci-e2e-depot.yml | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-backend-depot.yml b/.github/workflows/ci-backend-depot.yml index 3cf935ced141e..928886d44cf52 100644 --- a/.github/workflows/ci-backend-depot.yml +++ b/.github/workflows/ci-backend-depot.yml @@ -5,15 +5,7 @@ name: Backend CI (depot) on: - push: - branches: - - master pull_request: - workflow_dispatch: - inputs: - clickhouseServerVersion: - description: ClickHouse server version. Leave blank for default - type: string concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -41,7 +33,7 @@ jobs: changes: runs-on: depot-ubuntu-latest-4 timeout-minutes: 5 - if: github.repository == 'PostHog/posthog' + if: ${{ contains(github.event.pull_request.labels.*.name, 'test-depot') }} name: Determine need to run backend checks # Set job outputs to values from filter step outputs: diff --git a/.github/workflows/ci-e2e-depot.yml b/.github/workflows/ci-e2e-depot.yml index 4985dac9d746a..697d42e97f945 100644 --- a/.github/workflows/ci-e2e-depot.yml +++ b/.github/workflows/ci-e2e-depot.yml @@ -16,7 +16,7 @@ jobs: changes: runs-on: depot-ubuntu-latest-4 timeout-minutes: 5 - if: github.repository == 'PostHog/posthog' + if: ${{ contains(github.event.pull_request.labels.*.name, 'test-depot') }} name: Determine need to run E2E checks # Set job outputs to values from filter step outputs: From f43d7a9c7a0089e84ce6670c763358b8723172f1 Mon Sep 17 00:00:00 2001 From: Robbie Date: Fri, 22 Mar 2024 08:15:50 +0000 Subject: [PATCH 32/51] feat(web-analytics): HogQL sessions where extractor (#20986) * Add logic to extract a raw_sessions where clause from a query on the sessions table * Add failing test - need to figure out types * Replace sessions with raw_sessions * Use existing Visitor class hierarchy rather than creating a new one * Clear types * Add working test * Formatting * Fix typing * Better fix typing * Fix tests * Add a big comment * Increase comment * Add types to Visitor * Handles aliases better * Add test for joining events and sessions * Fix typing * More robust way of looking up tables * Add a test to make sure that collapsing ands works --- posthog/hogql/ast.py | 12 + posthog/hogql/base.py | 2 +- posthog/hogql/database/__init__.py | 0 posthog/hogql/database/models.py | 7 +- posthog/hogql/database/schema/__init__.py | 0 .../hogql/database/schema/cohort_people.py | 3 +- posthog/hogql/database/schema/groups.py | 3 +- posthog/hogql/database/schema/log_entries.py | 5 +- .../database/schema/person_distinct_ids.py | 3 +- posthog/hogql/database/schema/persons.py | 4 +- posthog/hogql/database/schema/persons_pdi.py | 3 +- .../database/schema/session_replay_events.py | 3 +- posthog/hogql/database/schema/sessions.py | 15 +- .../hogql/database/schema/util/__init__.py | 0 .../util/session_where_clause_extractor.py | 398 ++++++++++++++++++ .../test_session_where_clause_extractor.py | 284 +++++++++++++ posthog/hogql/test/test_bytecode.py | 2 +- posthog/hogql/test/test_visitor.py | 2 +- posthog/hogql/transforms/lazy_tables.py | 2 +- posthog/hogql/visitor.py | 13 +- 20 files changed, 730 insertions(+), 31 deletions(-) create mode 100644 posthog/hogql/database/__init__.py create mode 100644 posthog/hogql/database/schema/__init__.py create mode 100644 posthog/hogql/database/schema/util/__init__.py create mode 100644 posthog/hogql/database/schema/util/session_where_clause_extractor.py create mode 100644 posthog/hogql/database/schema/util/test/test_session_where_clause_extractor.py diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index a459514f2524f..806226b8f1b9e 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -46,8 +46,17 @@ def resolve_constant_type(self, context: HogQLContext): def resolve_database_field(self, context: HogQLContext): if isinstance(self.type, FieldType): return self.type.resolve_database_field(context) + if isinstance(self.type, PropertyType): + return self.type.field_type.resolve_database_field(context) raise NotImplementedException("FieldAliasType.resolve_database_field not implemented") + def resolve_table_type(self, context: HogQLContext): + if isinstance(self.type, FieldType): + return self.type.table_type + if isinstance(self.type, PropertyType): + return self.type.field_type.table_type + raise NotImplementedException("FieldAliasType.resolve_table_type not implemented") + @dataclass(kw_only=True) class BaseTableType(Type): @@ -339,6 +348,9 @@ def get_child(self, name: str | int, context: HogQLContext) -> Type: f'Can not access property "{name}" on field "{self.name}" of type: {type(database_field).__name__}' ) + def resolve_table_type(self, context: HogQLContext): + return self.table_type + @dataclass(kw_only=True) class PropertyType(Type): diff --git a/posthog/hogql/base.py b/posthog/hogql/base.py index fbdafffb2d08c..e8a74025b78be 100644 --- a/posthog/hogql/base.py +++ b/posthog/hogql/base.py @@ -32,7 +32,7 @@ def accept(self, visitor): return visit(self) if hasattr(visitor, "visit_unknown"): return visitor.visit_unknown(self) - raise NotImplementedException(f"Visitor has no method {method_name}") + raise NotImplementedException(f"{visitor.__class__.__name__} has no method {method_name}") @dataclass(kw_only=True) diff --git a/posthog/hogql/database/__init__.py b/posthog/hogql/database/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/posthog/hogql/database/models.py b/posthog/hogql/database/models.py index 95a00595c6472..d2da7868a7f9c 100644 --- a/posthog/hogql/database/models.py +++ b/posthog/hogql/database/models.py @@ -3,7 +3,6 @@ from posthog.hogql.base import Expr from posthog.hogql.errors import HogQLException, NotImplementedException -from posthog.schema import HogQLQueryModifiers if TYPE_CHECKING: from posthog.hogql.context import HogQLContext @@ -126,12 +125,14 @@ def resolve_table(self, context: "HogQLContext") -> Table: class LazyTable(Table): """ - A table that is replaced with a subquery returned from `lazy_select(requested_fields: Dict[name, chain], modifiers: HogQLQueryModifiers)` + A table that is replaced with a subquery returned from `lazy_select(requested_fields: Dict[name, chain], modifiers: HogQLQueryModifiers, node: SelectQuery)` """ model_config = ConfigDict(extra="forbid") - def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers) -> Any: + def lazy_select( + self, requested_fields: Dict[str, List[str | int]], context: "HogQLContext", node: "SelectQuery" + ) -> Any: raise NotImplementedException("LazyTable.lazy_select not overridden") diff --git a/posthog/hogql/database/schema/__init__.py b/posthog/hogql/database/schema/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/posthog/hogql/database/schema/cohort_people.py b/posthog/hogql/database/schema/cohort_people.py index 72080419b7355..11723f0194619 100644 --- a/posthog/hogql/database/schema/cohort_people.py +++ b/posthog/hogql/database/schema/cohort_people.py @@ -9,7 +9,6 @@ FieldOrTable, ) from posthog.hogql.database.schema.persons import join_with_persons_table -from posthog.schema import HogQLQueryModifiers COHORT_PEOPLE_FIELDS = { "person_id": StringDatabaseField(name="person_id"), @@ -67,7 +66,7 @@ def to_printed_hogql(self): class CohortPeople(LazyTable): fields: Dict[str, FieldOrTable] = COHORT_PEOPLE_FIELDS - def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], context, node): return select_from_cohort_people_table(requested_fields) def to_printed_clickhouse(self, context): diff --git a/posthog/hogql/database/schema/groups.py b/posthog/hogql/database/schema/groups.py index bb237d68e8070..3b9de7f08befc 100644 --- a/posthog/hogql/database/schema/groups.py +++ b/posthog/hogql/database/schema/groups.py @@ -13,7 +13,6 @@ FieldOrTable, ) from posthog.hogql.errors import HogQLException -from posthog.schema import HogQLQueryModifiers GROUPS_TABLE_FIELDS = { "index": IntegerDatabaseField(name="group_type_index"), @@ -83,7 +82,7 @@ def to_printed_hogql(self): class GroupsTable(LazyTable): fields: Dict[str, FieldOrTable] = GROUPS_TABLE_FIELDS - def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], context, node): return select_from_groups_table(requested_fields) def to_printed_clickhouse(self, context): diff --git a/posthog/hogql/database/schema/log_entries.py b/posthog/hogql/database/schema/log_entries.py index c14e90e26da50..9f5dc816ac4b0 100644 --- a/posthog/hogql/database/schema/log_entries.py +++ b/posthog/hogql/database/schema/log_entries.py @@ -9,7 +9,6 @@ LazyTable, FieldOrTable, ) -from posthog.schema import HogQLQueryModifiers LOG_ENTRIES_FIELDS: Dict[str, FieldOrTable] = { "team_id": IntegerDatabaseField(name="team_id"), @@ -35,7 +34,7 @@ def to_printed_hogql(self): class ReplayConsoleLogsLogEntriesTable(LazyTable): fields: Dict[str, FieldOrTable] = LOG_ENTRIES_FIELDS - def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], context, node): fields: List[ast.Expr] = [ast.Field(chain=["log_entries"] + chain) for name, chain in requested_fields.items()] return ast.SelectQuery( @@ -58,7 +57,7 @@ def to_printed_hogql(self): class BatchExportLogEntriesTable(LazyTable): fields: Dict[str, FieldOrTable] = LOG_ENTRIES_FIELDS - def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], context, node): fields: List[ast.Expr] = [ast.Field(chain=["log_entries"] + chain) for name, chain in requested_fields.items()] return ast.SelectQuery( diff --git a/posthog/hogql/database/schema/person_distinct_ids.py b/posthog/hogql/database/schema/person_distinct_ids.py index 02144b35fc3d8..3304eccda862e 100644 --- a/posthog/hogql/database/schema/person_distinct_ids.py +++ b/posthog/hogql/database/schema/person_distinct_ids.py @@ -14,7 +14,6 @@ ) from posthog.hogql.database.schema.persons import join_with_persons_table from posthog.hogql.errors import HogQLException -from posthog.schema import HogQLQueryModifiers PERSON_DISTINCT_IDS_FIELDS = { "team_id": IntegerDatabaseField(name="team_id"), @@ -82,7 +81,7 @@ def to_printed_hogql(self): class PersonDistinctIdsTable(LazyTable): fields: Dict[str, FieldOrTable] = PERSON_DISTINCT_IDS_FIELDS - def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], context, node): return select_from_person_distinct_ids_table(requested_fields) def to_printed_clickhouse(self, context): diff --git a/posthog/hogql/database/schema/persons.py b/posthog/hogql/database/schema/persons.py index a248da56b7307..c7abdd89e14c6 100644 --- a/posthog/hogql/database/schema/persons.py +++ b/posthog/hogql/database/schema/persons.py @@ -123,8 +123,8 @@ def to_printed_hogql(self): class PersonsTable(LazyTable): fields: Dict[str, FieldOrTable] = PERSONS_FIELDS - def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): - return select_from_persons_table(requested_fields, modifiers) + def lazy_select(self, requested_fields: Dict[str, List[str | int]], context, node): + return select_from_persons_table(requested_fields, context.modifiers) def to_printed_clickhouse(self, context): return "person" diff --git a/posthog/hogql/database/schema/persons_pdi.py b/posthog/hogql/database/schema/persons_pdi.py index 9f476f407b4d2..195643b90c08c 100644 --- a/posthog/hogql/database/schema/persons_pdi.py +++ b/posthog/hogql/database/schema/persons_pdi.py @@ -10,7 +10,6 @@ FieldOrTable, ) from posthog.hogql.errors import HogQLException -from posthog.schema import HogQLQueryModifiers # :NOTE: We already have person_distinct_ids.py, which most tables link to. This persons_pdi.py is a hack to @@ -63,7 +62,7 @@ class PersonsPDITable(LazyTable): "person_id": StringDatabaseField(name="person_id"), } - def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], context, node): return persons_pdi_select(requested_fields) def to_printed_clickhouse(self, context): diff --git a/posthog/hogql/database/schema/session_replay_events.py b/posthog/hogql/database/schema/session_replay_events.py index c9d564c7d4588..baaecef89e049 100644 --- a/posthog/hogql/database/schema/session_replay_events.py +++ b/posthog/hogql/database/schema/session_replay_events.py @@ -15,7 +15,6 @@ PersonDistinctIdsTable, join_with_person_distinct_ids_table, ) -from posthog.schema import HogQLQueryModifiers RAW_ONLY_FIELDS = ["min_first_timestamp", "max_last_timestamp"] @@ -115,7 +114,7 @@ class SessionReplayEventsTable(LazyTable): "first_url": StringDatabaseField(name="first_url"), } - def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], context, node): return select_from_session_replay_events_table(requested_fields) def to_printed_clickhouse(self, context): diff --git a/posthog/hogql/database/schema/sessions.py b/posthog/hogql/database/schema/sessions.py index 2a4865798eeb8..770daceaa23c5 100644 --- a/posthog/hogql/database/schema/sessions.py +++ b/posthog/hogql/database/schema/sessions.py @@ -1,5 +1,7 @@ from typing import Dict, List, cast +from posthog.hogql import ast +from posthog.hogql.context import HogQLContext from posthog.hogql.database.models import ( StringDatabaseField, DateTimeDatabaseField, @@ -11,7 +13,7 @@ LazyTable, ) from posthog.hogql.database.schema.channel_type import create_channel_type_expr -from posthog.schema import HogQLQueryModifiers +from posthog.hogql.database.schema.util.session_where_clause_extractor import SessionMinTimestampWhereClauseExtractor SESSIONS_COMMON_FIELDS: Dict[str, FieldOrTable] = { @@ -62,7 +64,9 @@ def avoid_asterisk_fields(self) -> List[str]: ] -def select_from_sessions_table(requested_fields: Dict[str, List[str | int]]): +def select_from_sessions_table( + requested_fields: Dict[str, List[str | int]], node: ast.SelectQuery, context: HogQLContext +): from posthog.hogql import ast table_name = "raw_sessions" @@ -134,10 +138,13 @@ def select_from_sessions_table(requested_fields: Dict[str, List[str | int]]): ) group_by_fields.append(ast.Field(chain=cast(list[str | int], [table_name]) + chain)) + where = SessionMinTimestampWhereClauseExtractor(context).get_inner_where(node) + return ast.SelectQuery( select=select_fields, select_from=ast.JoinExpr(table=ast.Field(chain=[table_name])), group_by=group_by_fields, + where=where, ) @@ -148,8 +155,8 @@ class SessionsTable(LazyTable): "channel_type": StringDatabaseField(name="channel_type"), } - def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): - return select_from_sessions_table(requested_fields) + def lazy_select(self, requested_fields: Dict[str, List[str | int]], context, node: ast.SelectQuery): + return select_from_sessions_table(requested_fields, node, context) def to_printed_clickhouse(self, context): return "sessions" diff --git a/posthog/hogql/database/schema/util/__init__.py b/posthog/hogql/database/schema/util/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/posthog/hogql/database/schema/util/session_where_clause_extractor.py b/posthog/hogql/database/schema/util/session_where_clause_extractor.py new file mode 100644 index 0000000000000..83933bdde8b85 --- /dev/null +++ b/posthog/hogql/database/schema/util/session_where_clause_extractor.py @@ -0,0 +1,398 @@ +from dataclasses import dataclass +from typing import Optional + +from posthog.hogql import ast +from posthog.hogql.ast import CompareOperationOp, ArithmeticOperationOp +from posthog.hogql.context import HogQLContext +from posthog.hogql.database.models import DatabaseField + +from posthog.hogql.visitor import clone_expr, CloningVisitor, Visitor + +SESSION_BUFFER_DAYS = 3 + + +@dataclass +class SessionMinTimestampWhereClauseExtractor(CloningVisitor): + """This class extracts the Where clause from the lazy sessions table, to the clickhouse sessions table. + + The sessions table in Clickhouse is an AggregatingMergeTree, and will have one row per session per day. This means that + when we want to query sessions, we need to pre-group these rows, so that we only have one row per session. + + We hide this detail using a lazy table, but to make querying the underlying Clickhouse table faster, we can inline the + min_timestamp where conditions from the select on the outer lazy table to the select on the inner real table. + + This class is called on the select query of the lazy table, and will return the where clause that should be applied to + the inner table. + + As a query can be unreasonably complex, we only handle simple cases, but this class is designed to fail-safe. If it + can't reason about a particular expression, it will just return a constant True, i.e. fetch more rows than necessary. + + This means that we can incrementally add support for more complex queries, without breaking existing queries, by + handling more cases. + + Some examples of failing-safe: + + `SELECT * FROM sessions where min_timestamp > '2022-01-01' AND f(session_id)` + only the` min_timestamp > '2022-01-01'` part is relevant, so we can ignore the `f(session_id)` part, and it is safe + to replace it with a constant True, which collapses the AND to just the `min_timestamp > '2022-01-01'` part. + + `SELECT * FROM sessions where min_timestamp > '2022-01-01' OR f(session_id)` + only the` min_timestamp > '2022-01-01'` part is relevant, and turning the `f(session_id)` part into a constant True + would collapse the OR to True. In this case we return None as no pre-filtering is possible. + + All min_timestamp comparisons are given a buffer of SESSION_BUFFER_DAYS each side, to ensure that we collect all the + relevant rows for each session. + """ + + context: HogQLContext + clear_types: bool = False + clear_locations: bool = False + + def get_inner_where(self, parsed_query: ast.SelectQuery) -> Optional[ast.Expr]: + if not parsed_query.where: + return None + + # visit the where clause + where = self.visit(parsed_query.where) + + if isinstance(where, ast.Constant): + return None + + return clone_expr(where, clear_types=True, clear_locations=True) + + def visit_compare_operation(self, node: ast.CompareOperation) -> ast.Expr: + is_left_constant = is_time_or_interval_constant(node.left) + is_right_constant = is_time_or_interval_constant(node.right) + is_left_timestamp_field = is_simple_timestamp_field_expression(node.left, self.context) + is_right_timestamp_field = is_simple_timestamp_field_expression(node.right, self.context) + + if is_left_constant and is_right_constant: + # just ignore this comparison + return ast.Constant(value=True) + + # handle the left side being a min_timestamp expression and the right being constant + if is_left_timestamp_field and is_right_constant: + if node.op == CompareOperationOp.Eq: + return ast.And( + exprs=[ + ast.CompareOperation( + op=ast.CompareOperationOp.LtEq, + left=ast.ArithmeticOperation( + op=ast.ArithmeticOperationOp.Sub, + left=rewrite_timestamp_field(node.left, self.context), + right=ast.Call(name="toIntervalDay", args=[ast.Constant(value=SESSION_BUFFER_DAYS)]), + ), + right=node.right, + ), + ast.CompareOperation( + op=ast.CompareOperationOp.GtEq, + left=ast.ArithmeticOperation( + op=ast.ArithmeticOperationOp.Add, + left=rewrite_timestamp_field(node.left, self.context), + right=ast.Call(name="toIntervalDay", args=[ast.Constant(value=SESSION_BUFFER_DAYS)]), + ), + right=node.right, + ), + ] + ) + elif node.op == CompareOperationOp.Gt or node.op == CompareOperationOp.GtEq: + return ast.CompareOperation( + op=ast.CompareOperationOp.GtEq, + left=ast.ArithmeticOperation( + op=ast.ArithmeticOperationOp.Add, + left=rewrite_timestamp_field(node.left, self.context), + right=ast.Call(name="toIntervalDay", args=[ast.Constant(value=SESSION_BUFFER_DAYS)]), + ), + right=node.right, + ) + elif node.op == CompareOperationOp.Lt or node.op == CompareOperationOp.LtEq: + return ast.CompareOperation( + op=ast.CompareOperationOp.LtEq, + left=ast.ArithmeticOperation( + op=ast.ArithmeticOperationOp.Sub, + left=rewrite_timestamp_field(node.left, self.context), + right=ast.Call(name="toIntervalDay", args=[ast.Constant(value=SESSION_BUFFER_DAYS)]), + ), + right=node.right, + ) + elif is_right_timestamp_field and is_left_constant: + # let's not duplicate the logic above, instead just flip and it and recurse + if node.op in [ + CompareOperationOp.Eq, + CompareOperationOp.Lt, + CompareOperationOp.LtEq, + CompareOperationOp.Gt, + CompareOperationOp.GtEq, + ]: + return self.visit( + ast.CompareOperation( + op=CompareOperationOp.Eq + if node.op == CompareOperationOp.Eq + else CompareOperationOp.Lt + if node.op == CompareOperationOp.Gt + else CompareOperationOp.LtEq + if node.op == CompareOperationOp.GtEq + else CompareOperationOp.Gt + if node.op == CompareOperationOp.Lt + else CompareOperationOp.GtEq, + left=node.right, + right=node.left, + ) + ) + + return ast.Constant(value=True) + + def visit_arithmetic_operation(self, node: ast.ArithmeticOperation) -> ast.Expr: + # don't even try to handle complex logic + return ast.Constant(value=True) + + def visit_not(self, node: ast.Not) -> ast.Expr: + return ast.Constant(value=True) + + def visit_call(self, node: ast.Call) -> ast.Expr: + if node.name == "and": + return self.visit_and(ast.And(exprs=node.args)) + elif node.name == "or": + return self.visit_or(ast.Or(exprs=node.args)) + return ast.Constant(value=True) + + def visit_field(self, node: ast.Field) -> ast.Expr: + return ast.Constant(value=True) + + def visit_constant(self, node: ast.Constant) -> ast.Expr: + return ast.Constant(value=True) + + def visit_placeholder(self, node: ast.Placeholder) -> ast.Expr: + raise Exception() # this should never happen, as placeholders should be resolved before this runs + + def visit_and(self, node: ast.And) -> ast.Expr: + exprs = [self.visit(expr) for expr in node.exprs] + + flattened = [] + for expr in exprs: + if isinstance(expr, ast.And): + flattened.extend(expr.exprs) + else: + flattened.append(expr) + + if any(isinstance(expr, ast.Constant) and expr.value is False for expr in flattened): + return ast.Constant(value=False) + + filtered = [expr for expr in flattened if not isinstance(expr, ast.Constant) or expr.value is not True] + if len(filtered) == 0: + return ast.Constant(value=True) + elif len(filtered) == 1: + return filtered[0] + else: + return ast.And(exprs=filtered) + + def visit_or(self, node: ast.Or) -> ast.Expr: + exprs = [self.visit(expr) for expr in node.exprs] + + flattened = [] + for expr in exprs: + if isinstance(expr, ast.Or): + flattened.extend(expr.exprs) + else: + flattened.append(expr) + + if any(isinstance(expr, ast.Constant) and expr.value is True for expr in flattened): + return ast.Constant(value=True) + + filtered = [expr for expr in flattened if not isinstance(expr, ast.Constant) or expr.value is not False] + if len(filtered) == 0: + return ast.Constant(value=False) + elif len(filtered) == 1: + return filtered[0] + else: + return ast.Or(exprs=filtered) + + def visit_alias(self, node: ast.Alias) -> ast.Expr: + return self.visit(node.expr) + + +def is_time_or_interval_constant(expr: ast.Expr) -> bool: + return IsTimeOrIntervalConstantVisitor().visit(expr) + + +class IsTimeOrIntervalConstantVisitor(Visitor[bool]): + def visit_constant(self, node: ast.Constant) -> bool: + return True + + def visit_compare_operation(self, node: ast.CompareOperation) -> bool: + return self.visit(node.left) and self.visit(node.right) + + def visit_arithmetic_operation(self, node: ast.ArithmeticOperation) -> bool: + return self.visit(node.left) and self.visit(node.right) + + def visit_call(self, node: ast.Call) -> bool: + # some functions just return a constant + if node.name in ["today", "now"]: + return True + # some functions return a constant if the first argument is a constant + if node.name in [ + "parseDateTime64BestEffortOrNull", + "toDateTime", + "toTimeZone", + "assumeNotNull", + "toIntervalYear", + "toIntervalMonth", + "toIntervalWeek", + "toIntervalDay", + "toIntervalHour", + "toIntervalMinute", + "toIntervalSecond", + "toStartOfDay", + "toStartOfWeek", + "toStartOfMonth", + "toStartOfQuarter", + "toStartOfYear", + ]: + return self.visit(node.args[0]) + + if node.name in ["minus", "add"]: + return all(self.visit(arg) for arg in node.args) + + # otherwise we don't know, so return False + return False + + def visit_field(self, node: ast.Field) -> bool: + return False + + def visit_and(self, node: ast.And) -> bool: + return False + + def visit_or(self, node: ast.Or) -> bool: + return False + + def visit_not(self, node: ast.Not) -> bool: + return False + + def visit_placeholder(self, node: ast.Placeholder) -> bool: + raise Exception() + + def visit_alias(self, node: ast.Alias) -> bool: + return self.visit(node.expr) + + +def is_simple_timestamp_field_expression(expr: ast.Expr, context: HogQLContext) -> bool: + return IsSimpleTimestampFieldExpressionVisitor(context).visit(expr) + + +@dataclass +class IsSimpleTimestampFieldExpressionVisitor(Visitor[bool]): + context: HogQLContext + + def visit_constant(self, node: ast.Constant) -> bool: + return False + + def visit_field(self, node: ast.Field) -> bool: + if node.type and isinstance(node.type, ast.FieldType): + resolved_field = node.type.resolve_database_field(self.context) + if resolved_field and isinstance(resolved_field, DatabaseField) and resolved_field: + return resolved_field.name in ["min_timestamp", "timestamp"] + # no type information, so just use the name of the field + return node.chain[-1] in ["min_timestamp", "timestamp"] + + def visit_arithmetic_operation(self, node: ast.ArithmeticOperation) -> bool: + # only allow the min_timestamp field to be used on one side of the arithmetic operation + return ( + self.visit(node.left) + and is_time_or_interval_constant(node.right) + or (self.visit(node.right) and is_time_or_interval_constant(node.left)) + ) + + def visit_call(self, node: ast.Call) -> bool: + # some functions count as a timestamp field expression if their first argument is + if node.name in [ + "parseDateTime64BestEffortOrNull", + "toDateTime", + "toTimeZone", + "assumeNotNull", + "toStartOfDay", + "toStartOfWeek", + "toStartOfMonth", + "toStartOfQuarter", + "toStartOfYear", + ]: + return self.visit(node.args[0]) + + if node.name in ["minus", "add"]: + return self.visit_arithmetic_operation( + ast.ArithmeticOperation( + op=ArithmeticOperationOp.Sub if node.name == "minus" else ArithmeticOperationOp.Add, + left=node.args[0], + right=node.args[1], + ) + ) + + # otherwise we don't know, so return False + return False + + def visit_compare_operation(self, node: ast.CompareOperation) -> bool: + return False + + def visit_and(self, node: ast.And) -> bool: + return False + + def visit_or(self, node: ast.Or) -> bool: + return False + + def visit_not(self, node: ast.Not) -> bool: + return False + + def visit_placeholder(self, node: ast.Placeholder) -> bool: + raise Exception() + + def visit_alias(self, node: ast.Alias) -> bool: + from posthog.hogql.database.schema.events import EventsTable + from posthog.hogql.database.schema.sessions import SessionsTable + + if node.type and isinstance(node.type, ast.FieldAliasType): + resolved_field = node.type.resolve_database_field(self.context) + table_type = node.type.resolve_table_type(self.context) + if not table_type: + return False + return ( + isinstance(table_type, ast.TableType) + and isinstance(table_type.table, EventsTable) + and resolved_field.name == "timestamp" + ) or ( + isinstance(table_type, ast.LazyTableType) + and isinstance(table_type.table, SessionsTable) + and resolved_field.name == "min_timestamp" + ) + + return self.visit(node.expr) + + +def rewrite_timestamp_field(expr: ast.Expr, context: HogQLContext) -> ast.Expr: + return RewriteTimestampFieldVisitor(context).visit(expr) + + +class RewriteTimestampFieldVisitor(CloningVisitor): + context: HogQLContext + + def __init__(self, context: HogQLContext, *args, **kwargs): + super().__init__(*args, **kwargs) + self.context = context + + def visit_field(self, node: ast.Field) -> ast.Field: + from posthog.hogql.database.schema.events import EventsTable + from posthog.hogql.database.schema.sessions import SessionsTable + + if node.type and isinstance(node.type, ast.FieldType): + resolved_field = node.type.resolve_database_field(self.context) + table = node.type.resolve_table_type(self.context).table + if resolved_field and isinstance(resolved_field, DatabaseField): + if (isinstance(table, EventsTable) and resolved_field.name == "timestamp") or ( + isinstance(table, SessionsTable) and resolved_field.name == "min_timestamp" + ): + return ast.Field(chain=["raw_sessions", "min_timestamp"]) + # no type information, so just use the name of the field + if node.chain[-1] in ["min_timestamp", "timestamp"]: + return ast.Field(chain=["raw_sessions", "min_timestamp"]) + return node + + def visit_alias(self, node: ast.Alias) -> ast.Expr: + return self.visit(node.expr) diff --git a/posthog/hogql/database/schema/util/test/test_session_where_clause_extractor.py b/posthog/hogql/database/schema/util/test/test_session_where_clause_extractor.py new file mode 100644 index 0000000000000..bc5324e739ad9 --- /dev/null +++ b/posthog/hogql/database/schema/util/test/test_session_where_clause_extractor.py @@ -0,0 +1,284 @@ +from typing import Union, Optional, Dict + +from posthog.hogql import ast +from posthog.hogql.context import HogQLContext +from posthog.hogql.database.schema.util.session_where_clause_extractor import SessionMinTimestampWhereClauseExtractor +from posthog.hogql.modifiers import create_default_modifiers_for_team +from posthog.hogql.parser import parse_select, parse_expr +from posthog.hogql.printer import prepare_ast_for_printing, print_prepared_ast +from posthog.hogql.visitor import clone_expr +from posthog.test.base import ClickhouseTestMixin, APIBaseTest + + +def f(s: Union[str, ast.Expr, None], placeholders: Optional[dict[str, ast.Expr]] = None) -> Union[ast.Expr, None]: + if s is None: + return None + if isinstance(s, str): + expr = parse_expr(s, placeholders=placeholders) + else: + expr = s + return clone_expr(expr, clear_types=True, clear_locations=True) + + +def parse( + s: str, + placeholders: Optional[Dict[str, ast.Expr]] = None, +) -> ast.SelectQuery: + parsed = parse_select(s, placeholders=placeholders) + assert isinstance(parsed, ast.SelectQuery) + return parsed + + +class TestSessionTimestampInliner(ClickhouseTestMixin, APIBaseTest): + @property + def inliner(self): + team = self.team + modifiers = create_default_modifiers_for_team(team) + context = HogQLContext( + team_id=team.pk, + team=team, + enable_select_queries=True, + modifiers=modifiers, + ) + return SessionMinTimestampWhereClauseExtractor(context) + + def test_handles_select_with_no_where_claus(self): + inner_where = self.inliner.get_inner_where(parse("SELECT * FROM sessions")) + assert inner_where is None + + def test_handles_select_with_eq(self): + actual = f(self.inliner.get_inner_where(parse("SELECT * FROM sessions WHERE min_timestamp = '2021-01-01'"))) + expected = f( + "((raw_sessions.min_timestamp - toIntervalDay(3)) <= '2021-01-01') AND ((raw_sessions.min_timestamp + toIntervalDay(3)) >= '2021-01-01')" + ) + assert expected == actual + + def test_handles_select_with_eq_flipped(self): + actual = f(self.inliner.get_inner_where(parse("SELECT * FROM sessions WHERE '2021-01-01' = min_timestamp"))) + expected = f( + "((raw_sessions.min_timestamp - toIntervalDay(3)) <= '2021-01-01') AND ((raw_sessions.min_timestamp + toIntervalDay(3)) >= '2021-01-01')" + ) + assert expected == actual + + def test_handles_select_with_simple_gt(self): + actual = f(self.inliner.get_inner_where(parse("SELECT * FROM sessions WHERE min_timestamp > '2021-01-01'"))) + expected = f("((raw_sessions.min_timestamp + toIntervalDay(3)) >= '2021-01-01')") + assert expected == actual + + def test_handles_select_with_simple_gte(self): + actual = f(self.inliner.get_inner_where(parse("SELECT * FROM sessions WHERE min_timestamp >= '2021-01-01'"))) + expected = f("((raw_sessions.min_timestamp + toIntervalDay(3)) >= '2021-01-01')") + assert expected == actual + + def test_handles_select_with_simple_lt(self): + actual = f(self.inliner.get_inner_where(parse("SELECT * FROM sessions WHERE min_timestamp < '2021-01-01'"))) + expected = f("((raw_sessions.min_timestamp - toIntervalDay(3)) <= '2021-01-01')") + assert expected == actual + + def test_handles_select_with_simple_lte(self): + actual = f(self.inliner.get_inner_where(parse("SELECT * FROM sessions WHERE min_timestamp <= '2021-01-01'"))) + expected = f("((raw_sessions.min_timestamp - toIntervalDay(3)) <= '2021-01-01')") + assert expected == actual + + def test_select_with_placeholder(self): + actual = f( + self.inliner.get_inner_where( + parse( + "SELECT * FROM sessions WHERE min_timestamp > {timestamp}", + placeholders={"timestamp": ast.Constant(value="2021-01-01")}, + ) + ) + ) + expected = f("((raw_sessions.min_timestamp + toIntervalDay(3)) >= '2021-01-01')") + assert expected == actual + + def test_unrelated_equals(self): + actual = self.inliner.get_inner_where( + parse("SELECT * FROM sessions WHERE initial_utm_campaign = initial_utm_source") + ) + assert actual is None + + def test_timestamp_and(self): + actual = f( + self.inliner.get_inner_where( + parse("SELECT * FROM sessions WHERE and(min_timestamp >= '2021-01-01', min_timestamp <= '2021-01-03')") + ) + ) + expected = f( + "((raw_sessions.min_timestamp + toIntervalDay(3)) >= '2021-01-01') AND ((raw_sessions.min_timestamp - toIntervalDay(3)) <= '2021-01-03')" + ) + assert expected == actual + + def test_timestamp_or(self): + actual = f( + self.inliner.get_inner_where( + parse("SELECT * FROM sessions WHERE and(min_timestamp <= '2021-01-01', min_timestamp >= '2021-01-03')") + ) + ) + expected = f( + "((raw_sessions.min_timestamp - toIntervalDay(3)) <= '2021-01-01') AND ((raw_sessions.min_timestamp + toIntervalDay(3)) >= '2021-01-03')" + ) + assert expected == actual + + def test_unrelated_function(self): + actual = f(self.inliner.get_inner_where(parse("SELECT * FROM sessions WHERE like('a', 'b')"))) + assert actual is None + + def test_timestamp_unrelated_function(self): + actual = f( + self.inliner.get_inner_where(parse("SELECT * FROM sessions WHERE like(toString(min_timestamp), 'b')")) + ) + assert actual is None + + def test_timestamp_unrelated_function_timestamp(self): + actual = f( + self.inliner.get_inner_where(parse("SELECT * FROM sessions WHERE like(toString(min_timestamp), 'b')")) + ) + assert actual is None + + def test_ambiguous_or(self): + actual = f( + self.inliner.get_inner_where( + parse( + "SELECT * FROM sessions WHERE or(min_timestamp > '2021-01-03', like(toString(min_timestamp), 'b'))" + ) + ) + ) + assert actual is None + + def test_ambiguous_and(self): + actual = f( + self.inliner.get_inner_where( + parse( + "SELECT * FROM sessions WHERE and(min_timestamp > '2021-01-03', like(toString(min_timestamp), 'b'))" + ) + ) + ) + assert actual == f("(raw_sessions.min_timestamp + toIntervalDay(3)) >= '2021-01-03'") + + def test_join(self): + actual = f( + self.inliner.get_inner_where( + parse( + "SELECT * FROM events JOIN sessions ON events.session_id = raw_sessions.session_id WHERE min_timestamp > '2021-01-03'" + ) + ) + ) + expected = f("((raw_sessions.min_timestamp + toIntervalDay(3)) >= '2021-01-03')") + assert expected == actual + + def test_join_using_events_timestamp_filter(self): + actual = f( + self.inliner.get_inner_where( + parse( + "SELECT * FROM events JOIN sessions ON events.session_id = raw_sessions.session_id WHERE timestamp > '2021-01-03'" + ) + ) + ) + expected = f("((raw_sessions.min_timestamp + toIntervalDay(3)) >= '2021-01-03')") + assert expected == actual + + def test_minus(self): + actual = f(self.inliner.get_inner_where(parse("SELECT * FROM sessions WHERE min_timestamp >= today() - 2"))) + expected = f("((raw_sessions.min_timestamp + toIntervalDay(3)) >= (today() - 2))") + assert expected == actual + + def test_minus_function(self): + actual = f( + self.inliner.get_inner_where(parse("SELECT * FROM sessions WHERE min_timestamp >= minus(today() , 2)")) + ) + expected = f("((raw_sessions.min_timestamp + toIntervalDay(3)) >= minus(today(), 2))") + assert expected == actual + + def test_real_example(self): + actual = f( + self.inliner.get_inner_where( + parse( + "SELECT * FROM events JOIN sessions ON events.session_id = raw_sessions.session_id WHERE event = '$pageview' AND toTimeZone(timestamp, 'US/Pacific') >= toDateTime('2024-03-12 00:00:00', 'US/Pacific') AND toTimeZone(timestamp, 'US/Pacific') <= toDateTime('2024-03-19 23:59:59', 'US/Pacific')" + ) + ) + ) + expected = f( + "(toTimeZone(raw_sessions.min_timestamp, 'US/Pacific') + toIntervalDay(3)) >= toDateTime('2024-03-12 00:00:00', 'US/Pacific') AND (toTimeZone(raw_sessions.min_timestamp, 'US/Pacific') - toIntervalDay(3)) <= toDateTime('2024-03-19 23:59:59', 'US/Pacific') " + ) + assert expected == actual + + def test_collapse_and(self): + actual = f( + self.inliner.get_inner_where( + parse( + "SELECT * FROM sesions WHERE event = '$pageview' AND (TRUE AND (TRUE AND TRUE AND (timestamp >= '2024-03-12' AND TRUE)))" + ) + ) + ) + expected = f("(raw_sessions.min_timestamp + toIntervalDay(3)) >= '2024-03-12'") + assert expected == actual + + +class TestSessionsQueriesHogQLToClickhouse(ClickhouseTestMixin, APIBaseTest): + def print_query(self, query: str) -> str: + team = self.team + modifiers = create_default_modifiers_for_team(team) + context = HogQLContext( + team_id=team.pk, + team=team, + enable_select_queries=True, + modifiers=modifiers, + ) + prepared_ast = prepare_ast_for_printing(node=parse(query), context=context, dialect="clickhouse") + pretty = print_prepared_ast(prepared_ast, context=context, dialect="clickhouse", pretty=True) + return pretty + + def test_select_with_timestamp(self): + actual = self.print_query("SELECT session_id FROM sessions WHERE min_timestamp > '2021-01-01'") + expected = f"""SELECT + sessions.session_id AS session_id +FROM + (SELECT + sessions.session_id AS session_id, + min(sessions.min_timestamp) AS min_timestamp + FROM + sessions + WHERE + and(equals(sessions.team_id, {self.team.id}), ifNull(greaterOrEquals(plus(toTimeZone(sessions.min_timestamp, %(hogql_val_0)s), toIntervalDay(3)), %(hogql_val_1)s), 0)) + GROUP BY + sessions.session_id, + sessions.session_id) AS sessions +WHERE + ifNull(greater(toTimeZone(sessions.min_timestamp, %(hogql_val_2)s), %(hogql_val_3)s), 0) +LIMIT 10000""" + assert expected == actual + + def test_join_with_events(self): + actual = self.print_query( + """ +SELECT + sessions.session_id, + uniq(uuid) +FROM events +JOIN sessions +ON events.$session_id = sessions.session_id +WHERE events.timestamp > '2021-01-01' +GROUP BY sessions.session_id +""" + ) + expected = f"""SELECT + sessions.session_id AS session_id, + uniq(events.uuid) +FROM + events + JOIN (SELECT + sessions.session_id AS session_id + FROM + sessions + WHERE + and(equals(sessions.team_id, {self.team.id}), ifNull(greaterOrEquals(plus(toTimeZone(sessions.min_timestamp, %(hogql_val_0)s), toIntervalDay(3)), %(hogql_val_1)s), 0)) + GROUP BY + sessions.session_id, + sessions.session_id) AS sessions ON equals(events.`$session_id`, sessions.session_id) +WHERE + and(equals(events.team_id, {self.team.id}), greater(toTimeZone(events.timestamp, %(hogql_val_2)s), %(hogql_val_3)s)) +GROUP BY + sessions.session_id +LIMIT 10000""" + assert expected == actual diff --git a/posthog/hogql/test/test_bytecode.py b/posthog/hogql/test/test_bytecode.py index cf0b8113b574d..f7d810700e74a 100644 --- a/posthog/hogql/test/test_bytecode.py +++ b/posthog/hogql/test/test_bytecode.py @@ -130,7 +130,7 @@ def test_bytecode_create(self): def test_bytecode_create_error(self): with self.assertRaises(NotImplementedException) as e: to_bytecode("(select 1)") - self.assertEqual(str(e.exception), "Visitor has no method visit_select_query") + self.assertEqual(str(e.exception), "BytecodeBuilder has no method visit_select_query") with self.assertRaises(NotImplementedException) as e: to_bytecode("1 in cohort 2") diff --git a/posthog/hogql/test/test_visitor.py b/posthog/hogql/test/test_visitor.py index 8aa6689328fbf..a01193f788d5f 100644 --- a/posthog/hogql/test/test_visitor.py +++ b/posthog/hogql/test/test_visitor.py @@ -125,7 +125,7 @@ def visit_arithmetic_operation(self, node: ast.ArithmeticOperation): with self.assertRaises(HogQLException) as e: UnknownNotDefinedVisitor().visit(parse_expr("1 + 3 / 'asd2'")) - self.assertEqual(str(e.exception), "Visitor has no method visit_constant") + self.assertEqual(str(e.exception), "UnknownNotDefinedVisitor has no method visit_constant") def test_hogql_exception_start_end(self): class EternalVisitor(TraversingVisitor): diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index bdbb322d54397..df8ce6962259c 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -309,7 +309,7 @@ def create_override(table_name: str, field_chain: List[str | int]) -> None: # For all the collected tables, create the subqueries, and add them to the table. for table_name, table_to_add in tables_to_add.items(): - subquery = table_to_add.lazy_table.lazy_select(table_to_add.fields_accessed, self.context.modifiers) + subquery = table_to_add.lazy_table.lazy_select(table_to_add.fields_accessed, self.context, node=node) subquery = cast(ast.SelectQuery, clone_expr(subquery, clear_locations=True)) subquery = cast(ast.SelectQuery, resolve_types(subquery, self.context, self.dialect, [node.type])) old_table_type = select_type.tables[table_name] diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index c11856169297f..2bf968abf2ab0 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, TypeVar, Generic, Any from posthog.hogql import ast from posthog.hogql.base import AST, Expr @@ -14,8 +14,11 @@ def clear_locations(expr: Expr) -> Expr: return CloningVisitor(clear_locations=True).visit(expr) -class Visitor(object): - def visit(self, node: AST): +T = TypeVar("T") + + +class Visitor(Generic[T]): + def visit(self, node: AST) -> T: if node is None: return node @@ -28,7 +31,7 @@ def visit(self, node: AST): raise e -class TraversingVisitor(Visitor): +class TraversingVisitor(Visitor[None]): """Visitor that traverses the AST tree without returning anything""" def visit_expr(self, node: Expr): @@ -258,7 +261,7 @@ def visit_hogqlx_attribute(self, node: ast.HogQLXAttribute): self.visit(node.value) -class CloningVisitor(Visitor): +class CloningVisitor(Visitor[Any]): """Visitor that traverses and clones the AST tree. Clears types.""" def __init__( From 5ae6b301208b766523dc22d33f3938f1b70eb890 Mon Sep 17 00:00:00 2001 From: Eric Duong Date: Fri, 22 Mar 2024 04:39:20 -0400 Subject: [PATCH 33/51] chore(data-warehouse): join UI cleanup (#21085) * add loading indicator * add custom settings * update sizing --- .../scenes/data-warehouse/ViewLinkModal.tsx | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx b/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx index 2116d2da6e74d..11f50ca4f0d27 100644 --- a/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx +++ b/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx @@ -1,6 +1,6 @@ import './ViewLinkModal.scss' -import { IconTrash } from '@posthog/icons' +import { IconCollapse, IconExpand, IconTrash } from '@posthog/icons' import { LemonButton, LemonDivider, @@ -35,7 +35,7 @@ export function ViewLinkModal(): JSX.Element { } isOpen={isJoinTableModalOpen} onClose={toggleJoinTableModal} - width={600} + width={700} > @@ -57,6 +57,7 @@ export function ViewLinkForm(): JSX.Element { selectedJoiningKey, sourceIsUsingHogQLExpression, joiningIsUsingHogQLExpression, + isViewLinkSubmitting, } = useValues(viewLinkLogic) const { selectJoiningTable, @@ -66,12 +67,13 @@ export function ViewLinkForm(): JSX.Element { selectSourceKey, selectJoiningKey, } = useActions(viewLinkLogic) + const [advancedSettingsExpanded, setAdvancedSettingsExpanded] = useState(false) return (
-
+
Source Table {isNewJoin ? ( @@ -86,7 +88,7 @@ export function ViewLinkForm(): JSX.Element { selectedSourceTableName ?? '' )}
-
+
Joining Table
-
-
+
+
Source Table Key <> @@ -124,7 +126,7 @@ export function ViewLinkForm(): JSX.Element {
-
+
Joining Table Key <> @@ -148,8 +150,22 @@ export function ViewLinkForm(): JSX.Element {
{sqlCodeSnippet && ( - <> +
+ setAdvancedSettingsExpanded(!advancedSettingsExpanded)} + sideIcon={advancedSettingsExpanded ? : } + > +
+

Advanced settings

+
Customize how the fields are accessed
+
+
+
+ )} + {sqlCodeSnippet && advancedSettingsExpanded && ( + <>
Field Name @@ -185,7 +201,7 @@ export function ViewLinkForm(): JSX.Element { Close - + Save
From 5f4cb5486187e4167b53b1131e2272c7fcf9aae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 22 Mar 2024 09:58:51 +0100 Subject: [PATCH 34/51] fix: Track last updated at for batch export run (#21082) --- posthog/batch_exports/service.py | 16 +++++++++++++--- posthog/temporal/batch_exports/batch_exports.py | 4 ++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/posthog/batch_exports/service.py b/posthog/batch_exports/service.py index c26be9a77ed1a..b00f0f4c98c69 100644 --- a/posthog/batch_exports/service.py +++ b/posthog/batch_exports/service.py @@ -439,8 +439,11 @@ def create_batch_export_run( return run -def update_batch_export_run_status( - run_id: UUID, status: str, latest_error: str | None, records_completed: int = 0 +def update_batch_export_run( + run_id: UUID, + status: str, + latest_error: str | None, + records_completed: int = 0, ) -> BatchExportRun: """Update the status of an BatchExportRun with given id. @@ -448,7 +451,14 @@ def update_batch_export_run_status( id: The id of the BatchExportRun to update. """ model = BatchExportRun.objects.filter(id=run_id) - updated = model.update(status=status, latest_error=latest_error, records_completed=records_completed) + update_at = dt.datetime.now() + + updated = model.update( + status=status, + latest_error=latest_error, + records_completed=records_completed, + last_updated_at=update_at, + ) if not updated: raise ValueError(f"BatchExportRun with id {run_id} not found.") diff --git a/posthog/temporal/batch_exports/batch_exports.py b/posthog/temporal/batch_exports/batch_exports.py index c776e1f245ef3..c40950c654426 100644 --- a/posthog/temporal/batch_exports/batch_exports.py +++ b/posthog/temporal/batch_exports/batch_exports.py @@ -22,7 +22,7 @@ create_batch_export_backfill, create_batch_export_run, update_batch_export_backfill_status, - update_batch_export_run_status, + update_batch_export_run, ) from posthog.temporal.batch_exports.metrics import ( get_export_finished_metric, @@ -542,7 +542,7 @@ async def update_export_run_status(inputs: UpdateBatchExportRunStatusInputs) -> """Activity that updates the status of an BatchExportRun.""" logger = await bind_temporal_worker_logger(team_id=inputs.team_id) - batch_export_run = await sync_to_async(update_batch_export_run_status)( + batch_export_run = await sync_to_async(update_batch_export_run)( run_id=uuid.UUID(inputs.id), status=inputs.status, latest_error=inputs.latest_error, From 6b6d40a66607adc55f461b05de4c4057255f5c59 Mon Sep 17 00:00:00 2001 From: David Newell Date: Fri, 22 Mar 2024 10:20:37 +0000 Subject: [PATCH 35/51] feat: sparkline errors (#21081) * feat: errors page playlist link * feat: create playlist from errors * remove sample data * update title * add frontend protection to samples * feat: sparkline errors --- ee/session_recordings/ai/error_clustering.py | 15 ++++++++++++--- .../errors/SessionRecordingErrors.tsx | 12 ++++++++++++ frontend/src/types.ts | 1 + 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/ee/session_recordings/ai/error_clustering.py b/ee/session_recordings/ai/error_clustering.py index edc06ff471355..7a3c12c44dec0 100644 --- a/ee/session_recordings/ai/error_clustering.py +++ b/ee/session_recordings/ai/error_clustering.py @@ -6,6 +6,7 @@ import pandas as pd import numpy as np from posthog.session_recordings.models.session_recording_event import SessionRecordingViewed +from datetime import date CLUSTER_REPLAY_ERRORS_TIMING = Histogram( "posthog_session_recordings_cluster_replay_errors", @@ -30,7 +31,7 @@ def error_clustering(team: Team, user: User): if not results: return [] - df = pd.DataFrame(results, columns=["session_id", "input", "embeddings"]) + df = pd.DataFrame(results, columns=["session_id", "error", "embeddings", "timestamp"]) df["cluster"] = cluster_embeddings(df["embeddings"].tolist()) @@ -42,7 +43,7 @@ def error_clustering(team: Team, user: User): def fetch_error_embeddings(team_id: int): query = """ SELECT - session_id, input, embeddings + session_id, input, embeddings, generation_timestamp FROM session_replay_embeddings WHERE @@ -76,13 +77,21 @@ def construct_response(df: pd.DataFrame, team: Team, user: User): clusters = [] for cluster, rows in df.groupby("cluster"): session_ids = rows["session_id"].unique() - sample = rows.sample(n=1)[["session_id", "input"]].rename(columns={"input": "error"}).to_dict("records")[0] + sample = rows.sample(n=1)[["session_id", "error"]].to_dict("records")[0] + + date_series = ( + df.groupby([df["timestamp"].dt.date]) + .size() + .reindex(pd.date_range(end=date.today(), periods=7), fill_value=0) + ) + sparkline = dict(zip(date_series.index.astype(str), date_series)) clusters.append( { "cluster": cluster, "sample": sample.get("error"), "session_ids": np.random.choice(session_ids, size=DBSCAN_MIN_SAMPLES - 1), "occurrences": rows.size, + "sparkline": sparkline, "unique_sessions": len(session_ids), "viewed": len(np.intersect1d(session_ids, viewed_session_ids, assume_unique=True)), } diff --git a/frontend/src/scenes/session-recordings/errors/SessionRecordingErrors.tsx b/frontend/src/scenes/session-recordings/errors/SessionRecordingErrors.tsx index 4b2dd2d1abed3..8b73fbcc1f924 100644 --- a/frontend/src/scenes/session-recordings/errors/SessionRecordingErrors.tsx +++ b/frontend/src/scenes/session-recordings/errors/SessionRecordingErrors.tsx @@ -2,6 +2,7 @@ import { IconFeatures } from '@posthog/icons' import { LemonButton, LemonTable, LemonTabs, Spinner } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { JSONViewer } from 'lib/components/JSONViewer' +import { Sparkline } from 'lib/lemon-ui/Sparkline' import { useState } from 'react' import { urls } from 'scenes/urls' @@ -45,6 +46,17 @@ export function SessionRecordingErrors(): JSX.Element { }, width: '50%', }, + { + title: '', + render: (_, cluster) => { + return ( + + ) + }, + }, { title: 'Occurrences', dataIndex: 'occurrences', diff --git a/frontend/src/types.ts b/frontend/src/types.ts index a7f65e84c5473..be96494253ae8 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -906,6 +906,7 @@ export type ErrorCluster = { sample: string occurrences: number session_ids: string[] + sparkline: Record unique_sessions: number viewed: number } From 3a995a5f9ecad0b1463d6e2f9818538946175d64 Mon Sep 17 00:00:00 2001 From: PostHog Bot <69588470+posthog-bot@users.noreply.github.com> Date: Fri, 22 Mar 2024 07:11:01 -0400 Subject: [PATCH 36/51] chore(deps): Update posthog-js to 1.116.4 (#21098) --- package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 79946a087fbf4..2128fa3207a76 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "pmtiles": "^2.11.0", "postcss": "^8.4.31", "postcss-preset-env": "^9.3.0", - "posthog-js": "1.116.3", + "posthog-js": "1.116.4", "posthog-js-lite": "2.5.0", "prettier": "^2.8.8", "prop-types": "^15.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7fc1d7cbd22a4..04806adc67be8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -254,8 +254,8 @@ dependencies: specifier: ^9.3.0 version: 9.3.0(postcss@8.4.31) posthog-js: - specifier: 1.116.3 - version: 1.116.3 + specifier: 1.116.4 + version: 1.116.4 posthog-js-lite: specifier: 2.5.0 version: 2.5.0 @@ -6796,7 +6796,7 @@ packages: '@storybook/csf': 0.1.3 '@storybook/global': 5.0.0 '@storybook/types': 7.6.17 - '@types/qs': 6.9.13 + '@types/qs': 6.9.14 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 @@ -8200,8 +8200,8 @@ packages: resolution: {integrity: sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==} dev: false - /@types/qs@6.9.13: - resolution: {integrity: sha512-iLR+1vTTJ3p0QaOUq6ACbY1mzKTODFDT/XedZI8BksOotFmL4ForwDfRQ/DZeuTHR7/2i4lI1D203gdfxuqTlA==} + /@types/qs@6.9.14: + resolution: {integrity: sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==} dev: true /@types/query-selector-shadow-dom@1.0.0: @@ -17454,8 +17454,8 @@ packages: resolution: {integrity: sha512-Urvlp0Vu9h3td0BVFWt0QXFJDoOZcaAD83XM9d91NKMKTVPZtfU0ysoxstIf5mw/ce9ZfuMgpWPaagrZI4rmSg==} dev: false - /posthog-js@1.116.3: - resolution: {integrity: sha512-KakGsQ8rS/K/U5Q/tiBrRrFRCgGrR0oI9VSYw9hwNCY00EClwAU3EuykUuQTFdQ1EuYMrZDIMWDD4NW6zgf7wQ==} + /posthog-js@1.116.4: + resolution: {integrity: sha512-PZg208/k5OZRQbd9tnGvUgtyRl1IAYyyh74teyIDIH3EnlsAolBlVM4gcoyEYoVkUi5sZLKitj9gTX3/vnEG4Q==} dependencies: fflate: 0.4.8 preact: 10.20.0 From d5cc9e43b9e940faa856be6504932569f8f8e1f8 Mon Sep 17 00:00:00 2001 From: Ben White Date: Fri, 22 Mar 2024 12:28:09 +0100 Subject: [PATCH 37/51] fix: Toolbar better auth issue (#21096) --- ...s-other-toolbar--unauthenticated--dark.png | Bin 10791 -> 12371 bytes ...-other-toolbar--unauthenticated--light.png | Bin 12070 -> 13639 bytes frontend/src/toolbar/bar/Toolbar.scss | 4 - frontend/src/toolbar/bar/Toolbar.tsx | 39 ++++++-- frontend/src/toolbar/bar/ToolbarButton.scss | 11 +++ frontend/src/toolbar/bar/ToolbarButton.tsx | 13 ++- .../src/toolbar/flags/flagsToolbarLogic.ts | 5 - frontend/src/toolbar/toolbarConfigLogic.ts | 6 +- frontend/src/toolbar/toolbarLogic.ts | 87 ------------------ 9 files changed, 53 insertions(+), 112 deletions(-) delete mode 100644 frontend/src/toolbar/toolbarLogic.ts diff --git a/frontend/__snapshots__/scenes-other-toolbar--unauthenticated--dark.png b/frontend/__snapshots__/scenes-other-toolbar--unauthenticated--dark.png index f5283e47bdf438cc2f95a77a1b24ccc90adc38b9..dc149beabe2037e498026227fe248d0fa4cac62c 100644 GIT binary patch literal 12371 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 diff --git a/frontend/__snapshots__/scenes-other-toolbar--unauthenticated--light.png b/frontend/__snapshots__/scenes-other-toolbar--unauthenticated--light.png index 15b96030b8a254d62484aa97934a3972fea6d9b8..ace3a2b7f937317a2e0525ef4a80cbf46ae0220b 100644 GIT binary patch literal 13639 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 TPFNd4G#+Mm&hZz(YJU4456HRb literal 12070 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 PwX*r$?l=6eUibb7qA3my diff --git a/frontend/src/toolbar/bar/Toolbar.scss b/frontend/src/toolbar/bar/Toolbar.scss index ff83b7c9d922a..ec98167332fbd 100644 --- a/frontend/src/toolbar/bar/Toolbar.scss +++ b/frontend/src/toolbar/bar/Toolbar.scss @@ -144,8 +144,4 @@ transform: var(--toolbar-translate) scale(0); } } - - &--unauthenticated { - width: calc(5rem + 1px); // Account for border - } } diff --git a/frontend/src/toolbar/bar/Toolbar.tsx b/frontend/src/toolbar/bar/Toolbar.tsx index f255ccc26800d..8ed031f904d86 100644 --- a/frontend/src/toolbar/bar/Toolbar.tsx +++ b/frontend/src/toolbar/bar/Toolbar.tsx @@ -77,15 +77,18 @@ function MoreMenu(): JSX.Element { } maxContentWidth={true} > - } title="More options" /> + + + ) } -export function ToolbarInfoMenu(): JSX.Element { +export function ToolbarInfoMenu(): JSX.Element | null { const ref = useRef(null) const { visibleMenu, isDragging, menuProperties, minimized, isBlurred } = useValues(toolbarLogic) const { setMenu } = useActions(toolbarLogic) + const { isAuthenticated } = useValues(toolbarConfigLogic) const content = minimized ? null : visibleMenu === 'flags' ? ( @@ -102,6 +105,10 @@ export function ToolbarInfoMenu(): JSX.Element { return () => setMenu(null) }, [ref.current]) + if (!isAuthenticated) { + return null + } + return (
} onClick={isAuthenticated ? toggleMinimized : authenticate} title={isAuthenticated ? 'Minimize' : 'Authenticate the PostHog Toolbar'} titleMinimized={isAuthenticated ? 'Expand the toolbar' : 'Authenticate the PostHog Toolbar'} - /> + > + + {isAuthenticated ? ( <> - } menuId="inspect" /> - } menuId="heatmap" /> - } menuId="actions" /> - } menuId="flags" title="Feature flags" /> + + + + + + + + + + + + - ) : null} + ) : ( + + Authenticate + + )}
diff --git a/frontend/src/toolbar/bar/ToolbarButton.scss b/frontend/src/toolbar/bar/ToolbarButton.scss index 0d0bb666fa540..ce480f3fbab35 100644 --- a/frontend/src/toolbar/bar/ToolbarButton.scss +++ b/frontend/src/toolbar/bar/ToolbarButton.scss @@ -15,6 +15,8 @@ width: 2rem; height: 2rem; min-height: var(--lemon-button-height); + margin: 0.25rem; + font-weight: 600; color: var(--muted-alt); appearance: none !important; // Important as this gets overridden by Ant styles... cursor: pointer; @@ -43,4 +45,13 @@ } } } + + &--flex { + flex-grow: 1; + width: auto; + + button { + width: 100%; + } + } } diff --git a/frontend/src/toolbar/bar/ToolbarButton.tsx b/frontend/src/toolbar/bar/ToolbarButton.tsx index add0e5f2580ce..f5dfc755be469 100644 --- a/frontend/src/toolbar/bar/ToolbarButton.tsx +++ b/frontend/src/toolbar/bar/ToolbarButton.tsx @@ -10,17 +10,18 @@ import React from 'react' import { MenuState, toolbarLogic } from './toolbarLogic' export type ToolbarButtonProps = { - icon: React.ReactElement | null + children: React.ReactNode onClick?: () => void title?: string titleMinimized?: JSX.Element | string menuId?: MenuState + flex?: boolean } export const ToolbarButton: FunctionComponent = React.forwardRef< HTMLDivElement, ToolbarButtonProps ->(({ icon, title, onClick, titleMinimized, menuId, ...props }, ref): JSX.Element => { +>(({ children, title, onClick, titleMinimized, menuId, flex, ...props }, ref): JSX.Element => { const { visibleMenu, minimized, isDragging } = useValues(toolbarLogic) const { setVisibleMenu } = useActions(toolbarLogic) @@ -54,9 +55,13 @@ export const ToolbarButton: FunctionComponent = React.forwar } const theButton = ( -
+
) diff --git a/frontend/src/toolbar/flags/flagsToolbarLogic.ts b/frontend/src/toolbar/flags/flagsToolbarLogic.ts index 60c1f568f45a6..e1f41cabca73c 100644 --- a/frontend/src/toolbar/flags/flagsToolbarLogic.ts +++ b/frontend/src/toolbar/flags/flagsToolbarLogic.ts @@ -40,11 +40,6 @@ export const flagsToolbarLogic = kea([ `/api/projects/@current/feature_flags/my_flags${encodeParams(params, '?')}` ) - if (response.status >= 400) { - toolbarConfigLogic.actions.tokenExpired() - return [] - } - breakpoint() if (!response.ok) { return [] diff --git a/frontend/src/toolbar/toolbarConfigLogic.ts b/frontend/src/toolbar/toolbarConfigLogic.ts index 1b4638b8f39f8..853b03bdeea32 100644 --- a/frontend/src/toolbar/toolbarConfigLogic.ts +++ b/frontend/src/toolbar/toolbarConfigLogic.ts @@ -119,10 +119,12 @@ export async function toolbarFetch( }) if (response.status === 403) { const responseData = await response.json() - // Do not try to authenticate if the user has no project access altogether - if (responseData.detail !== "You don't have access to the project.") { + if (responseData.detail === "You don't have access to the project.") { toolbarConfigLogic.actions.authenticate() } } + if (response.status == 401) { + toolbarConfigLogic.actions.tokenExpired() + } return response } diff --git a/frontend/src/toolbar/toolbarLogic.ts b/frontend/src/toolbar/toolbarLogic.ts deleted file mode 100644 index d5183a6734f20..0000000000000 --- a/frontend/src/toolbar/toolbarLogic.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { actions, afterMount, kea, listeners, path, props, reducers, selectors } from 'kea' -import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast' - -import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' -import { posthog } from '~/toolbar/posthog' -import { clearSessionToolbarToken } from '~/toolbar/utils' -import { ToolbarProps } from '~/types' - -import type { toolbarLogicType } from './toolbarLogicType' - -export const toolbarLogic = kea([ - path(['toolbar', 'toolbarLogic']), - props({} as ToolbarProps), - - actions({ - authenticate: true, - logout: true, - tokenExpired: true, - processUserIntent: true, - clearUserIntent: true, - showButton: true, - hideButton: true, - }), - - reducers(({ props }) => ({ - rawApiURL: [props.apiURL as string], - rawJsURL: [(props.jsURL || props.apiURL) as string], - temporaryToken: [props.temporaryToken || null, { logout: () => null, tokenExpired: () => null }], - actionId: [props.actionId || null, { logout: () => null, clearUserIntent: () => null }], - userIntent: [props.userIntent || null, { logout: () => null, clearUserIntent: () => null }], - source: [props.source || null, { logout: () => null }], - buttonVisible: [true, { showButton: () => true, hideButton: () => false, logout: () => false }], - dataAttributes: [props.dataAttributes || []], - posthog: [props.posthog ?? null], - })), - - selectors({ - apiURL: [(s) => [s.rawApiURL], (apiURL) => `${apiURL.endsWith('/') ? apiURL.replace(/\/+$/, '') : apiURL}`], - jsURL: [ - (s) => [s.rawJsURL, s.apiURL], - (rawJsURL, apiUrl) => - `${rawJsURL ? (rawJsURL.endsWith('/') ? rawJsURL.replace(/\/+$/, '') : rawJsURL) : apiUrl}`, - ], - isAuthenticated: [(s) => [s.temporaryToken], (temporaryToken) => !!temporaryToken], - }), - - listeners(({ values, props }) => ({ - authenticate: () => { - posthog.capture('toolbar authenticate', { is_authenticated: values.isAuthenticated }) - const encodedUrl = encodeURIComponent(window.location.href) - window.location.href = `${values.apiURL}/authorize_and_redirect/?redirect=${encodedUrl}` - clearSessionToolbarToken() - }, - logout: () => { - posthog.capture('toolbar logout') - clearSessionToolbarToken() - }, - tokenExpired: () => { - posthog.capture('toolbar token expired') - console.warn('PostHog Toolbar API token expired. Clearing session.') - if (values.source !== 'localstorage') { - lemonToast.error('PostHog Toolbar API token expired.') - } - clearSessionToolbarToken() - }, - processUserIntent: () => { - if (props.userIntent === 'add-action' || props.userIntent === 'edit-action') { - actionsTabLogic.actions.showButtonActions() - // the right view will next be opened in `actionsTabLogic` on `getActionsSuccess` - } - }, - })), - - afterMount(({ props, actions, values }) => { - if (props.instrument) { - const distinctId = props.distinctId - if (distinctId) { - posthog.identify(distinctId, props.userEmail ? { email: props.userEmail } : {}) - } - posthog.optIn() - } - if (props.userIntent) { - actions.processUserIntent() - } - posthog.capture('toolbar loaded', { is_authenticated: values.isAuthenticated }) - }), -]) From 7c0258cc0e5a346736fdf4a67801810a49b8d8d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 22 Mar 2024 14:20:13 +0100 Subject: [PATCH 38/51] refactor: Support for multiple file formats in S3 batch exports (#20979) * refactor: Support for multiple file formats in batch exports * refactor: Prefer composition over inheritance * refactor: More clearly separate writer from temporary file We now should be more explicit about what is the context in which the batch export temporary file is alive. The writer outlives this context, so it can be used by callers to, for example, check how many records were written. * test: More parquet testing * Update query snapshots * fix: Typing * refactor: Move temporary file to new module * test: Add writer classes tests and docstrings * feat: Add new type aliases and docstrings for FlushCallable * refactor: Get rid of ensure close method * fix: Use proper 'none' compression * refactor: Cover all possible file formats with FILE_FORMAT_EXTENSIONS.keys() * test: Also check if bucket name is set to use S3 * feat: Typing and docstring for get_batch_export_writer --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- posthog/batch_exports/service.py | 1 + .../test/__snapshots__/test_in_cohort.ambr | 8 +- .../temporal/batch_exports/batch_exports.py | 201 ------- .../batch_exports/bigquery_batch_export.py | 4 +- .../batch_exports/http_batch_export.py | 6 +- .../batch_exports/postgres_batch_export.py | 4 +- .../temporal/batch_exports/s3_batch_export.py | 203 +++++-- .../batch_exports/snowflake_batch_export.py | 4 +- .../temporal/batch_exports/temporary_file.py | 528 ++++++++++++++++++ .../tests/batch_exports/test_batch_exports.py | 182 ------ .../test_s3_batch_export_workflow.py | 159 +++++- .../batch_exports/test_temporary_file.py | 389 +++++++++++++ 12 files changed, 1238 insertions(+), 451 deletions(-) create mode 100644 posthog/temporal/batch_exports/temporary_file.py create mode 100644 posthog/temporal/tests/batch_exports/test_temporary_file.py diff --git a/posthog/batch_exports/service.py b/posthog/batch_exports/service.py index b00f0f4c98c69..d51dfdb2fbc3c 100644 --- a/posthog/batch_exports/service.py +++ b/posthog/batch_exports/service.py @@ -90,6 +90,7 @@ class S3BatchExportInputs: kms_key_id: str | None = None batch_export_schema: BatchExportSchema | None = None endpoint_url: str | None = None + file_format: str = "JSONLines" @dataclass diff --git a/posthog/hogql/transforms/test/__snapshots__/test_in_cohort.ambr b/posthog/hogql/transforms/test/__snapshots__/test_in_cohort.ambr index 9ff7f8ee0ab49..e0f5ea847110d 100644 --- a/posthog/hogql/transforms/test/__snapshots__/test_in_cohort.ambr +++ b/posthog/hogql/transforms/test/__snapshots__/test_in_cohort.ambr @@ -31,7 +31,7 @@ FROM events LEFT JOIN ( SELECT person_static_cohort.person_id AS cohort_person_id, 1 AS matched, person_static_cohort.cohort_id AS cohort_id FROM person_static_cohort - WHERE and(equals(person_static_cohort.team_id, 420), in(person_static_cohort.cohort_id, [12]))) AS __in_cohort ON equals(__in_cohort.cohort_person_id, events.person_id) + WHERE and(equals(person_static_cohort.team_id, 420), in(person_static_cohort.cohort_id, [11]))) AS __in_cohort ON equals(__in_cohort.cohort_person_id, events.person_id) WHERE and(equals(events.team_id, 420), 1, ifNull(equals(__in_cohort.matched, 1), 0)) LIMIT 100 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -42,7 +42,7 @@ FROM events LEFT JOIN ( SELECT person_id AS cohort_person_id, 1 AS matched, cohort_id FROM static_cohort_people - WHERE in(cohort_id, [12])) AS __in_cohort ON equals(__in_cohort.cohort_person_id, person_id) + WHERE in(cohort_id, [11])) AS __in_cohort ON equals(__in_cohort.cohort_person_id, person_id) WHERE and(1, equals(__in_cohort.matched, 1)) LIMIT 100 ''' @@ -55,7 +55,7 @@ FROM events LEFT JOIN ( SELECT person_static_cohort.person_id AS cohort_person_id, 1 AS matched, person_static_cohort.cohort_id AS cohort_id FROM person_static_cohort - WHERE and(equals(person_static_cohort.team_id, 420), in(person_static_cohort.cohort_id, [13]))) AS __in_cohort ON equals(__in_cohort.cohort_person_id, events.person_id) + WHERE and(equals(person_static_cohort.team_id, 420), in(person_static_cohort.cohort_id, [12]))) AS __in_cohort ON equals(__in_cohort.cohort_person_id, events.person_id) WHERE and(equals(events.team_id, 420), 1, ifNull(equals(__in_cohort.matched, 1), 0)) LIMIT 100 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -66,7 +66,7 @@ FROM events LEFT JOIN ( SELECT person_id AS cohort_person_id, 1 AS matched, cohort_id FROM static_cohort_people - WHERE in(cohort_id, [13])) AS __in_cohort ON equals(__in_cohort.cohort_person_id, person_id) + WHERE in(cohort_id, [12])) AS __in_cohort ON equals(__in_cohort.cohort_person_id, person_id) WHERE and(1, equals(__in_cohort.matched, 1)) LIMIT 100 ''' diff --git a/posthog/temporal/batch_exports/batch_exports.py b/posthog/temporal/batch_exports/batch_exports.py index c40950c654426..88cf9e32f274f 100644 --- a/posthog/temporal/batch_exports/batch_exports.py +++ b/posthog/temporal/batch_exports/batch_exports.py @@ -1,15 +1,10 @@ import collections.abc -import csv import dataclasses import datetime as dt -import gzip -import tempfile import typing import uuid from string import Template -import brotli -import orjson import pyarrow as pa from asgiref.sync import sync_to_async from django.conf import settings @@ -286,202 +281,6 @@ def get_data_interval(interval: str, data_interval_end: str | None) -> tuple[dt. return (data_interval_start_dt, data_interval_end_dt) -def json_dumps_bytes(d) -> bytes: - return orjson.dumps(d, default=str) - - -class BatchExportTemporaryFile: - """A TemporaryFile used to as an intermediate step while exporting data. - - This class does not implement the file-like interface but rather passes any calls - to the underlying tempfile.NamedTemporaryFile. We do override 'write' methods - to allow tracking bytes and records. - """ - - def __init__( - self, - mode: str = "w+b", - buffering=-1, - compression: str | None = None, - encoding: str | None = None, - newline: str | None = None, - suffix: str | None = None, - prefix: str | None = None, - dir: str | None = None, - *, - errors: str | None = None, - ): - self._file = tempfile.NamedTemporaryFile( - mode=mode, - encoding=encoding, - newline=newline, - buffering=buffering, - suffix=suffix, - prefix=prefix, - dir=dir, - errors=errors, - ) - self.compression = compression - self.bytes_total = 0 - self.records_total = 0 - self.bytes_since_last_reset = 0 - self.records_since_last_reset = 0 - self._brotli_compressor = None - - def __getattr__(self, name): - """Pass get attr to underlying tempfile.NamedTemporaryFile.""" - return self._file.__getattr__(name) - - def __enter__(self): - """Context-manager protocol enter method.""" - self._file.__enter__() - return self - - def __exit__(self, exc, value, tb): - """Context-manager protocol exit method.""" - return self._file.__exit__(exc, value, tb) - - def __iter__(self): - yield from self._file - - @property - def brotli_compressor(self): - if self._brotli_compressor is None: - self._brotli_compressor = brotli.Compressor() - return self._brotli_compressor - - def compress(self, content: bytes | str) -> bytes: - if isinstance(content, str): - encoded = content.encode("utf-8") - else: - encoded = content - - match self.compression: - case "gzip": - return gzip.compress(encoded) - case "brotli": - self.brotli_compressor.process(encoded) - return self.brotli_compressor.flush() - case None: - return encoded - case _: - raise ValueError(f"Unsupported compression: '{self.compression}'") - - def write(self, content: bytes | str): - """Write bytes to underlying file keeping track of how many bytes were written.""" - compressed_content = self.compress(content) - - if "b" in self.mode: - result = self._file.write(compressed_content) - else: - result = self._file.write(compressed_content.decode("utf-8")) - - self.bytes_total += result - self.bytes_since_last_reset += result - - return result - - def write_record_as_bytes(self, record: bytes): - result = self.write(record) - - self.records_total += 1 - self.records_since_last_reset += 1 - - return result - - def write_records_to_jsonl(self, records): - """Write records to a temporary file as JSONL.""" - if len(records) == 1: - jsonl_dump = orjson.dumps(records[0], option=orjson.OPT_APPEND_NEWLINE, default=str) - else: - jsonl_dump = b"\n".join(map(json_dumps_bytes, records)) - - result = self.write(jsonl_dump) - - self.records_total += len(records) - self.records_since_last_reset += len(records) - - return result - - def write_records_to_csv( - self, - records, - fieldnames: None | collections.abc.Sequence[str] = None, - extrasaction: typing.Literal["raise", "ignore"] = "ignore", - delimiter: str = ",", - quotechar: str = '"', - escapechar: str | None = "\\", - lineterminator: str = "\n", - quoting=csv.QUOTE_NONE, - ): - """Write records to a temporary file as CSV.""" - if len(records) == 0: - return - - if fieldnames is None: - fieldnames = list(records[0].keys()) - - writer = csv.DictWriter( - self, - fieldnames=fieldnames, - extrasaction=extrasaction, - delimiter=delimiter, - quotechar=quotechar, - escapechar=escapechar, - quoting=quoting, - lineterminator=lineterminator, - ) - writer.writerows(records) - - self.records_total += len(records) - self.records_since_last_reset += len(records) - - def write_records_to_tsv( - self, - records, - fieldnames: None | list[str] = None, - extrasaction: typing.Literal["raise", "ignore"] = "ignore", - quotechar: str = '"', - escapechar: str | None = "\\", - lineterminator: str = "\n", - quoting=csv.QUOTE_NONE, - ): - """Write records to a temporary file as TSV.""" - return self.write_records_to_csv( - records, - fieldnames=fieldnames, - extrasaction=extrasaction, - delimiter="\t", - quotechar=quotechar, - escapechar=escapechar, - quoting=quoting, - lineterminator=lineterminator, - ) - - def rewind(self): - """Rewind the file before reading it.""" - if self.compression == "brotli": - result = self._file.write(self.brotli_compressor.finish()) - - self.bytes_total += result - self.bytes_since_last_reset += result - - self._brotli_compressor = None - - self._file.seek(0) - - def reset(self): - """Reset underlying file by truncating it. - - Also resets the tracker attributes for bytes and records since last reset. - """ - self._file.seek(0) - self._file.truncate() - - self.bytes_since_last_reset = 0 - self.records_since_last_reset = 0 - - @dataclasses.dataclass class CreateBatchExportRunInputs: """Inputs to the create_export_run activity. diff --git a/posthog/temporal/batch_exports/bigquery_batch_export.py b/posthog/temporal/batch_exports/bigquery_batch_export.py index a0469de79bb9e..b754a7add16b4 100644 --- a/posthog/temporal/batch_exports/bigquery_batch_export.py +++ b/posthog/temporal/batch_exports/bigquery_batch_export.py @@ -15,7 +15,6 @@ from posthog.batch_exports.service import BatchExportField, BatchExportSchema, BigQueryBatchExportInputs from posthog.temporal.batch_exports.base import PostHogWorkflow from posthog.temporal.batch_exports.batch_exports import ( - BatchExportTemporaryFile, CreateBatchExportRunInputs, UpdateBatchExportRunStatusInputs, create_export_run, @@ -29,6 +28,9 @@ get_bytes_exported_metric, get_rows_exported_metric, ) +from posthog.temporal.batch_exports.temporary_file import ( + BatchExportTemporaryFile, +) from posthog.temporal.batch_exports.utils import peek_first_and_rewind from posthog.temporal.common.clickhouse import get_client from posthog.temporal.common.logger import bind_temporal_worker_logger diff --git a/posthog/temporal/batch_exports/http_batch_export.py b/posthog/temporal/batch_exports/http_batch_export.py index 8aca65c80ff38..2866d50c99876 100644 --- a/posthog/temporal/batch_exports/http_batch_export.py +++ b/posthog/temporal/batch_exports/http_batch_export.py @@ -13,7 +13,6 @@ from posthog.models import BatchExportRun from posthog.temporal.batch_exports.base import PostHogWorkflow from posthog.temporal.batch_exports.batch_exports import ( - BatchExportTemporaryFile, CreateBatchExportRunInputs, UpdateBatchExportRunStatusInputs, create_export_run, @@ -21,12 +20,15 @@ get_data_interval, get_rows_count, iter_records, - json_dumps_bytes, ) from posthog.temporal.batch_exports.metrics import ( get_bytes_exported_metric, get_rows_exported_metric, ) +from posthog.temporal.batch_exports.temporary_file import ( + BatchExportTemporaryFile, + json_dumps_bytes, +) from posthog.temporal.common.clickhouse import get_client from posthog.temporal.common.logger import bind_temporal_worker_logger diff --git a/posthog/temporal/batch_exports/postgres_batch_export.py b/posthog/temporal/batch_exports/postgres_batch_export.py index 5dbfc6faa4acf..98969ee78de79 100644 --- a/posthog/temporal/batch_exports/postgres_batch_export.py +++ b/posthog/temporal/batch_exports/postgres_batch_export.py @@ -17,7 +17,6 @@ from posthog.batch_exports.service import BatchExportField, BatchExportSchema, PostgresBatchExportInputs from posthog.temporal.batch_exports.base import PostHogWorkflow from posthog.temporal.batch_exports.batch_exports import ( - BatchExportTemporaryFile, CreateBatchExportRunInputs, UpdateBatchExportRunStatusInputs, create_export_run, @@ -31,6 +30,9 @@ get_bytes_exported_metric, get_rows_exported_metric, ) +from posthog.temporal.batch_exports.temporary_file import ( + BatchExportTemporaryFile, +) from posthog.temporal.batch_exports.utils import peek_first_and_rewind from posthog.temporal.common.clickhouse import get_client from posthog.temporal.common.logger import bind_temporal_worker_logger diff --git a/posthog/temporal/batch_exports/s3_batch_export.py b/posthog/temporal/batch_exports/s3_batch_export.py index 4d99cbeffd7c3..e83fe3f12915d 100644 --- a/posthog/temporal/batch_exports/s3_batch_export.py +++ b/posthog/temporal/batch_exports/s3_batch_export.py @@ -1,4 +1,5 @@ import asyncio +import collections.abc import contextlib import datetime as dt import io @@ -8,6 +9,8 @@ from dataclasses import dataclass import aioboto3 +import orjson +import pyarrow as pa from django.conf import settings from temporalio import activity, workflow from temporalio.common import RetryPolicy @@ -16,7 +19,6 @@ from posthog.batch_exports.service import BatchExportField, BatchExportSchema, S3BatchExportInputs from posthog.temporal.batch_exports.base import PostHogWorkflow from posthog.temporal.batch_exports.batch_exports import ( - BatchExportTemporaryFile, CreateBatchExportRunInputs, UpdateBatchExportRunStatusInputs, create_export_run, @@ -30,6 +32,15 @@ get_bytes_exported_metric, get_rows_exported_metric, ) +from posthog.temporal.batch_exports.temporary_file import ( + BatchExportTemporaryFile, + BatchExportWriter, + FlushCallable, + JSONLBatchExportWriter, + ParquetBatchExportWriter, + UnsupportedFileFormatError, +) +from posthog.temporal.batch_exports.utils import peek_first_and_rewind from posthog.temporal.common.clickhouse import get_client from posthog.temporal.common.logger import bind_temporal_worker_logger @@ -50,19 +61,31 @@ def get_allowed_template_variables(inputs) -> dict[str, str]: } +FILE_FORMAT_EXTENSIONS = { + "Parquet": "parquet", + "JSONLines": "jsonl", +} + +COMPRESSION_EXTENSIONS = { + "gzip": "gz", + "snappy": "sz", + "brotli": "br", + "ztsd": "zst", + "lz4": "lz4", +} + + def get_s3_key(inputs) -> str: """Return an S3 key given S3InsertInputs.""" template_variables = get_allowed_template_variables(inputs) key_prefix = inputs.prefix.format(**template_variables) + file_extension = FILE_FORMAT_EXTENSIONS[inputs.file_format] base_file_name = f"{inputs.data_interval_start}-{inputs.data_interval_end}" - match inputs.compression: - case "gzip": - file_name = base_file_name + ".jsonl.gz" - case "brotli": - file_name = base_file_name + ".jsonl.br" - case _: - file_name = base_file_name + ".jsonl" + if inputs.compression is not None: + file_name = base_file_name + f".{file_extension}.{COMPRESSION_EXTENSIONS[inputs.compression]}" + else: + file_name = base_file_name + f".{file_extension}" key = posixpath.join(key_prefix, file_name) @@ -311,6 +334,8 @@ class S3InsertInputs: kms_key_id: str | None = None batch_export_schema: BatchExportSchema | None = None endpoint_url: str | None = None + # TODO: In Python 3.11, this could be a enum.StrEnum. + file_format: str = "JSONLines" async def initialize_and_resume_multipart_upload(inputs: S3InsertInputs) -> tuple[S3MultiPartUpload, str]: @@ -451,7 +476,7 @@ async def insert_into_s3_activity(inputs: S3InsertInputs) -> int: last_uploaded_part_timestamp: str | None = None - async def worker_shutdown_handler(): + async def worker_shutdown_handler() -> None: """Handle the Worker shutting down by heart-beating our latest status.""" await activity.wait_for_worker_shutdown() logger.warn( @@ -466,50 +491,147 @@ async def worker_shutdown_handler(): asyncio.create_task(worker_shutdown_handler()) - record = None - async with s3_upload as s3_upload: - with BatchExportTemporaryFile(compression=inputs.compression) as local_results_file: + + async def flush_to_s3( + local_results_file, + records_since_last_flush: int, + bytes_since_last_flush: int, + last_inserted_at: dt.datetime, + last: bool, + ): + nonlocal last_uploaded_part_timestamp + + logger.debug( + "Uploading %s part %s containing %s records with size %s bytes", + "last " if last else "", + s3_upload.part_number + 1, + records_since_last_flush, + bytes_since_last_flush, + ) + + await s3_upload.upload_part(local_results_file) + rows_exported.add(records_since_last_flush) + bytes_exported.add(bytes_since_last_flush) + + last_uploaded_part_timestamp = str(last_inserted_at) + activity.heartbeat(last_uploaded_part_timestamp, s3_upload.to_state()) + + first_record_batch, record_iterator = peek_first_and_rewind(record_iterator) + first_record_batch = cast_record_batch_json_columns(first_record_batch) + column_names = first_record_batch.column_names + column_names.pop(column_names.index("_inserted_at")) + + schema = pa.schema( + # NOTE: For some reason, some batches set non-nullable fields as non-nullable, whereas other + # record batches have them as nullable. + # Until we figure it out, we set all fields to nullable. There are some fields we know + # are not nullable, but I'm opting for the more flexible option until we out why schemas differ + # between batches. + [field.with_nullable(True) for field in first_record_batch.select(column_names).schema] + ) + + writer = get_batch_export_writer( + inputs, + flush_callable=flush_to_s3, + max_bytes=settings.BATCH_EXPORT_S3_UPLOAD_CHUNK_SIZE_BYTES, + schema=schema, + ) + + async with writer.open_temporary_file(): rows_exported = get_rows_exported_metric() bytes_exported = get_bytes_exported_metric() - async def flush_to_s3(last_uploaded_part_timestamp: str, last=False): - logger.debug( - "Uploading %s part %s containing %s records with size %s bytes", - "last " if last else "", - s3_upload.part_number + 1, - local_results_file.records_since_last_reset, - local_results_file.bytes_since_last_reset, - ) + for record_batch in record_iterator: + record_batch = cast_record_batch_json_columns(record_batch) - await s3_upload.upload_part(local_results_file) - rows_exported.add(local_results_file.records_since_last_reset) - bytes_exported.add(local_results_file.bytes_since_last_reset) + await writer.write_record_batch(record_batch) - activity.heartbeat(last_uploaded_part_timestamp, s3_upload.to_state()) + await s3_upload.complete() - for record_batch in record_iterator: - for record in record_batch.to_pylist(): - for json_column in ("properties", "person_properties", "set", "set_once"): - if (json_str := record.get(json_column, None)) is not None: - record[json_column] = json.loads(json_str) + return writer.records_total - inserted_at = record.pop("_inserted_at") - local_results_file.write_records_to_jsonl([record]) +def get_batch_export_writer( + inputs: S3InsertInputs, flush_callable: FlushCallable, max_bytes: int, schema: pa.Schema | None = None +) -> BatchExportWriter: + """Return the `BatchExportWriter` corresponding to configured `file_format`. - if local_results_file.tell() > settings.BATCH_EXPORT_S3_UPLOAD_CHUNK_SIZE_BYTES: - last_uploaded_part_timestamp = str(inserted_at) - await flush_to_s3(last_uploaded_part_timestamp) - local_results_file.reset() + Raises: + UnsupportedFileFormatError: If no writer exists for given `file_format`. + """ + writer: BatchExportWriter - if local_results_file.tell() > 0 and record is not None: - last_uploaded_part_timestamp = str(inserted_at) - await flush_to_s3(last_uploaded_part_timestamp, last=True) + if inputs.file_format == "Parquet": + writer = ParquetBatchExportWriter( + max_bytes=max_bytes, + flush_callable=flush_callable, + compression=inputs.compression, + schema=schema, + ) + elif inputs.file_format == "JSONLines": + writer = JSONLBatchExportWriter( + max_bytes=settings.BATCH_EXPORT_S3_UPLOAD_CHUNK_SIZE_BYTES, + flush_callable=flush_callable, + compression=inputs.compression, + ) + else: + raise UnsupportedFileFormatError(inputs.file_format, "S3") - await s3_upload.complete() + return writer + + +def cast_record_batch_json_columns( + record_batch: pa.RecordBatch, + json_columns: collections.abc.Sequence = ("properties", "person_properties", "set", "set_once"), +) -> pa.RecordBatch: + """Cast json_columns in record_batch to JsonType. + + We return a new RecordBatch with any json_columns replaced by fields casted to JsonType. + Casting is not copying the underlying array buffers, so memory usage does not increase when creating + the new array or the new record batch. + """ + column_names = set(record_batch.column_names) + intersection = column_names & set(json_columns) + + casted_arrays = [] + for array in record_batch.select(intersection): + if pa.types.is_string(array.type): + casted_array = array.cast(JsonType()) + casted_arrays.append(casted_array) + + remaining_column_names = list(column_names - intersection) + return pa.RecordBatch.from_arrays( + record_batch.select(remaining_column_names).columns + casted_arrays, + names=remaining_column_names + list(intersection), + ) + + +class JsonScalar(pa.ExtensionScalar): + """Represents a JSON binary string.""" + + def as_py(self) -> dict | None: + if self.value: + return orjson.loads(self.value.as_py().encode("utf-8")) + else: + return None + + +class JsonType(pa.ExtensionType): + """Type for JSON binary strings.""" + + def __init__(self): + super().__init__(pa.string(), "json") + + def __arrow_ext_serialize__(self): + return b"" + + @classmethod + def __arrow_ext_deserialize__(self, storage_type, serialized): + return JsonType() - return local_results_file.records_total + def __arrow_ext_scalar_class__(self): + return JsonScalar @workflow.defn(name="s3-export") @@ -572,6 +694,7 @@ async def run(self, inputs: S3BatchExportInputs): encryption=inputs.encryption, kms_key_id=inputs.kms_key_id, batch_export_schema=inputs.batch_export_schema, + file_format=inputs.file_format, ) await execute_batch_export_insert_activity( diff --git a/posthog/temporal/batch_exports/snowflake_batch_export.py b/posthog/temporal/batch_exports/snowflake_batch_export.py index be94eca89a799..9053f3e1006ad 100644 --- a/posthog/temporal/batch_exports/snowflake_batch_export.py +++ b/posthog/temporal/batch_exports/snowflake_batch_export.py @@ -18,7 +18,6 @@ from posthog.batch_exports.service import BatchExportField, BatchExportSchema, SnowflakeBatchExportInputs from posthog.temporal.batch_exports.base import PostHogWorkflow from posthog.temporal.batch_exports.batch_exports import ( - BatchExportTemporaryFile, CreateBatchExportRunInputs, UpdateBatchExportRunStatusInputs, create_export_run, @@ -32,6 +31,9 @@ get_bytes_exported_metric, get_rows_exported_metric, ) +from posthog.temporal.batch_exports.temporary_file import ( + BatchExportTemporaryFile, +) from posthog.temporal.batch_exports.utils import peek_first_and_rewind from posthog.temporal.common.clickhouse import get_client from posthog.temporal.common.logger import bind_temporal_worker_logger diff --git a/posthog/temporal/batch_exports/temporary_file.py b/posthog/temporal/batch_exports/temporary_file.py new file mode 100644 index 0000000000000..f955f45553727 --- /dev/null +++ b/posthog/temporal/batch_exports/temporary_file.py @@ -0,0 +1,528 @@ +"""This module contains a temporary file to stage data in batch exports.""" +import abc +import collections.abc +import contextlib +import csv +import datetime as dt +import gzip +import tempfile +import typing + +import brotli +import orjson +import pyarrow as pa +import pyarrow.parquet as pq + + +def json_dumps_bytes(d) -> bytes: + return orjson.dumps(d, default=str) + + +class BatchExportTemporaryFile: + """A TemporaryFile used to as an intermediate step while exporting data. + + This class does not implement the file-like interface but rather passes any calls + to the underlying tempfile.NamedTemporaryFile. We do override 'write' methods + to allow tracking bytes and records. + """ + + def __init__( + self, + mode: str = "w+b", + buffering=-1, + compression: str | None = None, + encoding: str | None = None, + newline: str | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + *, + errors: str | None = None, + ): + self._file = tempfile.NamedTemporaryFile( + mode=mode, + encoding=encoding, + newline=newline, + buffering=buffering, + suffix=suffix, + prefix=prefix, + dir=dir, + errors=errors, + ) + self.compression = compression + self.bytes_total = 0 + self.records_total = 0 + self.bytes_since_last_reset = 0 + self.records_since_last_reset = 0 + self._brotli_compressor = None + + def __getattr__(self, name): + """Pass get attr to underlying tempfile.NamedTemporaryFile.""" + return self._file.__getattr__(name) + + def __enter__(self): + """Context-manager protocol enter method.""" + self._file.__enter__() + return self + + def __exit__(self, exc, value, tb): + """Context-manager protocol exit method.""" + return self._file.__exit__(exc, value, tb) + + def __iter__(self): + yield from self._file + + @property + def brotli_compressor(self): + if self._brotli_compressor is None: + self._brotli_compressor = brotli.Compressor() + return self._brotli_compressor + + def finish_brotli_compressor(self): + """Flush remaining brotli bytes.""" + # TODO: Move compression out of `BatchExportTemporaryFile` to a standard class for all writers. + if self.compression != "brotli": + raise ValueError(f"Compression is '{self.compression}', not 'brotli'") + + result = self._file.write(self.brotli_compressor.finish()) + self.bytes_total += result + self.bytes_since_last_reset += result + self._brotli_compressor = None + + def compress(self, content: bytes | str) -> bytes: + if isinstance(content, str): + encoded = content.encode("utf-8") + else: + encoded = content + + match self.compression: + case "gzip": + return gzip.compress(encoded) + case "brotli": + self.brotli_compressor.process(encoded) + return self.brotli_compressor.flush() + case None: + return encoded + case _: + raise ValueError(f"Unsupported compression: '{self.compression}'") + + def write(self, content: bytes | str): + """Write bytes to underlying file keeping track of how many bytes were written.""" + compressed_content = self.compress(content) + + if "b" in self.mode: + result = self._file.write(compressed_content) + else: + result = self._file.write(compressed_content.decode("utf-8")) + + self.bytes_total += result + self.bytes_since_last_reset += result + + return result + + def write_record_as_bytes(self, record: bytes): + result = self.write(record) + + self.records_total += 1 + self.records_since_last_reset += 1 + + return result + + def write_records_to_jsonl(self, records): + """Write records to a temporary file as JSONL.""" + if len(records) == 1: + jsonl_dump = orjson.dumps(records[0], option=orjson.OPT_APPEND_NEWLINE, default=str) + else: + jsonl_dump = b"\n".join(map(json_dumps_bytes, records)) + + result = self.write(jsonl_dump) + + self.records_total += len(records) + self.records_since_last_reset += len(records) + + return result + + def write_records_to_csv( + self, + records, + fieldnames: None | collections.abc.Sequence[str] = None, + extrasaction: typing.Literal["raise", "ignore"] = "ignore", + delimiter: str = ",", + quotechar: str = '"', + escapechar: str | None = "\\", + lineterminator: str = "\n", + quoting=csv.QUOTE_NONE, + ): + """Write records to a temporary file as CSV.""" + if len(records) == 0: + return + + if fieldnames is None: + fieldnames = list(records[0].keys()) + + writer = csv.DictWriter( + self, + fieldnames=fieldnames, + extrasaction=extrasaction, + delimiter=delimiter, + quotechar=quotechar, + escapechar=escapechar, + quoting=quoting, + lineterminator=lineterminator, + ) + writer.writerows(records) + + self.records_total += len(records) + self.records_since_last_reset += len(records) + + def write_records_to_tsv( + self, + records, + fieldnames: None | list[str] = None, + extrasaction: typing.Literal["raise", "ignore"] = "ignore", + quotechar: str = '"', + escapechar: str | None = "\\", + lineterminator: str = "\n", + quoting=csv.QUOTE_NONE, + ): + """Write records to a temporary file as TSV.""" + return self.write_records_to_csv( + records, + fieldnames=fieldnames, + extrasaction=extrasaction, + delimiter="\t", + quotechar=quotechar, + escapechar=escapechar, + quoting=quoting, + lineterminator=lineterminator, + ) + + def rewind(self): + """Rewind the file before reading it.""" + self._file.seek(0) + + def reset(self): + """Reset underlying file by truncating it. + + Also resets the tracker attributes for bytes and records since last reset. + """ + self._file.seek(0) + self._file.truncate() + + self.bytes_since_last_reset = 0 + self.records_since_last_reset = 0 + + +LastInsertedAt = dt.datetime +IsLast = bool +RecordsSinceLastFlush = int +BytesSinceLastFlush = int +FlushCallable = collections.abc.Callable[ + [BatchExportTemporaryFile, RecordsSinceLastFlush, BytesSinceLastFlush, LastInsertedAt, IsLast], + collections.abc.Awaitable[None], +] + + +class UnsupportedFileFormatError(Exception): + """Raised when a writer for an unsupported file format is requested.""" + + def __init__(self, file_format: str, destination: str): + super().__init__(f"{file_format} is not a supported format for {destination} batch exports.") + + +class BatchExportWriter(abc.ABC): + """A temporary file writer to be used by batch export workflows. + + Subclasses should define `_write_record_batch` with the particular intricacies + of the format they are writing as. + + Actual writing calls are passed to the underlying `batch_export_file`. + + Attributes: + _batch_export_file: The temporary file we are writing to. + max_bytes: Flush the temporary file with the provided `flush_callable` + upon reaching or surpassing this threshold. Keep in mind we write on a RecordBatch + per RecordBatch basis, which means the threshold will be surpassed by at most the + size of a RecordBatch before a flush occurs. + flush_callable: A callback to flush the temporary file when `max_bytes` is reached. + The temporary file will be reset after calling `flush_callable`. When calling + `flush_callable` the following positional arguments will be passed: The temporary file + that must be flushed, the number of records since the last flush, the number of bytes + since the last flush, the latest recorded `_inserted_at`, and a `bool` indicating if + this is the last flush (when exiting the context manager). + file_kwargs: Optional keyword arguments passed when initializing `_batch_export_file`. + last_inserted_at: Latest `_inserted_at` written. This attribute leaks some implementation + details, as we are assuming assume `_inserted_at` is present, as it's added to all + batch export queries. + records_total: The total number of records (not RecordBatches!) written. + records_since_last_flush: The number of records written since last flush. + bytes_total: The total number of bytes written. + bytes_since_last_flush: The number of bytes written since last flush. + """ + + def __init__( + self, + flush_callable: FlushCallable, + max_bytes: int, + file_kwargs: collections.abc.Mapping[str, typing.Any] | None = None, + ): + self.flush_callable = flush_callable + self.max_bytes = max_bytes + self.file_kwargs: collections.abc.Mapping[str, typing.Any] = file_kwargs or {} + + self._batch_export_file: BatchExportTemporaryFile | None = None + self.reset_writer_tracking() + + def reset_writer_tracking(self): + """Reset this writer's tracking state.""" + self.last_inserted_at: dt.datetime | None = None + self.records_total = 0 + self.records_since_last_flush = 0 + self.bytes_total = 0 + self.bytes_since_last_flush = 0 + + @contextlib.asynccontextmanager + async def open_temporary_file(self): + """Explicitly open the temporary file this writer is writing to. + + The underlying `BatchExportTemporaryFile` is only accessible within this context manager. This helps + us separate the lifetime of the underlying temporary file from the writer: The writer may still be + accessed even after the temporary file is closed, while on the other hand we ensure the file and all + its data is flushed and not leaked outside the context. Any relevant tracking information is copied + to the writer. + """ + self.reset_writer_tracking() + + with BatchExportTemporaryFile(**self.file_kwargs) as temp_file: + self._batch_export_file = temp_file + + try: + yield + finally: + self.track_bytes_written(temp_file) + + if self.last_inserted_at is not None and self.bytes_since_last_flush > 0: + # `bytes_since_last_flush` should be 0 unless: + # 1. The last batch wasn't flushed as it didn't reach `max_bytes`. + # 2. The last batch was flushed but there was another write after the last call to + # `write_record_batch`. For example, footer bytes. + await self.flush(self.last_inserted_at, is_last=True) + + self._batch_export_file = None + + @property + def batch_export_file(self): + """Property for underlying temporary file. + + Raises: + ValueError: if attempting to access the temporary file before it has been opened. + """ + if self._batch_export_file is None: + raise ValueError("Batch export file is closed. Did you forget to call 'open_temporary_file'?") + return self._batch_export_file + + @abc.abstractmethod + def _write_record_batch(self, record_batch: pa.RecordBatch) -> None: + """Write a record batch to the underlying `BatchExportTemporaryFile`. + + Subclasses must override this to provide the actual implementation according to the supported + file format. + """ + pass + + def track_records_written(self, record_batch: pa.RecordBatch) -> None: + """Update this writer's state with the number of records in `record_batch`.""" + self.records_total += record_batch.num_rows + self.records_since_last_flush += record_batch.num_rows + + def track_bytes_written(self, batch_export_file: BatchExportTemporaryFile) -> None: + """Update this writer's state with the bytes in `batch_export_file`.""" + self.bytes_total = batch_export_file.bytes_total + self.bytes_since_last_flush = batch_export_file.bytes_since_last_reset + + async def write_record_batch(self, record_batch: pa.RecordBatch) -> None: + """Issue a record batch write tracking progress and flushing if required.""" + record_batch = record_batch.sort_by("_inserted_at") + last_inserted_at = record_batch.column("_inserted_at")[-1].as_py() + + column_names = record_batch.column_names + column_names.pop(column_names.index("_inserted_at")) + + self._write_record_batch(record_batch.select(column_names)) + + self.last_inserted_at = last_inserted_at + self.track_records_written(record_batch) + self.track_bytes_written(self.batch_export_file) + + if self.bytes_since_last_flush >= self.max_bytes: + await self.flush(last_inserted_at) + + async def flush(self, last_inserted_at: dt.datetime, is_last: bool = False) -> None: + """Call the provided `flush_callable` and reset underlying file. + + The underlying batch export temporary file will be reset after calling `flush_callable`. + """ + if is_last is True and self.batch_export_file.compression == "brotli": + self.batch_export_file.finish_brotli_compressor() + + self.batch_export_file.seek(0) + + await self.flush_callable( + self.batch_export_file, + self.records_since_last_flush, + self.bytes_since_last_flush, + last_inserted_at, + is_last, + ) + self.batch_export_file.reset() + + self.records_since_last_flush = 0 + self.bytes_since_last_flush = 0 + + +class JSONLBatchExportWriter(BatchExportWriter): + """A `BatchExportWriter` for JSONLines format. + + Attributes: + default: The default function to use to cast non-serializable Python objects to serializable objects. + By default, non-serializable objects will be cast to string via `str()`. + """ + + def __init__( + self, + max_bytes: int, + flush_callable: FlushCallable, + compression: None | str = None, + default: typing.Callable = str, + ): + super().__init__( + max_bytes=max_bytes, + flush_callable=flush_callable, + file_kwargs={"compression": compression}, + ) + + self.default = default + + def write(self, content: bytes) -> int: + """Write a single row of JSONL.""" + n = self.batch_export_file.write(orjson.dumps(content, default=str) + b"\n") + return n + + def _write_record_batch(self, record_batch: pa.RecordBatch) -> None: + """Write records to a temporary file as JSONL.""" + for record in record_batch.to_pylist(): + self.write(record) + + +class CSVBatchExportWriter(BatchExportWriter): + """A `BatchExportWriter` for CSV format.""" + + def __init__( + self, + max_bytes: int, + flush_callable: FlushCallable, + field_names: collections.abc.Sequence[str], + extras_action: typing.Literal["raise", "ignore"] = "ignore", + delimiter: str = ",", + quote_char: str = '"', + escape_char: str | None = "\\", + line_terminator: str = "\n", + quoting=csv.QUOTE_NONE, + compression: str | None = None, + ): + super().__init__( + max_bytes=max_bytes, + flush_callable=flush_callable, + file_kwargs={"compression": compression}, + ) + self.field_names = field_names + self.extras_action: typing.Literal["raise", "ignore"] = extras_action + self.delimiter = delimiter + self.quote_char = quote_char + self.escape_char = escape_char + self.line_terminator = line_terminator + self.quoting = quoting + + self._csv_writer: csv.DictWriter | None = None + + @property + def csv_writer(self) -> csv.DictWriter: + if self._csv_writer is None: + self._csv_writer = csv.DictWriter( + self.batch_export_file, + fieldnames=self.field_names, + extrasaction=self.extras_action, + delimiter=self.delimiter, + quotechar=self.quote_char, + escapechar=self.escape_char, + quoting=self.quoting, + lineterminator=self.line_terminator, + ) + + return self._csv_writer + + def _write_record_batch(self, record_batch: pa.RecordBatch) -> None: + """Write records to a temporary file as CSV.""" + self.csv_writer.writerows(record_batch.to_pylist()) + + +class ParquetBatchExportWriter(BatchExportWriter): + """A `BatchExportWriter` for Apache Parquet format. + + We utilize and wrap a `pyarrow.parquet.ParquetWriter` to do the actual writing. We default to their + defaults for most parameters; however this class could be extended with more attributes to pass along + to `pyarrow.parquet.ParquetWriter`. + + See the pyarrow docs for more details on what parameters can the writer be configured with: + https://arrow.apache.org/docs/python/generated/pyarrow.parquet.ParquetWriter.html + + In contrast to other writers, instead of us handling compression we let `pyarrow.parquet.ParquetWriter` + handle it, so `BatchExportTemporaryFile` is always initialized with `compression=None`. + + Attributes: + schema: The schema used by the Parquet file. Should match the schema of written RecordBatches. + compression: Compression codec passed to underlying `pyarrow.parquet.ParquetWriter`. + """ + + def __init__( + self, + max_bytes: int, + flush_callable: FlushCallable, + schema: pa.Schema, + compression: str | None = "snappy", + ): + super().__init__( + max_bytes=max_bytes, + flush_callable=flush_callable, + file_kwargs={"compression": None}, # ParquetWriter handles compression + ) + self.schema = schema + self.compression = compression + + self._parquet_writer: pq.ParquetWriter | None = None + + @property + def parquet_writer(self) -> pq.ParquetWriter: + if self._parquet_writer is None: + self._parquet_writer = pq.ParquetWriter( + self.batch_export_file, + schema=self.schema, + compression="none" if self.compression is None else self.compression, + ) + return self._parquet_writer + + @contextlib.asynccontextmanager + async def open_temporary_file(self): + """Ensure underlying Parquet writer is closed before flushing and closing temporary file.""" + async with super().open_temporary_file(): + try: + yield + finally: + if self._parquet_writer is not None: + self._parquet_writer.writer.close() + self._parquet_writer = None + + def _write_record_batch(self, record_batch: pa.RecordBatch) -> None: + """Write records to a temporary file as Parquet.""" + + self.parquet_writer.write_batch(record_batch.select(self.parquet_writer.schema.names)) diff --git a/posthog/temporal/tests/batch_exports/test_batch_exports.py b/posthog/temporal/tests/batch_exports/test_batch_exports.py index 0afbfcabb71cb..756c07e442e4f 100644 --- a/posthog/temporal/tests/batch_exports/test_batch_exports.py +++ b/posthog/temporal/tests/batch_exports/test_batch_exports.py @@ -1,6 +1,4 @@ -import csv import datetime as dt -import io import json import operator from random import randint @@ -9,11 +7,9 @@ from django.test import override_settings from posthog.temporal.batch_exports.batch_exports import ( - BatchExportTemporaryFile, get_data_interval, get_rows_count, iter_records, - json_dumps_bytes, ) from posthog.temporal.tests.utils.events import generate_test_events_in_clickhouse @@ -558,181 +554,3 @@ def test_get_data_interval(interval, data_interval_end, expected): """Test get_data_interval returns the expected data interval tuple.""" result = get_data_interval(interval, data_interval_end) assert result == expected - - -@pytest.mark.parametrize( - "to_write", - [ - (b"",), - (b"", b""), - (b"12345",), - (b"12345", b"12345"), - (b"abbcccddddeeeee",), - (b"abbcccddddeeeee", b"abbcccddddeeeee"), - ], -) -def test_batch_export_temporary_file_tracks_bytes(to_write): - """Test the bytes written by BatchExportTemporaryFile match expected.""" - with BatchExportTemporaryFile() as be_file: - for content in to_write: - be_file.write(content) - - assert be_file.bytes_total == sum(len(content) for content in to_write) - assert be_file.bytes_since_last_reset == sum(len(content) for content in to_write) - - be_file.reset() - - assert be_file.bytes_total == sum(len(content) for content in to_write) - assert be_file.bytes_since_last_reset == 0 - - -TEST_RECORDS = [ - [], - [ - {"id": "record-1", "property": "value", "property_int": 1}, - {"id": "record-2", "property": "another-value", "property_int": 2}, - { - "id": "record-3", - "property": {"id": "nested-record", "property": "nested-value"}, - "property_int": 3, - }, - ], -] - - -@pytest.mark.parametrize( - "records", - TEST_RECORDS, -) -def test_batch_export_temporary_file_write_records_to_jsonl(records): - """Test JSONL records written by BatchExportTemporaryFile match expected.""" - jsonl_dump = b"\n".join(map(json_dumps_bytes, records)) - - with BatchExportTemporaryFile() as be_file: - be_file.write_records_to_jsonl(records) - - assert be_file.bytes_total == len(jsonl_dump) - assert be_file.bytes_since_last_reset == len(jsonl_dump) - assert be_file.records_total == len(records) - assert be_file.records_since_last_reset == len(records) - - be_file.seek(0) - lines = be_file.readlines() - assert len(lines) == len(records) - - for line_index, jsonl_record in enumerate(lines): - json_loaded = json.loads(jsonl_record) - assert json_loaded == records[line_index] - - be_file.reset() - - assert be_file.bytes_total == len(jsonl_dump) - assert be_file.bytes_since_last_reset == 0 - assert be_file.records_total == len(records) - assert be_file.records_since_last_reset == 0 - - -@pytest.mark.parametrize( - "records", - TEST_RECORDS, -) -def test_batch_export_temporary_file_write_records_to_csv(records): - """Test CSV written by BatchExportTemporaryFile match expected.""" - in_memory_file_obj = io.StringIO() - writer = csv.DictWriter( - in_memory_file_obj, - fieldnames=records[0].keys() if len(records) > 0 else [], - delimiter=",", - quotechar='"', - escapechar="\\", - lineterminator="\n", - quoting=csv.QUOTE_NONE, - ) - writer.writerows(records) - - with BatchExportTemporaryFile(mode="w+") as be_file: - be_file.write_records_to_csv(records) - - assert be_file.bytes_total == in_memory_file_obj.tell() - assert be_file.bytes_since_last_reset == in_memory_file_obj.tell() - assert be_file.records_total == len(records) - assert be_file.records_since_last_reset == len(records) - - be_file.seek(0) - reader = csv.reader( - be_file._file, - delimiter=",", - quotechar='"', - escapechar="\\", - quoting=csv.QUOTE_NONE, - ) - - rows = [row for row in reader] - assert len(rows) == len(records) - - for row_index, csv_record in enumerate(rows): - for value_index, value in enumerate(records[row_index].values()): - # Everything returned by csv.reader is a str. - # This means type information is lost when writing to CSV - # but this just a limitation of the format. - assert csv_record[value_index] == str(value) - - be_file.reset() - - assert be_file.bytes_total == in_memory_file_obj.tell() - assert be_file.bytes_since_last_reset == 0 - assert be_file.records_total == len(records) - assert be_file.records_since_last_reset == 0 - - -@pytest.mark.parametrize( - "records", - TEST_RECORDS, -) -def test_batch_export_temporary_file_write_records_to_tsv(records): - """Test TSV written by BatchExportTemporaryFile match expected.""" - in_memory_file_obj = io.StringIO() - writer = csv.DictWriter( - in_memory_file_obj, - fieldnames=records[0].keys() if len(records) > 0 else [], - delimiter="\t", - quotechar='"', - escapechar="\\", - lineterminator="\n", - quoting=csv.QUOTE_NONE, - ) - writer.writerows(records) - - with BatchExportTemporaryFile(mode="w+") as be_file: - be_file.write_records_to_tsv(records) - - assert be_file.bytes_total == in_memory_file_obj.tell() - assert be_file.bytes_since_last_reset == in_memory_file_obj.tell() - assert be_file.records_total == len(records) - assert be_file.records_since_last_reset == len(records) - - be_file.seek(0) - reader = csv.reader( - be_file._file, - delimiter="\t", - quotechar='"', - escapechar="\\", - quoting=csv.QUOTE_NONE, - ) - - rows = [row for row in reader] - assert len(rows) == len(records) - - for row_index, csv_record in enumerate(rows): - for value_index, value in enumerate(records[row_index].values()): - # Everything returned by csv.reader is a str. - # This means type information is lost when writing to CSV - # but this just a limitation of the format. - assert csv_record[value_index] == str(value) - - be_file.reset() - - assert be_file.bytes_total == in_memory_file_obj.tell() - assert be_file.bytes_since_last_reset == 0 - assert be_file.records_total == len(records) - assert be_file.records_since_last_reset == 0 diff --git a/posthog/temporal/tests/batch_exports/test_s3_batch_export_workflow.py b/posthog/temporal/tests/batch_exports/test_s3_batch_export_workflow.py index e04e345d11245..e6583d049e2a8 100644 --- a/posthog/temporal/tests/batch_exports/test_s3_batch_export_workflow.py +++ b/posthog/temporal/tests/batch_exports/test_s3_batch_export_workflow.py @@ -10,10 +10,12 @@ import aioboto3 import botocore.exceptions import brotli +import pyarrow.parquet as pq import pytest import pytest_asyncio from django.conf import settings from django.test import override_settings +from pyarrow import fs from temporalio import activity from temporalio.client import WorkflowFailureError from temporalio.common import RetryPolicy @@ -27,6 +29,7 @@ update_export_run_status, ) from posthog.temporal.batch_exports.s3_batch_export import ( + FILE_FORMAT_EXTENSIONS, HeartbeatDetails, S3BatchExportInputs, S3BatchExportWorkflow, @@ -107,6 +110,15 @@ def s3_key_prefix(): return f"posthog-events-{str(uuid4())}" +@pytest.fixture +def file_format(request) -> str: + """S3 file format.""" + try: + return request.param + except AttributeError: + return f"JSONLines" + + async def delete_all_from_s3(minio_client, bucket_name: str, key_prefix: str): """Delete all objects in bucket_name under key_prefix.""" response = await minio_client.list_objects_v2(Bucket=bucket_name, Prefix=key_prefix) @@ -138,6 +150,61 @@ async def minio_client(bucket_name): await minio_client.delete_bucket(Bucket=bucket_name) +async def read_parquet_from_s3(bucket_name: str, key: str, json_columns) -> list: + async with aioboto3.Session().client("sts") as sts: + try: + await sts.get_caller_identity() + except botocore.exceptions.NoCredentialsError: + s3 = fs.S3FileSystem( + access_key="object_storage_root_user", + secret_key="object_storage_root_password", + endpoint_override=settings.OBJECT_STORAGE_ENDPOINT, + ) + + else: + if os.getenv("S3_TEST_BUCKET") is not None: + s3 = fs.S3FileSystem() + else: + s3 = fs.S3FileSystem( + access_key="object_storage_root_user", + secret_key="object_storage_root_password", + endpoint_override=settings.OBJECT_STORAGE_ENDPOINT, + ) + + table = pq.read_table(f"{bucket_name}/{key}", filesystem=s3) + + parquet_data = [] + for batch in table.to_batches(): + for record in batch.to_pylist(): + casted_record = {} + for k, v in record.items(): + if isinstance(v, dt.datetime): + # We read data from clickhouse as string, but parquet already casts them as dates. + # To facilitate comparison, we isoformat the dates. + casted_record[k] = v.isoformat() + elif k in json_columns and v is not None: + # Parquet doesn't have a variable map type, so JSON fields are just strings. + casted_record[k] = json.loads(v) + else: + casted_record[k] = v + parquet_data.append(casted_record) + + return parquet_data + + +def read_s3_data_as_json(data: bytes, compression: str | None) -> list: + match compression: + case "gzip": + data = gzip.decompress(data) + case "brotli": + data = brotli.decompress(data) + case _: + pass + + json_data = [json.loads(line) for line in data.decode("utf-8").split("\n") if line] + return json_data + + async def assert_clickhouse_records_in_s3( s3_compatible_client, clickhouse_client: ClickHouseClient, @@ -150,6 +217,7 @@ async def assert_clickhouse_records_in_s3( include_events: list[str] | None = None, batch_export_schema: BatchExportSchema | None = None, compression: str | None = None, + file_format: str = "JSONLines", ): """Assert ClickHouse records are written to JSON in key_prefix in S3 bucket_name. @@ -175,28 +243,24 @@ async def assert_clickhouse_records_in_s3( # Get the object. key = objects["Contents"][0].get("Key") assert key - s3_object = await s3_compatible_client.get_object(Bucket=bucket_name, Key=key) - data = await s3_object["Body"].read() - # Check that the data is correct. - match compression: - case "gzip": - data = gzip.decompress(data) - case "brotli": - data = brotli.decompress(data) - case _: - pass + json_columns = ("properties", "person_properties", "set", "set_once") - json_data = [json.loads(line) for line in data.decode("utf-8").split("\n") if line] - # Pull out the fields we inserted only + if file_format == "Parquet": + s3_data = await read_parquet_from_s3(bucket_name, key, json_columns) + + elif file_format == "JSONLines": + s3_object = await s3_compatible_client.get_object(Bucket=bucket_name, Key=key) + data = await s3_object["Body"].read() + s3_data = read_s3_data_as_json(data, compression) + else: + raise ValueError(f"Unsupported file format: {file_format}") if batch_export_schema is not None: schema_column_names = [field["alias"] for field in batch_export_schema["fields"]] else: schema_column_names = [field["alias"] for field in s3_default_fields()] - json_columns = ("properties", "person_properties", "set", "set_once") - expected_records = [] for record_batch in iter_records( client=clickhouse_client, @@ -225,9 +289,9 @@ async def assert_clickhouse_records_in_s3( expected_records.append(expected_record) - assert len(json_data) == len(expected_records) - assert json_data[0] == expected_records[0] - assert json_data == expected_records + assert len(s3_data) == len(expected_records) + assert s3_data[0] == expected_records[0] + assert s3_data == expected_records TEST_S3_SCHEMAS: list[BatchExportSchema | None] = [ @@ -255,6 +319,7 @@ async def assert_clickhouse_records_in_s3( @pytest.mark.parametrize("compression", [None, "gzip", "brotli"], indirect=True) @pytest.mark.parametrize("exclude_events", [None, ["test-exclude"]], indirect=True) @pytest.mark.parametrize("batch_export_schema", TEST_S3_SCHEMAS) +@pytest.mark.parametrize("file_format", FILE_FORMAT_EXTENSIONS.keys()) async def test_insert_into_s3_activity_puts_data_into_s3( clickhouse_client, bucket_name, @@ -262,6 +327,7 @@ async def test_insert_into_s3_activity_puts_data_into_s3( activity_environment, compression, exclude_events, + file_format, batch_export_schema: BatchExportSchema | None, ): """Test that the insert_into_s3_activity function ends up with data into S3. @@ -339,12 +405,15 @@ async def test_insert_into_s3_activity_puts_data_into_s3( compression=compression, exclude_events=exclude_events, batch_export_schema=batch_export_schema, + file_format=file_format, ) with override_settings( BATCH_EXPORT_S3_UPLOAD_CHUNK_SIZE_BYTES=5 * 1024**2 ): # 5MB, the minimum for Multipart uploads - await activity_environment.run(insert_into_s3_activity, insert_inputs) + records_total = await activity_environment.run(insert_into_s3_activity, insert_inputs) + + assert records_total == 10005 await assert_clickhouse_records_in_s3( s3_compatible_client=minio_client, @@ -358,6 +427,7 @@ async def test_insert_into_s3_activity_puts_data_into_s3( exclude_events=exclude_events, include_events=None, compression=compression, + file_format=file_format, ) @@ -371,6 +441,7 @@ async def s3_batch_export( exclude_events, temporal_client, encryption, + file_format, ): destination_data = { "type": "S3", @@ -385,6 +456,7 @@ async def s3_batch_export( "exclude_events": exclude_events, "encryption": encryption, "kms_key_id": os.getenv("S3_TEST_KMS_KEY_ID") if encryption == "aws:kms" else None, + "file_format": file_format, }, } @@ -410,6 +482,7 @@ async def s3_batch_export( @pytest.mark.parametrize("compression", [None, "gzip", "brotli"], indirect=True) @pytest.mark.parametrize("exclude_events", [None, ["test-exclude"]], indirect=True) @pytest.mark.parametrize("batch_export_schema", TEST_S3_SCHEMAS) +@pytest.mark.parametrize("file_format", FILE_FORMAT_EXTENSIONS.keys(), indirect=True) async def test_s3_export_workflow_with_minio_bucket( clickhouse_client, minio_client, @@ -421,6 +494,7 @@ async def test_s3_export_workflow_with_minio_bucket( exclude_events, s3_key_prefix, batch_export_schema, + file_format, ): """Test S3BatchExport Workflow end-to-end by using a local MinIO bucket instead of S3. @@ -508,6 +582,7 @@ async def test_s3_export_workflow_with_minio_bucket( batch_export_schema=batch_export_schema, exclude_events=exclude_events, compression=compression, + file_format=file_format, ) @@ -537,6 +612,7 @@ async def s3_client(bucket_name, s3_key_prefix): @pytest.mark.parametrize("encryption", [None, "AES256", "aws:kms"], indirect=True) @pytest.mark.parametrize("bucket_name", [os.getenv("S3_TEST_BUCKET")], indirect=True) @pytest.mark.parametrize("batch_export_schema", TEST_S3_SCHEMAS) +@pytest.mark.parametrize("file_format", FILE_FORMAT_EXTENSIONS.keys(), indirect=True) async def test_s3_export_workflow_with_s3_bucket( s3_client, clickhouse_client, @@ -549,6 +625,7 @@ async def test_s3_export_workflow_with_s3_bucket( exclude_events, ateam, batch_export_schema, + file_format, ): """Test S3 Export Workflow end-to-end by using an S3 bucket. @@ -646,6 +723,7 @@ async def test_s3_export_workflow_with_s3_bucket( exclude_events=exclude_events, include_events=None, compression=compression, + file_format=file_format, ) @@ -1206,6 +1284,49 @@ async def never_finish_activity(_: S3InsertInputs) -> str: ), "nested/prefix/2023-01-01 00:00:00-2023-01-01 01:00:00.jsonl.br", ), + ( + S3InsertInputs( + prefix="/nested/prefix/", + data_interval_start="2023-01-01 00:00:00", + data_interval_end="2023-01-01 01:00:00", + file_format="Parquet", + compression="snappy", + **base_inputs, # type: ignore + ), + "nested/prefix/2023-01-01 00:00:00-2023-01-01 01:00:00.parquet.sz", + ), + ( + S3InsertInputs( + prefix="/nested/prefix/", + data_interval_start="2023-01-01 00:00:00", + data_interval_end="2023-01-01 01:00:00", + file_format="Parquet", + **base_inputs, # type: ignore + ), + "nested/prefix/2023-01-01 00:00:00-2023-01-01 01:00:00.parquet", + ), + ( + S3InsertInputs( + prefix="/nested/prefix/", + data_interval_start="2023-01-01 00:00:00", + data_interval_end="2023-01-01 01:00:00", + compression="gzip", + file_format="Parquet", + **base_inputs, # type: ignore + ), + "nested/prefix/2023-01-01 00:00:00-2023-01-01 01:00:00.parquet.gz", + ), + ( + S3InsertInputs( + prefix="/nested/prefix/", + data_interval_start="2023-01-01 00:00:00", + data_interval_end="2023-01-01 01:00:00", + compression="brotli", + file_format="Parquet", + **base_inputs, # type: ignore + ), + "nested/prefix/2023-01-01 00:00:00-2023-01-01 01:00:00.parquet.br", + ), ], ) def test_get_s3_key(inputs, expected): @@ -1271,7 +1392,7 @@ def assert_heartbeat_details(*details): endpoint_url=settings.OBJECT_STORAGE_ENDPOINT, ) - with override_settings(BATCH_EXPORT_S3_UPLOAD_CHUNK_SIZE_BYTES=5 * 1024**2): + with override_settings(BATCH_EXPORT_S3_UPLOAD_CHUNK_SIZE_BYTES=1, CLICKHOUSE_MAX_BLOCK_SIZE_DEFAULT=1): await activity_environment.run(insert_into_s3_activity, insert_inputs) # This checks that the assert_heartbeat_details function was actually called. diff --git a/posthog/temporal/tests/batch_exports/test_temporary_file.py b/posthog/temporal/tests/batch_exports/test_temporary_file.py new file mode 100644 index 0000000000000..4fd7e69c0c12f --- /dev/null +++ b/posthog/temporal/tests/batch_exports/test_temporary_file.py @@ -0,0 +1,389 @@ +import csv +import datetime as dt +import io +import json + +import pyarrow as pa +import pyarrow.parquet as pq +import pytest + +from posthog.temporal.batch_exports.temporary_file import ( + BatchExportTemporaryFile, + CSVBatchExportWriter, + JSONLBatchExportWriter, + ParquetBatchExportWriter, + json_dumps_bytes, +) + + +@pytest.mark.parametrize( + "to_write", + [ + (b"",), + (b"", b""), + (b"12345",), + (b"12345", b"12345"), + (b"abbcccddddeeeee",), + (b"abbcccddddeeeee", b"abbcccddddeeeee"), + ], +) +def test_batch_export_temporary_file_tracks_bytes(to_write): + """Test the bytes written by BatchExportTemporaryFile match expected.""" + with BatchExportTemporaryFile() as be_file: + for content in to_write: + be_file.write(content) + + assert be_file.bytes_total == sum(len(content) for content in to_write) + assert be_file.bytes_since_last_reset == sum(len(content) for content in to_write) + + be_file.reset() + + assert be_file.bytes_total == sum(len(content) for content in to_write) + assert be_file.bytes_since_last_reset == 0 + + +TEST_RECORDS = [ + [], + [ + {"id": "record-1", "property": "value", "property_int": 1}, + {"id": "record-2", "property": "another-value", "property_int": 2}, + { + "id": "record-3", + "property": {"id": "nested-record", "property": "nested-value"}, + "property_int": 3, + }, + ], +] + + +@pytest.mark.parametrize( + "records", + TEST_RECORDS, +) +def test_batch_export_temporary_file_write_records_to_jsonl(records): + """Test JSONL records written by BatchExportTemporaryFile match expected.""" + jsonl_dump = b"\n".join(map(json_dumps_bytes, records)) + + with BatchExportTemporaryFile() as be_file: + be_file.write_records_to_jsonl(records) + + assert be_file.bytes_total == len(jsonl_dump) + assert be_file.bytes_since_last_reset == len(jsonl_dump) + assert be_file.records_total == len(records) + assert be_file.records_since_last_reset == len(records) + + be_file.seek(0) + lines = be_file.readlines() + assert len(lines) == len(records) + + for line_index, jsonl_record in enumerate(lines): + json_loaded = json.loads(jsonl_record) + assert json_loaded == records[line_index] + + be_file.reset() + + assert be_file.bytes_total == len(jsonl_dump) + assert be_file.bytes_since_last_reset == 0 + assert be_file.records_total == len(records) + assert be_file.records_since_last_reset == 0 + + +@pytest.mark.parametrize( + "records", + TEST_RECORDS, +) +def test_batch_export_temporary_file_write_records_to_csv(records): + """Test CSV written by BatchExportTemporaryFile match expected.""" + in_memory_file_obj = io.StringIO() + writer = csv.DictWriter( + in_memory_file_obj, + fieldnames=records[0].keys() if len(records) > 0 else [], + delimiter=",", + quotechar='"', + escapechar="\\", + lineterminator="\n", + quoting=csv.QUOTE_NONE, + ) + writer.writerows(records) + + with BatchExportTemporaryFile(mode="w+") as be_file: + be_file.write_records_to_csv(records) + + assert be_file.bytes_total == in_memory_file_obj.tell() + assert be_file.bytes_since_last_reset == in_memory_file_obj.tell() + assert be_file.records_total == len(records) + assert be_file.records_since_last_reset == len(records) + + be_file.seek(0) + reader = csv.reader( + be_file._file, + delimiter=",", + quotechar='"', + escapechar="\\", + quoting=csv.QUOTE_NONE, + ) + + rows = [row for row in reader] + assert len(rows) == len(records) + + for row_index, csv_record in enumerate(rows): + for value_index, value in enumerate(records[row_index].values()): + # Everything returned by csv.reader is a str. + # This means type information is lost when writing to CSV + # but this just a limitation of the format. + assert csv_record[value_index] == str(value) + + be_file.reset() + + assert be_file.bytes_total == in_memory_file_obj.tell() + assert be_file.bytes_since_last_reset == 0 + assert be_file.records_total == len(records) + assert be_file.records_since_last_reset == 0 + + +@pytest.mark.parametrize( + "records", + TEST_RECORDS, +) +def test_batch_export_temporary_file_write_records_to_tsv(records): + """Test TSV written by BatchExportTemporaryFile match expected.""" + in_memory_file_obj = io.StringIO() + writer = csv.DictWriter( + in_memory_file_obj, + fieldnames=records[0].keys() if len(records) > 0 else [], + delimiter="\t", + quotechar='"', + escapechar="\\", + lineterminator="\n", + quoting=csv.QUOTE_NONE, + ) + writer.writerows(records) + + with BatchExportTemporaryFile(mode="w+") as be_file: + be_file.write_records_to_tsv(records) + + assert be_file.bytes_total == in_memory_file_obj.tell() + assert be_file.bytes_since_last_reset == in_memory_file_obj.tell() + assert be_file.records_total == len(records) + assert be_file.records_since_last_reset == len(records) + + be_file.seek(0) + reader = csv.reader( + be_file._file, + delimiter="\t", + quotechar='"', + escapechar="\\", + quoting=csv.QUOTE_NONE, + ) + + rows = [row for row in reader] + assert len(rows) == len(records) + + for row_index, csv_record in enumerate(rows): + for value_index, value in enumerate(records[row_index].values()): + # Everything returned by csv.reader is a str. + # This means type information is lost when writing to CSV + # but this just a limitation of the format. + assert csv_record[value_index] == str(value) + + be_file.reset() + + assert be_file.bytes_total == in_memory_file_obj.tell() + assert be_file.bytes_since_last_reset == 0 + assert be_file.records_total == len(records) + assert be_file.records_since_last_reset == 0 + + +TEST_RECORD_BATCHES = [ + pa.RecordBatch.from_pydict( + { + "event": pa.array(["test-event-0", "test-event-1", "test-event-2"]), + "properties": pa.array(['{"prop_0": 1, "prop_1": 2}', "{}", "null"]), + "_inserted_at": pa.array([0, 1, 2]), + } + ) +] + + +@pytest.mark.parametrize( + "record_batch", + TEST_RECORD_BATCHES, +) +@pytest.mark.asyncio +async def test_jsonl_writer_writes_record_batches(record_batch): + """Test record batches are written as valid JSONL.""" + in_memory_file_obj = io.BytesIO() + inserted_ats_seen = [] + + async def store_in_memory_on_flush( + batch_export_file, records_since_last_flush, bytes_since_last_flush, last_inserted_at, is_last + ): + in_memory_file_obj.write(batch_export_file.read()) + inserted_ats_seen.append(last_inserted_at) + + writer = JSONLBatchExportWriter(max_bytes=1, flush_callable=store_in_memory_on_flush) + + record_batch = record_batch.sort_by("_inserted_at") + async with writer.open_temporary_file(): + await writer.write_record_batch(record_batch) + + lines = in_memory_file_obj.readlines() + for index, line in enumerate(lines): + written_jsonl = json.loads(line) + + single_record_batch = record_batch.slice(offset=index, length=1) + expected_jsonl = single_record_batch.to_pylist()[0] + + assert "_inserted_at" not in written_jsonl + assert written_jsonl == expected_jsonl + + assert inserted_ats_seen == [record_batch.column("_inserted_at")[-1].as_py()] + + +@pytest.mark.parametrize( + "record_batch", + TEST_RECORD_BATCHES, +) +@pytest.mark.asyncio +async def test_csv_writer_writes_record_batches(record_batch): + """Test record batches are written as valid CSV.""" + in_memory_file_obj = io.StringIO() + inserted_ats_seen = [] + + async def store_in_memory_on_flush( + batch_export_file, records_since_last_flush, bytes_since_last_flush, last_inserted_at, is_last + ): + in_memory_file_obj.write(batch_export_file.read().decode("utf-8")) + inserted_ats_seen.append(last_inserted_at) + + schema_columns = [column_name for column_name in record_batch.column_names if column_name != "_inserted_at"] + writer = CSVBatchExportWriter(max_bytes=1, field_names=schema_columns, flush_callable=store_in_memory_on_flush) + + record_batch = record_batch.sort_by("_inserted_at") + async with writer.open_temporary_file(): + await writer.write_record_batch(record_batch) + + reader = csv.reader( + in_memory_file_obj, + delimiter=",", + quotechar='"', + escapechar="\\", + quoting=csv.QUOTE_NONE, + ) + for index, written_csv_row in enumerate(reader): + single_record_batch = record_batch.slice(offset=index, length=1) + expected_csv = single_record_batch.to_pylist()[0] + + assert "_inserted_at" not in written_csv_row + assert written_csv_row == expected_csv + + assert inserted_ats_seen == [record_batch.column("_inserted_at")[-1].as_py()] + + +@pytest.mark.parametrize( + "record_batch", + TEST_RECORD_BATCHES, +) +@pytest.mark.asyncio +async def test_parquet_writer_writes_record_batches(record_batch): + """Test record batches are written as valid Parquet.""" + in_memory_file_obj = io.BytesIO() + inserted_ats_seen = [] + + async def store_in_memory_on_flush( + batch_export_file, records_since_last_flush, bytes_since_last_flush, last_inserted_at, is_last + ): + in_memory_file_obj.write(batch_export_file.read()) + inserted_ats_seen.append(last_inserted_at) + + schema_columns = [column_name for column_name in record_batch.column_names if column_name != "_inserted_at"] + + writer = ParquetBatchExportWriter( + max_bytes=1, + flush_callable=store_in_memory_on_flush, + schema=record_batch.select(schema_columns).schema, + ) + + record_batch = record_batch.sort_by("_inserted_at") + async with writer.open_temporary_file(): + await writer.write_record_batch(record_batch) + + written_parquet = pq.read_table(in_memory_file_obj) + + for index, written_row_as_dict in enumerate(written_parquet.to_pylist()): + single_record_batch = record_batch.slice(offset=index, length=1) + expected_row_as_dict = single_record_batch.select(schema_columns).to_pylist()[0] + + assert "_inserted_at" not in written_row_as_dict + assert written_row_as_dict == expected_row_as_dict + + # NOTE: Parquet gets flushed twice due to the extra flush at the end for footer bytes, so our mock function + # will see this value twice. + assert inserted_ats_seen == [ + record_batch.column("_inserted_at")[-1].as_py(), + record_batch.column("_inserted_at")[-1].as_py(), + ] + + +@pytest.mark.parametrize( + "record_batch", + TEST_RECORD_BATCHES, +) +@pytest.mark.asyncio +async def test_writing_out_of_scope_of_temporary_file_raises(record_batch): + """Test attempting a write out of temporary file scope raises a `ValueError`.""" + + async def do_nothing(*args, **kwargs): + pass + + schema_columns = [column_name for column_name in record_batch.column_names if column_name != "_inserted_at"] + writer = ParquetBatchExportWriter( + max_bytes=10, + flush_callable=do_nothing, + schema=record_batch.select(schema_columns).schema, + ) + + async with writer.open_temporary_file(): + pass + + with pytest.raises(ValueError, match="Batch export file is closed"): + await writer.write_record_batch(record_batch) + + +@pytest.mark.parametrize( + "record_batch", + TEST_RECORD_BATCHES, +) +@pytest.mark.asyncio +async def test_flushing_parquet_writer_resets_underlying_file(record_batch): + """Test flushing a writer resets underlying file.""" + flush_counter = 0 + + async def track_flushes(*args, **kwargs): + nonlocal flush_counter + flush_counter += 1 + + schema_columns = [column_name for column_name in record_batch.column_names if column_name != "_inserted_at"] + writer = ParquetBatchExportWriter( + max_bytes=10000000, + flush_callable=track_flushes, + schema=record_batch.select(schema_columns).schema, + ) + + async with writer.open_temporary_file(): + await writer.write_record_batch(record_batch) + + assert writer.batch_export_file.tell() > 0 + assert writer.bytes_since_last_flush > 0 + assert writer.bytes_since_last_flush == writer.batch_export_file.bytes_since_last_reset + assert writer.records_since_last_flush == record_batch.num_rows + + await writer.flush(dt.datetime.now()) + + assert flush_counter == 1 + assert writer.batch_export_file.tell() == 0 + assert writer.bytes_since_last_flush == 0 + assert writer.bytes_since_last_flush == writer.batch_export_file.bytes_since_last_reset + assert writer.records_since_last_flush == 0 + + assert flush_counter == 2 From fcc192e83db4273e0904993922cba9a62c2d3752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 22 Mar 2024 14:51:00 +0100 Subject: [PATCH 39/51] feat: Frontend support for file_format in S3 batch export (#21076) * refactor: Support for multiple file formats in batch exports * refactor: Prefer composition over inheritance * refactor: More clearly separate writer from temporary file We now should be more explicit about what is the context in which the batch export temporary file is alive. The writer outlives this context, so it can be used by callers to, for example, check how many records were written. * test: More parquet testing * refactor: Move temporary file to new module * feat: Frontend support for file_format * fix: Remove redefinition caused by rebase --- .../src/scenes/batch_exports/BatchExportEditForm.tsx | 9 +++++++++ .../src/scenes/batch_exports/BatchExports.stories.tsx | 1 + .../src/scenes/batch_exports/batchExportEditLogic.ts | 1 + frontend/src/types.ts | 1 + 4 files changed, 12 insertions(+) diff --git a/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx b/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx index b015659cfaef1..a2a9f9968f82c 100644 --- a/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx +++ b/frontend/src/scenes/batch_exports/BatchExportEditForm.tsx @@ -242,6 +242,15 @@ export function BatchExportsEditFields({ ]} /> + + + +
diff --git a/frontend/src/scenes/batch_exports/BatchExports.stories.tsx b/frontend/src/scenes/batch_exports/BatchExports.stories.tsx index 0dd616c44982a..dbd6779cb208d 100644 --- a/frontend/src/scenes/batch_exports/BatchExports.stories.tsx +++ b/frontend/src/scenes/batch_exports/BatchExports.stories.tsx @@ -42,6 +42,7 @@ export default { include_events: [], encryption: null, kms_key_id: null, + file_format: 'JSONLines', }, }, start_at: null, diff --git a/frontend/src/scenes/batch_exports/batchExportEditLogic.ts b/frontend/src/scenes/batch_exports/batchExportEditLogic.ts index bc86d1618fe4f..30c123256d81a 100644 --- a/frontend/src/scenes/batch_exports/batchExportEditLogic.ts +++ b/frontend/src/scenes/batch_exports/batchExportEditLogic.ts @@ -90,6 +90,7 @@ export const batchExportFormFields = ( aws_secret_access_key: isNew ? (!config.aws_secret_access_key ? 'This field is required' : '') : '', compression: '', encryption: '', + file_format: isNew ? (!config.file_format ? 'This field is required' : '') : '', kms_key_id: !config.kms_key_id && config.encryption == 'aws:kms' ? 'This field is required' : '', exclude_events: '', include_events: '', diff --git a/frontend/src/types.ts b/frontend/src/types.ts index be96494253ae8..58f54b786a7df 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -3597,6 +3597,7 @@ export type BatchExportDestinationS3 = { encryption: string | null kms_key_id: string | null endpoint_url: string | null + file_format: string } } From 17c5876ef880f3771af2f4a08f88b01e9bb752a9 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Fri, 22 Mar 2024 15:05:54 +0100 Subject: [PATCH 40/51] chore(blobby): raise overflow trigger again (#21104) --- plugin-server/src/config/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin-server/src/config/config.ts b/plugin-server/src/config/config.ts index dcaebe4c1097a..def72eea474bb 100644 --- a/plugin-server/src/config/config.ts +++ b/plugin-server/src/config/config.ts @@ -164,8 +164,8 @@ export function getDefaultConfig(): PluginsServerConfig { SESSION_RECORDING_KAFKA_DEBUG: undefined, SESSION_RECORDING_MAX_PARALLEL_FLUSHES: 10, SESSION_RECORDING_OVERFLOW_ENABLED: false, - SESSION_RECORDING_OVERFLOW_BUCKET_REPLENISH_RATE: 2_000_000, // 2MB/second uncompressed, sustained - SESSION_RECORDING_OVERFLOW_BUCKET_CAPACITY: 100_000_000, // 100MB burst + SESSION_RECORDING_OVERFLOW_BUCKET_REPLENISH_RATE: 5_000_000, // 5MB/second uncompressed, sustained + SESSION_RECORDING_OVERFLOW_BUCKET_CAPACITY: 200_000_000, // 200MB burst } } From e5bec128fc75cf42ab61bfa274fa856b6ef52556 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Fri, 22 Mar 2024 17:48:43 +0000 Subject: [PATCH 41/51] fix: invalid text node should not render (#21093) --- .../increment-with-child-duplication.json | 7 + .../__snapshots__/transform.test.ts.snap | 162 ++++++++++++++++++ ee/frontend/mobile-replay/transform.test.ts | 37 ++++ .../mobile-replay/transformer/transformers.ts | 26 ++- 4 files changed, 223 insertions(+), 9 deletions(-) diff --git a/ee/frontend/mobile-replay/__mocks__/increment-with-child-duplication.json b/ee/frontend/mobile-replay/__mocks__/increment-with-child-duplication.json index c17efc6d9e246..7ffc2e5f38e5c 100644 --- a/ee/frontend/mobile-replay/__mocks__/increment-with-child-duplication.json +++ b/ee/frontend/mobile-replay/__mocks__/increment-with-child-duplication.json @@ -191,6 +191,13 @@ "x": 66, "y": 556 } + }, + { + "parentId": 209272202, + "wireframe": { + "id": 52129787123, + "type": "text" + } } ], "removes": [ diff --git a/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap b/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap index a421f7ff220bf..bbde91f8defc6 100644 --- a/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap +++ b/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap @@ -160,6 +160,147 @@ exports[`replay/transform transform can convert images 1`] = ` ] `; +exports[`replay/transform transform can convert invalid text wireframe 1`] = ` +[ + { + "data": { + "height": 600, + "href": "", + "width": 300, + }, + "timestamp": 1, + "type": 4, + }, + { + "data": { + "initialOffset": { + "left": 0, + "top": 0, + }, + "node": { + "childNodes": [ + { + "id": 2, + "name": "html", + "publicId": "", + "systemId": "", + "type": 1, + }, + { + "attributes": { + "data-rrweb-id": 3, + "style": "height: 100vh; width: 100vw;", + }, + "childNodes": [ + { + "attributes": { + "data-rrweb-id": 4, + }, + "childNodes": [ + { + "attributes": { + "type": "text/css", + }, + "childNodes": [ + { + "id": 102, + "textContent": " + body { + margin: unset; + } + input, button, select, textarea { + font: inherit; + margin: 0; + padding: 0; + border: 0; + outline: 0; + background: transparent; + padding-block: 0 !important; + } + .input:focus { + outline: none; + } + img { + border-style: none; + } + ", + "type": 3, + }, + ], + "id": 101, + "tagName": "style", + "type": 2, + }, + ], + "id": 4, + "tagName": "head", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 5, + "style": "height: 100vh; width: 100vw;", + }, + "childNodes": [ + { + "attributes": { + "data-rrweb-id": 12345, + "style": "border-width: 4px;border-radius: 10px;border-color: #ee3ee4;border-style: solid;color: #ee3ee4;width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;overflow:hidden;white-space:normal;", + }, + "childNodes": [], + "id": 12345, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, + ], + "id": 5, + "tagName": "body", + "type": 2, + }, + ], + "id": 3, + "tagName": "html", + "type": 2, + }, + ], + "id": 1, + "type": 0, + }, + }, + "timestamp": 1, + "type": 2, + }, +] +`; + exports[`replay/transform transform can convert navigation bar 1`] = ` [ { @@ -1453,6 +1594,20 @@ exports[`replay/transform transform incremental mutations de-duplicate the tree }, "parentId": 52129787, }, + { + "nextId": null, + "node": { + "attributes": { + "data-rrweb-id": 52129787123, + "style": "position: fixed;left: 0px;top: 0px;overflow:hidden;white-space:normal;", + }, + "childNodes": [], + "id": 52129787123, + "tagName": "div", + "type": 2, + }, + "parentId": 209272202, + }, ], "attributes": [], "removes": [ @@ -1689,6 +1844,13 @@ AAAAAAAAAAAAAAAAAAAAAAAAgCN/AW0xMqHnNQceAAAAAElFTkSuQmCC "y": 556, }, }, + { + "parentId": 209272202, + "wireframe": { + "id": 52129787123, + "type": "text", + }, + }, ], "removes": [ { diff --git a/ee/frontend/mobile-replay/transform.test.ts b/ee/frontend/mobile-replay/transform.test.ts index 788bb65655d3d..92384e48b2986 100644 --- a/ee/frontend/mobile-replay/transform.test.ts +++ b/ee/frontend/mobile-replay/transform.test.ts @@ -480,6 +480,43 @@ describe('replay/transform', () => { expect(converted).toMatchSnapshot() }) + test('can convert invalid text wireframe', () => { + const converted = posthogEEModule.mobileReplay?.transformToWeb([ + { + data: { + width: 300, + height: 600, + }, + timestamp: 1, + type: 4, + }, + { + type: 2, + data: { + wireframes: [ + { + id: 12345, + type: 'text', + x: 11, + y: 12, + width: 100, + height: 30, + style: { + color: '#ee3ee4', + borderColor: '#ee3ee4', + borderWidth: '4', + borderRadius: '10px', + }, + // text property is missing + }, + ], + }, + timestamp: 1, + }, + ]) + expect(converted).toMatchSnapshot() + }) + test('can set background image to base64 png', () => { const converted = posthogEEModule.mobileReplay?.transformToWeb([ { diff --git a/ee/frontend/mobile-replay/transformer/transformers.ts b/ee/frontend/mobile-replay/transformer/transformers.ts index 1527a24d7dbeb..f2b7324a475fa 100644 --- a/ee/frontend/mobile-replay/transformer/transformers.ts +++ b/ee/frontend/mobile-replay/transformer/transformers.ts @@ -105,6 +105,10 @@ export function _isPositiveInteger(id: unknown): id is number { return typeof id === 'number' && id > 0 && id % 1 === 0 } +function _isNullish(x: unknown): x is null | undefined { + return x === null || x === undefined +} + function isRemovedNodeMutation(x: addedNodeMutation | removedNodeMutation): x is removedNodeMutation { return isObject(x) && 'id' in x } @@ -218,6 +222,17 @@ function makeTextElement( // because we might have to style the text, we always wrap it in a div // and apply styles to that const id = context.idSequence.next().value + + const childNodes = [...children] + if (!_isNullish(wireframe.text)) { + childNodes.unshift({ + type: NodeType.Text, + textContent: wireframe.text, + // since the text node is wrapped, we assign it a synthetic id + id, + }) + } + return { result: { type: NodeType.Element, @@ -227,15 +242,7 @@ function makeTextElement( 'data-rrweb-id': wireframe.id, }, id: wireframe.id, - childNodes: [ - { - type: NodeType.Text, - textContent: wireframe.text, - // since the text node is wrapped, we assign it a synthetic id - id: id, - }, - ...children, - ], + childNodes, }, context, } @@ -983,6 +990,7 @@ function isMobileIncrementalSnapshotEvent(x: unknown): x is MobileIncrementalSna function makeIncrementalAdd(add: MobileNodeMutation, context: ConversionContext): addedNodeMutation[] | null { const converted = convertWireframe(add.wireframe, context) + if (!converted) { return null } From 060ebc2b0b56e51fcee2e12e4433c38f99911e2e Mon Sep 17 00:00:00 2001 From: ted kaemming <65315+tkaemming@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:59:12 -0700 Subject: [PATCH 42/51] feat(hogql): Add basic support for PoE v3 (distinct ID overrides) (#21059) --- ...-funnel-top-to-bottom-breakdown--light.png | Bin 105910 -> 105965 bytes frontend/src/queries/schema.json | 2 +- frontend/src/queries/schema.ts | 2 +- frontend/src/scenes/debug/HogQLDebug.tsx | 1 + mypy-baseline.txt | 2 + posthog/hogql/database/database.py | 25 ++++ .../schema/person_distinct_id_overrides.py | 92 ++++++++++++++ .../test/__snapshots__/test_database.ambr | 116 ++++++++++++++++++ posthog/hogql/test/test_modifiers.py | 7 ++ posthog/schema.py | 1 + 10 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 posthog/hogql/database/schema/person_distinct_id_overrides.py diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown--light.png index 7067293aa1a1eec5e1360ece7c1c77d2f3e46d80..2ed59b8746a73cb5c382c733e3d4bd520133f0c0 100644 GIT binary patch literal 105965 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%0DU2eZ(@}n2kW`8;s-25h2fFf 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

_!X9%CyIOjyy`~GTL`RKF{6$ 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%0DhydriVb}dgbTIehtHyclW>xmdB(})5R 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 z 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 setQuery({ diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 9d5fe48cb5777..b8d2d1c94da64 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -85,6 +85,7 @@ posthog/hogql/parser.py:0: error: "None" has no attribute "text" [attr-defined] posthog/hogql/parser.py:0: error: "None" has no attribute "text" [attr-defined] posthog/hogql/parser.py:0: error: Statement is unreachable [unreachable] posthog/hogql/database/schema/person_distinct_ids.py:0: error: Argument 1 to "select_from_person_distinct_ids_table" has incompatible type "dict[str, list[str]]"; expected "dict[str, list[str | int]]" [arg-type] +posthog/hogql/database/schema/person_distinct_id_overrides.py:0: error: Argument 1 to "select_from_person_distinct_id_overrides_table" has incompatible type "dict[str, list[str]]"; expected "dict[str, list[str | int]]" [arg-type] posthog/hogql/database/schema/cohort_people.py:0: error: Argument "chain" to "Field" has incompatible type "list[str]"; expected "list[str | int]" [arg-type] posthog/hogql/database/schema/cohort_people.py:0: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql/database/schema/cohort_people.py:0: note: Consider using "Sequence" instead, which is covariant @@ -105,6 +106,7 @@ posthog/hogql/database/database.py:0: error: "FieldOrTable" has no attribute "fi posthog/hogql/database/database.py:0: error: "FieldOrTable" has no attribute "fields" [attr-defined] posthog/hogql/database/database.py:0: error: "FieldOrTable" has no attribute "fields" [attr-defined] posthog/hogql/database/database.py:0: error: "FieldOrTable" has no attribute "fields" [attr-defined] +posthog/hogql/database/database.py:0: error: "FieldOrTable" has no attribute "fields" [attr-defined] posthog/hogql/database/database.py:0: error: Incompatible types (expression has type "Literal['view', 'lazy_table']", TypedDict item "type" has type "Literal['integer', 'float', 'string', 'datetime', 'date', 'boolean', 'array', 'json', 'lazy_table', 'virtual_table', 'field_traverser', 'expression']") [typeddict-item] posthog/warehouse/models/datawarehouse_saved_query.py:0: error: Argument 1 to "create_hogql_database" has incompatible type "int | None"; expected "int" [arg-type] posthog/warehouse/models/datawarehouse_saved_query.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "SelectQuery | SelectUnionQuery") [assignment] diff --git a/posthog/hogql/database/database.py b/posthog/hogql/database/database.py index 6909211070e59..de12a8267d911 100644 --- a/posthog/hogql/database/database.py +++ b/posthog/hogql/database/database.py @@ -31,6 +31,11 @@ from posthog.hogql.database.schema.events import EventsTable from posthog.hogql.database.schema.groups import GroupsTable, RawGroupsTable from posthog.hogql.database.schema.numbers import NumbersTable +from posthog.hogql.database.schema.person_distinct_id_overrides import ( + PersonDistinctIdOverridesTable, + RawPersonDistinctIdOverridesTable, + join_with_person_distinct_id_overrides_table, +) from posthog.hogql.database.schema.person_distinct_ids import ( PersonDistinctIdsTable, RawPersonDistinctIdsTable, @@ -66,6 +71,7 @@ class Database(BaseModel): groups: GroupsTable = GroupsTable() persons: PersonsTable = PersonsTable() person_distinct_ids: PersonDistinctIdsTable = PersonDistinctIdsTable() + person_distinct_id_overrides: PersonDistinctIdOverridesTable = PersonDistinctIdOverridesTable() person_overrides: PersonOverridesTable = PersonOverridesTable() session_replay_events: SessionReplayEventsTable = SessionReplayEventsTable() @@ -81,6 +87,7 @@ class Database(BaseModel): raw_persons: RawPersonsTable = RawPersonsTable() raw_groups: RawGroupsTable = RawGroupsTable() raw_cohort_people: RawCohortPeople = RawCohortPeople() + raw_person_distinct_id_overrides: RawPersonDistinctIdOverridesTable = RawPersonDistinctIdOverridesTable() raw_person_overrides: RawPersonOverridesTable = RawPersonOverridesTable() raw_sessions: RawSessionsTable = RawSessionsTable() @@ -186,6 +193,24 @@ def create_hogql_database( database.events.fields["poe"].fields["id"] = database.events.fields["person_id"] database.events.fields["person"] = FieldTraverser(chain=["poe"]) + elif modifiers.personsOnEventsMode == PersonsOnEventsMode.v3_enabled: + database.events.fields["event_person_id"] = StringDatabaseField(name="person_id") + database.events.fields["override"] = LazyJoin( + from_field=["distinct_id"], # ??? + join_table=PersonDistinctIdOverridesTable(), + join_function=join_with_person_distinct_id_overrides_table, + ) + database.events.fields["person_id"] = ExpressionField( + name="person_id", + expr=parse_expr( + # NOTE: assumes `join_use_nulls = 0` (the default), as ``override.distinct_id`` is not Nullable + "if(not(empty(override.distinct_id)), override.person_id, event_person_id)", + start=None, + ), + ) + database.events.fields["poe"].fields["id"] = database.events.fields["person_id"] + database.events.fields["person"] = FieldTraverser(chain=["poe"]) + database.persons.fields["$virt_initial_referring_domain_type"] = create_initial_domain_type( "$virt_initial_referring_domain_type" ) diff --git a/posthog/hogql/database/schema/person_distinct_id_overrides.py b/posthog/hogql/database/schema/person_distinct_id_overrides.py new file mode 100644 index 0000000000000..ae31440b10b7d --- /dev/null +++ b/posthog/hogql/database/schema/person_distinct_id_overrides.py @@ -0,0 +1,92 @@ +from typing import Dict, List +from posthog.hogql.ast import SelectQuery +from posthog.hogql.context import HogQLContext + +from posthog.hogql.database.argmax import argmax_select +from posthog.hogql.database.models import ( + Table, + IntegerDatabaseField, + StringDatabaseField, + BooleanDatabaseField, + LazyJoin, + LazyTable, + FieldOrTable, +) +from posthog.hogql.database.schema.persons import join_with_persons_table +from posthog.hogql.errors import HogQLException +from posthog.schema import HogQLQueryModifiers + +PERSON_DISTINCT_ID_OVERRIDES_FIELDS = { + "team_id": IntegerDatabaseField(name="team_id"), + "distinct_id": StringDatabaseField(name="distinct_id"), + "person_id": StringDatabaseField(name="person_id"), + "person": LazyJoin( + from_field=["person_id"], + join_table="persons", + join_function=join_with_persons_table, + ), +} + + +def select_from_person_distinct_id_overrides_table(requested_fields: Dict[str, List[str | int]]): + # Always include "person_id", as it's the key we use to make further joins, and it'd be great if it's available + if "person_id" not in requested_fields: + requested_fields = {**requested_fields, "person_id": ["person_id"]} + return argmax_select( + table_name="raw_person_distinct_id_overrides", + select_fields=requested_fields, + group_fields=["distinct_id"], + argmax_field="version", + deleted_field="is_deleted", + ) + + +def join_with_person_distinct_id_overrides_table( + from_table: str, + to_table: str, + requested_fields: Dict[str, List[str]], + context: HogQLContext, + node: SelectQuery, +): + from posthog.hogql import ast + + if not requested_fields: + raise HogQLException("No fields requested from person_distinct_id_overrides") + join_expr = ast.JoinExpr(table=select_from_person_distinct_id_overrides_table(requested_fields)) + join_expr.join_type = "LEFT OUTER JOIN" + join_expr.alias = to_table + join_expr.constraint = ast.JoinConstraint( + expr=ast.CompareOperation( + op=ast.CompareOperationOp.Eq, + left=ast.Field(chain=[from_table, "distinct_id"]), + right=ast.Field(chain=[to_table, "distinct_id"]), + ) + ) + return join_expr + + +class RawPersonDistinctIdOverridesTable(Table): + fields: Dict[str, FieldOrTable] = { + **PERSON_DISTINCT_ID_OVERRIDES_FIELDS, + "is_deleted": BooleanDatabaseField(name="is_deleted"), + "version": IntegerDatabaseField(name="version"), + } + + def to_printed_clickhouse(self, context): + return "person_distinct_id_overrides" + + def to_printed_hogql(self): + return "raw_person_distinct_id_overrides" + + +class PersonDistinctIdOverridesTable(LazyTable): + fields: Dict[str, FieldOrTable] = PERSON_DISTINCT_ID_OVERRIDES_FIELDS + + def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): + return select_from_person_distinct_id_overrides_table(requested_fields) + + def to_printed_clickhouse(self, context): + return "person_distinct_id_overrides" + + def to_printed_hogql(self): + return "person_distinct_id_overrides" diff --git a/posthog/hogql/database/test/__snapshots__/test_database.ambr b/posthog/hogql/database/test/__snapshots__/test_database.ambr index db4dfc8f6df9f..63c2d16ce87aa 100644 --- a/posthog/hogql/database/test/__snapshots__/test_database.ambr +++ b/posthog/hogql/database/test/__snapshots__/test_database.ambr @@ -304,6 +304,31 @@ ] } ], + "person_distinct_id_overrides": [ + { + "key": "distinct_id", + "type": "string" + }, + { + "key": "person_id", + "type": "string" + }, + { + "key": "person", + "type": "lazy_table", + "table": "persons", + "fields": [ + "id", + "created_at", + "team_id", + "properties", + "is_identified", + "pdi", + "$virt_initial_referring_domain_type", + "$virt_initial_channel_type" + ] + } + ], "person_overrides": [ { "key": "old_person_id", @@ -790,6 +815,39 @@ "type": "integer" } ], + "raw_person_distinct_id_overrides": [ + { + "key": "distinct_id", + "type": "string" + }, + { + "key": "person_id", + "type": "string" + }, + { + "key": "person", + "type": "lazy_table", + "table": "persons", + "fields": [ + "id", + "created_at", + "team_id", + "properties", + "is_identified", + "pdi", + "$virt_initial_referring_domain_type", + "$virt_initial_channel_type" + ] + }, + { + "key": "is_deleted", + "type": "boolean" + }, + { + "key": "version", + "type": "integer" + } + ], "raw_person_overrides": [ { "key": "old_person_id", @@ -1155,6 +1213,31 @@ ] } ], + "person_distinct_id_overrides": [ + { + "key": "distinct_id", + "type": "string" + }, + { + "key": "person_id", + "type": "string" + }, + { + "key": "person", + "type": "lazy_table", + "table": "persons", + "fields": [ + "id", + "created_at", + "team_id", + "properties", + "is_identified", + "pdi", + "$virt_initial_referring_domain_type", + "$virt_initial_channel_type" + ] + } + ], "person_overrides": [ { "key": "old_person_id", @@ -1641,6 +1724,39 @@ "type": "integer" } ], + "raw_person_distinct_id_overrides": [ + { + "key": "distinct_id", + "type": "string" + }, + { + "key": "person_id", + "type": "string" + }, + { + "key": "person", + "type": "lazy_table", + "table": "persons", + "fields": [ + "id", + "created_at", + "team_id", + "properties", + "is_identified", + "pdi", + "$virt_initial_referring_domain_type", + "$virt_initial_channel_type" + ] + }, + { + "key": "is_deleted", + "type": "boolean" + }, + { + "key": "version", + "type": "integer" + } + ], "raw_person_overrides": [ { "key": "old_person_id", diff --git a/posthog/hogql/test/test_modifiers.py b/posthog/hogql/test/test_modifiers.py index eba1f5195ab3d..b2b0ef1e40630 100644 --- a/posthog/hogql/test/test_modifiers.py +++ b/posthog/hogql/test/test_modifiers.py @@ -74,6 +74,13 @@ def test_modifiers_persons_on_events_mode_mapping(self): "events.person_properties AS properties", "toTimeZone(events.person_created_at, %(hogql_val_1)s) AS created_at", ), + ( + PersonsOnEventsMode.v3_enabled, + "events.event AS event", + "if(not(empty(events__override.distinct_id)), events__override.person_id, events.person_id) AS id", + "events.person_properties AS properties", + "toTimeZone(events.person_created_at, %(hogql_val_0)s) AS created_at", + ), ] for mode, *expected in test_cases: diff --git a/posthog/schema.py b/posthog/schema.py index dc77da163db17..c88f8bf3f76de 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -418,6 +418,7 @@ class PersonsOnEventsMode(str, Enum): v1_enabled = "v1_enabled" v1_mixed = "v1_mixed" v2_enabled = "v2_enabled" + v3_enabled = "v3_enabled" class HogQLQueryModifiers(BaseModel): From 3bacde5dfa3a19933dfaded10f255e1939a63d5d Mon Sep 17 00:00:00 2001 From: ted kaemming <65315+tkaemming@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:00:26 -0700 Subject: [PATCH 43/51] fix: Update `PersonDistinctIdOverridesTable` to be consistent with updated `LazyTable` interface (#21113) --- posthog/hogql/database/schema/person_distinct_id_overrides.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/posthog/hogql/database/schema/person_distinct_id_overrides.py b/posthog/hogql/database/schema/person_distinct_id_overrides.py index ae31440b10b7d..34df59655c24d 100644 --- a/posthog/hogql/database/schema/person_distinct_id_overrides.py +++ b/posthog/hogql/database/schema/person_distinct_id_overrides.py @@ -14,7 +14,6 @@ ) from posthog.hogql.database.schema.persons import join_with_persons_table from posthog.hogql.errors import HogQLException -from posthog.schema import HogQLQueryModifiers PERSON_DISTINCT_ID_OVERRIDES_FIELDS = { "team_id": IntegerDatabaseField(name="team_id"), @@ -82,7 +81,7 @@ def to_printed_hogql(self): class PersonDistinctIdOverridesTable(LazyTable): fields: Dict[str, FieldOrTable] = PERSON_DISTINCT_ID_OVERRIDES_FIELDS - def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], context: HogQLContext, node: SelectQuery): return select_from_person_distinct_id_overrides_table(requested_fields) def to_printed_clickhouse(self, context): From ced62f45d1b9a4a530605969864aaf2c34fb4cef Mon Sep 17 00:00:00 2001 From: Zach Waterfield Date: Fri, 22 Mar 2024 14:46:52 -0600 Subject: [PATCH 44/51] chore: signup page footer support copy (#21089) * Remove slack from signup support methods * Update SignupContainer.tsx * Update UI snapshots for `chromium` (1) --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .../scenes-other-signup--cloud--light.png | Bin 63485 -> 64643 bytes ...cenes-other-signup--self-hosted--light.png | Bin 62733 -> 63330 bytes ...s-other-signup--self-hosted-sso--light.png | Bin 70840 -> 71521 bytes .../authentication/signup/SignupContainer.tsx | 8 ++------ 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/__snapshots__/scenes-other-signup--cloud--light.png b/frontend/__snapshots__/scenes-other-signup--cloud--light.png index c527248055ffa4a131f6540dbcf48b9de1a055a8..05ee6352a0fad41d9d96e5804390b7a97cc74a1e 100644 GIT binary patch literal 64643 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

2O&+JSidI%xI{$GXWr{qS}a_76z^0L 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%0AI$BBk_3O7=Cnm&Ev)ga6+Hkpj=?<$WDFqdf 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%0AeQ$vMKc;|k0JN=l(+q67Ugbh%*p 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 QmYlPaa~_7w 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⪻eQ!n3U+SyH@ zaQzG?L<;zx&AX@?utwd;vyIpw<|$g01;;`8o_s*1D>t^O00>~iM7J8|kGPly&a}qn zXqCHTfYZI%tHE_D_$7-WY|@S{nF!Ovx4HW(2bM~u;s$&>OB>$)RRDtU&M}a2yC_n) zZGKkvIPe3l5r@r>v`YcThR{AmtF)_Z#ovtU0|(VCnBQc)nU>pj)EwcFvNIZ=_{ws# zZ@_-k`1cqWhxve=+x~o>Vons99yr5fK%xn1mr~+~W}@Ny;YulQGz?j^#7rvxX#7QD-Qn8_c>y5--uzP+ssRdJ!iunV8^0gG4{ z^Z|N43zh5lk@8p+M6;XIiy=77#p7_`bXLGhK>Z#Ix` zlq2jb=`J)0R)#NrbzF^Rw*E9wAa)RE1sYB{c&?i^JLE3o0__<~rwms6Bm2!b88p9& zhR{@kWgUrVFbrx)gp!7Ce?e()*U5BX6K~4u0R(<`G5Nl1Y-jVZPE$vup|ZYy5;Vmy zMOqs8bn>ct_5R_tL7M`#P;BHIw)O&C{z)QTBJ}QOz>?=GcW5$9`);oMs$L>wgX)Pz zPneFGnR!@`&x=cP+x~GXS6Ih<>!m_l+8GW7YOi5uvf4Vfbz)_mnbF?6W(D2xx({#}^r1e3szfyS`}g2%HBgS} z&$65S5|qeh@@P%h;WY2{%IELuHLq?oDNOl=9eacKPyi1Q5j#7(6cdx7q#y^g(V!cs zm0zFnjN0Mn1`Ahr+`RpKAa(To86o42SAv%!*?kZ~%YCY6bJKlU3CbF+oN9MA!jhYM z4wKP^i@Y;KhRlS)?~DO!87P>)xjcMOE*<|c3;1^TI63#^n|kWm^Tpu-06IE9pIVk? zAERu7S;sN*Bqbn7`LJ>I!U{$Mhz_!S7LE^uzW3}$km-eYe*LLj`dPnn?}n^Mt`{q& zZ5cL~OqoSkl3?rj%`{V6c^!8qv6K)Gs#pb%`6I)NCNyHKPvRoRfA;gd*L5^I82UKX zhmPJp;9{GMB^!^YLJK~x!#$L&|9XdY1uK_wB=B>yPnrljLK{FWbfeEw%v-^xI}$+! zxXB23;1wPTM@X~a zhP_-_+)bj}cPv(R#22@!VqQ)qb@q{r_RYHrjGAALB3t$K4|Cp-Z(@tTw#ZBOa%~;r zzs5|f#PnKI4C`?rTctAV=vCUjfIo}$x)q)vCP;K%@E!2!e@CAp#vBM(gI|XX6l)hvP!On5O`^3YZ)fnP9~ncfNN^m&D4j zI+LM0*ul-__Vvxp7App|ET()t0zy^IGZ${iiZ-?$cZAJyK}Cj0zD2 zb>`-|3J8&JxHSK6$6CZKl^p-RwHnG69oAfb1J-EceB_DqQ_p7GsQI|r?%k8G2B~1a zp?jLk7KIA5k>p8DS)+QsG~6drC00TF(QF#QiiuAWPM$i&;eL1;csU|4C!u50n62Bq zySrHdR(4n5tvBomSd#sT=61xi-LAb^R4Q1Xofyy;H@D-x3sNqtWMcBI(E$rN)ws=u z`Dp3kP>C<2gbzf4=Lw6}tt)HNVxf5{GD?!heszRlK3X{gtjW+a5scjs;*ll2x4YXo zIH;lHwp9omoQ&>NA&$Kxyg!eO$7M53+v)geYb1-*G&y}LgqbuDjI^oixN)e~dinMI zzA6OyO5pMF0fiqKn>MV$P8h$t(j1xGkY}fwM_m(qqi;t~O#bogXV%JciInC0Q_lMd zq~s&4WP3`%k8s+3Lsyt37(crSM5RIBj$PCBPjO(8#F8-olg1p(Qd2x9=FD#2uNVme5cAI)@=Oejel>;NO0z7xhRHz(n zzMkC*F@(t*yJ^5BxtS!$t;RbXo8Qi@^0a%P2YuGp*Sp{~3sX%B z_tZ1f9y%-~jE(igXS(K$8Wop!XXcPBO_M7veYFJe#EmFmCESg)q6lsvO609 z8hZocFGh8?6L>~1wvqQpTDu8^8*JT(@?Q~H&8bosIjQ019OfNX+@0rl!}VG+NWYgW#zoC3z4t6~=s^lRWkG4f z2NYM^{LDSe?dH3?zg6TOg75B*hPGsv82eYh776JCZ$OQK5vn@E9xdL`{UKaTftW; z3BpyoL(;Q{^7awc2Ys3*ux@Aqz8_z23+{!pxyi=L8d=fQu{RAcaB2QjR%!(=TTAcy@ zCTF_&WhVUJtP920tzC?yLVL+RU}(R&@6eyl{B0OXti0Lro&31X+)ZOfVePRa)`}H{ zdh(}%LYc$k&iF`ASxas&(a`PMQqE(3uGhtf>buQKQu~bJ3Vp)HFB(qnWTnt3Rjg=e zpiny^e_tn~QFatPaV^%!2rW9mK2I}~o^T*9|RCPToPJ1$|TI-tjrHn>PYxlQQJ zz}~{QM1ols{Eau!} z5uEgGc^xer)gx3=kR2xXJmT6`4$)Qd0*P3PjbMy!N4ieV{9gafO(iTU?7nVOjS?)0 z)}zW02DmQN6thvzFU9XnlRJYkg}21za=Q*Aj@Xml3S^J3Vw%LujqG2Whd4O9G5!Ipht10P~Saz%S^x$olg(eJG- z;27LrHI5-7(m@F>Xs4~-!3$xx!QgB8OUEU+<#DLLl%`5kpO3+7NYfb+N@eOVT#b)u zcH8;%LE)D{Ik95n&k#E@?~m07{f_SZ!eqEIikgq*DZVq&E_kF~4ieywzt`-W;` z67B1X%_o5o(aLK*Q4*YL3~Dh^=i$B~UFVWnaDccA=NV{=e?Dt`S*@u`zq9?(+u66TL2!q|Up9UNGjxdB7l*;bekYA+(Qilq)3v-7#>K zdv8v!r*JnWAOE1}XO7=Fy{(B)J}&9u5f@xY3a8s2Eb~MqR55Iz<*3`~3ooM@`;iCU z4A-3|cc%etLf)rqFJIkSBFcn}BzZAE!9>wR5omr&J_K1e6bs*LFAYe6s=O&XSHDTV z@WqHco(ejJF&3K)gr=l{k#r@U45`KKwwxTz(nO%i1DJ58%^dWCDu6jn9lA_=Gxd}y z@hDw%c>>^8-%ossEfZ$&+jbfmb=}u02gpPZ%O8{3#jOC@OjEDStP9LMWUOs-LWSXXsnp$W)$<*eC z;2z)zB_4cFIV~}c?+qJob};A)9r)Isn4VF}`i!mkoe14c+rG*05svPK0IKC7qC^4) zb+mfN9hdn!>;m-K(A$%}2FpZ8RiM7jjpj7Y_R3J~zQ|OOHtK*4U+CH`sOQRZPue0c z887D;&49a@H)z@R9_R99CD=dq6T2TT726)Pv>lZ8P@q`TCCn`u7MF7t40tz>st8+$ zos?#8n-ll4RP*C(M__C=R8r-S4c(5*s>vBlJ7;h872KYRHsKrl9OiaXv5RTLmVV^G z@sSdPaVM?Dm#uEB!3w&n*d^mljykUy1{*8gu~Q9;Jj2!HtLVDgc=BQEex9Z0g39lX z;+J}xo$V_23?rZAWcC&cY1a)(&4I;Je@PIf#_80d;a%k*g+NLdZrp|G=P9OBALP^z z*B%3ms#(E*WQQK{@Cb%%OE`e|i?tr@+!t;N@xIy&8UABMn3X68=F$b=bs|NGbx2?4 zFzJyM8YMa)`6&oXY!t$9-!H*Aw3MYIYQbbWxXl1rQcH*G5I}7#_Y}V-&`1)_ z4#9+9M^?*`lMEVVn%8Ok6=2>QkWp+9$ZX*EGOQZ*DLB{MnAekDM!EM0X?S1nr) z*5|&{+BK<9547?4HxnG*Ey;s11G~|Rh|A$xnacv2Rw+f2Xg1Q>MO{I2QhL&5MpFK6 z(Nu`c*2v4%hoeGz!U5eCd$xI+mM(pgIRc=!S-@;4T_*jd1HN{>u&Ov*z?L?Oho{wj zC~#z_4n|iMLapaVB7ijYx|%tOhL=dC!lxakt43X8Mmep*JK$Q&fk6!ID(ku8Digq;1 zu^LD50eNMh$}RPwqq{gjor*y0D*&ZaT|)zuN5(J{fSAQ5a>=0_9%L4}J0EF@%h5#hGXDm38y796DE3z~8NCVE z8*%}2EMIyOpW4pX?>tp40dp?rhN>ZBSGF#^;c-4I9!pD04$Bcu)4nXnv!0;Qlm*2) zD+U)yJNh6k3N-k)coJhj3m#~!H#R&yQ2>od1cUbOUjP{;9A*yD=A0H?qHu3TFQ6Qk zI=u3Yj2DZR2X+ql>+0)}MltXQRG(Ykp6)4Fir^GV7GV4h|T7Jvh<%!At8@?`}`uVJpi+$DC)v;(JM z#ZxsrO)ZPT>bs6Y!0ThTnR)@9znY+he`qy^2aD)VH`1v?S&^ND!$-Gx5R*073?s^w z8*rDmoQBKRzq+Iyc!adONG!iP&9xpi_=Y&9)uCb1@&#DCxK!^)OULmho3gN^gHE5A zGc9MC#{p0|W!QDn4u=R*L{OBhq@ZkIM?cdR%LMfwld?M+>#!O=@h}R#RbSG-?1)>& zs@@%|-xq1n_aN0VHnZnt@ln)KC;jrVeKA=6Wx|vUxbf1&>%9r|T8qk@8eNs!zV%bZ zbCpMnNjjFRu(>*tOw_iHm+{e;i<0#`mjnPL*D}LQo-9s&lG|;syAZ zw=JuoIheneAjddR3uZY1NuO>O=mu?b{^q`ZvOn|$U}V>0xN@01@N)fwSBd3U4jOPW zm)*><7T$+>tzY0EP6Imz`1q8SJo98(n$8>F?l@j|g;QA(4$!`p;cDu^<3V#BBM|}C zpSroghERsVGnEJ|2DEDZbTZ(aiOE0tac1_ofP5QCOeqhLQzsafxbw@Gi(o;g3=7s_ zd0#1H6iZn<8zV`BM2C7`qEtX(<~QeM-Hx`n_6@g+jpK~|6Mf-$5m_LaXV@wN-gaKV zufV2B4-d30R_~j91)4k>)OU33?20gCSGJw}oBDcFSNTGDb#*+j34nl;)Y#Z~Dd1gd zL@M}pA##COFzeJ^ab}(v8Y^GZ2s(@X-QTM`2(&)_hbF zR;SzP!(@+vDe{FNmwX4LS+CW;h5U9E{g(o)U);PL_8qo}$#|Vav()*5&<^6E=M{kz zS|1Rf0Opm8sqyZ;o5oEMY}G7QvMJ$EDMPML3ORq{H3|w#hJOz^yw6!%zdsCpDE8-v zPk^o?jY(JOM*3E^?fmgG&PbSw<`_kfY!P`XXD9)UE3|ezuz07HcDS-<`6=`hjeA@0 z?ANF0@EMd90Vt8RvT84%E1grIW3G zOGjsv9z%R+!bBuR<44FZ4&yLnWeX`8bgC5j`Ni}C!}@6N+gGgrY|e{hAQo_cjbNEJ zc;Vdz#tLjls1Jh;g(B^n5%))Z`W{psgGSQ$ciZsZl=aLbC6>&$7wdd9&B_i5jzih64JcF9Ien1K= zlF&L`Ym19G{mFegiy5QaOEzj!G6gS^zX_d18#L3hTAgLR&2+qU{x{P?*LF3Ty5iQ) zb0r05QH|yfq#tD)9NWqY-ha55ZBAizt_3P(%5z}*;AsDQ>{jJkJqp=iNLzlj;wW34 zw{wVk>(c}G$BSHtX+NH5czZ3u)u9^65RO{oqkU-Fvl+Gk)%j9afBx)^D_0VF&DGK1 zvBro_)hLdbe!kT)MYq}n?G|bdIzq5`L4BfeQTdwx~c_LVa0aU;JhAJYwUn(Kt)KZM6`(M*D@&hzmfUi@J(jEr9GWL0(5Z&mpPEXl^Ryr&V^ z!1=Z1TKc_t9obauQ05n-FVpeuw<$8z$PJHWNKtl2aUPx4A0JZSGkxiopeE0}ujOrH zul9!l0k2(u+r<&zmt0+?@GO_LjMVwjXBEadb+dwN=A1xiL~+0F*AFgERdPS$o#vSn zpZ%j>v8?xEHsi(rpj^r(y3lIz?qyu_b-)i5I^%YviLiK7*Z7mEas7Fu?r1ND3e+R4JT$l`FBQSpe6Q`ktkR&LE&O#J&L8e< zVCrdmSB=l-loxyA+U=)XD7?#EM*h%GpS7x}o;jH;Ul^zIF##9o-GZEdU=+bPg+z7Q zu(aN$e^^0(2&wS?sQ)R8R{zwSZDhcPzx`x^7zdpLrD@TNziYG z^OsQs98%UOCjt`q*A={zf|MlS!wW>qHsrl*KJMvrv%%2_k39|<&=dknq-sQTkZR%D% zRm{Ek#i`?{Z}2IhOIQ>|tWb zf$WY^)P`0x7N`Mr{WgL#b7!aQVuTUvs=v#Ngj7Jqesz}3v#bre$axTuyky>23V;1d zfeA_A;$j$3bAZpsdQL!F9zq{ph`O;b;@kw7ImMn5W`qJl?w4C9iv1${He-&cL+ zbATLhPM*!9}b{_a?&Le^6N zL7)@7Zn(%g9FWL;72ZyexY*h6MsP`Ag2jT!0tuWK2Jxo?y;uO|lws1l&_mJ(VNPm4 z$`XlTz_LM}QLbSVs4fG)e$>lYeF`~<_f;5in-HZ`2RFNub{~8&(0SQv$6lLKd#pBo zQc=i#!C5ds2KoFM=3gnLlXlgZ%(6+lnlqtpArCG!?=-13bOtK5dNYNH#{C1Z4w?71 z-duz)AbFc?Op(ee_3X2@(IR!sm|H;8-1ks{_5r3{$-60u1W1Bjgd zG8fZ)i)s^Cj<*7Vur?$h$Rup@DENE;F!^~Tw{idhw$=1#>|O2g@o~rAtjL@sv9zyoFJHv$0azSK|-TAq5qn`5k%H)xWo zE{q{R$0F;}riR`WE6SOaS;mOCI-(kasWol9tm>cz5^VC`ODpzXah2rlXYZ5^w@Nvr z$6}_QxL?H2UKm-EII%un`(R|a2#AV%9D8;FjJn?2|W;r0g@Pr*QsgJqNx zugHB?K^{1Cs2{SD)y3dl1!F6F{K(Y9%kM2sL7Wj-I-tkc13ap91^^sVJB$|)fdR25 zbu-1w+F|w(h|t63f*=V-qYPJ~-6}L)MGbPkZ&j(kIaA?BbqsUFWLz+>0By=3 zASGe|)Q$srMzBON{p}iaKbI_&r)>cTkQiQYNC@%V3a$Ho@Ei=oj8EML?!aHgvS5}4 z@bR)3X=0WR9zxVA5YQnW3@d2~+Cn2Sb8qeTgZD_ zK=^b>7S3<;BMnJ@Bw_PvKf_MNl?YfxfxLdw`&^&1l9fuA!5W%tT%o}TAR$P3E^LFh zckD_5aT3uOTXQ)_g1Mi}n7stBBN)#SIL{2g{ERa$VpT!ZCCfX=wh(Qt=vMveM}E&p za!kZPTtCqbFx#ztbV(vb_tiFy-fpi!>y2u#d~|4VF)6x1nJFai-zVS~+mbmL|J(~( zwjad2XV!owd{wk1gc%g8_w z_M`y^i`t_pVS}A8c92$}>%y_aE@q?)rD{Du3S^QCH_s&pfTHDaW@YV-XU7;`U0q$h zc)ba@9|pj+MuIxcrT`MeT~NSf5fp9@FCImj{P+%Itfam3`#4*b&d^g`innX7H9q&Y z8SxK%f8a!Ps_s&T{Bz~gw`P-z0pHko0B2mmcDi%&UyMZ!;Q?xT9*`HafF*92qg%HL z(qwMn5}n`!%~{kxxHuOnH?9j4K5$@~jJ;XAwfus?Kbg7M4U~bez@ll5l^6sx6hrnd zl2<`?NCqS?vgIZM7(NrQHs`3I&+P;p9pPazcp%1^0h>dBE%Tyib%N_`Pv#3CIj4d! zp((Hsox)nha^>Dm9fv$IquQkA<~*du=41bc@0Nk>o6e+0zWH-cU`F zl0PXCJ>L8acmJH1K^xgHk^h-WTa18lg@BT-(7lGg8eTf55{Bw90hjYiQqOs*k0&`+b#T0hhjxr)2*TR0aBTHM5(roTfe{Lkm=23Zf`bZ4y>}9S;PBxX zJRUEkosa>czQ^7Fb?d>V|eeE`&G0U_l-@aG(b!d8DE&1N(g=fskV^^cLw> zO^tz3Fb062Qy8a%bM^ytjRA~lcz>kk>+^^{0VH_BH{j(N^ux63Ra#=dY)Q!xc*tHX zzF;eZjM#+{Fl{De<7&JYB}0LS~)w^j|RO`ydC(9s=77$VOXxT+V) zrzEdmBF(gS?ooJ&% z&}P{^2JY5k`lXzza%->ZPeQ=mXFRcA-KU=_{%t2pSvV&>DvKbn)o(ui)weh{-H$^c zPF6Co0JaR0$Ah)Ke4(5Gy6!25emmSSQsJ40GQJJ^76T-?8(22{g2lE-)3WBB0I$5ZJU zdvDr^pj6AqI)`OhWCXXEUT6Ok#XgMBPV&=$69@M6TSncg_0hW`MQJUM%TXIE_lFKU zN7cz)%^Ij%-srr+$jsK+HA5iMe`xuUnG^Av9daBB-|tKmKEVGt(P8`}B(?uX8HoQ2 z*2Dh>p5!P8Bu;snkk?<_nnx5v4h9c}@`6AxLd8O@z-D7;&jZNq;NmDpY@yjh$n-L*%?UOq@=b+5yebAV2 zf6o2!Abv9K?@2-KNGW-}d9!}XdHd{Mq%C!&t*V`be_)dz-?7@}wce(QE}Wwa&fR9d zkuGFaK|G`vDku5qPDDsf$e;V4pSF7Lt1J4qt~WMkuObl7%P}J4`I7Yl&mI5dTD#-} zNATGE?dL2kERB^Sh>-mq3qEAW5%wqHrcIS1A;Lz(!=`bM80d|kG@8G6NuECD1c!ol zcXv_3MM1DTm#RD#L>$6(f-;zII#%Jk5>_xV`&!^;la}G*cI9zHCO*H69(cK=&>3eR z>d=|KiHU5ooV+3j8i6?2WLxrfi|Oyjf}h~~b4tI_DSg?%XlcE`ui#zVqAXI@>UWupCXi*r9*o%gX)71UcO~s*& zkz`wb;-jLUK9$}Z51;tVZ(Q|nT_eFn*)f8+2M1a)IxX9<3tf zwB=IoWT-{s4TlM%cOMoSXGeMLXmnbi*BoYTailOTOsyK|^Fe}pLfyCaofZ1dv$JM* zzG)-1QQqPxztjNSqOZJUrgXBIUE{XK9aH~G5s{ZUH8s~%HtJMbY)c~1?4cJ7Jd2AW zv=M*5Vm@uvRXW#u$=_$-FH}7-?`7fY5uL(;s?8N}O_GnSw%DZ52lk|U?p~I5+fmHS z&L$_S)B>h6Z}pq?i>A^G?>pC3WvzV-RXB4Nkhg@p6KVaNpRR|&fc&o5II_2M(UwDGQslwfl`_Zzi4{cgC2-FsmU z5)yO4!nsE$cv&SuHZTFmt=Xf~)3)2|!P^FyzHfi&LF;XBYjhJDYcT@-0~H2@eBI1U z8!Q%kH?AQqBSWBb%@uFT&VI|_PO&O16c$U1fG#b(kG#D6D!hE_sE*b-^ zTrNiNGh6`kT~) z)$g>=$cTuE_1xl*CiTsU7}wWR?R(NK=))Z86E}_GZZ?VP17^Z)zwaA7 z4*oA1((2jShxLk8lY=eN`$`RTNKb)JLeJegbzJ8z%MZfN@wIQ-T?i*8i z8r9t$+F3Fuo#h`>w1Itxub~c5psJZmAly(u2cUzWH*e*&bBS? zU$L#3@P8a&bY#!@`vm+#7|~6uJh@w{9VN!sPLPhm2S&&854Z)d_(QkD?C4QhZyz7h z0{^yAKFTy2=9D678^qeVG4yzUOZHt6Ma9v=onP51^`Gn2P7%YM^73N?*e zsdUo{IOwOM9EANKn+F2bO2pbXaVNfx+Fx49eKWqo z9vacsEF$`fb%9o|KC$nZ{@yti#9894tBBB=9hwt$ZOw0Ib$y*Re|~gylfkFY_x2t4 zEH^7QsgF)nLU-9^Yro4HvQj$s4G{7M9zO5Jo~DxYi;tFjI4!$mR1{+jZXZR5s;UM6$~8lF;4`}IqbacK_b0~9EMxvrV;$C5uB zz_UXXY794s1=PZivis1_a_ysXK{-YqW>Z0ZSs({ILwr0J?R)4LHaGW>h9&)u`_G*F z_pM7EdNmWELgKTyqYDFF!tg47^KlqH?H<}8a=-6(#m+{!N^a3o+CX9OwRkQr5fv{Z z2>AJZOx+gP`lWv4jqYRfuqiV7=@ICe)ITr)KEkg<`mFcL0DgRUd)#sY<>ZtH72y=S z8r*G;3w`8zC4R0QWyvd<85ub^Dt5*@MQU`!M~y2|SgrBW*`jYbsqR7NE$N^~-uLNK z2saP60Fp{Y?~HVH)}c%DdG<*CSS&PzSz93RL>(gQivwe6IFT{=>Ca3`i554!6#dbvBcLjn?)-%R8t)m)N9ZKh<(h5ZRh+RPbTQSObZS5m3Us zK8;#1mXVbe5f}IVQXJXR(J|oJsYFI4LVq;t7Jp(UF#3_`*c#j={A>&RBF7zlVm2u= zwuU&Ah0DANa!o5*(_wi!zP{zQ9a?=^?GnObVp>C2h8!In^xz`2^`)`2gN8vlW`~=! z*qxhWp}7hk`2j!)dV9~lYnLU}tO<&X8-i5sdjVlM303HGyQI%g{OUUk+FIKB5^y@2 z81PdD?i){KU1%?XF%cCXo!hBQH*0SfZ)x%SNEsjIz*>Jsm4hg!hkUPrDY*v{(+vNa zq4!skp5?r$*d&d-84k=(S7kf-(~t7gBP+Yr)|xX6y(aAAjYj)huyWT2y4XGLj=TRr zOZwQ0NN(<#@MW9?AOs;a?;n)2AK&t&O}=1*H}8XWkBK>{ovdsgD`7rR(y4S@H+Z8r z8Keoi^>y;2)Hr#<%K8V_N#c|Z?(hIA`C}mON{aR)$@JA3p207c+XDH&&@)fI`Lk`MKs+=bMwa0@N298ikNOBPz6|l=b@0HRVHc zK*m57jNWl@h=+qgaep*Sd1cMymFA28fle$aD{}<2Rp^c~Y<|1X;(}LvyU2Zg28r^4 z!%l%FCMMKxlMPAR;ZL44&v}u&4EuA)(tXj>w&E70_FeV#{m9674Xu1 zsi9MT9#aRZe|=sFiha_Yo}1ge5+L&5o>}80ec#10?yKZ{}h2^9vqbfIF5vbgs|;OI8mYV&<$9) zz0Z->f>A3~R%XG>d5e1^mk<$<kqJZx%IQ-GQS|GQ2yY*fxxBMTh!)j-}HPp2N ztUP>j=P;ec;jX=2MaXB*#)GNO17hLa+}seUvY$LDgdfQd3k$o(@lB0nouOje?vXQh zzc=NX9$5UbW5)o?CWn4YQ~0sJaY;5NtW3h7>G2zn^z`&783j#TK?#XMx$EOo2G~eT z8|EHLO^vcSL6`A`lPo;#xLTno=so!bc`Sre&m~_Iyvy+W+U6AtI1y%j>L#5Sz9E#I z490j?S{1}r=*mOckH0WOPgEE#oYP6{p?2K1dGcNAP~GCTy$RBEDY_e|F32%%}eK4|nU zU}ez9?T<&|7yIo;rQ{Dk@fc;B0>3Z<>STuc`gv(-_wxw>#?45imDR zo#3PF&Us7d{mIUo+13^jQZ`Y6ySlKj0Ii>aBI{@qoL{s>z|y`>JByw1Rz_2=17s~B z{Q{+%m8<4*Gj4a>ER)NsYqeoAcDiHw8+mbJ@+M_#6pF_DsNxE zspwc%^FQ}-*GD>YIFB=8smpQJd4++dTRapl_mrTSf;@nd6aixA$`}R@w z2_>x+<4djXn$H()?J(ouA<*lvo%IZdAz^wvZIp|oBe(k zgw}}&2ZZF((Y!~^Pnb_j?jel1E{x-DoqPd}*B3xOBbp0FyiH2tV?6ANXtmKL7v# literal 70840 zcmeFZWmHw``!>1+6%_>&P|BcFk&spqkdg*z5$SGe)D0-9ba!_*EEMU^MI+tKV$qy? zxqrWNzMKzdjPoDwct5;rjJ=&MVa_M+=Z@>T?)gpTjp(&2_pTrih-+f6U&$d57vK`7 z`_e`DqswJn3H~@|EhqXCk=seOgh1R!h`oBD@G*LI+~KW~?djPTs)OZz`a1?3vu_#k z^DO0_I%bA?DWxhZs!}TQMo)uOwTdQ#RfCvA^+pte)L2v=g{IEevR0^>xvMcx_6hd73 z_xjmmoag^u)qT2w!1?!*z!!1--z$rE=iUFka>spyIREb@8Ogc(|6cv6yM*}9{qaQp z&n^FduBAFdC3acI9Qnqwng=?ha#_r*tfrA-xQOKFXfj`m5J#IE1o1Zi4oW>-@>ok6;qJ0;k0-;k zAM_uy`9G$H_#Z!GTpqKtA8O7{kCq3r!tSvf#H*GV(K9hgO7o+G2Xi&ldgceid$gaYVA@!b>9#9_k?e+ncI#sq$;ni@dV0(45l>YMKYY7LNRcf{ z>7o$EqQMZd5+Z7#DEpJ-6B*d%zXx8yNUh9l0GE(2{`c=!Vshx^0d%(zq2X-v52Jz1 zke+q7v!gf)*M04|*;xgJ3=QhowM?#N8{S}myLa8z&!dMOuSMn4~j1-eP znke0}FW;hh9mr`i6z_pgY_!;&a7WHuHz+9R5iRYZ3!*N^-|ZHdA|kz*h*dLtXy~n2 zU_d~?e0zk>jZ?8+t+PYr60`A<{aS;bM4Sa~yVY<_$0h0#qXF>}W0X=5jTHO)-mYdQ$$sFf+j@YvoaAxU2!uQG1* zz1!N}&eX%0+BDIcBX|^{0C6-LU!=Lbhh(m5P{2 zH>8}rJag$8tmPpZrB&jHHeyySe%;yG$r-nEc7i-hGwe%MG2ykG{^w2fvxE!yLZDC=nA_|2I9G26sTy}dz8!@}PHZ_;Y}ZB?sq%^_$Tk#cjlaoH$yJ6X{Zmy}d^ z|2}cPBTC;SPd?3m997}EMc_1U-Nmn3VU@C8wIOh@zfUq6Kq;83QlyswkFsNKFEAQl zuwCvK1N-XDL!L57OG^ujh#0L76>ur&8y27)z_kTiQX>#*e}f*lq>IcgtlUQ}G_ZW3fm8h3OMi z3{`|A&CCv*exK|u@FFN!cMj)$3qN?gp`oGCSb2db0l87TFr5lP2=n^A1Lfhhk&>() zyRGR4;lcMliMeWl3j+y4cr!cE+}2Evc&__XUR>&|h(C=5R~~~2v|h+UBqf)c3@4t$ zy-3H)t0uQjW?$okQG$qJyV(5z{OlV&>vb(-NAz-v!_NHO2dHniX!}znh;>e@VaKwZ zFzeufvf;jXNnztzd9|6!S}p_SdTsW}q&7Fd=HG69bYZ?cC>eZ~hldCKg9k4H z`%|Sz`8VruE3M|dAwVR-DI_hK%L!E*Z zb8PGcazA~N#>PYq_B=X#{75p2^?9Dx zNS7hxDcISSVm-S~69GB5)$j1QT$pntr)gwEYlU*I`V*x}KOdjIy|vMSd~Kl;IEK$% zSXfw$dXv9}vTEf*Vi?0~|0YX0Z?MvKB{(>kir(H}4NhV%WCWu&e!jj}b+n9(GVRtz zQowQycNcp`Ds8hY{k?a9f8TUb?<(AfZH@iNd6mp-4^q`N zG?@LDUe_u(7({C}#6=Hk!rd!shuSZt6B66m>upBf(P7=h#Od~R+ksjI<-bcEPj{jH?p z$)cq)Q&Or%i79Ys(j=S+@7%-T(N+K6Hs`Xg$h7daBrXm6_|st%XIK z41OM8@K_J{dP#Ywdwv^@X5LZtW;36_X=wcYIuPf}XLggJ?>MHMrX$%MQJloX*m4#@ zcJ17Kgo1k%{dwPS-+F@S`@BtVk&z{H(Mror*1EJ7htkWZw%DL|hAefM_B!H_b(b)| z9{s|nsDX!lbUGh4I?jEVp8TBRw$WY4zs6L`IzFyx zE@htEPmG$~+aGmm-ma_hnsu;B?ETy%9;nWGYEUpbw_VGYbL5zyo#6fTQW%x+tJio0 zWZl^jabkgqI!sQ8^+~tqj7driU`7J9wO6j0<_fqts>QHcG3lIno-&jCYsb4pUK6 z8!q?TbL{k~T-ZA}JY4Q*31WI#)kNWS`KRd88i^y$u+N^t_^T1y!iqrV#u}5R= zpZKv@s~N4R^aGD)IxQ_q^xCK>qtdIpQ(7Nil|Ov=i^B~p^7n6piRuf> zw<*l*{i3)`m88x_ILpc?PF2>qqB{LrZ50#|1l)orrex=(uc9 zaF`dueQxJv-EL(UQTVORx=&w5iVcg_YwRr|JZ17~IA7r6O{9Io-RJ{*_BDn~z+kof z3HC>X_qiOlsTvyMP#aAFP>UGzqiNKyjS-MNGcY0LST=+(A!3ZmQG$^Q>|0cVJ5SsI z>jTV%$o6+boU;0{g9~OnkhC(cdcxZQ;%MSz0Hq>)>!aybr01%~WEJRkBk)I3ET>UI z6D5q?z4a^B!c`2t-1TT#eNOQ9DCu^tZ#qI;IowQIxjig`?eYx{=H0d5 zRtvgqQxf*h#>N+ps!2O7omf4k(@TEG-TwZJ4Wn(_`hVJ5z3(vk>4(@;I`Gqp@;{ZE zI#~8my^*rGNCFO)bZy7+f_QJarOxepBh~H3f6uuS_`T;BnkjJz3>~RROguK$n4u-u38Rw^VfmGh;Tu^AS`|ih9nSKiJ30dfs^^J`+%nMT@6YQd5qRTvK0hZ zF|fhI5DGNs!J!gdAzhCm#vvN)lXfC(*_oVDS~w2M^>Fvd_&Qa0;8n*Gj;xy=Il@rp zzwO|nqk9ck-TPY=KAzR+!KjfMeXr2DD3p1hZPwYmpt9j&--2o$GCe(nXhBL`yzuQT z6?<{T=PzGoXJ#JJm*MH0HVEmt^0d8wBZM76`qj4=ND!?p;M&wQPVAS~_Z*EKty5|Z zT+mu;g0i#A$WFs1pK3GBOXViR(`*{#&$_y`h1hjl*6}_Byj$z^~1(7D;Q1J**k#aT7)qBV#14>NmBu z6NnbbZ8qLrgEg%c5QvOpt-E^V3R~_|voVrK`&j4DacraXE$?rv()FXjF4!S5{vq+H z#fb9ufLq?e%uEv2?-p)^lJF0-7SYD%<>l2LFXQ`YTFIwM_JJ+jMG=B;Y%!n9&F6)K zm9z#2j@{yqi{o0!<;H{UF2ZoPJ5#tvga)m_hTf_L+n&a!oApbL-4g&!z|%uhB_s5* z4_DwpXU^Ox13_?6M7DEb_N>??lu^OjEcdf0g{*$%GiFJPns+c~MzF;MZ4^;*GyXXV z<3$r$FqM+kDr%y7Gi+S3-MG;;a-QGQV*&yK_uwE5<{k|v5-g2Wd+0Rz$;JuPaM+EJavx|#K zr4n2eJbNu07#IlYpNYJh(;fTu>Ia?RJ-Xd$qm9Ru!EKKeo0;$0`1%NHEtj0*dg_K$ zZQueVk7w&LF+=6~KdDV*u%n%!>t?JqNR3-(N?ps*?gvgds!_Kd=h6O|`#xsTMQx}9 z^)(mmxc41d)q;oOZZyF4b@kTPQgSohwzd$v^Jt239kI7V8gBwU% zlF6ECjXO8xn)cUA7bJZ4hBY}t$gNCu)xsp4nD-W*IWeZSt`UvGg4z73LyNtia&~sU z(r%15N)3^-!8-)df=>jig7*Q|xNrf_5YEy)PTvVPcaHxj1r+@k{KR6QbeD0S|F_(M zjsIT)iT?@x{^Lgihw&g5ml!NI?Arwp${2F6##1PPI%0Usa2T1?8pgHM)uSgI7GkWj zFCkm-K$15x_Fma|SU~fpg z+M<&tDG_x%3s9t2%k~JvZ`N%&I65lZTrT&})zvjT+FQGF`SK6Q)Cy_(Ty9z?~a6v{xq!biA--4gi%)yGwnUN;#2wozWRzzg}u+XfTAjE>kfpl!!&$pg2Pd zSvxj9j^BTsm^dv!;P5q+MWsWb_8sNr&sd?3%5?2|Pi!^`GlfynVu2+iCv$W2fn1HzOUG~G1ahEQ zrRU^S0_YG>R|ON3?0cLh2A0u2K0Z%wkBpA0QBY7c)&ef8HYROiV)AVR4R_5nv#44Q+Mm}Hs_U+q+k-0debRZVw9W|@U>E0R4kQdb}QW-5X=W?VQ z%+u0Jla3o)>Py4FdUoOehV!3Q1Bk@P!J*g|%G?L@3=R)Z2RBOxxb)i>T!Q-g`Xz>a zB8V#t%dsNTLl31=4po;`#=^>2DMgVzd0ssg=NotBjm}>C~kS0pG>KVYH0P=1Kn2*u|MSC80 zlWZ2h-`J}tFQSO3|J`pH=FNhgcX`BI zG8H55d~so-G(GK;7p?8zskjv}K`odv33GE$Lo5Oy*}u92o|f4R85X_5Cw{0A*9wF>=YR~SmeWloN_J6ETuvuSxd@=m5> zm&~a%j>ShT^*-;-f+f`byC(CLgu`Sqt>gkjD$L6Y{V82umOlTl7N97jMk48YMX8BY zx6I{~G_9UPzbKpiX4$9_dy`2eX0_t`m$J|ImYT~nPPtf__P>gaIeU*P$RJgw-`SU~ z;4sZ#SgQ(_^`N`<-&{_V5blkrd!y1rYpXlMDq=1e<lGOEMlu_ z1R`H(enZq&ca5Ry7t$C{pN{v?W{KG2Cb!N0S0Yw5Gv)oT1#uD2P$rFBA+Lbo7rObv zdb^eptZC{efeG!4Gj`-r82uqw-vvHoteIN}AD~mQ7D6Q;D zk^MTxYtQP_`z~#0=&34tXM~ieDa7|`&Sbur+wX=DImLWY%?%`<%0kzO^D5djPmmb# z>>k$S5eUv^F|khaJ!21f^gS!nfz-P0iR{IoR=#2_ZsN{cr-~1kmXvqvy+ax?O3d~B znY#Go0?rCfdncK`FCEWUR#Yr?hBEgT8{UGz1aqM1gU6njHv~Ao-+wfxC_ojFmrP8u zXWS(-v4V?iUkqZ?5({e?PwF>1@80^)Utl!<#e8jvlGh2*oz;r-T;C-uaCJ{z6>`hC z=vQuPC)FIemXPd`l3c&9MM-J9+WiL730ZL~Zyel}dq;hJd33-Got~aH{-P8RKoMDs zy%Fn)tvRyM&kCKhzg1S=9v*bzlrNGsjgI%hn@(5ZuD0-)>-MIL`If(~>NZ9PkFH5c zR4E|RZ~sdL-KOQ0VmWs-OJ*_rz9^^WP@M6&4KfKV)%qreCj$}*qmkkanm5tu;MS)J zSsq~{9gw3M+Nd&R$HO~o#aRSule&P- zme?%yzVA{Va{34mtzweMwJM{kyAy_u8(ukPNUM)=g`IKq+Y7BzE_N|RkLS|Cv?;eLZunv9TcXy|} zD2;#)I$TPN0}@On7d__tCYnzrQCKi{VSQb1>jaF?pHeVE#P8mSO?A%3_D+|evW(34 zh63!oz?JSos&wMHF{2k86qvn=Ke%wz#^?GfqDi9kvBaK7QLC6(sJvp_G^vmMr9bm8 z|Jvv6`shN`{;E2o6iuFFg!3rz$mv)dH{0;X^<~XBR`$%b;{ckYv7{|L3>E+4i|^Xh z)YLL6FAh>kegM@b8nY%U{p0eZxpIvZ%{h!6y~Y24jrbob7HUH3$q*_!aWBsgc&Ec3IRV4i$xt)_iN$G(H4p+ zr+8~&-@_y-_n?DoG96eaBJD3!So#XPh`{>+(y3DT3WkM^f55_)d}s;8Stw5F(2sLT z&Ch>);r#i=5}5uQ(Cbi*O-Z2 zzWMl+Ej@FZ&^^_T0kplbC=PIQESU}!dzDcsr)2*~X?we*9|gZE_EtTK9(rA|rYPIi zz@=kZ#VwAuQC3kiig{9jIJJ;5YwQ;`r|D8n3cq>VArA#&tiRM$4U%e6avqy*NZy%7 z6y`A( zm?D-%1a=jL{cD`-VM{LOr0Y?RrVCopcBxkwXg;|&Z~UPKQTz}icy?Sw;V^q++_H&0 zlG{4f5wo5H*0&raEl{u|&^8iLw7^u0+Ql$#Rg8^aiMy3gw^!YEY&4ji0r6WLifzLr zxM*!_Ya5D#It@$69p5fZw6vV>jA0CFNrwHQfB5hvfLBj*HB`c*sW6#(4u|v2vme~9 z8(6jS4~~yA1;w~MWy!LC8w`^%1RA<>A=YVkSU)ZUN-4*^VSQkNIFmX$HYb2?{nzht zeZuB3hli>rRW?h34fc>yi}ga$G?1N`k zji3Izf%sINDc7*6syz>=8f?}@1(i(ie)Pf#W)cOYzqxdahtW`eLaKBe3q7WW64>83 zm%RuI=gpsT+-`WSNR;>q>}z`Kfw?wzem5mT!&s=}_(`aInaT#;gtubtE!wuQRw}U>^=4uA1C}u{^$^ACAs_iSK zaL`2)_^{cnzI8c4qo9lnfB5JTx>tgAiPi1EM>wkW80N_|kHF^8-rAWXt^3Dnp#h;O zgFT*tuB^o_S&l49Zfr$YJeembN>ApTK@o&a%|Z8|CRV|}%wxBjPQT-7x$%vQ9{^kc&K9`CqAaN-5x-*F6(wNoNk;cphvh%SKs*T=HcZYu1f+Z*zCIzrq@Y)14%m z!|DibfnWxfcMz07kTgu;PX^O2{JE}Mbx2e>*`Tq#%hA?yFcQZ_t6#+D;zMv&2npk? z7Ao}~!!$Vypay_cq@Mysy0X~i=l(MM!t18&C24#+rAST?-PE>21E3irtC_zO*z9a{-2k^K#_{1!W zKCHEmM{4({eH<4PympUwy0Wy8u7|*;+RirL@JWa-Eq0jxM$4|(PLc$k(%1%E&chYw zrIfH;tgs6-E|tKP+rZCC#qj(U+a&nV1gD8yyJxT=(DtBxh07$cDk=&DKn8u>d#mGV zXi_fXp^42(#ZUM;U`eTvZ2&|R0@|LOi5jO-n+`qzXa;|I;NQ73@KVwsm$U1$rAi2W0$Wx_Be$$yFiP?6TIu)+YHp8>}VM^3mhmVYH|?$ghdd-PCMHgss; zhWRA7*udoXLUnyx+#{|JtLpX|OIAt0Keb<1$9hgMq(0C$%+AfF09IlICIsH-$q0IV zT&+mIYY>7aBtUZ5!WRBodg-2t;m+?{(u**Uet_1n|8|Xn?=8q&UL`O_&&SwzMsqu2 z#_iD%ny_a!Ni<*(Qj^t6RN+mMx5|oHj*QfEfjq^=jFfSihr0DNC)d$QMu$fkvh^1; zNw3G(J#UMBS5TmDGd)XBE@;`iT>Z%wcexqW)HKgy;u1FK4C#^M!CV*`P&6zkhy`If zP%$7Wh;^91TXQfU1>C?O??!bJATf6gFuQ%yi~zS_(I?2Yb^)Nmvg~CCJaa$Mto0!k z+6B?_oo~m@tpe4vqNwd#f>rk&3bO`7jKhA~X(5>=v_Yhnn3%{kVz)9V1;83s(T4`* z)v^b%ri_I}K8Eqj+*U%hvF(oIg}jov30 zSBioDz0javo<`~qu<1XN^w8w)XWfg``($CXvtxZ#zt^HFroIYM%QL`A_o=H2Ko8{{(g*|f%JVWi?nhWBM`s3FmyB%;hf${qcCfX4=xJ0(=BU4{Z>xAe#Q0xvRdkad>5hmn z@wq>Ph31oas

XAZB{)>gu{(FoO)mMVM*+m4f`bW@&FH&w!B)>{q)4`y#& zBjuF5d-pDYd4pJl5>mNWxEHU=oSmL9R#xB4l1-|7uhT^brqT(VD^NhGk*=48J|oZWR+_prY@)T*h=RiQ*m~i$b)Dz{J`O)%Jv26mJhA6Z%UI#sTQ2ink}t@ z+9_xv`ly1EqsfT5OO{v{M6^9(h(cZ)jm&{cu6(Hn_qxUP2Yx=WW0)axueOZ9L`bK! z@}!RiXO6fgol{EJMnc<4Zh^>bz3ObR%!f!KFxJ5?Skv`@+K2NXZU&FCHgBS4gT*2i z%8Nv|(;1E#CtZiqa*nx^PSqoqiwsdwHF)o0lZd5_#v|6Kd7m{Y=7#ZP>R2#y;4?XL zjJMu@`cxidJSfNnu|OPPch^WBKL->V06b;kFngiizC44i-i_OF-{Vv50TSuFvAs#> zcxQ5n(naoF6f!REBH_Kas|9pU^KD|SV+UW%_{D{e3-7jc<$2J zGYCjzzHry-8b)(MpNKeD8hU%hp9w;g3O=FC-rR{c&gcO*j*}C@;OJrs{;PS`|jW-{#mUGc6hNo?l+h#F{N= zP3~xjXY6cRp#HqTE%~@)&*MCsPC&_>S?7+>Xem$h@)Q7vdg+XTI4CS{tdc*w{Y0>t zd*Pn}&fz!L<_72H*J`dHsD%-if^RJN;m=yx6y0{S4gc=;y-*b+yBD*U)328&O6l}_ z-QqszaymQb`tYa5Dy##OWBE6FH=8IKQ%lyotGu{qr9!FDYba61Q({Gfs$)=WWy;e^ zXSjYjY=kP9U&w?OsaFig z6y(z+`?6+E6Nt3!muCXTb7Z_8%^DmOq+>~;Had6k z=S#rt-tvt0`7sqdz0Tw8y8JmGVpb4SwJc9C%O!NEWjd9;1~^Rx(6{0hifqv2y*fMD z2y;9;!I-!z1+u%HzVzAHsV&IEXjyhGt+`3b!dQ$3*(E_K&E|i!S~T3~5-UJ%x^s|h z+_f-nf0jcixc@qaM&hw=`P?zZj{$OGUg3#P)XQrJKJ*2yi*d{$$QYhkCNkz<4MI35JwinCQX z7CO#j)dP?6zj%7=%BPLwC^QwNjzz2colNA$X2On|8gZijh}5vhPig$)7V_Q8s|q$T z`zMA#rv4&&ZeD5s7n7Npnb8QX)}I`A=EV9OGKx zC*j?f@s8$V`F<2lthEfEr^32>#+M4c7@xO>A(p^rafev^8Z($L{5( z3n3lnp6}mm7zC^XD6JD^V^sD)V9I2sqiq|KU8TzF7goWbQS#fV*;si&UC*}f!1CD zy?kNRx%N0w>D5wuL=jXLJ%BYAf7p_9aBwjE^$%)xeO_tyDE!%!E$oJ*+`5(>h}WQe zvs)Xhkg%TbR7!h8&N#=fb;kST-5>tL;~nnS=H_kyj{A%Blc|=%v<4*t*Q<*{fQ9yU z4v=L`+-;(?#L5~0YNkORgod)Hq=KZJftgt&BSRk@fJCNZm6KvY-*29OT~MO5XzQS?YO zSe}FK8jZA8MJcIbk1pbQ%5%c!@xjkF`~zVxL&%@JS~@EmF)9xmKUl7%nX2GF-xg2% zM_J1?hb5*;?YRBG+3Y8%TT4ua->(f-D7v|va*OqPQ936>PTJg+=UJ;(t5Ahm?Jm^y znOk4a6~;#bzK$FoU_sJ~@+Dj&Mcti@gCNp?Axw2k2PhDD#X;h;h$M2}vSpvM_8-t2 z0NGR)9J1zACpzC6p%-VpMQLCZsZhkrGb*3biDBb(N@^+t9i7aLn0g<6FXk?KcG$L+ z1%fA?_$k0hC2LgKNnp!gebm8r8$b`9en^A1K|iCheLFEZg?@1{xuW?FZ)Zvej5B7u zrq~z*dsiZb8SB%sUAbt0UXEkj;McfWeJNFo38(Q&XilK`ew8!s216I*yy%$##$u-~0>8M; z$O2Pzmk08%!lCRU4lI0mM-*L=)^|2fslef)%{vXziG&O@9u;gy7g|hDf6L{~Y|ph- zT1! zv-qAQQF{+B`#>uApB#{y)MAi_55lT4=bGi-vze?3%)NHhaWsiXQLaT3lrdyIbQpbz z-e6o(C4^Cx(ZS)M2@-QTLKhr4JFI(GOPrp%Ztv|Wp>iv_o(Ql^q#hCGDoY0zJ7FkX zwi?2K+$^K170}FbI~BV{Q?0aUdB>$|(3N)m?swAd9IZH~9H5fu;ijN=T^=n9@b0Uy z*2kvxP;oVu5|EFyb#!zj(tem8n5v;Z(5U@kAF{ye_D$i7=QS!ZLV=ISf|CHj=~bS; zl9eWCcnijX2>C($D?R72^+fseb@}z6zV(n+l4l~R`{8mM7xvqWl5AGX>WKuCL~_B?iU>9QanGH*dXJT-n|ICPF4`3*$TGOuxDjuohizS%36J5 zyUc4eSRDZx;NA6N%jtK5NOmVBqW@%*50wgauDUVAdHu}qV8`N zN~K%IQqA?_iQqdK>$&3f7}DFF4zesxN<>HZ8M|FhY1pr%~%938HooYVq@ zSKdT2yJ2rJ%jd=Z(NS2d?P||oH8M(x*M7C1G;d)-A9@x{Vx>D^#_6(;#(Q=_&FuVY z#Z4ve+6tF=A=wq^zndvU#ytt6FZ()>r)UD3j<`tO+ zr<<%N1Joj{KUZm{y-S}Tw^Vcq+9 zjZ?_Jb7||!&(P>#6B7wZ$$s~9U)cY|I+fVADPgmyyi=F#hmSS_sbaXT_fo4Csx{ru z4yFaYeSCt-%E}za2F6o$))bYHZG#ioraZ(DNU7;`JFAiubk%ve&DFmj)qLN~V{_bU zAfDaWNW+G;KkhG#`YLMQ^|LbMCuPC1 zW+B!<=NeW3IjVJcr!bQJvMexSF@kPy5)u*|XU=GhKn2P*&aVtYa)=I3Y6fA(LBuY7n8y5%o02tn*`0co0yU7P#ZNZ%=I~XYGq15bCu5Ej0H>31zMn@)G_SW_@kemevv+ zNmZ1V*Q=$Gxcc>hQoSHNWGqxVXqffuCLx_pUNxJNscIQ&ecXv^j-ZG77)yd#Fix32 ze@@>*!YP)zPg3Es|IZB^&AU&=9rrO)x7Zq*=ycu==gwYv&f_?n&i3@_9cWDx3!3N# zAdR7t3OheKW#Adae-!-ja{JD9(O=f?H#S{^r_u{FlCWHbUOeEI5$=KSENSRX9vHi1gZ@)=@P25R}GS(;jnHYy11>Yqk!lYB*0RBawfQkm9g}>?qChmY>E( zi!a5RU6?zl((mZl(+71wqWn&Ug3UREc$aC}0J{L*`SWy79>{9Bpi4lp0urrp9_XTI zo8tjd1zS`{<N|&W# zqhVE$2fdn#;W2J!obj~-rzIggfhP9wC>US*ly^rpKb{KLY;V7vQ7JJ;iB;Ke$Zk{}(L;?k-skq`On;+x z#+dI+&>#6QE3o&W$ooOf0#V&4jF^tPNYN0ash^%En9py5!a^xe7 zrW#lf^J@WJONMHLpXarE*6SE+=c5i22E}agnMR6eK8M!n!$2o3YRrU`gjO2bg&Yf247k|@a%y;a(-(8e7*_n8m=1)%Y z_}zUkf`hxQ9dR2C)fGE;pIAK`b*WhzOSu(9^QM$8)M2OkZK)0SEm}YQwn3?21`Th_ z#EH9Tz)QpKcmW?*b;NHL9SRP|SAh#8JhRrP)i&R_t!s5=Qm;LG_Uy4^KS}**Q^2FQ zl{R zCtQ|>rf@k;RRfBwDH{FnnkxyGnjX1{*h)unUid1NC>68ZqW|UMpS2Tyk&earKr{$#ohU2W;=6>R)YF0Ty?NLB{(5?&r`OP)?k_$)s~*NQJ!iWy@!D3&5l z`62dm*Dn;|^FhY#^nlj7HcsNGZ1ip^bVNJ`VKyk95MRB$e;J>?Y?LOEs5!Jf*BmEE zd5+6$vp%|_KYdE?ncj9-kO8bb;$VA@`tc@FAHRP19SH@6+xXQ)+dw-_uWORJPt|+< zfCgC#e&^S@V-*%wRye+4PiWb#2W{TJf8QKPjgJ5>Az2g?D}ugC%J~oUB;q`N_s&gV zjjL{T+meio%mCEV69S_dsyPgZ(Xu1ZDdHnm*Vcl*VwSEZ2ag<4ZC?iJncci6TD`)G z&FuIs+_!zX4SLOKGkJMDb9{;_8RNZ1Yb;c)H1@T^*+K8x#D)#g@0! zN=KeLdKzPM2>QUv<2>=U}l(p7b#( zF>!GcNeOJJ&HC66z+ihf`T{I1EaI)h{B4$p1`_Qe9JQ!z$IqWXpIn*uz;t70cfb1P zk6DmgMnaF7<xKJ|AMcSs z%UhBT2`ijI zHaq(pLLL(XWg0N3z7+hSko?{xA!%Of(+}rAA}yL`Ppg5+#%ql3ePlNl@1ya%_N}qM z++m02=Zl}a2b+&fOkxH29`W!r(rvvLJ7U&$mWm+uNs(-zfSZ6vye5|Y%#35~>gBp> zhn>_)*n~dgyqZR+1r{gW?gJezwmlIwaI=3&sow8khxn}mAqmDo4l1Q{`6We3IJM2sjr&wMfSA7)&Uz z?~c-RT8rpMerUN##PqhV!H2jdn)@lFirPS3-DcG!bB8qZDYV#+lu99K69E{Cfrg1~ zkT0i=a^G?!vP52jEL*QPS&U53H5yoP9Qpv2=$0Vbr;t8U9YGK3>d?rD)%qB>6};pi zeFEB9^Wj-Zg{h4>&_4aq(XkL3wh#B#qBJ{*Z#WpzoudowZfhDa zwq#IqI)fqsNOK>onWsucS^cRy2j1yz{=0JrI}6g7%D9p+j`e+TXW@JfY?Lm$KOP+D zcZN3%7wY08pzeCZW{8<>xdm#Z5ISw*d-qylW0}1NdU}4qMbXEt_tXgC*^p!>2vzqz zuG~qMN%&k|ULGEDY;D&m1X%Sm*W>b@J?I^gNtHCLe==O8uY>B;5NRUg`PjT^E-ETo z_mTenanyQM`Li8pu>P}v*{%I7D=W*SRr`6gOy?~AHT_SElS;uRe~S7xV+wvpyEM&{ z%6~jOQidA)L(57^8tRki1q4R7wuawi4d(dnMR{|j?0I^4SZ+D-Pi0;&>Go zJ`vM>iBP813G$=v!kg6MBzinZNzK9X84y5_LbqA5`Q$`?NA!~~L!$%Hf^MatsQ#6mle08Xg?Fn_ z`aLF>fBy(CUk|Mc2lI7YEu{c|sR5#jyikD3 zd!VJ|G9DftDEXzIKS#jIX^-?T>6LMC%&C?X;t?~{$6%)*>kny(E5EBX@3=jC8G6MZ@bHY}=RhlcG{hosiioco zdk3I5zPksbBZoH_0IMzq0hV5)k{xW|G0(>F!H%V^E%Z`&fJ1w1yR1;C-(|)Y3erbe zK&9abrz_>$gOr-x;TS;ekHD-qhp|$c*Pw~`o%YxdV;*Ra&HvVYGb*W4W=0M|`QO9C z5uhF@G#lqhIth(+gFS;2&}=}H#I19&py<@Z)D)sSYkpCY4wO{iI<+co3;}Qoc%`Mp zs!D{W-dA@0H;qJa})w2QUuy9fO~}x`I!v=dT*| ze{>|wq2RCT%bRSHjNHE<6~(SIkjWt#b6BE{3<%XfLH z4za0ZxFx&!V4$&b-V>KY z1)*WFAetT7N$GYo(&u5hF$ci5ho7w3e`GiPIaDxXIGpJ*Wmr33ET|pTeizmM`#rt& z`wi5F4&(xEzIXZkY5bxJR@3Hu?{Sai3+A}Q%0+<$q4U^56Jq#HV`Jm7abx)!M|j)U zwsF!9yimonMa$Vkv~QkCqtbWjDPM+S);w5NaI&b&<-DlkU0c=3h40%7Az$CpoUS61 zDfYCNaG!U^@*B(E%m%nU> zJV81zwZ7g7!TzhizaAJBO&4Y*57`TH8u3u37J$mOHaFije}90@@`l+ULzsu}AWmY% zKtO-he}B+2N$_*5T(YDi7;>?`B7JEnT_d>6DRf(d6To1h4oN*cPJ0u>Ml~=U&Z`H` z>(}@=^hs?0fd{8beM4p&jug{_d;0*6$_l%e^5fFG%eX@dqHCKkqr z+l56}t`JC`NL9Rg#g-2*HMqxR)Cw(>2EECatrvR5;s^y3oe!*bPtv=rvvzceZ`~3C z-C0ksy&$sQd3!63jMsJORm*d7Tw%TE4$P*)@C2dP`$6k28&|Ld)jT`-J_c z{THv2d|!M)2`^Y76g=MiBD?^QFgN6ZPgjN(|1b95IxMTM{T2l~FhN2@6bS*5k}d-g z6+}T41VOq%Lb@!Zk&*_Lv_Pa=q(n*trMso18_rn1@Ao_VT>E$SIoH|$p6B|$S3rE8 zwdR`hp5q?(xW^pUCy)t#elL5^MO<+h1Q|UKC3e!UJ{NPa20LJ?B>a1FQZC10fLg*? z6s-oam^x3MJxhbI@d7c`1~8WG1r7q0mi%U&k?6QVVAjo^I|oGoMu80J#bus(Ea@3Y zIFR7TkgP9{auQ$^TWr9CP}-0;nPQHMTsMxu&?NWOo_$9}@6|<%DXFNC)6jV2<%wWB z*1{H`8zu4T{L@`in1VESSq8mZ5FL}QzuKgbcr6}S=ag#p^Ww+bEzz{Hi{j+HMhS?n zPTfg&msnrijIEYgCiF8>1VNW|QDA6Yi7i_+^;jT$jDSZH4{{MkKmUz{Zjmdko^ipt zB%bb!J(ET2x)be7l2TGPQq($z?ku2mVg9b?#q>i>F1zX5R^zRY0W&Q6%J=N0o7S49 zQvZ=;ej)AKySfOnSAEZZ7m<_GjwXdG%_Qti-Pdxa?m!3)2C6^VLQ3`N^(2 z_?qNA7^R%dpV^CPaxGI8=-IRFC}k3oZm+;ex|*a$yZ2=QlKpb!Xzw*itiI~FGy{EH+o>V`efcG z1sRP}s^(T)=e5_{mBLP#?zC2wzbVFk(ACwhITeNk~1a@F8h#w=jH{?fH4Bfe6?T~<`5HZly^*bPK#U=Z0n z>+z&J8Ei1g8EB9TT#uAnWL#W)9UGgP%Dv8e@-*vnaTjPXwnL*39Xb(O_#w1U7kL!yE7)z^!Mt{0?gR^J z#jC#{Gns)rj@E?zPWnj{oKLA_dLvL-j{f2Rr>}4AJ+*b)woT7PvUE>)eSLRn=h*}P zjL`1QJZ&5u9{ybXctuURgEI}7p2x6phm_B!RW-Z1*t)AhCzA(?hLSEK%Log&_FHBbkwBJdj`gWr+>V?CPW{KsTIp^^;ymP=tgI@D+MpZDlh1~64JlPHkiK5u^2 zII#Ek?{63zB=S6`NX2dcyewNV_vVy~7WJ4&4EYh-k#84>#I1L5upC;rJuB#feiRc! zFJRV*y|yJu6?1oKIM&bhPd90Da`Kzp7u_QlOxP6P2ECnTVBq8ha3*LTMrBB-y8+{Q z?2k!1B3Q&B*t<0mk?G3!IDX~L-(k4};8{C2+QfQ0`3M>is-wjskgnGQ&fknm!}BIP zDjEt6(i7Qkr1 z(jiu{aYt@2IxP=Jr9oorh-S`65gT84jf8rm;?C>QI_t8r;DY?|Bc7fo6{l(XnLAiA zueQ8&Cti%B8BJ;!!lMK#!k-5zD4t;JMwq?QV0_Bb`!Js;mO-j<3lAYHJwiukZeyc| zC#{VKuQ)XJ>jNzj&ehb^-m4cyqxUQVxqFn#NJ|=vS^m^E2+QBp zlVvXQ=+#yu4p8$MQXr0(l$X09k>RyNk-(?wAlesg?d(wZyGg_q6|;h;At6-w;Tv&1 zc+@H7STI3M@){m?XTND>8ZFn%; zNP^bnol4ee3_USq*rMZe3y4`DwpEFk1<||l#X38L1m+eNJo!gOZMlGBdXX7(a=qO! zRju@JcmMqJXE0j!cjMd^pY27=b>j-3BJo$9-D_wI_OrVu2{JdUMY zBxrdqZhRB%$gfY*DXu~-Y{i9*O>^W&Otgc;-}NjhSt@G319wc^-<=HOlIQ{vb8G9t zv9L6#6ZR%Li}*f%ycrwgD}{>W$mMZCB)g68dukVVYEAvoHx#URuiRzPySS5*Q(0TE zYeK9D)#puTL91b_2mDG&x|{dnReN$&9)i|kTtc)%-bKm&f!EJmG0cX;$B!`>=*_&| zyx)JeZ)}Xr^+OaLe(_6jF)8Q{2jsD&*>=3o!3*4ozj7${)5JuE@+aFj^_kiI>c%+{ zTefd)7%K7mn3~FO?C0xyoLQ>t#J@widK+k;VH+ke>(Mk=E&#l6oCa(6_r>WpL8Joa z|8veiqFpsm8TbsF!U1Y(=~w5s?vWsoio}o80YYe?8*Rj8s6FLWA=_Iow(~j zB%d|EGfpXed@k-JaYi}WtV`Vc2=`$#GkO$M1@CtsD{z?8sO;5o>)YnU|3o>h(CMfw zx-b8}c=dOF{K}pLgTm(4W*+liM$@jnukZVRsg4wSe3-9Y=Edlb3|_2K-TAC8^YPXP z7DJV?(9sV!Bxd!5R=KP_&B(~`Nc;D;zs|%F`@6gQSIbw#7Z_sJ&kP2MOrQU~b@0HU z|4jbBKluN@AqJ$zYwuYito7S1&itT>Z7TE3H7uGpBK}9SaY3M%_v3;2 zd)KjEnxLY4e9+CguQ|9$0ZQjE+9-0gnohjLL5>EPT~Ba1lv@3lG%}! z<1tSB$68G48WPvG-3MdFqw`I2=yX)H!&*v!_xGmDcT+fX?{Y4SG5&q`H;FW^3^b*0 z6pE%?Hky3rMg{g$Q@djo!-uV`X@RQKVPP|<3KJ{GDb4tkYVRo;66+k9r$k3*Hf}RD zP3TB)=nlRg$zNE{<2?CkBYC#)7LTENV#eK+-wyJ`4N9Kbe7!#`wX^78#P03K7wyNk zC9X=|GL*V`(N9mlDFkF8Xxs;J^goG*v;c}7^9S9{xHvcf7{GC<~R2+ST2iR z$zv_+Zl!2C+b@>`0+EDOo8~q`AGh#Z^3VGlzgBBS9d~i zaWDPc+LK)a>^D+OFJNhl53S$peR6rr_U*j)@43Xj{-@nd9K&D?| zJn^>cPbNAGs@lfaA+Z-AvF-B_Ktsf~t0AZQx*uM6uTm5aoM(w8)M>h~cYU2ys)9ph znLN*WLKFKda9Yi4@!fk1OWMpf>h_OF<}jvLCeO~!>R98vWT5e;3pdW@cu$%j8A()^hi(Lf8d%nf$Q*6U!?zyRL9emp{w2gW;0zm~nf0G&zgk zC%iwPfO3K19xBdt18eUWnIeTrNhjyWyGo81tx>Swq9DPpU9xUBQ$SQii;ExDsPNw( zThuVj4hvc3N%YaVTP2@#{dl**^dQQesPSxag~C>;{hyUMGu5*9EW(_ib1xnqeF8Ma z{WYUseCdk9f380PnD_4L*?el*{Gmp~hYx!$`5x88<0U~A^N&b>NCMVjLp%N80Xn)G zV($Gii4e4}u6r)c`|Q6Sn!?+gmrFt?TId`)fQ9cvy>N{G++Y05u(1b7IN;jYE$`#Z z^Rxo1|Iq?ma%Sj^IqO|QTX{5Oq;*nqR;AQPi&kS{hWZN|-_?-i(MEH+4?(0 ztjCTaJ|Jh*fVgO^BhSxn#^vQTx&?~5nc9t3;{RyN_|oOFQBxD~aLbm`+FUpZU$)AB zc1Z!xIqG+|&3NnPvw_S$Z{WX7GWWZwFK@DFz$8Abvik-WIIE~(tKQ9U>UGG8h!8TlR0McM${P*8e|)IXVvX|g;!NFD z;6B=UdJ*d*uDNr+sV7LAGF0@O*X?wA1LHsWaNOY^LNKJ&T|-ZeJA7&0zkjda`8up@ zh&Rh5f#Rgn^BI@Op;C{))y2MpDE^m(rp6*p^mnPVzPX#nzjyE6>>Nb_<9BKWhe3@< z@$Q;rR=Q^V?n@n|F+LhL-HL}9T`oh=1@xz@g&Dn>v)bB>{QUf9%%a6(O#bf60YpV3Zu96Gdupy_R7K1wp0HIf6^7O9l~W9@Wc<~e zyssoO-b4RU7?=5xJ{IqRFHgM?If5_lc7cHmizD~Q)!&N=ktDM~n~h)V-B#W!tEdEm zE7$gO>8ojE(IR>DSS#?{&X6j8)Ao~<5b)ONr5Ctr7`(N+93NM^lbXZNd#0|a@`$CD z3O08zfjjn{yw(=Jn%p^p~C&$u-`k*ijSuTH6ZOor3yZpu^ z0cl*#Mlu1rWQQw!^Zw^v$+@WA$VKME@_t<#g65E%&wsLJLv}VW%*q6qFeoSSuvfqC)jN!JNpKGBHT>Oc^9kw zk4`qFd%yAW+8zAv=HOy#-&kis#r+Prb+x{p`8P_(qof)4tb0^4ILW});E?fZf7N1T z(I1Ubva~I+R~nymr%N#LV*5WL++XqPf-|9tKpXRBqE-n)WAjVtdIl+-UR@5cMVoE% zkyl;RHoZg+5_C#20E(nfS{2v4w6097?);gezRwkO!AzT3BI)8l2pvIcqh@;#wU^bR zil18{0O~lhydZ9bLK;uCpl6%e&IRENtzF(;5jN!=XSGR|8 z7vBZn5BRG)Fx26td?ZvLt(F-GZ#7AX4r7T=12KlK}OIfB_0V|BcKMXszJv9_MS z`Qc94YibUIET-Ir;ccaz4K=X_mTzw+n`U!r*k>Hj3?bd@<}mxyIwe@2CXs1~_qM## z3sej`?cePms#CuQ*qUx!AD$o^o~ChO?0wNf=Soc3oT%L3yu#!@2Y;Xlj1lpvxGwX> zKL<)SJIQNSuJGj;@qW;1eIK?xGc$9`_AXKgK&7;6BU|1t{lvD$2@JFbUw%9n)vK_5Li#@f~~iFrgTkuHO>* z)998w1hla{P;r(6;qSvLR$I16jcea-{#3Tt$%G7UaJRc%J_F<3eA~CUc?07 zA{UZ}4+U6NGEeoD`)PdrUQF3EBO2BwcZmH~_p_WXg-zjW`@WJt|3Kt@ z@$~&3;K>>Jh-7G-7z{mRsi`X5 z+un&Zk-D=TSo!0V{p<~QhmZ(agT(vK>wl&%|F{3Yl%P{Lm}`bo(`!R|?kJ`9>a`m+#hONK!>nOyjrl{!=kuiM2#-L_Yb9$&bhw=R48E)Cc@SV5PAI`wg5Kg$N3#}|(k68mlj%ihN4Q+p9Z^^)|D;ou4VrLf$4oqE`O+X(1vM*b z28Im-3{n20b)(B}Lv(UrSd{auf9`&_p-6Sa5xG4Jmqp`!XSF5rU%!5N_+A#2;v}b! z8EzH>R`kuwZQQfym=+`}*@H2R~|-)zt}IIp9A#N63ucD?KbKl0cb( zs0uVD-z6k-AK`sTMc3)_4XQOM6jf@3`H}abj-wDKeIVCn(l;=W9A#AJ@&kBwQgL{c z`2kQfpzxdW9`_E1kqby5?ps@5o6in0_lC_y%T!g0S{4J}E+?xB z5u3v0z;egdN%9vQYa?4F*%<IxT~g8Y zx?GW0cV-$u{(_>gLY}n=#2UU39ia2Zp}gMXpUaZohTr~6d3P49=r<;@f?D{Vqi`Fz zXs2sd*1ivSGD%;85+I`$Zoh8*^V?4d5zYxUc|uiD36h&U$_2vc#+0V#1P$`)f}Ss( zm~Q%$Pv@7epyuTp&S%JL`~CVc{vKZ4SwX{JR*8-|p0g7j+g&zXtvpz2(Mtn<{KubV z6nTP9s)aAj6p*!{_$!y_d)NDgQ}|mrG;BU&I`+`0WtY|1_b^O_(6JS!t1s%4G{_0I zW+iK*0(nZ*Ftvh7>nA&QKBHW6Iy#eP%KiJ#g7?a{pS$yRdFwrhI`x}haufHKAS;4y zJdd-nmxul+)?=KgBHBfc?`=^UWBv&XnWueLqI2F-2 zZD9FZZX(xDtCsrVNb2HBN?a8Qkk&IFA3o4|rQg2oLjAw;C~cx-#(lRHQ^1!pG461^&VCsMk^>9`ZvhBLzg(s*x z!d$c#toYRItTv{5dy$JoWoBpqpV2L3(>eBYY{!ouUxiyk5L@u-+ow20ggABw+&5X3 z8`Wvm(37M_Y~oxUMU~?sxB)daIhRQos`Mer9sJXixIwkNw3G_op=1>6Xc`7sTqdn& z2*)n47C}jMfw{L32Fg+94ikRBhE1WTHcCWyE)n5YX6IRdvf-(|9Vix+H@;C%v-r8| z%a<>qP^1%8x{ooL3>ye!Ar)sqKof#_;!I5bR#@Q@YG{b{wCp zj7U#h^0}9nA^-(R-%QU5pWl1`uay+Hy-Sf2oOd;ABPW})4K^c9^ghbRTr*XA8ya9p z0hqbC_PeiL=xA*{Ty#OsWkl4r`xsCOH(QA68VNMFL@`7u$8amb7_~R=tejt3T~e!# z5Q#Z(;#pcpQ$+1uVQzz7?N}{&Lxsm~b32_VzD;RW?9p+|&r*O|6+F@Nx6WY_U0x8z zqa;qzmY?+jqLFZBI3eZiTQ_fFHPQ-kx*MFV#M88%#Z`#E$~4)i zODal~jnKTMZ24Q@K}Up$l(}X_?~T`~CxJZ74mq<|^6bxB%|fvc2&Vp}#+r5rX$A6m zK8wL`!TJu;(mUwqC$3iPrT=KqU>_MZ!)0B)s9mR3s}#X2l@Lb}7~hnbW1d(VX&pu$ z@urSV!9;F+jv?TNsQs)v6eiO{bus?SQa+nH_5hgq|oBl@-{sMUM&9utIB6i7NBR_g9;tB-O&S z1ULOX!{|2DW{c4M?Wr(r`Si5A`yR@Ymk`=gp}wrBsOZhq{TAc_*+eR|Wzf@JfK&^* z^H-HTATn2Rs1$Kv1!~1a#R>g{-+=LTZaF3_H#HV!b=rH0*i8KCF5Y~M|3)YPak5&0 zVbTJ`5EKL{0Iyg}7~4QfE(tjdD3F5)OJ{_|-xv)*H-ge z+k03-;}kfdt;Z07wzOv#@3S0~k9DKnzw+$M=Z9-=6e{Rkp>H^FgrXF(w8l4mrq%_Q zeVopt_;szZOTcU!GaK!Z)!7I~%TZ$f@=8sW`Chz`@03YfuDN2Zjz{Wm+}8VI<67AA zArc7q;Jx1CO)IdCHX?2#Zw;P$VKZy0twkND>kE;c?mv3;+6-k4^z|%&-Lmsi%Y(id zLR#9MZ$jl~3i$*kg5nxaTWaynDeJr)f0Lso$rDiFN546BA@iyy9u2fgQf)Z%?mR$+ zf}*;ut)RcZe-N9ZC!k3b5Xs?AKccZi)b=^z`^+zu)SZh0owl3NOKUyZDGR*>c~o6H z4}K*(FE5bkDkY{w^<>e8FVs^=qi!avw&?4sK&eJ%vOP&Yei!Q5>DeDpHE|2G!#_&b z_n#iRg7yn^DpH(QrFI1*m>RA>f&NtEW?eRMg*79?E`-->$Y!wBu|d(HT3{ax^``~S z!9scotzCInR}Ab83APbPzyRj}^=yp8eXJ1Qv86O)X$UoSmmTu%q04bdiT{REyy?mv^{(+!v_dVlHf4a{V zt~m3r%+q%_Rw_J^aP|gvLb<39n6onagt+Aj7Z(>%9qt*_jeMV^RuEoh0-@Aa7!Ya9 zhTZ^o5SXUtGPEMp75`SPdh_j* z>XrIkZ@R8AdUEG{Dj(-6TJJBfj^w_Q^xyS5L}>dh%!tT9;CI|WF*bn~>ji843FhHp zU$4=SuU}8XaT=-B!f?yGORr}JY9d0g$iF9>IEaSx#Tx{cyY%&n|s(l<~BA@XW{75t0+5r0_mNYHDL01HMm zCbV;{{DGQ|2)C%c2e39YlwITIu5_VPmS>*Wy$K`=h)}YHcNq{^_ zvWx0;HBgohz#}sCSCIQc!|@bM3}7p+xwNv<1G9;olJbTAFTz@hyr$9Hf6fo&lJq!>MumAu%y&M&=q z>HN)`H(zTh7%G@7v_d6lfzVbPDgHP@Pk1j(#AS*K66@)x=(^n`nHeE>O?& zvavKU2qG?oq>?XNy^<7p`FrHkZpp6hT`KF`NHL9KtcvHW?;blsI|-)3JChz=8NK{1 z&CNJOX?tg<_~N2*q7H2QV@^1$f>yjbE4%z~ZTm@ug!KB%#k5T%*j(hG*OXT@i(ITI zr;9oK=7WT|W%_XqT#(r`m9;4M{fbCA39XVStvp+_)!8x1n=tXWY<3RE_LsN zKIT(-Xc&=M$MJCgbI(Q`eUe6{Qp7<4OP2M6V~hK)a34}IEU%2rR`9GmGFbZJ$KQ2b ziFJJdE$_WULm|Un!59;B1Z9*WjOKGd^pNTFp}gGmXA=Fe$aN(jZrTd9D=D7S5KK7F zj**F>Hv!Y_>kE#Ep6vk}KUny5WYB-)@@HWo5!~#}cei|?n+4HD?o~(5GU4Tg?+9@q z&S}(~K6lKu-f8x)px0u}p{OOOm-lB;!aOO#et~|4w)W)Br2Qm#a=keC4V%W#k6VO* zxe<7T>Mwj;Utci`tnAHB_1yTl-@PCUoJ1(ZQeW9mqR6JCtek2&bQ*Fn*NhB4T#81& zd%dm0%KrWP37zOj$Gd$%!kaZWvd90MUI%_B30mwUXaiKrG8Kd801-?L6cQ7KqT{py z*#P6FX6o4Aj33RrNoej+Gw?^$k?-Ib8HnCFniYi4gcqrHd-hIra_n~5AVC}OXE&b+ zZ(r^#Sq_zqdwp)Qt5X$r*tLV+T2lA^?s&Yjk0sTi>FDa(W~DTHLfMbgg!cLPM*Y)P z;YZX7y(mxjmWUT9D4N+{^;@o?iH%D){`p9jR={$io&JnTt6)pt>gryEI4MXnSmoo~ zdlHSZlF`Iz0gBYCl?=@;Oq~WBr}J#adk~|HyCx#^D+6(;(Geqs>A?rZXqg}~O~3}o z9aE~qm#jRB06}=K4VFg9%M5a{WCn@9tESm!nL_37{k5jX2dZ;;%~vA3%W>Hdhrg6I z;4d`BT$N!DD=WkK*a7f*Le!m!EJ-!r)*s~w+IdWwG}s3sp(=%+O$plQ$adzT3eYn+ z=$4fw0B}BpOq&Sacwd&4{m&bNxXbG64`5|L)L(`7Kx59Gw-a(92uJ|f&AAWQ(cnY0 zY5`$)k2q0btF1eFTD`Cuzq>S^)rIQa4!A#XvHt}l^rg;kmVc?3nM3SdvPOZEA}@h` zN121q(X@PS3He>N)yTnx$u44!OPMn>p@5DPFz9e}3US84L|0KAG@MJIH8>{J5BT3E znpyK7p@ptEdrFI1NRt9*#cGBRQRG^U%K+4ep)mq=ov3y@iw0cj@@mAEO~Yu07#km> z=CwakTwENKay|OSiD%HhR8;;NfV}t7(-|6G`%`H}YrgXFSAHzS?xPXzpE*+z7=*mz zliduZ;kwA?@132|Y>I4HDUb0|UaNm+^Q|h7{C9nt2q+|?aRgB{L3PQ3D3whXtUT>nqGdX7a0VSObqh`q1gLa2Mz3G=pLQW=sG=g&y>0uO$wSaHA8k{)kzOwkiVq z;5%(Nog(#m;JR=^QI<(;*w(*VWkG}W_v{-AZhL9Z z2Qr7(GbN1JUk+37P4C!#O;Tp}u3ehIL}-N`yZDSDAo;)4WMGdatNGEfKuAb%$Oxlc zbUPxk4=0Pu#n|#a>Q?#;BNIfkLmrBjQc}vRBs6w&!}f#_$A5zdO6^kPwPmMjfv#%Z zKQE7uc9ysOM+<=UO2k;;Po2-Ma(~|_`M+8LJbT<`87k}N{#L>^bna|AMSNBF6iW>( zGh~X=ts_tq{^#}oT{~D;iDq~Gj@)sty%+u}$|Tje!PD*_0ap|UN)b{p#ErmX(@8)q?edWSY+lYEk-N;`>-T?)9mU=-KCd9g^YxewIWg3>IyOk8X>u~Z#aObufKoGSuok8|4a7) zssCG>0r%M03!dhf8C(#9{cbeh{t^ic)FIq1(Nqt9XS2}Iw#%f-;*$v)0IQhhOqI158jE@)VEnV%3ze!J|~x48TCBX`eDY`tuH*$bWFu@qN6q^sue z)%<9*(D8inT3^Ugr&it2_r;0ZzNIZ6xolWZvORkA?fdQ`3HOLGhXVzVCWrcp`zi{C zA}1GS)*>fIruzDpq8mQ6(N?zZ*tZXfjVG0w<3qHFa`nXSgY`Y{0LzEeM2ZX2UwtIU z^^C{11Y|;Ay11 zKHw?2(wMAJmEKWa&U}NrCNdi~2*Jaa$flLk^)I_=$vwYr>*Y7M>v+=z>hCW5t*y5; zETpEIC%jgZCo~Hey#}ppbI46vGZP-X8+tQm_I|`iY;3Sbn>()mhR?qS^0Rs=TT+q+2SY6&#;9m)G=O^Zg&lAZN8?*0*f z*7vB;x8AbdBOQrUvJrg8VG5t#fd*aa{4H~EefDUTzGKCv+8hOH(GvFCl3n!8F0Q){ zecRmKbAuY&(dycU1M|yzeQXtcfCOS4*#}|NezJZD_*9{A)rE6O`f!9$-{o%|9duM7 zt56osRP!>)NRTW{l*QR1yJrNs{>nP471y{WMrJJ(*>xD7wWYGKH4$*}y84FXWWb*6 zoE*T>)BMKG(m*y))amA?#27gEGaN;C2bV#>K42`p3g0M4VI%9;{OC}GY+VCt38bl7 zT-3vDDFWmY(;9UXF+0e}G}m%durac3-ZsDe^F6Nbu*G6_?k_>*Y-#07mrU67PUwb9 zDl1uWGmF#Tns=YvN3$UMDPUUBoXGdiB<{gm1f?tYLE67dg+eS9#_o4W6 zGj|`3W8W6cc3X4qcd($)z$R(lIsd2Ax_u<;<)19mL}qt*d6DtRp~R zD2d>*LO_Y!_*$@EO>La!&)MSfbHQwin*B)!(9-%4g$$q%&zP8$7A=8GQsvGQt9E4D zE*$>wZwJO|#h|_PpAxapS0ou9BT&h%Sa(K>TmEQ$C2B1<(|oaiWMJ^l*H-L1(v%by zeQz@^?5~{uAGP1K3Z&QniRBQ2(>5HuN6ZhqQ-6Pj=loUIi$|j;NGOOO60gegoBvPW z*va7Fu`+1pKG3;gWr*s`9wb!gc+co3A+GQ)h|A?Tb7t0mVRB^TGD17jCJcCZ?-X)D zr1O6HE(iSIOLLMQu(P%P^{>j^+z7`lf?D<~=t8}Pz%t_inL@Vtl?f$kzRHwRWx$K)_59^`K|vKD0zTFTD6F(XkaO;UW&zLLB7WaCV47 znUh)A8ksnRbI(8=+(1JG4wgTM5&D!V7bCuw{frRA%W)t2r@2g?OZ)47JYDpk3c-m% z5CE?fjkq8+P`azJ>2i98^Zg7qK>G6Tzg4? z$HFuDOj;>iL56Xgb&7(LCHo98!Xxw-pj9Q2{FQ6UMO~UY_`E*$(hO2}1@uJb7ZkjK zb_LEaHI3rL4g8%if4?Y32N+4Hm;*nW#fpFa{Q2zwp+_ifp!=tbsgXv=Li;t-XA;QC z?34KK-o3kX-@Zi9qNswq!M#Q7rUS91gk9Wv_V3jB4PmE1Lz^o4L!suw!H?^N1g|%`?$eAc5RL9ct~&2efILT-zMC*jI=sFpMrr1&9@-dnbM3o zM&83d)MVR?o=r*GhFkd4RkVI^xb3#bVY@3Z<@dBpX1L!_Axla~#6MbC5u?wbxw8Y5 zXeC)iR=40M%Dq>gX$t=N>C#2Nxa(PqnE4Uq-6c~Tx4icgrB1>6X9==*uBV>|da1tXhU(bi0ZOYxqHZ|=T3+L1foJruJqd>*ZN>g053_tu9z#97iu zL`;NZrJT>Aog!4kMqEK+D&Ckzj_p>`mne}9AFZA*aB;*E4KhrYw;2D;C*^na>f>b} zxD@f2w59iX#aFre_{gSr(X*_-R4*cJOj3(@tN6BWyyGDP$N?&F3ng3SJ61 zT~$jO4O7ScE_ZD+Ts+fy&V>DWB`Fi;?KC&RqS7 zf_D{arbH#hEk&Rg4oTTKxZanwAk%qi;5M8#r2&{w)rk~y@O5_DuGC)lxZH2$`JN4N zKY~!eBe59%uHk!Bto#QjB{=K5N5;lk!NT26QRAb#jgEP36O;Dzu;-)4!P04c%qXkM z69Xm3ggyWyQsKK;B-YZYTv7Ru&$s1H)7rB$=UjzvN_a2qlmt*$>Fe`Lw3u%f!tn`P zJ|jwl0%ZrI-VFTR-Tk@JHBcse_yWqilZ7G_A5X;$&PUxjU0w4)esIva?bkSHwueZZ z3370xa`~^Sm{b?rju<(gdu+x4{U{%jBb4@_mzYxv>rFPFY90OEECfz zamfmPNAL3N<)Mi!t78o<7<~;cYNh!BdEX;*dMzPWQORXGIn0BZt#wO8D%Rg=#ijS< zhBKDip3e~D^HM0-n>ZETI}SHO!g6BLWzp@#r8U`$8Q!hPe@Wow(Azv!azxD99gOge zIQQ+~0Jgkzd^$bWR#ahC?fP(y+=;);rR!^gth!}?l7!iwqfZitEECLE(^(3KsSX?{ z=B#ib{alq-a51%Z-FPpaQ`yg-Piwl2`xiMcHK&gl{UtH23r5PwiZhO0w5NC^0u?yiCG_W_qi6a@og?uEh#!jr_ZL zy&IvXzuvQAx83~GdCmTe!4GOzXlf&V5@l0!OG_qP;2F~+F2-Nyo|BaYsi_pLFSC=# zhVv)Dq#DA=AF-FR$mW7y+Ik!$A<`FgK@Y*G#CzlZDG_Vati{?ScgjMrBNkd_I!y)nD=bn;FB(?)Tuf10* zcr?PB0Fm!j=T-CV=gv*ph~p+h{~gy`r+IjrE{knA?vCbIIz^IF*jhQe2Ea)=V;OLO z)#s=D3$;r$InKQHA5MpE2>Bf5k2j6NA%v^jAn|5K*(PCV)1AX|y0wo{^1lzBNm2qg zd^t0QX^QC-o!O6cl;4AjfBkw9ZDyTy@^8_GLxyNv^dYtm=RV9Wa z9is}G`CYsnTlL?YDxVwHc=Dulqenemsa~=RU{$n!eirmom~)**Oy4WbD=C5NbrqV@nhX}B)T#Sn?IusT=;<@?|MmU(CNFaW0{dNOcFx?yniCN z13`)3|J`WNFx>^}EIgxh`P{zf!0jPY%-KtmY1NxHKSrV+ITB{L?eRl+t}wTC1rUMu zQILgySk5)Jy3Ku6VZ4bW3r~i~!y^K{rT=T*t#CCNei5=Uuatvdu$|GLV^BRryt3u7 zJW590EOF!b!QPmRY+`RHOk*sJ&=4TKZ(}3iw9q{nutxOr(1JC^E!^HbnN2l+ePf*W7#(P9#*W|Ph&z-OmS-<^H92}z{SZFPsO^f4Z9CIoE%v~5|XIh$jlv(z8R8pNvN zrpzm$Gu0dKG5^o=^(TAPA5_TPT#yS=LGb$xp!ho1!tptZ1zqgjSM5;$HBhnfSYaoyad3 zd7r3$QA#cxiItV^704G?TJ<`dQEV0RGF8=SN9vf#M^+)bX-1$+J@{ccE{rdbto|u+ zsau?$b*!CrNYj-iX)bWsw|(F8=07ua+huK%n2cQlP)ge43PT21v>0e=X(@BLXn3yV z=H`-2_3hr7Q%o@OQx$X`^Jq{ns~XkGZ!LPVan41}HkBi`lm46+Wew?$UAd9^E^RLA zf9@F^uc&%QN1}duC1A8Eh0wr<^O+gmpe6A+EU=}!Vq8SfZkm$>$2ON-<$TazM%5ZI zVJhMj-ITJ7`P?31|GF6hHbO{=qx7(!$Dlt? zNrdr0B_jbkd4RG6ecwL?Mwp6GU|rWqxb-2rqNHTAD{LEXWL()JZtHS}gwN<_+*DCr z-X>dz;r*hbqFjf4W{20AGYo&l>-njbmY4VT^b|kO!t-_9_fCDI5TOw{*W;|FDm00P zK#Y!E_&H8!{cwjAP54`XHyM51ZPL~^7HM0Vh(rsOcQTi?2cEEa`!R8m^G=riLtOpj z+28F;KP6W}9FmWapmpMv>?l=@ZywI_BPeJ~P|bQ{Ye7Jvev;bWx`o zr$Vf~No&FI#m_OM&21vrA*v@~Q%v6S`2~HZe5^eO{EVcInE$^W;{yAlouv}ApYY*H z%CUR%Y&L>=3I{5!=Es!Dwmlx92UKQ!fPzv*ZLKf(hC3n@2P(G^BfBk2lY0H`wj(R5 zPQ9LrpEX9VA9+D0o3^MYILDiP$YFQ_X-my-KK4KJ^Ix#DBA2-VNj(l$sCs=dCSoGt z;i5#TfU#=DXo}4%3>xaCR5ms?YAyx4)yWP!ZUIRR7zfx}mAaAxmH#h1Zfh z2RTCnd+*YoklLGgH;<|^xavUGCewTYO+k(rvoK=`^{zK$E@spa=P+?`xp#N7-+Zs+ zzmwiauk`Cez_xVKq# znRl*N{R{WwBHQMfru^cYeK=RJdCtC!nV=_j^c+h+uu5}1-0w4fg$3@VT%7$Bi>E>N*= zM~?rhjunF@O~DpPMsjM}LaFw_k!Ba(s3b&(1AK;?=Z(tA8+0^3^~Th6s1Sc4*rds} zuA@-$%y5?G9oh%D^)7ss4oyN!{oNOrhnOc!GW4K|Aju9OH)2>=nC^A zWenLpI#Fai`i!GXS!BzB)a!_tvS!Sb#G6!J12jV?G>6oBkD0BdKgc$x_ht>9fa{FK*bUAB(eST&k z$6?_#R73q}oWc67-xd%{A;!|0)HK_U*I-E@F)nQN;vrJ{S3rP)T;3}m_Ik(-ee{I3B;&! z_wN&4DB3tpzEYDghXzfBYJL!y5SG?k>H0SiJ<4&wAcT-K&~>tyb>`f;^ z36@(ygx+P98^$y*0SZXQx{6{@`6h>|5MmpHj$A(It`nwk96UF~%nC01%EP$UL)7d& zI8-tTZXnlcWYcGO5iNrL$#GiYXSx#99ei62a)7eNYxZm55lyJ86FzSdW`&iIK46QD z$LFLQ{`7>wQTXyz&ILr&1Vl#T)^q|yKq0gfM(5sXX=yDvGQZ6SU};f$zf6vv83C|7ZZZj07idsS|_%0B0ff;$T=E#GU+U7OG z(*a|XKl|eZ+>1w#rTC|eO8uVEN>@=>MOhhBTT@F54PjAWY;641|HQMA89ZH?6SzcxXiA3<24VdNvk6N9Vb-Un|6^^iWi7sVymr97$(lt?g- z)xG&aK58##(U+_)N4-{OpxtjTJX|tma4BkPoDn}wznFtZxoYWP*cQ!xyZDR0C=Dv6 zms)tD$J#;`H@p)=^3tcL1ht!Hdir>u&X4DNfZLi~gJkQ?jRO7+l^Mmi6$v&b+WeM- zu1rnc4aeB)WGg$}_R@XB@yJHYH2lWYh%s5&QQ6sol&4j{f(d{LQvx1m{`5EYk)t8k zpIBH~q5mxszo2mAD^#`0^w!oJYb(wOTZFwgfdGm&)%790V*QM}|8vclxf7)9*9R&V|OU z`TnQxs+Cme?ee1JxR&Vx4ad^*XvQt~GouKqwCIlqKh9CV@Ga^A8O=0haq;bzZ0;#u zKgw!#_h*+h)a)`8JS&v#O+-(JiTS<1YfD*a3Zrg#hV?=ejpY;Oe9XD>O+0!Ra_;wR z!bD0y#K3$HkKP_a>EPgStZ@G@O2nmI8r!Bevi;NjwojJxQjz^{@?dup58Ez%_Wh>tk7`@C_rZRR35C+5e7l#KT%%OmHaV887=%^ z7dH80eK{`I@8rmQyCZ}G!euX_##stYFzS`}(B#?pUNKNFnzfic#aa`{wt99Yh^TCc zFl*3wFxr5vuwKm$%<4S+)vwKlQH43KUCbGVm1K4fTrp0<5eSQas|{#LU%WTFLQUpI z;aBxFV=7z!sE23{XfcEO*INm-Euu5asZ-wAln9Lrm?=GT2N=B(dLN=pLrGbVd~Xp4 z*Z&YZjYo{zgtg8g*qRYq7=RJjC_jPnc|R3Z1(72n=(IpXQzy2d`nnI5fp1W*?%cKO zHN;IGl$i)gGq+R-I5 zzke)4{X;A=lsBqx4ug8u8oxby%6F>!oC~Cuv*S(M5kXeh_Qg}zff{{tCti58$?TbIc)<|AV==j;eC+)`k}rdmEr4DoCg_(kYEamvp0a2}n1XC?V1f zN;gV3sB}s$>CQz+EE?7~Z`{v$-*evQjPZ`|e1Ck5y~kL>TKBr+7js_ox~>U~J`%(a zA0RY_73+nfpwZ!M_!h9cPF8I%FEl+8#Pd4CwqL-foTutHmtg$^XqwUKft$0U-R=O1 z1p=U{l%cOu?t}rUf||^JT37{y5Z%>ryeO!NaF|cyUkzYL*^LP=)MqzgLO2`AMt-Wm zNQsgo*OPUm^KT0I1ezWsaDaH_5)kU3L@nBvsp0}XnVGCwVR#99?Ad_7DFk7#nw(>X zZqnO#;jSPapMW%?=6f@U01tZ#vmahbni0G4{oi#PQ>wPh| zS@&!B<5zeO9FY%pZ9ovXg(^A#wE_vmD~Jq$`PvAvmCMANYuy25AZGzPm;}TE*`VP( z?2b^j{cY!scwBF z)!7a4e?%>f6pz80X&OLbD6}h$toCN~6k5-`fol^N9cc>+d!#T zxnAHr6gFB9l~hhwi0?`3U^8x0KSMw&QDS8s8P@~%;i-i|D|JF3NYU8=403m;rS+SG zt3YZQw6ZriHh)KGq%e3!hNQMf3vS-+(Qn?J z@2Q}4S{a==+!mjNDn_&4+>4XWZZ2z@bD%Obp@6W37t`-$ z)FSU{H~!7NYI@|w+W=BFXk`sK^N!8IDX?;lta)NpKF7$*;*w z=I@5mELo}<@!6mf#vKKGGv&&6?ot$v-nj+b6ueLBUlUL1RJXm{O+cgv_68UJ555nP zkfmEktV>%gH}n^r8Y@%-RWPmm7Id3mFusN&0`=0y;VDp8YqFd7;>$#`2@mKPUc}*l zd;Ry6Y$xg2zsvAy1s?ePEPYOeLbP8^SPYY9WgXX+|A!v!g*ZZQjf6d@o{0lLA2r{f za|etEn?tV$&(c&Kwe=NhfPGK~;sfQ9gu!H7#nm2I{1`4X<02*DxBm-3MMx)dA)Sc2 zWnJ?K$_MkKNq3<7C)1ltV^?d+g}C1Kj8Lvt3WS#UR2mG%tFFMamFc9tqyEMFk%82L zUa81O3Jfl~L_^^IGee0Eu&zfy*hTz*LCsoM zUyn!%U@K&R&@O)}SFff6LSewsoLv`5J`+ADy8a;Xb6@3%_mY1`D6bVpKbovK(`~hHCCNVK2fbUdN4i)i?k8b=U8# z+R>2@`1)37h$`lJ>Ok^w*M6Q2RHpK4k)ghHMVMZ>%`&U1awbi~(Ksu!faB-!PVZ_F z^r`$niF;Ek4US2tpk+s#89Q~mwC16P{!guw12s4l6!E0&MC|e|NY5GrAr!CasdXTb zZXx8|bFw5zH6fz?Vow0tvX;$?n3x+7qp^Dwb2)|kHwRB9Ksy_r@bU4T(#3vR?caGJ z)M~eb)gc7}M;>|zp^c!zk1&i33|@h<4BD7Tu0_Iw4u*_F*Xt0goDjk|`~CO=4rotUICF?d53J+3$2FcUZ|;o05SZ^~3HCVJy$zASdWoZu_10pP-?KK0GpFwkN)b}%Hz$JR{d0%dpt_`Km;nUpfZ`y2#ir?%I|8E#(xRGY;7k0s>*rT>En>mf|xYUMW z3#uHES*9ofNezB{Ma&&ey9ia0SvKDA-aUyX$?imVvC&M%^+Kecnpe5*JFp)}67tmq zYfJ;uxj>`c)t-;ifVPYRuO`S63M*$=SGSlB+#lzc^ns$yUy_I}gx7lU&1J&C?@)Z5 zT3xl?>wrn`iX{d2JFC4FKh~?9cKzp5<+lx1bz8^0)l0t!fcq-1P?O*{5T5{xz;glm zzY+Z&)rtp@7%&pf!)P(?-KW|yTx`tNxq%BN1o#r68<3H;!5w8gL=9J5Gf?%1 z;wELu>iCG0PyTMqh13Lqa=&=&2VNy+fQ>P9egha`Yn$#XpBtkzSai-#&j%$j=~jLm z^>FPf)F6VdYWKZWwZqvxmh@z$=xN7r+q*(YK-!vhx?c_@k@ozEZ~XpdDz9gx!jW$8 zXuK1jlPu&I3QCqmCYwGqUjaHxcit)rsZEREv<;!f#(9zh9&1AK`@k7e66*i&$Z4F! zt|(jn*<|o10AJ*nPGck;a95apyWj&Qde#V=Eq+7-|p?%~g!X|}iq98b!s9PhHFfpiLMWvvNaSp(i*WB-#UOOb2CrO#hU zh28n;ij@L~OPZk)w?Hc(F~s8?j_Dtw^)#-SBnVeEKaF@L8RA~*xXcf=5@=u*l{&W! zvZ(QYOUXl}gD@FUyMy`-WGad|nkoke?y}1uQzVCq!E9T6YrS6t{j~01ta)4Kum9{U z3=HhN!h)}3JbJ*_JSnX{8)H8VSAetw)AIZ!rN*gCJ30QObDAOKI`_IqM?y_)L8tH! zt}%Jj!9vMYf6g5C$kt|tYtrE?`Uj!C7SF&&@UJFI8Mb$68QT zm6>I1&U&X9NU%DAYE@>(R1g@9DfeETkx5`@{$U`65sV~~c6HreAjTOlONM~?04dmF zHK3gscy)w>H?wXTL}y#Bpu1sRFgJt z2KvTW+{)Uj0Xb2*=77I_6Ni2`SDI>Qdw6N|>bb93n)IbMGt zm5(XSCym2z&PDn$Ej=3nAgLVEJouN{G8e*!P-!+A-{`zVFA!F7x9I%w; znQIXUcD*-UqktC4nL)IJ07?@RFQDUf(T~eeiXE=@hykz>gr8uc!U6^eC5{regC+mr zHBy|jV8!$N31?ge3LWryfF(gwfDHH>FO!m}VEaZ8K8QTwcSLahF3tcXz%ytvSffV3 zHEN6kp2GCR0=P*2>t`SmNw9$vn8u*h&@=edupAmfY-pFA?I)lbl8j{iTVzJ)PFH!~ zxopj%y|0@wgT>jNxtvq{=LrcLp~~pD?;bnXtvR>2wJadB9e1xFl{pMbfrwdZwM_MHouA%H!|vPXxHzkO7Y(iItIwL#I0nX5p2`39aJIv4Uc-Hs)9m z!ZK&LeuWZ(MKS0W)RVTqHk4Px{-YCJxcbG(2Wc)~ytAtaBuUTxWnNm@SQ!XXp=i=F zbv^iatf~Ng`1Wnu)90?T9j;7F+?x+j0Arab8eg#i4In}?3DZ_2w04TAUsN4`an7^xB8 z_?2&T2NH&p3P7a;^462VVlNzCyjS&wFhH6sJpwwp;HJaiv{j7>*irhe1YaGn$Pq|K%?7pICzW=o2 zk>&=K&`kUt@h49{4Y;@{KaU*Zt?j1K`dVd~%p_E|c67NWO;*bB1yuWpzo|uc=gM!iI6eoL_<*|R1AuEEhorJh}%FjsJd&kJjpfP~w znby(F*;Lv6*LnT9(03{NZ_1->{8gxBX+U-g^~D*e4t3>VZd7h`RFDbITdUTPk9JWvi<>uk##`7h4{F2%;J@=+Bnd_^Pks%bwy0 z&p{w*?sm>@P(0AG6+b<>SFo|3KFOA5?G1Yjl2$eNleZTyYXQb^a@C8Cw%xHOIxW2I ztG}*Kcn68}oY>3cIgS*^w&v%q=oCJ{M%!A5v!bRqE{%EMF2v6WEgTFY-L%o!E=XG! z$XV_K+N5MzqnZ;9_N1(ojt&kf25AJF3nPRgH5vz7L#t4b*SvCUmL{Q!_@WPZgI@(K zW3{hZk0w`>Z}2S&fnxeXt-vb5%<`>O)DtdJjY?AnO4);tkWr)7TIXJ}=ox=I=!7bk zRr%ptA0HwR3!h`H7KOS`QWNyb70 zJxoU{Nl<`t-MYKO=*eju-RGqUe}?Ez0oDmHiE4qIm1KQO3sGFJ2$jP!BkyNR1jrtw zfzS>Kb(4oD9K3+oA@U^Uu|z@O^e9qrz{jj7^;l=3FSR^O^GtwxTI_A-qZW_kRVd1| zC2aG>@jJ2!u!c8Y5|KYnVsVhfcBJrmG&VHkgCl})EL%LlO(cCCqx z_r&E*0FBAUu_I>Eajwz@+UA5WY=klhLtFP(eOdiqfGdP_#u=>|OP-4n0yPBt9V;Y8 z3d)v2qR(6;ZW|jJ9xk<;BS5L;o40{_0UApbHmm zgM9&42KY)i6aRQYL`rbiDucZJNK|l!D{uuK|+z;WAE-y{$8Ct_Qvhqid zBE)rIS`rBbH;`Or84d-K3yJNn$1{-q4OQVp`GnC>7M1Aj(Hj~6+PjXmK;OId?@fGP zL5fH4Nt(T*+M|3s?BRAL9d+;f@C}DAfKBVb7X*@tc`bMWDV!S-K6LC$u{7O?7 zmAFQw^R24=%|x>TS|o_MlHe!p2~PNN^ZTzvD7mt5K2PPW6em|GDD9v)443zP^Q6tH zu1scQl-&x&tajuO{uAGUA3vxxzBR>G8mf{>M)j0bxZMJ!J))Z@TDIeAp2JV!m1mAK z2>-yEWY<7u7S|dY8De0U+qyZM2^A5jeynZdR?A+(Z12lL+dw~u+DgId;RzZjnMlaU zObU)bzz;ZY*H@(j+c`*;y8!6jULBJ!)U(uP0?ymQ78owV!zM>c{;~kvJwbdMbt-`J zFYn~_>YM18XI0cr5Fwx-51%}_h(UX_LvTg{rb4JkzRu%7MM{R4aM#)T_F z5jtFrzp4T9F>o}XDSkF2_x|!){W&OOLTRNQEK8tlFbULrnQbhmn;`H8A?sv&`u!o4 zr)1$Pz~e~2XV$uH5%)AQB&PSnjsI43guQFmG4dn!oZY@eAm8zR+=DdA1DNRs=+ILLe`~QOXBs{BUw;lX<&6WfHl^w>6@Uv$g-6hCs=HZp7f-uJ7vu#Rt`)96mOfv(yoJ7Rx1Vr-68Jcgt8VEy& zo)Se%&MFu41+vzr9S#l?3Sv83WMLZBK8@H>6`wf?jVb4#0E8&JfX_8KeFY~YXE&P# z89#iaU zCiSo*P9*T#6N&ynBtVsLJf>$(5cDd>Nqd=caZBZbR{2qp?NXhB%H?Te3{~O zEG!*}XzkrQ;^SNcC^^u{%I$(TblRip+6H2F66`F{U-KP^`q}jB{(|q88OqbCX!vyR z6+N_z0;mkB{6PJY)|{%!0}9u^z)XbN*r{ z$Pf1$AD@%sum$Z|!JGw|{CS|Az`S_`X%^xF2z(TzT@(trR4*?tgpY?9!~>0hgx3oE zkR8FbWh~nR0@*rM~tc z$iMRH8)DrCdwD;>qlXm1fAoGqX{xZaCAhEu4bEgd+#dg4YSvfS2~GQNxU7aFfvEzn(UP#+ou+1;7NXBdaXItqR=eQ*46tQx%j5r{ z5KU(k0Eh&Ijd~dKvaFNNKcUb7IbP8p%^r8x7A!j8WY~h(bVB<=7<5x!xP3|#ViD*@ zxs!+5fvOeiC-};f@>(Gn0d%?G^@#Wcvjc@tq`YCnMj<^Ufk4+o<^kU{2n1g!H_GuWNwzV&DfgP$=UJO<8{p zrXH^D!^q@^n9K-vMa1zA9x>wZ0dzP@4ujkBJiz1-!e={qS@#6&sR{qE0wuH6zrKXR zG;o5vY&K?PK}{|Vv-0wC0Vwkk1CTO1I>2E+0deg7bvAEMO2U(Yf^!BGjlhU64aOOw zU;+&R*x!IIo{jh~BBaQjA|1iuv1gB%W@(qQL7~xjy!OSy2a8&7JkWL{3`C?=pg##G z5W>D>jME3}08&cr+EW4w*9MUPexW$Hg^Z`bSbzpCeBc>tC^Q9R!Jb3i+Jm?||MQdo zFV!mY@Be@QL%;$b8LGF}OW%mUzI4emWImGKzhS?hK>XqK`zLI_CY~gO2)b;9@X8BY z_)y)BlK4j`nyFoJ6;P+?*44MKJmi*^Y!PaF5Z!lpufZyk6u|5dN=$nOf*x1lx-*6t>X0dd0^OBW$Ozwgjc! zcdnD2M5(DcS@^(N*4f!P(6Mw#Jgk#N{v9Ry&*?BeeC-1bf^;o>Zru(xns#gvcYYXo z%yRzpuS6lyAkpSaCxXI0%s&srVBmde2%P^zI)@#lN$HzuWu6Jxd8^lUu=qfp5iEO< zP_<8BT|PW^*E7v5m_^f6`b9YN75vjzP8l|hVw|<-(h|u+Z~2~&%J-r7ZBiUyynVLA zw?*L|&pU67H-}N^Rr;i#-Ko!l$0)-@7g$ove(XEwRGC&!xd?ZLB%4`Rzd~I^9sk%E zobvwj%KHWt>eQc?!`Ja{{&_Xt@D~d2&r9Cfg~b!cFKYv&B_$tgYsX%`e0h3dfdSYX zE-o(3t*xznDR?&QCq$3m<@+@`nHrV{8N>$I-a|s-9XRMqT>NYJ`#*=IPi4_A1#gV4 z=AIrUh~LyiMZE!^pl;qhgzc^njGJIsH+6P)))Sw!IsLDre%{$}1~gb6AY))jskysX z0)LPyrkz%jgERmzUIo}~?ZNN#5GdZSy=$3$kFT`pJCKU7z~+<;cHLUa0Mny%Kq_lgq^SZc(;P=uUnVH~qhx5|zc+8+E>70_bzs3N zDGb_5fd%6osWapEPkF37^dJWO?>g;5zTY)(R) zrwnh`(K+LN`(2Fp@c~dMEC2r&lS2&k*PoA<{u%$nm;J@$Q&Pe8>O6{`M^Q=1kju3k zUZ3_pK6uF~zX9p~X==3yJXHPLzG!N{`qn-p6g}#cHyoyvW@kS_@gU{y@t+$~cFwJD z6WTkFpw6Ni*H4_TvnpvemsPa24PJ7L$!f(zUAad=Ar{rHo3CCuJ@M4ufv+U?GK~}? z`$E3HK@u^Uc1Pa~pm9JCXHM!;qB%IBt#j7Q|tKF z+=RoX>~>zRefCiib-LDEHn`a^yrd){OB&AYsaVpSNCNTS`Ia`tkgqK)a=?8$dzu#y@2vNT)l{J#N;H=wAYPmGD*XP<+y4)qARMPQ;LlWpm;cj2 zA+L4~j)J;UJa${0aSz=Y8f^dA(dWnUgBgFy)-+B2X?o#~mF~Jrn#Qw@o;V+{7yvJ%?Zp*D@RY zxPevq?x@KvrKQ}Bmf68aq1oJA(?jz#xbIg^$B?DST93~C`M?yO@28rh!Mxv#PIqg} z(dAk?6$jFg#e|JmIaxWaO+Ip1-V^=sY(#e|H%7-cSBGQ>&|qyH-yVo(xS$bMUsYQ5 zO4+_{z;$6ophvgZ4>gCRrAyCQEyUwXmIJGPaa2i<(yOqNO-f97cate*28|tZen{D5 zKZc2zD_T8R(YvzW(yC43wJR5%feUx4F<)QMdZbG<)LObQ71NDtSBUhOnB84&{mhY8 zm#P0H4~CTY?L5UV3I6>`dxu#O|1p%G#*|q0&NAO1CfR)xzkn?aXWA7#se64|dv$d+ zuTxlS;``Z=AE5Ltf?`g4EH*Wq#X}E7`#sZ=N*p6laGJY9WGAKPeksaoci3t64p(6; zh3E9=@AM?RO^)J91B-?7+izkTOO48y3d&oZZPJ#AEn0MsLgyAsbaW`J zZgtgoWOP@H9Zy^_GrgolufyKsiLHI|BK`OK_oZ+fI%X=liW;)fT&mDEV6z=PmZ>!N zA(%R(dAj_>NZpwDhfM2)QS8Lnfov2VR^r>Mfhcqv;dWU0qzMxHRRVUvt zL$ky@t70WP#fQTEA^4Is=U3&ac_u-}fK-7vZbn9_iN4jJK|j>C-EdVj=6(4Ax6S(> zmssKY_Z&p{(K>u7+nO$X%XevL9t8^Z>i{Fzc10^~OK7n4d5C6-Roc5~hfTw63f~VO z9TP*8b9gl&*nR>N02L3z;!yiISte9yy^zLc_a88U#KvdNN^7IDu)QOqfFCNuRK~w9G00QaWoFWl$tdt64%7FNSKSCp?d2jqx zit8aZL$i943QQl&Cto)&G)BqFZjX&AgVB_tjEsMB;g=RuF7y3j@(!M2tAR}8_SH^> zPH57y*%F$u*pucI&h6&vYF_v5bF_%{3&*7$Nl4E7-(6BW863lIvN6Zn+f5j5);<0A z-d;?eePf){yhxyM>g(67!O2e_h|kx4d*wP8S8`>yFJgUdtt+z2AZ~w-E6wFOn44l3 z4zuH(57<4heb+#=oZS!*H?Cb{IUo+F%*(Y?{{pJ`7Qldh2*^n zH7gu*t};cY1+iY}ME_cEEp-ZEDu@ql9II(j#Zoj+e4NWHG1O#d$9Q-i>V`YHwoXm! z<&UV!zx6Ze{_sxgP%eIt$D>9fHO#u@qqF4J#}*FOX1?s!cK2)3?t|9y0_FXM`Y$VW z%^5d*Setjys?8$mXX)b>gzKNl!Rqau=&$7cLZ7yFOb_(GB;iArJj~i()RlCjvY`eh z5!@0%s3gJQpa1l_6RTp}s)ICCa)<8HtIOiXiCM zaT-YhO7{Zvvj5r7Jk(oYLI~OxndatZh{OQoDiI5m^tv|%UT=#jK}p=ZGw&CF-rBeL z&TZYL4SojQJwk2^YjhM!&=<>kFm?d0L$g#}DXlsYpg4(Ou@7Xq5RcK{64tG*7LI1O zD<>oHfqng!ikUsfd+9kC_Y zS%2QxG-aPRZx&`E=rBd-)C{-nMox0Qdu>raCVqlzDKRN&^i;8KQgo_YZ1=NwiscuY zRYaoJw0BddzX%Mm#|kj4Mv%5{%!Nt09L%(B?>ahaj_I6XNEy25VzsY4d%CmL4m%`e zHYC2@G=l3Q;bq=Wi+f?VY>CsJ-PK4?j2YVPXmy{h8=Sh}!t8pF!YeyMybaH#j_l(c zp`THnws`;1YRltH?YADjJ+Vm0{)im!rb3(cJ3mM{MBmPU_=w-@sFIXNhBBPXR6%5D z3#4+%K(jV$i+t?+{(ToHCUW`gj3t8;lX?cT!O)}JBbw(~A`LF~C|XY>ckENiY-N&c zj8|yAe)E^3;vp+LPf9YfY;ZCc?CZ_mJsd1Ivd~18I%JKyx_#hzHkzonb&yD%r2-gI zrmoN-_jqaz`-43Bn8ou7cHOIGCPiQx;Vng%wLM`=KGy^Be19J9 zG02{}kip}$s!KzYBBH{P#n+w%Y*!>%!g-G0Dq8(1w>((<{0xYJx_pI*vZ_7yntL|P z(=AE_VsAZFox1amul1R_QAJXQVVUbS1@QrMm&G|(;Auzquf&6A}(3~@5m1m|c| z=Kr!Rwj8qPF8rFUngw`Tj)*7j0T5`_B%(f&Vc4#pFJt8=+L&ej`0dG0x;w&sUT$0T zAGh*G(liS8`&I|$?3aE$*O8M^4j`Q{A58vQY&nt&i7&VHDASLl=H`BT^jd3kSkiLt zK}^Kn`s8AlLlnNLV}*{8s~}j;pK@64*4XB@q%Ww?j>mb>jyUq}#p+;|on}qvifp_76hNe_II%j zrQ^+|IPAktP+@GO6GGbRzj(F_fp9p5#Mm@FB7h~5vaYkQgIHMwxDUX ze>Pmo(73#z7((5bt0V1L@xbp|RaQd)sU(m*tdf_9iqmv(c^VBETn=sxAMk~l^6-QH z2sGch3(RTY<^OEV6T4^06C>Li?vV_J$@4pFM0+lCotmXK)$yLGY0)L7sdaUA?t!<` z_5e370!mSzwSwLc@$-FIN_t(s#Adv^^jbX24%dl@k`Q@k4K)J|4PVcZ)hvcdWJzT; zh&t+s?}G9kU2X96Uzs<>MWg)pm9wLYM_4VWCEs#3PWP1fR^p2p4g}`t5Ash@iX3gM zmKO>iNDJ)S9N2{>HhvZ9E*^+jtI{XTSnoW-^c=>74OjcvQkB>l?HPx+ZR|JOYzAft z2ppB|ew1&ZicXp2PJ=-z`nd6ux9BCuJ^R5pPF9`rbRjp-zSsfljK@>^y-+x9pE0vdKTIK!f;B+o`{GjteRl7DZ9txCX(}>h_+R~I9(HK zK3M&%0+8x#r`c*|N$7Q?VrrTWKyo(T$vq56ETZ`g96xxty0~S)(B^jEQMf?5lL%qE zfeKVh8V_xiLa1GwozsCmySFnn6TLThCpfv>VYN%oYdNCb7#akT$Om{n|E-{st^D}p zna$3xA8ur@=Ghr@k@gjs1QzlzUsa*k)1_oO1cQehRq&dfhTNW#k`n0K*A|D%lH_9F zbr+)3=G@Nn^*KSC4WXl7A+V*r%^X!#vFOiMfj}>|2ufsn0j_5L(ESBK*V~)hW8&OT z52(38K14(pz#7We`rd3odO;2^oEmcu;2uQ^GKQ%v0mI>ZZC$#pmZw`mYqj#h=cQbn z{X?4{+cL!AH|@+$t!rb8&--jQQMuNH#?os|i5FzwDl+gQFy$0?23 zdaTnWhku`3*66^fsLc<**{)rHr@k~+0?V1I~&D*UH@x)c+G7V>lhlHmzRfx zExJ_+{RV-Ddf`JNhcfbDS}GgAnGXp7=x!|*u;{c!2#CU;iENK*$zSz*1glo34L^pn zQ;<0%4Sc6%`+;|$j4jTLW}lLs3pfGx7pXOTvOrx`L9-CjC#t@Fd!9UdZf}O=p&JB- z{Py!-_O@K)5BOI*R?>pzeMaVYmGvk1W^$QqX_ZfX7{XfC2$S;IKdfHt z%&30%?yn0sgp~oJM*}=%zdho><*|q9I80bvONL0E8$QGDxK!E_%A5pLf^_S#?A2`d z0zDIS>r#5&9hjkyyQTG-H|sI$cT1Hhwv-1l(P~n2FY?6=J(=&iJ(SkabCyqS;x!bk z)FPntN-4x-h2rOX5?B^#weBssN2yjbOua~$rdLsA$a#AwWx3BqYURkcO>&KcVweHI zCQsvHrXC78w479oZuc9d_2JFWf^y`KZMQQxSSjcq==VQWu^M~8*SxHqQtYWouzH!} z@XOt0eh&hlr7&~GA{W9Go>pgr>lr)k3^1z|LP247(Wvy;-#4)HRC=~nblbMYl=q}D=7qY0$4scS z#XBL);1Qlv?IplBSDGfMGz+!_Qc?#ws@TuKSD{e!qko}ZXlVyh$hTUuTMXXFYqsR6 zC?`Aqn?fz}x8b(|7;PPhNkoIb>|1AD&6WSjDjq@{F|Sa6Gl{}%JPBtpO~o1_{<(jB zmBgg?OZ1~3{QK9;YXBmAc#L$7hi?ybw2~e+pAOqxQ)uKLJ^Op>XQz0!tI-Ti&JnZZ z>h^_7vs+JgKALjpJnc`^z1LyHT8LFDQ{?ru1L%hOgeY=0C8a;ol(>!x?y9g(Wo+-C z$p~Ey8_#)t_3(OLhMrz`OcsloA=>+XR>p4J=$rm)sM^CnfJ|u$^N6iRzp~ln13dI= zFS8@J8gSBzOJBwh>0uVR9+HTzDNBde^UO6RnHmoSb}_{qv~xTY4-)&n7Dtc{8}|>1 z)L|48b=p^dch~uxm|k7}d|^Mh*P_csvOBdVm{^!v(?e_5+y)QL!X!t@n$Zr|WWzvt&2C$W&s$Tx6*zw!O3Go=Fi! zoTXf`IbJoIA9;RnDXrCYX}Ms}{mGZiw~@A1r%(z@S60F-5tR4SJ3e9)CG{sOc_q9i z%QQMt!B+*z(e|hoYoeKg}eqnr*xiK+%2Zrv`P>?y-QMPWgSeN z;r=Nt-t6)H;NHTw8l8popIucr-+nAJyMr$Bx|ZiB>B#J!8mhZ^_0u(hJ&Wh7H)+!N zH#@f1_;_JthW`OyZxZsX%B!j~5RT~VnyahjWj=!21Xs&FQe5H_t^8epS$mViCEZm+ zJ}#hSE^F+isl}9@Dx84I;8;4*oT9^|cIH9l`LnDh-CGZu{~Y=5F*0AvoFn7?n67ik zw(79VSt~Vd@$G3+?9kUx`&Redp*BM<36ANswi9L;Wo^wTzieBg&SU&3KDhUtM$zxx zJ#kSqXdSJ$@8A|taW)5m#SHg7?rlNism~`Y3hwUBCk6HuO4@N()TCGz$Ksk8=jSGj zi=sk+Mw)4X##kd%q!} z2u@JB1s&$4&u3V%;w@?+wWq-PiEN-xI5k7rV%p{$N8=QQ#xzd+0l1zk?YFfboIvGoIUDFj1$0MtPTJqPel#C_<~! zA6m#>r!CP(NvGKJtmp3V?N?-HnT?cV{<1E5|6A2hpvr!|QueCS(Fd7Rs4E_^C(ffv z7XVM8^ZD@^BEXg>rP!lVwkWF=a$;-j?rHBE4q7B34INi<{o(3nw$TQvf62#Y0k5yDu-27!Q!fiX9Zr{40gaM28zDkWdU;vP`A}^Q%%&oiV znc3al&7RJ_`D$9y^~o-*>u7Tq+~!A(W< zW!D<(k4&jqUi>P5t@k!|;xELv(0WuJ=7(F?WFybGOGJI<Op-NIKr~fxVW*UefnFr(70$uD$HLJDi>8NVgbAIqASR z@cAp@4yKI9b9dziN}=%ishi&JH+bw{UuMzk7g()Kfo!8al3fYHroM_*=3wgo4gAVi zXCmcwtfZBTM1pD>2IfjvToobL^KNG(EQkJ%(Sl}D;fEt$UP=LEYr&u_R4%sE0Bli5 zEa~x3$+AM8Zl^@>KiF7A=vDtpK95x0cn(RY5r?!boP#JJtl`{z8Y2}Vxuo3Ri*U8o zf&pdF_sw;?>d~HcU|Nw&tC)+M+l)C&F1O>7a~PwBA`l(ySz0st+u9Op6ewAAQkz-j zm7zf$cEYE1GeMlPXK+=u6^*8AjJ`}1QV6^TW_31sqt>u?CvxaD)a$m&($&f;-~Fu5 zs^`P90GG}9&GxwM4=WXUpDCQj^^oeGT}Ndl6UktcrLKIu%(?pTO56)ww`G+%3)6$r z_tBuIuEw-yw8u8s{R70EA*4tL+ZMBxVHdbtFK0X41csh#2lebjr;$gRmGe5Xv0V3{ z?JH2^b_s1kYR~0R*s{t<2#y zsr*bUODY`2g`)u%`^eFCTkH;_^Cw6tP1hg%iEnh%@vL~cnb z0UoS9Ux$_#yQa*b*-yGfL`Qkv(+5$^#@GYvy`<6n@>}1++-LVbH5eBdQ zJB8CAiNe_GCxRAUt%o+C$OywjLqn4RW)};^xTOvG?l;c{`a<=};-FFQ)utzG^C83u zi#0-ShNg6^MY>VE6+#=I?v!vFrGA>t{xr*%=D4o38t`H6-@{NFef>q2#f28jF3!m{ z4_C2))(>?XN0ZGAG;&nI`$Eg>;K+xRD?RTc$F0+&gm6{fF+NaSZ;F7eY996oi;3^M zu6t4O=A(&GfXvucdHol7>Dj=FT7@I?z%l`|?q)8SKT=DY_m&0~TMxxM9@ZF}p>cP# zcybQGdQ=5kj_uM&GB7l|keHtV*A%!yr!k#$WKMzX#A2#Jwqs^DgBz-OAS5h=J}h$_ zG3}{Ft-{@q8LwjxrqezEcLCNkMS!%`%4f8Ath)gDj(X;WZnbz~&lx{8>`%we{uoGO z3WsN~-0pP>Rs2}u9QEhe1}fgm?qSSYVqlW?6IjD(T1OiWfp&@?JN=ps6@gb$2qMrN z-6}Dia>r&R5%BE~PX6_)paVNFvV;$JMf4l+Z9cLB7K;=wpi|ywP-`y#S_?_75*9-5 zgGbmuAWu4YkYFGN@mQ;mKu{jb?Tz2R6+&nOuxpL2eaU8nX2A}a(Iljf3k|U{&s@L= z40&x0RFh)6uLDk?3H@0I^Nq4xw->W$R$1F|wKS(spKf`=2EC53<Zrv!L7A!(l9zuyN$KNElr zK&yW2L8Xt&PHe9yN{>vCOFr%HNw}#^H{|fnd}M{1f@Y!fi-r#*X?z7^-Q-TgxhDJA zt}madY)2Y;IyXbROnav#Sux;UlBrS=uIuHId9*QaFtCEIbm7d=-Yp_Mv^(_)xF%Ju z4l_1)r>E?&a!&GCXGNfsMsw~n1FcEhs3rU{I#;K+#CrAM!X72?k)V@swpbOHX21>J z{z6{e)*fxaU1cXAbEf8hG_<~z3~|`oBvGl#$uw?r=IrG4F7?!P!*=Z2&%j-sfE|$r-0Ioj|?4|m0m9&z?1C+7rr&$D*Ade)T}=7v+ouI< zxgMyW^%b?AMBtlS5AZQ%o`-SI$oSoVxhNeY2d6u2Xn9h{^WG8;>%&@{qnKh_O?wqJ zT{mGv&&{nu8`4~6xxYp3dQ^68b0e3WB`f1UAsbC$T5APVchmi)_i^TiB;1)4OWmZX zPo)E{H&u1?_-49E^*hUpy=uvAPThn`NfR>Gppm`nTKzpntzv)B`lqU;R%Z4;(=1Dl zhdw=~?SreK(dMl-A>{DfQS~SBkdFa1xznsan-8L3gb1byv&($!bKDsM_ir@V&H1&$ zwyOoHRcp~u&yY4pK4)WqB&78Bftktex+zU6v`3>Baa(fm%k#J_>Ek!N`a>gebgg`0{xU`ft&TfByI-wdLkz(OPogu%J-m;ZN-D zUU3_kIR4u^*)OfL<8!p*1jl)>jr5nD)6!zrla4h$%T=|l3Eo3EPHuGVztvYZyh z8>hVqM+Z7kL{`T7JsO9}&I|m`jQ8$&((Y;s8f{JE>hMIRDrWUwa;uoI(6s*O(Ry0K z9onI1>!1N#Aw1;6R(}&?gi|-XJaT^KhuKd|YCNis>zheFl`?3V%W?tbdrCg(v9Rvd z(`R1O*NP}SSB+upcN^|A!ut;@((AsJRJ%q`MPDBsni&O2oTysFDbyp?eCyNA0?MB+ z#pxhqqS{>o_^wi;hxRTS?`Jv6=e1-TK5DN2Ca`soL@5|LJL5NBSCqb1$=JF`o*5lS zTV76$I{g|FE_edc@8|g+J+67QxRV=5?6mHrG1XZLf2P080OB zPQUv@o{IgVIdVlc3%F14rSw;j^z-=VAMO>r{=Z@Fob~qcdCa4gi5%q$5xfu-^hXF0 z089OKLi7_VvRzk7PRty& zmAv6DD3}5)cVcqlX(;ffDyt5G*8+!ty^|;ng`5)n{7)%JCq$8>{QG+&SuA8>G_(^0 z_rm*x_%V7_nnCHga}P8Wz|YVrp8?wz6;Lcoo5OmX_wv=NjjFt?#fE?i=e0y|K+b~- zN*0*7xvrGW*{@C3GnLc~w|{CKId2=gQ}EMVCVt>Sf0R(N-`6+&bf_z5PKYAsA^S*7 ztOdBA(4!nidr4&5_d)Qr#DM!?5B#46g0n?f=SkW1U;8n(#cmIuTz6sBT<_GAxPW>@ zjCT_bG5!{qby%?CG6jTY51dfLqG1vS+xQ9C&wqr*PRUU1NQd~0l+QL9ab#7C2#b%% zQ+}BwgI7vK(W^Q z>E21;a%KWUToVk&%00jhGYdIqpy;cqt4AJfB;e*je6mm+CE^9ls5y!-1|=~N%=N>Y z;xJ$&%Q*s!()^+24kMt8t37%0q=0P>4%;J6Szxni z4oU|w^qkU;80d0C$Cto3N@*X!H7(qz3WOj%kiaJaAAym9LGr=d^UW%< zupcWxqvE$m&=NF@Q6+QQbr{`hMghfN(5UAD{9_J5Y`ObxK7=IUt6Rn$u?YLf6k>Mh z(~$*aa^STo$K8x{;8Z)uMg(gHSg1honnpCbe`h*78|p&qpO z>uVA+u+kq>Np`F0c>qn!m40YVHj?cgAZZ^F`&%g?*G-$;)?8Ys_&Ln~ya#qpDFrnk zzccYFBj3j6V>Vi}Wn%Bbt=s61hujt_Nm!UxkYR@hCj;py3j%dujs}CFuC2BRoQ!OO zunr(8Vbm&_%91df(BJ{*0Fo^ef$cI9ASXWt$~kogw8SFWf&z95vPywX_`%?B@tWSNB9kwAZI;68YIO z)}AS%nSt^&OD*9bY>~L({!E2C`T{B!n6z;BOQS$T#lX$oM=}ey3L(k>uvQSwYaPEJ ztoB_*(%07)l8!X27jD~Y;lzWA5f+~x6YiE@)eDW=Ntb_peE@94K45$x>;cJcxM1LK zmz{sn9j5dk&-wQ^d~Va9H<2;K@3Ni@*>cZYL}w zW}sxz=nsvm^k2|~>R~s$Gwz#>=KvuT!%0@HBDSnY2+ZzG$~8Y1)_nVkZ2-p?w%x+t zSxz1r8(8hnO)3>E$k;EV^+dMGYXUWX;*>Ys7Vnt%{{Fo%)sI>6`P0*eu<9F=pp1=S zHCta_mns2W#Ax`nKu4}5TNaG^mX z0m5nrdf#I(hAhazNL@g;W~AOZOi4~Qg#~A@%B=)gGK+$m%)C<_@(BaW3Fxidwz(oS zd!?!zrXXa9bfe;sST&1VKJUkHYcj>Y$p6#bRYyhjuInN7M~DJHNGvjB7jSz6a;FAPsr{em4Omc7FqN?OyoGm*!?_I=U=aqM2SkAW32hmM@wO3YGmoa`db2H_bxfvtl&EZTmz)%(2!2zt^s&99VpU#pK5x+ zo?)_A1f((s8ShGv^h|+us0YoyJJI4mZsmmp3HQjm4^0j13LxgU)F`tlJOsJv;@tr+cu!Zv*f4hSOXS2ohsA zzfeft!aYBq!=+3hhQ0yvUJm4eb^9|Rp1Z@35|L6Mx(rMhT+{?rEnPras1+W62fGXq znruj&doP;JDcRdbrbBqtF*#`l>U*92W*W1-sAM9ER}iL1zNiJY-=9fd>jO}dUGeU9 zHk@qm^8*$OAO(#9vj()AAt19P4m}a3BLlciD)BkSEI{=w<4&n`oZWVDv377Mg8zJ? zHFdv-UVC5b9l7cy_VqmVRrKt;xnZnr z$D_kN2b?Edu4Z>ikhVj#zJo9Q5i+?l2BbN5lr3?5E5Zp zUQ?l&-kurbY=13lCA*y)T4tzx`=x2K+ZE8hHc$q#)0} zN^Le0K=nTk;yg$ubH6^17mz-RoO9sB2D(}OMkbP-Gzpe(T-%}Iuc;PBaO(U?Ql7~- zaWx{I9ZgX`m}+yReSo|CzYoYEF(rJ4JBSOV{ug4_|Ky&)IscdZ=g*HL{yo?0|I<&( zr4|iqn*dlLC3d=mfRm2!8H}8hTJK3DE<>jl@_vdS`UjsaU`BrIzJ3fGwccMvkPzct zf^U*2nw!`1nT5OkBayp2{gD!gCvxKsS4*_DBVD(?c`UNHArO{rmc{2!@Q7aiGXbpi zQLU^dM96)9@P;CpUhety9}_{%kQS(wp&G!+KFs~Z=_BMj^A0~uOlTm5yiO;Dqu+%L z8(kGrf**SQXKDuYT9$9NLs3NT@-+1WxHkO#Crt<3{rvx*wEvN#qz@#&C^&&pOmUBw z{>^**3*Ad*1~hIh;LtgbzhaBxXNg{#t;Hdi_094J7u1dB0Gcp0mEv7?PX|}ouHT=z zBVwaDiOEpr_QZ~5t(f@%dXtTx9|aZZg_<6Jn=6v*yNrmT>Ro5+p}v}5Lnj2PKsALp zPuzU<1}t_;6@{BRme=V>>XPD*ed{rEt}^4tLd=|Muo;B|CsWAgpi7 z241m<{RBiLH&84=E}SGi$^YrGvwr{1nM%oUgu?T)6omyK!k77*JD*TfdP-_hbC<%>t$UR6Wyq7d25qN^-LGv^|_}! zSe^8f3p~f77kF;%TU~NhhIo_|MPtU1(Npye4)Nc_G(05BW&r zQ9k;<^NIh517~9&#~Y8ZKZG9jEMe7*ayu096xTWuFZo9j+Ty<_BqV2l)>td&q^wq) z{IccS=5ZcImulB{cSp)=W74^^erlz#p(*tiQBZg|agoRGzU0#*dgzi6_?|inG$`n= z$sGS8K|FABBB5Y*xhyN&=kcMg>4Dt6dqOuhT+A&kshz6GY|BQfP7V)m9R5Dj|Lt3F zO`@BC;2|9p#gtXFra|Fyf9{4A@^GmU6l5p#grd3SFQd>0>$Mw_DcYP}CEIHOd_$fXs~VOOQR#mN+3HnW?c z4m?0{)qc!Ielbx)%6FR(fcu3a<=O`qvMCUB7cYJZyQp62TtgMX9442lc}X{MxuhT? zXq~i5jkwPDEN@SKKgF`ia{ICSuhM!lIWsd^LnG8o=MOsJI2SSs*=FoY*w6L#fG^X} z^y23QD75EETLmsht$+6@Klc6*7uOth&94b0F;ca&Uv4kt+WSO#J$#7A%bRaggLySk z@7vwoed*dYAv=^T>#4i#qjPM`&pIuekB+2*gGulRnZNktFTQRNu#y}QRp+MJNFR4x zd}1UmUAMomf^t~>p#}F_>%N5#0w)~ZJ-wG@{hXVp0MJu;cuc{`#rxctZ!cHoDEsaQ zwe_;FJ)Q3mM<@Tr=#Ra%-5IaIw#%Nx%$*?cpX|wxx2M$QmhA9Bg+t54WNq6=ULK#C zgb3K)iE9nYY9#5X(sw8CD>l;2-zX=m6|zt=`?;gX#|Z-DpEoz(>TTDBE*Mh^oqwgL z+f8uIPt?CIRhpOy^6~`6{656iAxZ1Za|`pHp{I@3lk4!tH8VfWz&QJDzG)h`D;ImS z`pEn373v&hw7pqRKaBa<{;s1^)s|jvRceI!Ye}i0an&|G;u77ZlEJ`(?`p}S#gCi|FoBk8hmBq$)EG)il!2OsQ>grnhW~BLgA?3U0 zpw~glVA=cn6Fg93nwDQ9T9tJ`Fs)Sx?CgYY9ugET7kpaaS>09iJ{{kSO-N`NF2Qtk zcgvwrEYPnjyWQ0%ieIPwrB3xkZ^_w!W{;%Tb3OXwhunKGQ)A0|X6$1uxsN>UoNICxI*`#GIDa$7t`V5PLR|m_@G9duSZ%l9-f*S z$jK#vhY=8_BP<@O=h?)(VcS)6n%5R#M(X?lK>@rKTnh%AgzSwlk}xkl76@AT8<#t*xa zpG#}EdUJaa+(sQuciUGTl?eE5o?>TS>JlGVqLZDF0z z#`4lE@@Hum7YjOWV=zxkoSlDeurw+y_FduQqcO~D>{?h` z69duyT$W7jR_RG7W;svjF7k!f{fyn*!pbX{b1Ilzj#T?S{3VP2iSkv0+m`Qt5tiHh z%lPnr6zCYOTI}2Z5??WK;{{IdJH-#Nw{aTud(Th7$%)d|wwF~}dVKbu(`Uv(d=uJz zHQ86prbYN}IB%mYEC?1dtrr)&x=6rWDMzad*YDT(y%=Cd(dM0G{4qEKI~~%|fu3p# zUZhI;)YOWK0epR*gER^E`nfqfZh@_!q4jIpE+*L7*~!Y;-auyFL!%K>wcUOp-lG@U zEX>WX3t1>WS~QU|a}WRUp~sA5===Al2L9?QDg~&ERGsM6Ri0o9+L|M2UUgoH=}#KY zb#qcCX-#QLSHMh^3~TD>oUQWSnR!rU9NryD78)A9E-t%nr*{p!fyu;_M;j7Bui7~} z!BB-6*jntuw6rKJHdI{gD4&Qe#|V(kO%JEuxT^4tx1&$V$Vd`w1P{MtQBlz|bY||6 zD#6)2`@8QG-+QLP;@+K^G)Ufo-)jIl*4n1ANn z6W`FVG`c^+6I)S$@V32erOVD3&JMN!HaGYUCV#ji9qQ3^7dsBut)fN>C0|QwW0zI3 zM(gAb=}IdU6_p-ZdE9rB7>K1-p5)Xy&Q5H*^k8w8;)B{N@bS~YUWB#$P?+v|*3j(l zje?mpzvH?)!xhS{yl=$?#k0;J(layfXe>xWOmOq~d=B?rb8l~aB$7!p7ehx!_tIB0 z;&i{yZ?7`R#(T@{l-P@h=D*(}@Ye#AsW}i_oS@)RdlEU{J1HiLFK2JhcVh%)VeZY? zz{7hpTZfgSy&?Bf(Pun$&+4x)U)6-~j?#h9#6zviu(01pM@{7x)^ABRG+R-3P$BH{ zYCds9*8aY+vf}TtEqvW!G^OESOOFMQk;HeW964;yga}xUJ+CY##!t({RPj)de%ipP z_E1-9bfcC?yFynts(;BPmX0OR%862XTv}Yqsd?j?oek>4iO0zNu*CDW%?B;Me&JrJ zqUx7uYEB-X!P7N2f_{%JFgs%7;{1IaFWkTXp=j9ddVG(8v9W@;@z|5T;PJcX>~#ip&TC#i_?qd`kvUH@v|oP-3>lb|MWhX!P1IO)9S*!j@irHo zI%7!`92~MaPir=Ph`pVcpMP0k344U>Q{bGrQ`BZ|bQHE@YEjWxscN%YcBk*kh@?24 z^5x9XAP})|^G%A6-;oRh#D?YLk_WkR-jF#XBE;0#3hMlXQ_fhb#BDa z(=)YBe`)QlaP8^q^HTd-=Xka>l+^<^tlNI|6(kDn^(9sE3yDaxA1v@hsp9+@7V8R@ zGq14li{N8N8HAy*EGrh`uImya&=-#U(XgcC1}m4q!2GhP-Q3VZP_`hj1r!6WetiR9^vsiUTl;~s;U|(A3rkEgb=MEnfh&ndueN{ysFBHtIT(T zOTn}odspB=_T`3Xlw7o}jpo(aVvk+>teikAouq(sm#e>|Mg}N+6bc>i-^lY_5h6&t zlBTSxDz9V7MvOgt=DP$6MM|UjqVQnTW-J3h*CHxrW~A)4u;_%`&6_ewJUtl88v_L5N|B6?G^2^K3gV4_H{B z`RO77A(_MDtXdDzJu(Q3J32dG4cIiVt}-q!+xisTGt~2cGQ==2;z@PG2Db!y;cg_SF~BItTOheM_h$PM6U4j(^65%K6Kx|FyT|tR27<89Zi`xyQaRWQ?JQH z#kzzKW5qL!Gb+MMDb5j05yt0oTj??S5%cERw_@!)9{w+ug2&thq$70;i;Ds>GS4xX zztz;$EtF+nuCBh|LvfP{zY58twX4U4MvQp|9L4z{{gZ{UjqRa@GNikF*l`~!f2{yxmW5c_%l7tHxZvXENtv;oBCw?n5#&Zz&TE890(RsU{F$dg770(nCr_SKdX$jAi&lZs zbd6NjcO&2Bl@**5b-qUm3k*IOGcde83K~=j=Q~4A?iw2(e+wOE4XaY?-o3D~F+Z0e zDU9oA{?ns1RI(TEt)lP>v8H7)n=iD6AnYRFuD*QBV_zf3w^i&BLSQ(l@1 z3RBiA>Mj#?XVj~S$!VqC^d6%;Ax$hWJ;sBS&;9Th7k4f<$Slu;_q?10J3wLQqKPcw#+%>PbUz8`ItP5DqZK^26)6=swwY0)H?zUYQ6O0SAg;*J z+N$`o7QKXBt4R&!Z?cO}oT>e}#U@w~X=u%{_}0cqflbl6OnsyGjz!QIWXzPwzMe!x z2M1XaZo5Ohy#-GXw%f%$it(eOPU<-4E2fq)Ea#b+*1QS$E)1Y9j0&@U9PiW5vrcwF zou}6i*!dCCObWATk`UB4KMN2 Date: Fri, 22 Mar 2024 17:54:26 -0400 Subject: [PATCH 45/51] chore(data-warehouse): assume timestamp type when defining expression (#21084) * assume timestamp type when defining expression * Update query snapshots * Update query snapshots * add logic --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- posthog/hogql/database/database.py | 21 ++++++++++++---- .../test_trends_data_warehouse_query.ambr | 24 +++++++++---------- posthog/warehouse/models/table.py | 13 +++++++++- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/posthog/hogql/database/database.py b/posthog/hogql/database/database.py index de12a8267d911..afeac3c26a143 100644 --- a/posthog/hogql/database/database.py +++ b/posthog/hogql/database/database.py @@ -58,7 +58,6 @@ from posthog.models.team.team import WeekStartDay from posthog.schema import HogQLQueryModifiers, PersonsOnEventsMode - if TYPE_CHECKING: from posthog.models import Team @@ -234,10 +233,22 @@ def create_hogql_database( ) if "timestamp" not in tables[warehouse_modifier.table_name].fields.keys(): - tables[warehouse_modifier.table_name].fields["timestamp"] = ExpressionField( - name="timestamp", - expr=ast.Call(name="toDateTime", args=[ast.Field(chain=[warehouse_modifier.timestamp_field])]), - ) + table_model = DataWarehouseTable.objects.filter( + team_id=team.pk, name=warehouse_modifier.table_name + ).latest("created_at") + timestamp_field_type = table_model.get_clickhouse_column_type(warehouse_modifier.timestamp_field) + + # If field type is none or datetime, we can use the field directly + if timestamp_field_type is None or timestamp_field_type.startswith("DateTime"): + tables[warehouse_modifier.table_name].fields["timestamp"] = ExpressionField( + name="timestamp", + expr=ast.Field(chain=[warehouse_modifier.timestamp_field]), + ) + else: + tables[warehouse_modifier.table_name].fields["timestamp"] = ExpressionField( + name="timestamp", + expr=ast.Call(name="toDateTime", args=[ast.Field(chain=[warehouse_modifier.timestamp_field])]), + ) # TODO: Need to decide how the distinct_id and person_id fields are going to be handled if "distinct_id" not in tables[warehouse_modifier.table_name].fields.keys(): diff --git a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends_data_warehouse_query.ambr b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends_data_warehouse_query.ambr index 1e3bc1b5cbad6..bd7142030fe3a 100644 --- a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends_data_warehouse_query.ambr +++ b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends_data_warehouse_query.ambr @@ -4,7 +4,7 @@ SELECT toString(e.prop_1) AS value, count(e.id) AS count FROM s3('http://host.docker.internal:19000/posthog/test_storage_bucket-posthog.hogql.datawarehouse.trendquery/*.parquet', 'object_storage_root_user', 'object_storage_root_password', 'Parquet', 'id String, prop_1 String, prop_2 String, created DateTime64(3, \'UTC\')') AS e - WHERE and(and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0)), and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0))) + WHERE and(and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0)), and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0))) GROUP BY value ORDER BY count DESC, value DESC LIMIT 26 SETTINGS readonly=2, @@ -36,10 +36,10 @@ JOIN breakdown_value AS breakdown_value) AS sec ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.id) AS total, - toStartOfDay(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC')) AS day_start, + toStartOfDay(toTimeZone(e.created, 'UTC')) AS day_start, transform(ifNull(toString(e.prop_1), '$$_posthog_breakdown_null_$$'), ['d', 'c', 'b', 'a'], ['d', 'c', 'b', 'a'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM s3('http://host.docker.internal:19000/posthog/test_storage_bucket-posthog.hogql.datawarehouse.trendquery/*.parquet', 'object_storage_root_user', 'object_storage_root_password', 'Parquet', 'id String, prop_1 String, prop_2 String, created DateTime64(3, \'UTC\')') AS e - WHERE and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), or(ifNull(equals(toString(e.prop_1), 'd'), 0), ifNull(equals(toString(e.prop_1), 'c'), 0), ifNull(equals(toString(e.prop_1), 'b'), 0), ifNull(equals(toString(e.prop_1), 'a'), 0))) + WHERE and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), or(ifNull(equals(toString(e.prop_1), 'd'), 0), ifNull(equals(toString(e.prop_1), 'c'), 0), ifNull(equals(toString(e.prop_1), 'b'), 0), ifNull(equals(toString(e.prop_1), 'a'), 0))) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -57,7 +57,7 @@ SELECT toString(e.prop_1) AS value, count(e.id) AS count FROM s3('http://host.docker.internal:19000/posthog/test_storage_bucket-posthog.hogql.datawarehouse.trendquery/*.parquet', 'object_storage_root_user', 'object_storage_root_password', 'Parquet', 'id String, prop_1 String, prop_2 String, created DateTime64(3, \'UTC\')') AS e - WHERE and(and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0)), and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), equals(e.prop_1, 'a'))) + WHERE and(and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0)), and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), equals(e.prop_1, 'a'))) GROUP BY value ORDER BY count DESC, value DESC LIMIT 26 SETTINGS readonly=2, @@ -89,10 +89,10 @@ JOIN breakdown_value AS breakdown_value) AS sec ORDER BY sec.breakdown_value ASC, day_start ASC UNION ALL SELECT count(e.id) AS total, - toStartOfDay(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC')) AS day_start, + toStartOfDay(toTimeZone(e.created, 'UTC')) AS day_start, transform(ifNull(toString(e.prop_1), '$$_posthog_breakdown_null_$$'), ['a'], ['a'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM s3('http://host.docker.internal:19000/posthog/test_storage_bucket-posthog.hogql.datawarehouse.trendquery/*.parquet', 'object_storage_root_user', 'object_storage_root_password', 'Parquet', 'id String, prop_1 String, prop_2 String, created DateTime64(3, \'UTC\')') AS e - WHERE and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), equals(e.prop_1, 'a'), ifNull(equals(toString(e.prop_1), 'a'), 0)) + WHERE and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), equals(e.prop_1, 'a'), ifNull(equals(toString(e.prop_1), 'a'), 0)) GROUP BY day_start, breakdown_value) GROUP BY day_start, @@ -119,9 +119,9 @@ UNION ALL SELECT 0 AS total, toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC'))) AS day_start UNION ALL SELECT count(e.id) AS total, - toStartOfDay(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC')) AS day_start + toStartOfDay(toTimeZone(e.created, 'UTC')) AS day_start FROM s3('http://host.docker.internal:19000/posthog/test_storage_bucket-posthog.hogql.datawarehouse.trendquery/*.parquet', 'object_storage_root_user', 'object_storage_root_password', 'Parquet', 'id String, prop_1 String, prop_2 String, created DateTime64(3, \'UTC\')') AS e - WHERE and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0)) + WHERE and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0)) GROUP BY day_start) GROUP BY day_start ORDER BY day_start ASC) @@ -145,9 +145,9 @@ UNION ALL SELECT 0 AS total, toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC'))) AS day_start UNION ALL SELECT count(e.id) AS total, - toStartOfDay(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC')) AS day_start + toStartOfDay(toTimeZone(e.created, 'UTC')) AS day_start FROM s3('http://host.docker.internal:19000/posthog/test_storage_bucket-posthog.hogql.datawarehouse.trendquery/*.parquet', 'object_storage_root_user', 'object_storage_root_password', 'Parquet', 'id String, prop_1 String, prop_2 String, created DateTime64(3, \'UTC\')') AS e - WHERE and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), equals(e.prop_1, 'a')) + WHERE and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), equals(e.prop_1, 'a')) GROUP BY day_start) GROUP BY day_start ORDER BY day_start ASC) @@ -171,9 +171,9 @@ UNION ALL SELECT 0 AS total, toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC'))) AS day_start UNION ALL SELECT count(e.id) AS total, - toStartOfDay(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC')) AS day_start + toStartOfDay(toTimeZone(e.created, 'UTC')) AS day_start FROM s3('http://host.docker.internal:19000/posthog/test_storage_bucket-posthog.hogql.datawarehouse.trendquery/*.parquet', 'object_storage_root_user', 'object_storage_root_password', 'Parquet', 'id String, prop_1 String, prop_2 String, created DateTime64(3, \'UTC\')') AS e - WHERE and(ifNull(greaterOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toDateTime(toTimeZone(e.created, 'UTC'), 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), equals(e.prop_1, 'a')) + WHERE and(ifNull(greaterOrEquals(toTimeZone(e.created, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-01 00:00:00', 6, 'UTC')))), 0), ifNull(lessOrEquals(toTimeZone(e.created, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2023-01-07 23:59:59', 6, 'UTC'))), 0), equals(e.prop_1, 'a')) GROUP BY day_start) GROUP BY day_start ORDER BY day_start ASC) diff --git a/posthog/warehouse/models/table.py b/posthog/warehouse/models/table.py index 91c6f61709d6e..23cc5a7ce9541 100644 --- a/posthog/warehouse/models/table.py +++ b/posthog/warehouse/models/table.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Dict, Optional from django.db import models from posthog.client import sync_execute @@ -175,6 +175,17 @@ def hogql_definition(self) -> S3Table: structure=", ".join(structure), ) + def get_clickhouse_column_type(self, column_name: str) -> Optional[str]: + clickhouse_type = self.columns.get(column_name, None) + + if isinstance(clickhouse_type, dict) and self.columns[column_name].get("clickhouse"): + clickhouse_type = self.columns[column_name].get("clickhouse") + + if clickhouse_type.startswith("Nullable("): + clickhouse_type = clickhouse_type.replace("Nullable(", "")[:-1] + + return clickhouse_type + def _safe_expose_ch_error(self, err): err = wrap_query_error(err) for key, value in ExtractErrors.items(): From bb04a18cb9c635d2e08d0ab0c5009647e98dc51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Oberm=C3=BCller?= Date: Sat, 23 Mar 2024 10:29:58 +0100 Subject: [PATCH 46/51] fix(hogql): correctly parse breakdown values with quotes for funnels (#21101) --- .../hogql_queries/insights/funnels/base.py | 23 ++++-- .../insights/funnels/funnel_query_context.py | 2 +- .../insights/funnels/funnel_trends.py | 11 ++- .../insights/funnels/test/test_funnel.py | 75 ++++++++++++++++++- .../funnels/test/test_funnel_persons.py | 42 +++++++++++ .../funnels/test/test_funnel_trends.py | 40 ++++++++++ .../hogql_queries/insights/funnels/utils.py | 29 +++---- 7 files changed, 200 insertions(+), 22 deletions(-) diff --git a/posthog/hogql_queries/insights/funnels/base.py b/posthog/hogql_queries/insights/funnels/base.py index ef8782fade54a..4e97d79b94534 100644 --- a/posthog/hogql_queries/insights/funnels/base.py +++ b/posthog/hogql_queries/insights/funnels/base.py @@ -284,6 +284,7 @@ def _get_breakdown_expr(self) -> ast.Expr: properties_column = f"group_{breakdownFilter.breakdown_group_type_index}.properties" return get_breakdown_expr(breakdown, properties_column) elif breakdownType == "hogql": + assert isinstance(breakdown, list) return ast.Alias( alias="value", expr=ast.Array(exprs=[parse_expr(str(value)) for value in breakdown]), @@ -530,6 +531,7 @@ def _add_breakdown_attribution_subquery(self, inner_query: ast.SelectQuery) -> a # so just select that. Except for the empty case, where we select the default. if self._query_has_array_breakdown(): + assert isinstance(breakdown, list) default_breakdown_value = f"""[{','.join(["''" for _ in range(len(breakdown or []))])}]""" # default is [''] when dealing with a single breakdown array, otherwise ['', '', ...., ''] breakdown_selector = parse_expr( @@ -613,7 +615,7 @@ def _build_step_query( event_expr = ast.Constant(value=True) else: # event - event_expr = parse_expr(f"event = '{entity.event}'") + event_expr = parse_expr("event = {event}", {"event": ast.Constant(value=entity.event)}) if entity.properties is not None and entity.properties != []: # add property filters @@ -657,11 +659,15 @@ def _get_funnel_person_step_condition(self) -> ast.Expr: raise ValueError("Missing both funnelStep and funnelCustomSteps") if funnelStepBreakdown is not None: - breakdown_prop_value = funnelStepBreakdown - if isinstance(breakdown_prop_value, int) and breakdownType != "cohort": - breakdown_prop_value = str(breakdown_prop_value) + if isinstance(funnelStepBreakdown, int) and breakdownType != "cohort": + funnelStepBreakdown = str(funnelStepBreakdown) - conditions.append(parse_expr(f"arrayFlatten(array(prop)) = arrayFlatten(array({breakdown_prop_value}))")) + conditions.append( + parse_expr( + "arrayFlatten(array(prop)) = arrayFlatten(array({funnelStepBreakdown}))", + {"funnelStepBreakdown": ast.Constant(value=funnelStepBreakdown)}, + ) + ) return ast.And(exprs=conditions) @@ -898,7 +904,12 @@ def _get_breakdown_prop_expr(self, group_remaining=False) -> List[ast.Expr]: BreakdownType.group, ]: breakdown_values = self._get_breakdown_conditions() - return [parse_expr(f"if(has({breakdown_values}, prop), prop, {other_aggregation}) as prop")] + return [ + parse_expr( + f"if(has({{breakdown_values}}, prop), prop, {other_aggregation}) as prop", + {"breakdown_values": ast.Constant(value=breakdown_values)}, + ) + ] else: # Cohorts don't have "Other" aggregation return [ast.Field(chain=["prop"])] diff --git a/posthog/hogql_queries/insights/funnels/funnel_query_context.py b/posthog/hogql_queries/insights/funnels/funnel_query_context.py index 66a0d28ad3d7f..3b777e3ff8026 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_query_context.py +++ b/posthog/hogql_queries/insights/funnels/funnel_query_context.py @@ -25,7 +25,7 @@ class FunnelQueryContext(QueryContext): interval: IntervalType - breakdown: List[Union[str, int]] | None + breakdown: List[Union[str, int]] | str | int | None breakdownType: BreakdownType breakdownAttributionType: BreakdownAttributionType diff --git a/posthog/hogql_queries/insights/funnels/funnel_trends.py b/posthog/hogql_queries/insights/funnels/funnel_trends.py index 5c370512a20e8..9d486f1b06196 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_trends.py +++ b/posthog/hogql_queries/insights/funnels/funnel_trends.py @@ -203,7 +203,16 @@ def get_query(self) -> ast.SelectQuery: [ ast.Alias( alias="breakdown_value", - expr=ast.Array(exprs=[parse_expr(str(value)) for value in self.breakdown_values]), + expr=ast.Array( + exprs=[ + ( + ast.Array(exprs=[ast.Constant(value=sub_value) for sub_value in value]) + if isinstance(value, list) + else ast.Constant(value=value) + ) + for value in self.breakdown_values + ] + ), hidden=False, ) ] diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel.py b/posthog/hogql_queries/insights/funnels/test/test_funnel.py index 98f4d060fb905..89382bebfb994 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel.py @@ -18,7 +18,14 @@ from posthog.models.group_type_mapping import GroupTypeMapping from posthog.models.property_definition import PropertyDefinition from posthog.queries.funnels import ClickhouseFunnelActors -from posthog.schema import ActorsQuery, EventsNode, FunnelsActorsQuery, FunnelsQuery +from posthog.schema import ( + ActorsQuery, + BreakdownFilter, + DateRange, + EventsNode, + FunnelsActorsQuery, + FunnelsQuery, +) from posthog.test.base import ( APIBaseTest, BaseTest, @@ -3576,6 +3583,72 @@ def test_funnel_window_ignores_dst_transition(self): self.assertEqual(results[1]["average_conversion_time"], 1_207_020) self.assertEqual(results[1]["median_conversion_time"], 1_207_020) + def test_parses_breakdowns_correctly(self): + _create_person( + distinct_ids=[f"user_1"], + team=self.team, + ) + + events_by_person = { + "user_1": [ + { + "event": "$pageview", + "timestamp": datetime(2024, 3, 22, 13, 46), + "properties": {"utm_medium": "test''123"}, + }, + { + "event": "$pageview", + "timestamp": datetime(2024, 3, 22, 13, 47), + "properties": {"utm_medium": "test''123"}, + }, + ], + } + journeys_for(events_by_person, self.team) + + query = FunnelsQuery( + series=[EventsNode(event="$pageview"), EventsNode(event="$pageview")], + dateRange=DateRange( + date_from="2024-03-22", + date_to="2024-03-22", + ), + breakdownFilter=BreakdownFilter(breakdown="utm_medium"), + ) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(results[0][1]["breakdown_value"], ["test'123"]) + self.assertEqual(results[0][1]["count"], 1) + + def test_funnel_parses_event_names_correctly(self): + _create_person( + distinct_ids=[f"user_1"], + team=self.team, + ) + + events_by_person = { + "user_1": [ + { + "event": "test''1", + "timestamp": datetime(2024, 3, 22, 13, 46), + }, + { + "event": "test''2", + "timestamp": datetime(2024, 3, 22, 13, 47), + }, + ], + } + journeys_for(events_by_person, self.team) + + query = FunnelsQuery( + series=[EventsNode(event="test'1"), EventsNode()], + dateRange=DateRange( + date_from="2024-03-22", + date_to="2024-03-22", + ), + ) + results = FunnelsQueryRunner(query=query, team=self.team).calculate().results + + self.assertEqual(results[0]["count"], 1) + return TestGetFunnel diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py index 4c342d2f2926c..dec7bdd933b3e 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_persons.py @@ -626,3 +626,45 @@ def test_funnel_person_recordings(self): } ], ) + + def test_parses_step_breakdown_correctly(self): + person1 = _create_person( + distinct_ids=["person1"], + team_id=self.team.pk, + properties={"$country": "PL"}, + ) + journeys_for( + { + "person1": [ + { + "event": "sign up", + "timestamp": datetime(2020, 1, 1, 12), + "properties": {"$browser": "test''123"}, + }, + { + "event": "play movie", + "timestamp": datetime(2020, 1, 1, 13), + "properties": {"$browser": "test''123"}, + }, + ], + }, + self.team, + create_people=False, + ) + + filters = { + "insight": INSIGHT_FUNNELS, + "date_from": "2020-01-01", + "date_to": "2020-01-08", + "interval": "day", + "funnel_window_days": 7, + "events": [ + {"id": "sign up", "order": 0}, + {"id": "play movie", "order": 1}, + ], + "breakdown_type": "event", + "breakdown": "$browser", + } + + results = get_actors(filters, self.team, funnelStep=1, funnelStepBreakdown=["test'123"]) + self.assertCountEqual([results[0][0]], [person1.uuid]) 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 6ca333b036f14..f9c7b107074de 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_trends.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_trends.py @@ -1387,3 +1387,43 @@ def test_trend_for_hour_based_conversion_window(self): results = FunnelsQueryRunner(query=query, team=self.team, just_summarize=True).calculate().results conversion_rates = [row["conversion_rate"] for row in results] self.assertEqual(conversion_rates, [50.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + + def test_parses_breakdown_correctly(self): + journeys_for( + { + "user_one": [ + { + "event": "step one", + "timestamp": datetime(2021, 5, 1), + "properties": {"$browser": "test''123"}, + }, + { + "event": "step two", + "timestamp": datetime(2021, 5, 3), + "properties": {"$browser": "test''123"}, + }, + ], + }, + 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_days": 7, + "events": [ + {"id": "step one", "order": 0}, + {"id": "step two", "order": 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) diff --git a/posthog/hogql_queries/insights/funnels/utils.py b/posthog/hogql_queries/insights/funnels/utils.py index 47c1487e5fbcc..cdccce0251a33 100644 --- a/posthog/hogql_queries/insights/funnels/utils.py +++ b/posthog/hogql_queries/insights/funnels/utils.py @@ -61,23 +61,26 @@ def funnel_window_interval_unit_to_sql( def get_breakdown_expr( - breakdown: List[str | int] | None, properties_column: str, normalize_url: bool | None = False + breakdowns: List[str | int] | str | int, properties_column: str, normalize_url: bool | None = False ) -> ast.Expr: - if isinstance(breakdown, str) or isinstance(breakdown, int) or breakdown is None: - return parse_expr(f"ifNull({properties_column}.\"{breakdown}\", '')") + if isinstance(breakdowns, str) or isinstance(breakdowns, int) or breakdowns is None: + return ast.Call( + name="ifNull", args=[ast.Field(chain=[*properties_column.split("."), breakdowns]), ast.Constant(value="")] + ) else: exprs = [] - for b in breakdown: - expr = parse_expr(normalize_url_breakdown(f"ifNull({properties_column}.\"{b}\", '')", normalize_url)) + for breakdown in breakdowns: + expr: ast.Expr = ast.Call( + name="ifNull", + args=[ast.Field(chain=[*properties_column.split("."), breakdown]), ast.Constant(value="")], + ) + if normalize_url: + regex = "[\\\\/?#]*$" + expr = parse_expr( + f"if( empty( replaceRegexpOne({{breakdown_value}}, '{regex}', '') ), '/', replaceRegexpOne({{breakdown_value}}, '{regex}', ''))", + {"breakdown_value": expr}, + ) exprs.append(expr) expression = ast.Array(exprs=exprs) return expression - - -def normalize_url_breakdown(breakdown_value, breakdown_normalize_url: bool | None): - if breakdown_normalize_url: - regex = "[\\\\/?#]*$" - return f"if( empty( replaceRegexpOne({breakdown_value}, '{regex}', '') ), '/', replaceRegexpOne({breakdown_value}, '{regex}', ''))" - - return breakdown_value From bc7770cd52101b01f662a64f6b386fbb53eb8012 Mon Sep 17 00:00:00 2001 From: PostHog Bot <69588470+posthog-bot@users.noreply.github.com> Date: Sat, 23 Mar 2024 15:29:37 -0400 Subject: [PATCH 47/51] chore(deps): Update posthog-js to 1.116.5 (#21118) --- package.json | 2 +- pnpm-lock.yaml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 2128fa3207a76..58e9fcdba1d3a 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "pmtiles": "^2.11.0", "postcss": "^8.4.31", "postcss-preset-env": "^9.3.0", - "posthog-js": "1.116.4", + "posthog-js": "1.116.5", "posthog-js-lite": "2.5.0", "prettier": "^2.8.8", "prop-types": "^15.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04806adc67be8..b6375a3382ae2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -254,8 +254,8 @@ dependencies: specifier: ^9.3.0 version: 9.3.0(postcss@8.4.31) posthog-js: - specifier: 1.116.4 - version: 1.116.4 + specifier: 1.116.5 + version: 1.116.5 posthog-js-lite: specifier: 2.5.0 version: 2.5.0 @@ -13633,7 +13633,7 @@ packages: hogan.js: 3.0.2 htm: 3.1.1 instantsearch-ui-components: 0.3.0 - preact: 10.20.0 + preact: 10.20.1 qs: 6.9.7 search-insights: 2.13.0 dev: false @@ -17454,19 +17454,19 @@ packages: resolution: {integrity: sha512-Urvlp0Vu9h3td0BVFWt0QXFJDoOZcaAD83XM9d91NKMKTVPZtfU0ysoxstIf5mw/ce9ZfuMgpWPaagrZI4rmSg==} dev: false - /posthog-js@1.116.4: - resolution: {integrity: sha512-PZg208/k5OZRQbd9tnGvUgtyRl1IAYyyh74teyIDIH3EnlsAolBlVM4gcoyEYoVkUi5sZLKitj9gTX3/vnEG4Q==} + /posthog-js@1.116.5: + resolution: {integrity: sha512-Dfsy07WGPRqbXNQgfhR5zhcQw4A078xCRX1y0oGmy8xVUmL6QLNNTVKENB4xpo45Ox+88cysi+hEMhd9SfV5Qg==} dependencies: fflate: 0.4.8 - preact: 10.20.0 + preact: 10.20.1 dev: false /potpack@2.0.0: resolution: {integrity: sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==} dev: false - /preact@10.20.0: - resolution: {integrity: sha512-wU7iZw2BjsaKDal3pDRDy/HpPB6cuFOnVUCcw9aIPKG98+ZrXx3F+szkos8BVME5bquyKDKvRlOJFG8kMkcAbg==} + /preact@10.20.1: + resolution: {integrity: sha512-JIFjgFg9B2qnOoGiYMVBtrcFxHqn+dNXbq76bVmcaHYJFYR4lW67AOcXgAYQQTDYXDOg/kTZrKPNCdRgJ2UJmw==} dev: false /prelude-ls@1.2.1: From a63fd083e223fbb82b130255dd44bc1ae1048eea Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sun, 24 Mar 2024 12:11:54 +0000 Subject: [PATCH 48/51] chore: remove flag that has been on for four months (#21115) * chore: remove flag that has been on for four months * fix --- frontend/src/lib/constants.tsx | 1 - .../sessionRecordingFilePlaybackLogic.ts | 7 +------ .../player/sessionRecordingDataLogic.ts | 11 ++++------- .../playlist/SessionRecordingsPlaylist.tsx | 11 ++++------- .../SessionRecordingsPlaylistTroubleshooting.tsx | 12 +++--------- 5 files changed, 12 insertions(+), 30 deletions(-) diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 53036d5c6c511..e42695b029c3c 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -185,7 +185,6 @@ export const FEATURE_FLAGS = { INSIGHT_HORIZONTAL_CONTROLS: 'insight-horizontal-controls', // owner: @benjackwhite SURVEYS_WIDGETS: 'surveys-widgets', // owner: @liyiy SCHEDULED_CHANGES_FEATURE_FLAGS: 'scheduled-changes-feature-flags', // owner: @jurajmajerik #team-feature-success - SESSION_REPLAY_MOBILE: 'session-replay-mobile', // owner: #team-replay INVITE_TEAM_MEMBER_ONBOARDING: 'invite-team-member-onboarding', // owner: @biancayang YEAR_IN_HOG: 'year-in-hog', // owner: #team-replay SESSION_REPLAY_EXPORT_MOBILE_DATA: 'session-replay-export-mobile-data', // owner: #team-replay diff --git a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts index fb01f15b9a0cc..b6f547603114b 100644 --- a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts +++ b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts @@ -3,7 +3,6 @@ import { eventWithTime } from '@rrweb/types' import { BuiltLogic, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import { beforeUnload } from 'kea-router' -import { FEATURE_FLAGS } from 'lib/constants' import { dayjs } from 'lib/dayjs' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { uuid } from 'lib/utils' @@ -178,11 +177,7 @@ export const sessionRecordingFilePlaybackLogic = kea => { - if (!postHogEEModule && withMobileTransformer) { + if (!postHogEEModule) { postHogEEModule = await posthogEE() } const lineCount = items.length @@ -239,11 +240,7 @@ async function processEncodedResponse( let untransformed: RecordingSnapshot[] | null = null const transformed = deduplicateSnapshots( - await parseEncodedSnapshots( - encodedResponse, - props.sessionRecordingId, - !!featureFlags[FEATURE_FLAGS.SESSION_REPLAY_MOBILE] - ), + await parseEncodedSnapshots(encodedResponse, props.sessionRecordingId), existingData?.snapshots ?? [] ) diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx index 22dd5881ae378..3c2a9842c0dbc 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx @@ -6,7 +6,6 @@ import clsx from 'clsx' import { range } from 'd3' import { BindLogic, useActions, useValues } from 'kea' import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage' -import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { FEATURE_FLAGS } from 'lib/constants' import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' @@ -58,12 +57,10 @@ function UnusableEventsWarning(props: { unusableEventsInFilter: string[] }): JSX the Web SDK - - ,{' '} - - the Android SDK - - + ,{' '} + + the Android SDK +

) diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistTroubleshooting.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistTroubleshooting.tsx index bda13153d9ccb..961b0b54fa246 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistTroubleshooting.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistTroubleshooting.tsx @@ -1,7 +1,5 @@ import { LemonDivider, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { FlaggedFeature } from 'lib/components/FlaggedFeature' -import { FEATURE_FLAGS } from 'lib/constants' import { playerSettingsLogic } from '../player/playerSettingsLogic' import { sessionRecordingsPlaylistLogic } from './sessionRecordingsPlaylistLogic' @@ -21,9 +19,7 @@ export const SessionRecordingsPlaylistTroubleshooting = (): JSX.Element => {

    - -
    All recording sources:
    -
    +
    All recording sources:
    {otherRecordings.length > 0 && hideViewedRecordings && (
  • Viewed recordings hidden.{' '} @@ -42,10 +38,8 @@ export const SessionRecordingsPlaylistTroubleshooting = (): JSX.Element => { They are outside the retention period
  • - - -
    Web recordings
    -
    + +
    Web recordings
  • Date: Sun, 24 Mar 2024 12:33:27 +0000 Subject: [PATCH 49/51] feat(web-analytics): Sessions backfill: add settings to increase execution time (#21121) Add settings to increase execution time --- .../management/commands/backfill_sessions_table.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/posthog/management/commands/backfill_sessions_table.py b/posthog/management/commands/backfill_sessions_table.py index 798a501eb5b60..e4e6967d9dfa7 100644 --- a/posthog/management/commands/backfill_sessions_table.py +++ b/posthog/management/commands/backfill_sessions_table.py @@ -16,6 +16,10 @@ TARGET_TABLE = "sessions" +SETTINGS = { + "max_execution_time": 3600 # 1 hour +} + @dataclass class BackfillQuery: @@ -109,12 +113,12 @@ def select_query(select_date: Optional[datetime] = None) -> str: # print the count of entries in the main sessions table count_query = f"SELECT count(), uniq(session_id) FROM {TARGET_TABLE}" - [(sessions_row_count, sessions_event_count)] = sync_execute(count_query) + [(sessions_row_count, sessions_event_count)] = sync_execute(count_query, settings=SETTINGS) logger.info(f"{sessions_row_count} rows and {sessions_event_count} unique session_ids in sessions table") if dry_run: count_query = f"SELECT count(), uniq(session_id) FROM ({select_query()})" - [(events_count, sessions_count)] = sync_execute(count_query) + [(events_count, sessions_count)] = sync_execute(count_query, settings=SETTINGS) logger.info(f"{events_count} events and {sessions_count} sessions to backfill for") logger.info(f"The first select query would be:\n{select_query(self.start_date)}") return @@ -125,11 +129,12 @@ def select_query(select_date: Optional[datetime] = None) -> str: sync_execute( query=f"""INSERT INTO writable_sessions {select_query(select_date=date)} SETTINGS max_execution_time=3600""", workload=Workload.OFFLINE if self.use_offline_workload else Workload.DEFAULT, + settings=SETTINGS, ) # print the count of entries in the main sessions table count_query = f"SELECT count(), uniq(session_id) FROM {TARGET_TABLE}" - [(sessions_row_count, sessions_event_count)] = sync_execute(count_query) + [(sessions_row_count, sessions_event_count)] = sync_execute(count_query, settings=SETTINGS) logger.info(f"{sessions_row_count} rows and {sessions_event_count} unique session_ids in sessions table") From 37f3ff392bea59f26930bac3f28db826251d4cf2 Mon Sep 17 00:00:00 2001 From: Julian Bez Date: Sun, 24 Mar 2024 20:53:41 +0000 Subject: [PATCH 50/51] feat(hogql): Format actors modal date labels (#21069) --- frontend/src/queries/schema.json | 10 ++++ frontend/src/queries/schema.ts | 6 ++- .../__snapshots__/test_dashboard.ambr | 18 +++++++ .../insights/lifecycle_query_runner.py | 2 +- .../trends/test/test_trends_query_runner.py | 50 ++++++++++--------- .../insights/trends/trends_query_runner.py | 8 ++- .../hogql_queries/utils/query_date_range.py | 15 +++--- .../utils/test/test_query_date_range.py | 25 ++++++++-- posthog/schema.py | 6 ++- 9 files changed, 99 insertions(+), 41 deletions(-) diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index c00c309797421..400ef8d1774e3 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -949,6 +949,10 @@ }, "type": "object" }, + "DatetimeDay": { + "format": "date-time", + "type": "string" + }, "Day": { "type": "integer" }, @@ -2788,6 +2792,9 @@ { "type": "string" }, + { + "$ref": "#/definitions/DatetimeDay" + }, { "$ref": "#/definitions/Day" } @@ -3839,6 +3846,9 @@ { "type": "string" }, + { + "$ref": "#/definitions/DatetimeDay" + }, { "$ref": "#/definitions/Day" } diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index e07838648f4af..9bd04dd3c62a9 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -1179,9 +1179,13 @@ export interface FunnelCorrelationQuery { response?: FunnelCorrelationResponse } +/** @format date-time */ +export type DatetimeDay = string + export type BreakdownValueInt = integer export interface InsightActorsQueryOptionsResponse { - day?: { label: string; value: string | Day }[] + // eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents + day?: { label: string; value: string | DatetimeDay | Day }[] status?: { label: string; value: string }[] interval?: { label: string diff --git a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr index cccef08bc4a1f..9ae54e6e582eb 100644 --- a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr +++ b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr @@ -2762,6 +2762,24 @@ 5 /* ... */) /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ ''' # --- +# name: TestDashboard.test_listing_dashboards_is_not_nplus1.57 + ''' + SELECT "posthog_sharingconfiguration"."id", + "posthog_sharingconfiguration"."team_id", + "posthog_sharingconfiguration"."dashboard_id", + "posthog_sharingconfiguration"."insight_id", + "posthog_sharingconfiguration"."recording_id", + "posthog_sharingconfiguration"."created_at", + "posthog_sharingconfiguration"."enabled", + "posthog_sharingconfiguration"."access_token" + FROM "posthog_sharingconfiguration" + WHERE "posthog_sharingconfiguration"."dashboard_id" IN (1, + 2, + 3, + 4, + 5 /* ... */) /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ + ''' +# --- # name: TestDashboard.test_listing_dashboards_is_not_nplus1.6 ''' SELECT "posthog_team"."id", diff --git a/posthog/hogql_queries/insights/lifecycle_query_runner.py b/posthog/hogql_queries/insights/lifecycle_query_runner.py index 62968f5349e0e..ea883eec542bc 100644 --- a/posthog/hogql_queries/insights/lifecycle_query_runner.py +++ b/posthog/hogql_queries/insights/lifecycle_query_runner.py @@ -126,7 +126,7 @@ def to_actors_query( def to_actors_query_options(self) -> InsightActorsQueryOptionsResponse: return InsightActorsQueryOptionsResponse( - day=[{"label": day, "value": day} for day in self.query_date_range.all_values()], + day=[{"label": format_label_date(value), "value": value} for value in self.query_date_range.all_values()], status=[ { "label": "Dormant", diff --git a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py index 8d14950ec23b2..6bb41b19c79cf 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py @@ -1,4 +1,6 @@ +import zoneinfo from dataclasses import dataclass +from datetime import datetime from typing import Dict, List, Optional from unittest.mock import MagicMock, patch from django.test import override_settings @@ -1478,18 +1480,18 @@ def test_to_actors_query_options(self): response = runner.to_actors_query_options() assert response.day == [ - DayItem(label="2020-01-09", value="2020-01-09"), - DayItem(label="2020-01-10", value="2020-01-10"), - DayItem(label="2020-01-11", value="2020-01-11"), - DayItem(label="2020-01-12", value="2020-01-12"), - DayItem(label="2020-01-13", value="2020-01-13"), - DayItem(label="2020-01-14", value="2020-01-14"), - DayItem(label="2020-01-15", value="2020-01-15"), - DayItem(label="2020-01-16", value="2020-01-16"), - DayItem(label="2020-01-17", value="2020-01-17"), - DayItem(label="2020-01-18", value="2020-01-18"), - DayItem(label="2020-01-19", value="2020-01-19"), - DayItem(label="2020-01-20", value="2020-01-20"), + DayItem(label="9-Jan-2020", value=datetime(2020, 1, 9, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="10-Jan-2020", value=datetime(2020, 1, 10, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="11-Jan-2020", value=datetime(2020, 1, 11, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="12-Jan-2020", value=datetime(2020, 1, 12, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="13-Jan-2020", value=datetime(2020, 1, 13, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="14-Jan-2020", value=datetime(2020, 1, 14, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="15-Jan-2020", value=datetime(2020, 1, 15, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="16-Jan-2020", value=datetime(2020, 1, 16, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="17-Jan-2020", value=datetime(2020, 1, 17, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="18-Jan-2020", value=datetime(2020, 1, 18, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="19-Jan-2020", value=datetime(2020, 1, 19, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="20-Jan-2020", value=datetime(2020, 1, 20, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), ] assert response.breakdown is None @@ -1513,18 +1515,18 @@ def test_to_actors_query_options_compare(self): response = runner.to_actors_query_options() assert response.day == [ - DayItem(label="2020-01-09", value="2020-01-09"), - DayItem(label="2020-01-10", value="2020-01-10"), - DayItem(label="2020-01-11", value="2020-01-11"), - DayItem(label="2020-01-12", value="2020-01-12"), - DayItem(label="2020-01-13", value="2020-01-13"), - DayItem(label="2020-01-14", value="2020-01-14"), - DayItem(label="2020-01-15", value="2020-01-15"), - DayItem(label="2020-01-16", value="2020-01-16"), - DayItem(label="2020-01-17", value="2020-01-17"), - DayItem(label="2020-01-18", value="2020-01-18"), - DayItem(label="2020-01-19", value="2020-01-19"), - DayItem(label="2020-01-20", value="2020-01-20"), + DayItem(label="9-Jan-2020", value=datetime(2020, 1, 9, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="10-Jan-2020", value=datetime(2020, 1, 10, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="11-Jan-2020", value=datetime(2020, 1, 11, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="12-Jan-2020", value=datetime(2020, 1, 12, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="13-Jan-2020", value=datetime(2020, 1, 13, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="14-Jan-2020", value=datetime(2020, 1, 14, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="15-Jan-2020", value=datetime(2020, 1, 15, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="16-Jan-2020", value=datetime(2020, 1, 16, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="17-Jan-2020", value=datetime(2020, 1, 17, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="18-Jan-2020", value=datetime(2020, 1, 18, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="19-Jan-2020", value=datetime(2020, 1, 19, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), + DayItem(label="20-Jan-2020", value=datetime(2020, 1, 20, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC"))), ] assert response.breakdown is None diff --git a/posthog/hogql_queries/insights/trends/trends_query_runner.py b/posthog/hogql_queries/insights/trends/trends_query_runner.py index d61720740f52b..67e160084e68e 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/trends_query_runner.py @@ -183,7 +183,13 @@ def to_actors_query_options(self) -> InsightActorsQueryOptionsResponse: res_compare: List[CompareItem] | None = None # Days - res_days: List[DayItem] = [DayItem(label=day, value=day) for day in self.query_date_range.all_values()] + res_days: list[DayItem] = [ + DayItem( + label=format_label_date(value, self.query_date_range.interval_name), + value=value, + ) + for value in self.query_date_range.all_values() + ] # Series for index, series in enumerate(self.query.series): diff --git a/posthog/hogql_queries/utils/query_date_range.py b/posthog/hogql_queries/utils/query_date_range.py index 5453d878b4017..b6386ac85f4ed 100644 --- a/posthog/hogql_queries/utils/query_date_range.py +++ b/posthog/hogql_queries/utils/query_date_range.py @@ -1,7 +1,7 @@ import re from datetime import datetime, timedelta from functools import cached_property -from typing import Literal, Optional, Dict, List +from typing import Literal, Optional, Dict from zoneinfo import ZoneInfo from dateutil.parser import parse @@ -140,16 +140,15 @@ def interval_relativedelta(self) -> relativedelta: hours=1 if self.interval_name == "hour" else 0, ) - def all_values(self) -> List[str]: + def all_values(self) -> list[datetime]: start = self.align_with_interval(self.date_from()) end: datetime = self.date_to() - values: List[str] = [] + delta = self.interval_relativedelta() + + values: list[datetime] = [] while start <= end: - if self.interval_name == "hour": - values.append(start.strftime("%Y-%m-%d %H:%M:%S")) - else: - values.append(start.strftime("%Y-%m-%d")) - start += self.interval_relativedelta() + values.append(start) + start += delta return values def date_to_as_hogql(self) -> ast.Expr: diff --git a/posthog/hogql_queries/utils/test/test_query_date_range.py b/posthog/hogql_queries/utils/test/test_query_date_range.py index fd38ef700e137..f377e06880bbe 100644 --- a/posthog/hogql_queries/utils/test/test_query_date_range.py +++ b/posthog/hogql_queries/utils/test/test_query_date_range.py @@ -61,32 +61,47 @@ def test_all_values(self): QueryDateRange( team=self.team, date_range=DateRange(date_from="-20h"), interval=IntervalType.day, now=now ).all_values(), - ["2021-08-24", "2021-08-25"], + [parser.isoparse("2021-08-24T00:00:00Z"), parser.isoparse("2021-08-25T00:00:00Z")], ) self.assertEqual( QueryDateRange( team=self.team, date_range=DateRange(date_from="-20d"), interval=IntervalType.week, now=now ).all_values(), - ["2021-08-01", "2021-08-08", "2021-08-15", "2021-08-22"], + [ + parser.isoparse("2021-08-01T00:00:00Z"), + parser.isoparse("2021-08-08T00:00:00Z"), + parser.isoparse("2021-08-15T00:00:00Z"), + parser.isoparse("2021-08-22T00:00:00Z"), + ], ) self.team.week_start_day = WeekStartDay.MONDAY self.assertEqual( QueryDateRange( team=self.team, date_range=DateRange(date_from="-20d"), interval=IntervalType.week, now=now ).all_values(), - ["2021-08-02", "2021-08-09", "2021-08-16", "2021-08-23"], + [ + parser.isoparse("2021-08-02T00:00:00Z"), + parser.isoparse("2021-08-09T00:00:00Z"), + parser.isoparse("2021-08-16T00:00:00Z"), + parser.isoparse("2021-08-23T00:00:00Z"), + ], ) self.assertEqual( QueryDateRange( team=self.team, date_range=DateRange(date_from="-50d"), interval=IntervalType.month, now=now ).all_values(), - ["2021-07-01", "2021-08-01"], + [parser.isoparse("2021-07-01T00:00:00Z"), parser.isoparse("2021-08-01T00:00:00Z")], ) self.assertEqual( QueryDateRange( team=self.team, date_range=DateRange(date_from="-3h"), interval=IntervalType.hour, now=now ).all_values(), - ["2021-08-24 21:00:00", "2021-08-24 22:00:00", "2021-08-24 23:00:00", "2021-08-25 00:00:00"], + [ + parser.isoparse("2021-08-24T21:00:00Z"), + parser.isoparse("2021-08-24T22:00:00Z"), + parser.isoparse("2021-08-24T23:00:00Z"), + parser.isoparse("2021-08-25T00:00:00Z"), + ], ) diff --git a/posthog/schema.py b/posthog/schema.py index c88f8bf3f76de..9d83587351683 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -180,6 +180,10 @@ class DateRange(BaseModel): date_to: Optional[str] = None +class DatetimeDay(RootModel[AwareDatetime]): + root: AwareDatetime + + class Day(RootModel[int]): root: int @@ -458,7 +462,7 @@ class DayItem(BaseModel): extra="forbid", ) label: str - value: Union[str, int] + value: Union[str, AwareDatetime, int] class IntervalItem(BaseModel): From 064f175925e529effa78015e0a30fe21cbbb2f51 Mon Sep 17 00:00:00 2001 From: Robbie Date: Sun, 24 Mar 2024 21:29:30 +0000 Subject: [PATCH 51/51] feat(web-analytics): Sessions table backfill: Make printing counts beforehand optional (#21123) Make printing counts beforehand optional --- .../commands/backfill_sessions_table.py | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/posthog/management/commands/backfill_sessions_table.py b/posthog/management/commands/backfill_sessions_table.py index e4e6967d9dfa7..c01f4b6159749 100644 --- a/posthog/management/commands/backfill_sessions_table.py +++ b/posthog/management/commands/backfill_sessions_table.py @@ -30,6 +30,7 @@ class BackfillQuery: def execute( self, dry_run: bool = True, + print_counts: bool = True, ) -> None: def source_column(column_name: str) -> str: return get_property_string_expr( @@ -112,9 +113,10 @@ def select_query(select_date: Optional[datetime] = None) -> str: """ # print the count of entries in the main sessions table - count_query = f"SELECT count(), uniq(session_id) FROM {TARGET_TABLE}" - [(sessions_row_count, sessions_event_count)] = sync_execute(count_query, settings=SETTINGS) - logger.info(f"{sessions_row_count} rows and {sessions_event_count} unique session_ids in sessions table") + if print_counts: + count_query = f"SELECT count(), uniq(session_id) FROM {TARGET_TABLE}" + [(sessions_row_count, sessions_event_count)] = sync_execute(count_query, settings=SETTINGS) + logger.info(f"{sessions_row_count} rows and {sessions_event_count} unique session_ids in sessions table") if dry_run: count_query = f"SELECT count(), uniq(session_id) FROM ({select_query()})" @@ -133,9 +135,10 @@ def select_query(select_date: Optional[datetime] = None) -> str: ) # print the count of entries in the main sessions table - count_query = f"SELECT count(), uniq(session_id) FROM {TARGET_TABLE}" - [(sessions_row_count, sessions_event_count)] = sync_execute(count_query, settings=SETTINGS) - logger.info(f"{sessions_row_count} rows and {sessions_event_count} unique session_ids in sessions table") + if print_counts: + count_query = f"SELECT count(), uniq(session_id) FROM {TARGET_TABLE}" + [(sessions_row_count, sessions_event_count)] = sync_execute(count_query, settings=SETTINGS) + logger.info(f"{sessions_row_count} rows and {sessions_event_count} unique session_ids in sessions table") class Command(BaseCommand): @@ -154,11 +157,25 @@ def add_arguments(self, parser): parser.add_argument( "--use-offline-workload", action="store_true", help="actually execute INSERT queries (default is dry-run)" ) + parser.add_argument( + "--print-counts", action="store_true", help="print events and session count beforehand and afterwards" + ) - def handle(self, *, live_run: bool, start_date: str, end_date: str, use_offline_workload: bool, **options): + def handle( + self, + *, + live_run: bool, + start_date: str, + end_date: str, + use_offline_workload: bool, + print_counts: bool, + **options, + ): logger.setLevel(logging.INFO) start_datetime = datetime.strptime(start_date, "%Y-%m-%d") end_datetime = datetime.strptime(end_date, "%Y-%m-%d") - BackfillQuery(start_datetime, end_datetime, use_offline_workload).execute(dry_run=not live_run) + BackfillQuery(start_datetime, end_datetime, use_offline_workload).execute( + dry_run=not live_run, print_counts=print_counts + )